1use crate::waf::{MatchCondition, MatchValue, WafRule};
6use chrono::Utc;
7use serde::{Deserialize, Serialize};
8use sha2::{Digest, Sha256};
9
10#[derive(Debug, Clone, Serialize, Deserialize, Default)]
11pub struct RuleMetadata {
12 pub external_id: Option<String>,
13 pub name: Option<String>,
14 #[serde(rename = "type")]
15 pub rule_type: Option<String>,
16 pub enabled: Option<bool>,
17 pub priority: Option<u32>,
18 pub conditions: Option<Vec<CustomRuleCondition>>,
19 pub actions: Option<Vec<CustomRuleAction>>,
20 pub ttl: Option<u64>,
21 pub created_at: Option<String>,
22 pub updated_at: Option<String>,
23 pub hit_count: Option<u64>,
24 pub last_hit: Option<String>,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct StoredRule {
29 #[serde(flatten)]
30 pub rule: WafRule,
31 #[serde(default)]
32 pub meta: RuleMetadata,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct CustomRuleCondition {
37 pub field: String,
38 pub operator: String,
39 #[serde(default)]
40 pub value: serde_json::Value,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct CustomRuleAction {
45 #[serde(rename = "type")]
46 pub action_type: String,
47 #[serde(default)]
48 pub params: Option<serde_json::Value>,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct CustomRuleInput {
53 pub id: String,
54 pub name: String,
55 #[serde(rename = "type")]
56 pub rule_type: String,
57 #[serde(default = "default_enabled")]
58 pub enabled: bool,
59 #[serde(default = "default_priority")]
60 pub priority: u32,
61 #[serde(default)]
62 pub conditions: Vec<CustomRuleCondition>,
63 #[serde(default)]
64 pub actions: Vec<CustomRuleAction>,
65 #[serde(default)]
66 pub ttl: Option<u64>,
67}
68
69#[derive(Debug, Clone, Serialize, Deserialize, Default)]
70pub struct CustomRuleUpdate {
71 pub name: Option<String>,
72 #[serde(rename = "type")]
73 pub rule_type: Option<String>,
74 pub enabled: Option<bool>,
75 pub priority: Option<u32>,
76 pub conditions: Option<Vec<CustomRuleCondition>>,
77 pub actions: Option<Vec<CustomRuleAction>>,
78 pub ttl: Option<u64>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct RuleView {
83 pub id: String,
84 pub name: String,
85 #[serde(rename = "type")]
86 pub rule_type: String,
87 pub enabled: bool,
88 pub priority: u32,
89 pub conditions: Vec<CustomRuleCondition>,
90 pub actions: Vec<CustomRuleAction>,
91 #[serde(default, skip_serializing_if = "Option::is_none")]
92 pub ttl: Option<u64>,
93 #[serde(rename = "hitCount")]
94 pub hit_count: u64,
95 #[serde(rename = "lastHit", skip_serializing_if = "Option::is_none")]
96 pub last_hit: Option<String>,
97 #[serde(rename = "createdAt")]
98 pub created_at: String,
99 #[serde(rename = "updatedAt")]
100 pub updated_at: String,
101}
102
103fn default_enabled() -> bool {
104 true
105}
106
107fn default_priority() -> u32 {
108 100
109}
110
111fn now_rfc3339() -> String {
112 Utc::now().to_rfc3339()
113}
114
115fn derive_rule_id(external_id: &str) -> u32 {
116 let mut hasher = Sha256::new();
117 hasher.update(external_id.as_bytes());
118 let digest = hasher.finalize();
119 let value = u32::from_be_bytes([digest[0], digest[1], digest[2], digest[3]]);
120 if value == 0 {
121 1
122 } else {
123 value
124 }
125}
126
127pub fn rule_identifier(rule: &StoredRule) -> String {
128 rule.meta
129 .external_id
130 .clone()
131 .unwrap_or_else(|| rule.rule.id.to_string())
132}
133
134pub fn matches_rule_id(rule: &StoredRule, rule_id: &str) -> bool {
135 if let Some(external) = rule.meta.external_id.as_deref() {
136 return external == rule_id;
137 }
138 rule.rule.id.to_string() == rule_id
139}
140
141fn normalize_rule_type(rule_type: &str) -> String {
142 rule_type.trim().to_ascii_uppercase()
143}
144
145fn default_rule_type(rule: &WafRule) -> String {
146 if rule.blocking.unwrap_or(false) {
147 "BLOCK".to_string()
148 } else {
149 "MONITOR".to_string()
150 }
151}
152
153fn default_actions(rule: &WafRule) -> Vec<CustomRuleAction> {
154 vec![CustomRuleAction {
155 action_type: if rule.blocking.unwrap_or(false) {
156 "block".to_string()
157 } else {
158 "log".to_string()
159 },
160 params: None,
161 }]
162}
163
164fn apply_meta_overrides(meta: &mut RuleMetadata, value: &serde_json::Value) {
165 if let Some(created_at) = value.get("createdAt").and_then(|v| v.as_str()) {
166 meta.created_at = Some(created_at.to_string());
167 } else if let Some(created_at) = value.get("created_at").and_then(|v| v.as_str()) {
168 meta.created_at = Some(created_at.to_string());
169 }
170
171 if let Some(updated_at) = value.get("updatedAt").and_then(|v| v.as_str()) {
172 meta.updated_at = Some(updated_at.to_string());
173 } else if let Some(updated_at) = value.get("updated_at").and_then(|v| v.as_str()) {
174 meta.updated_at = Some(updated_at.to_string());
175 }
176
177 if let Some(hit_count) = value.get("hitCount").and_then(|v| v.as_u64()) {
178 meta.hit_count = Some(hit_count);
179 } else if let Some(hit_count) = value.get("hit_count").and_then(|v| v.as_u64()) {
180 meta.hit_count = Some(hit_count);
181 }
182
183 if let Some(last_hit) = value.get("lastHit").and_then(|v| v.as_str()) {
184 meta.last_hit = Some(last_hit.to_string());
185 } else if let Some(last_hit) = value.get("last_hit").and_then(|v| v.as_str()) {
186 meta.last_hit = Some(last_hit.to_string());
187 }
188}
189
190fn risk_for_type(rule_type: &str, actions: &[CustomRuleAction]) -> f64 {
191 if actions
192 .iter()
193 .any(|a| a.action_type.eq_ignore_ascii_case("block"))
194 {
195 return 95.0;
196 }
197 if rule_type.eq_ignore_ascii_case("block") {
198 return 90.0;
199 }
200 if rule_type.eq_ignore_ascii_case("challenge") {
201 return 70.0;
202 }
203 if rule_type.eq_ignore_ascii_case("rate_limit") {
204 return 60.0;
205 }
206 15.0
207}
208
209fn blocking_for_rule(rule_type: &str, actions: &[CustomRuleAction]) -> bool {
210 if actions
211 .iter()
212 .any(|a| a.action_type.eq_ignore_ascii_case("block"))
213 {
214 return true;
215 }
216 rule_type.eq_ignore_ascii_case("block")
217}
218
219fn scalar_to_string(value: &serde_json::Value) -> Option<String> {
220 match value {
221 serde_json::Value::String(s) => Some(s.clone()),
222 serde_json::Value::Number(n) => Some(n.to_string()),
223 serde_json::Value::Bool(b) => Some(b.to_string()),
224 _ => None,
225 }
226}
227
228fn value_to_match_value(value: &serde_json::Value) -> Option<MatchValue> {
229 match value {
230 serde_json::Value::String(s) => Some(MatchValue::Str(s.clone())),
231 serde_json::Value::Number(n) => n.as_f64().map(MatchValue::Num),
232 serde_json::Value::Bool(b) => Some(MatchValue::Bool(*b)),
233 serde_json::Value::Array(arr) => {
234 let mut converted = Vec::new();
235 for item in arr {
236 if let Some(v) = value_to_match_value(item) {
237 converted.push(v);
238 }
239 }
240 Some(MatchValue::Arr(converted))
241 }
242 _ => None,
243 }
244}
245
246fn base_match_condition(kind: &str, match_value: Option<MatchValue>) -> MatchCondition {
247 MatchCondition {
248 kind: kind.to_string(),
249 match_value,
250 op: None,
251 field: None,
252 direction: None,
253 field_type: None,
254 name: None,
255 selector: None,
256 cleanup_after: None,
257 count: None,
258 timeframe: None,
259 }
260}
261
262fn negate_condition(condition: MatchCondition) -> MatchCondition {
263 MatchCondition {
264 kind: "boolean".to_string(),
265 match_value: Some(MatchValue::Arr(vec![MatchValue::Cond(Box::new(condition))])),
266 op: Some("not".to_string()),
267 field: None,
268 direction: None,
269 field_type: None,
270 name: None,
271 selector: None,
272 cleanup_after: None,
273 count: None,
274 timeframe: None,
275 }
276}
277
278fn operator_condition(operator: &str, value: &serde_json::Value) -> Result<MatchCondition, String> {
279 match operator {
280 "eq" => {
281 let match_value = scalar_to_string(value)
282 .map(MatchValue::Str)
283 .ok_or_else(|| "eq operator requires scalar value".to_string())?;
284 Ok(base_match_condition("equals", Some(match_value)))
285 }
286 "contains" => {
287 let match_value = scalar_to_string(value)
288 .map(MatchValue::Str)
289 .ok_or_else(|| "contains operator requires scalar value".to_string())?;
290 Ok(base_match_condition("contains", Some(match_value)))
291 }
292 "matches" => {
293 let match_value = scalar_to_string(value)
294 .map(MatchValue::Str)
295 .ok_or_else(|| "matches operator requires scalar value".to_string())?;
296 Ok(base_match_condition("regex", Some(match_value)))
297 }
298 "gt" | "lt" => {
299 let number = match value {
300 serde_json::Value::Number(n) => n.as_f64(),
301 serde_json::Value::String(s) => s.parse::<f64>().ok(),
302 _ => None,
303 }
304 .ok_or_else(|| "compare operator requires numeric value".to_string())?;
305 let mut cond = base_match_condition("compare", Some(MatchValue::Num(number)));
306 cond.op = Some(operator.to_string());
307 Ok(cond)
308 }
309 "in" => {
310 let arr = match value {
311 serde_json::Value::Array(items) => items,
312 _ => return Err("in operator requires array value".to_string()),
313 };
314 let mut converted = Vec::new();
315 for item in arr {
316 if let Some(value) = scalar_to_string(item) {
317 converted.push(MatchValue::Str(value));
318 }
319 }
320 if converted.is_empty() {
321 return Err("in operator requires non-empty array".to_string());
322 }
323 Ok(base_match_condition(
324 "hashset",
325 Some(MatchValue::Arr(converted)),
326 ))
327 }
328 "ne" => Err("ne operator not supported for WAF rules".to_string()),
329 other => Err(format!("Unsupported operator: {}", other)),
330 }
331}
332
333fn field_condition(
334 field: &str,
335 operator: &str,
336 value: &serde_json::Value,
337) -> Result<MatchCondition, String> {
338 if operator == "ne" {
339 let condition = field_condition(field, "eq", value)?;
340 return Ok(negate_condition(condition));
341 }
342
343 let field_lower = field.to_lowercase();
344 let op_condition = operator_condition(operator, value)?;
345
346 if field_lower == "method" {
347 if operator == "eq" {
348 if let Some(method) = scalar_to_string(value) {
349 return Ok(base_match_condition(
350 "method",
351 Some(MatchValue::Str(method)),
352 ));
353 }
354 }
355 if operator == "in" {
356 if let serde_json::Value::Array(items) = value {
357 let mut methods = Vec::new();
358 for item in items {
359 if let Some(method) = scalar_to_string(item) {
360 methods.push(MatchValue::Str(method));
361 }
362 }
363 if !methods.is_empty() {
364 return Ok(base_match_condition(
365 "method",
366 Some(MatchValue::Arr(methods)),
367 ));
368 }
369 }
370 }
371 return Ok(base_match_condition(
372 "method",
373 Some(MatchValue::Cond(Box::new(op_condition))),
374 ));
375 }
376
377 if matches!(field_lower.as_str(), "uri" | "path" | "url") {
378 return Ok(base_match_condition(
379 "uri",
380 Some(MatchValue::Cond(Box::new(op_condition))),
381 ));
382 }
383
384 if field_lower == "args" || field_lower == "query" {
385 return Ok(base_match_condition(
386 "args",
387 Some(MatchValue::Cond(Box::new(op_condition))),
388 ));
389 }
390
391 if let Some(name) = field_lower.strip_prefix("arg.") {
392 let mut cond = base_match_condition(
393 "named_argument",
394 Some(MatchValue::Cond(Box::new(op_condition))),
395 );
396 cond.name = Some(name.to_string());
397 return Ok(cond);
398 }
399
400 if let Some(name) = field_lower.strip_prefix("param.") {
401 let mut cond = base_match_condition(
402 "named_argument",
403 Some(MatchValue::Cond(Box::new(op_condition))),
404 );
405 cond.name = Some(name.to_string());
406 return Ok(cond);
407 }
408
409 if let Some(name) = field_lower.strip_prefix("header.") {
410 let mut cond =
411 base_match_condition("header", Some(MatchValue::Cond(Box::new(op_condition))));
412 cond.field = Some(name.to_string());
413 cond.direction = Some("c2s".to_string());
414 return Ok(cond);
415 }
416
417 if let Some(name) = field_lower.strip_prefix("header:") {
418 let mut cond =
419 base_match_condition("header", Some(MatchValue::Cond(Box::new(op_condition))));
420 cond.field = Some(name.to_string());
421 cond.direction = Some("c2s".to_string());
422 return Ok(cond);
423 }
424
425 if field_lower == "body" || field_lower == "request" {
426 return Ok(base_match_condition(
427 "request",
428 Some(MatchValue::Cond(Box::new(op_condition))),
429 ));
430 }
431
432 let mut cond = base_match_condition("header", Some(MatchValue::Cond(Box::new(op_condition))));
433 cond.field = Some(field.to_string());
434 cond.direction = Some("c2s".to_string());
435 Ok(cond)
436}
437
438fn conditions_to_matches(
439 conditions: &[CustomRuleCondition],
440) -> Result<Vec<MatchCondition>, String> {
441 let mut matches = Vec::new();
442 for condition in conditions {
443 matches.push(field_condition(
444 condition.field.as_str(),
445 condition.operator.as_str(),
446 &condition.value,
447 )?);
448 }
449 if matches.is_empty() {
450 return Err("custom rule must include at least one condition".to_string());
451 }
452 Ok(matches)
453}
454
455impl StoredRule {
456 pub fn from_custom(custom: CustomRuleInput) -> Result<Self, String> {
457 let matches = conditions_to_matches(&custom.conditions)?;
458 let rule_id = derive_rule_id(&custom.id);
459 let risk = risk_for_type(&custom.rule_type, &custom.actions);
460 let blocking = blocking_for_rule(&custom.rule_type, &custom.actions);
461
462 let rule = WafRule {
463 id: rule_id,
464 description: custom.name.clone(),
465 contributing_score: None,
466 risk: Some(risk),
467 blocking: Some(blocking),
468 matches,
469 };
470
471 let now = now_rfc3339();
472 let meta = RuleMetadata {
473 external_id: Some(custom.id),
474 name: Some(custom.name),
475 rule_type: Some(custom.rule_type),
476 enabled: Some(custom.enabled),
477 priority: Some(custom.priority),
478 conditions: Some(custom.conditions),
479 actions: Some(custom.actions),
480 ttl: custom.ttl,
481 created_at: Some(now.clone()),
482 updated_at: Some(now),
483 hit_count: Some(0),
484 last_hit: None,
485 };
486
487 Ok(Self { rule, meta })
488 }
489}
490
491impl RuleView {
492 pub fn from_stored(rule: &StoredRule) -> Self {
493 let meta = &rule.meta;
494 let id = rule_identifier(rule);
495 let name = meta
496 .name
497 .clone()
498 .unwrap_or_else(|| rule.rule.description.clone());
499 let rule_type = meta
500 .rule_type
501 .as_deref()
502 .map(normalize_rule_type)
503 .unwrap_or_else(|| default_rule_type(&rule.rule));
504 let enabled = meta.enabled.unwrap_or(true);
505 let priority = meta.priority.unwrap_or(100);
506 let conditions = meta.conditions.clone().unwrap_or_default();
507 let actions = meta
508 .actions
509 .clone()
510 .filter(|items| !items.is_empty())
511 .unwrap_or_else(|| default_actions(&rule.rule));
512 let ttl = meta.ttl;
513 let hit_count = meta.hit_count.unwrap_or(0);
514 let last_hit = meta.last_hit.clone();
515 let created_at = meta.created_at.clone().unwrap_or_else(now_rfc3339);
516 let updated_at = meta
517 .updated_at
518 .clone()
519 .unwrap_or_else(|| created_at.clone());
520
521 Self {
522 id,
523 name,
524 rule_type,
525 enabled,
526 priority,
527 conditions,
528 actions,
529 ttl,
530 hit_count,
531 last_hit,
532 created_at,
533 updated_at,
534 }
535 }
536}
537
538pub fn parse_rule_value(value: serde_json::Value) -> Result<StoredRule, String> {
539 if value.get("matches").is_some() {
540 let rule: WafRule = serde_json::from_value(value.clone())
541 .map_err(|err| format!("invalid waf rule: {}", err))?;
542 let mut meta = RuleMetadata::default();
543 meta.external_id = Some(rule.id.to_string());
544 meta.name = Some(rule.description.clone());
545 meta.rule_type = Some(default_rule_type(&rule));
546 meta.enabled = Some(true);
547 meta.priority = Some(100);
548 meta.actions = Some(default_actions(&rule));
549 meta.created_at = Some(now_rfc3339());
550 meta.updated_at = Some(now_rfc3339());
551 apply_meta_overrides(&mut meta, &value);
552 Ok(StoredRule { rule, meta })
553 } else {
554 let custom: CustomRuleInput = serde_json::from_value(value.clone())
555 .map_err(|err| format!("invalid custom rule: {}", err))?;
556 let mut stored = StoredRule::from_custom(custom)?;
557 apply_meta_overrides(&mut stored.meta, &value);
558 Ok(stored)
559 }
560}
561
562pub fn parse_rules_payload(value: serde_json::Value) -> Result<Vec<StoredRule>, String> {
563 let serde_json::Value::Array(items) = value else {
564 return Err("rules payload must be an array".to_string());
565 };
566
567 let mut rules = Vec::with_capacity(items.len());
568 for item in items {
569 rules.push(parse_rule_value(item)?);
570 }
571 Ok(rules)
572}
573
574pub fn merge_rule_update(
575 existing: &StoredRule,
576 update: CustomRuleUpdate,
577) -> Result<StoredRule, String> {
578 let mut meta = existing.meta.clone();
579 if meta.created_at.is_none() {
580 meta.created_at = Some(now_rfc3339());
581 }
582 meta.updated_at = Some(now_rfc3339());
583
584 if let Some(name) = update.name {
585 meta.name = Some(name);
586 }
587 if let Some(rule_type) = update.rule_type {
588 meta.rule_type = Some(rule_type);
589 }
590 if let Some(enabled) = update.enabled {
591 meta.enabled = Some(enabled);
592 }
593 if let Some(priority) = update.priority {
594 meta.priority = Some(priority);
595 }
596 if let Some(conditions) = update.conditions {
597 meta.conditions = Some(conditions);
598 }
599 if let Some(actions) = update.actions {
600 meta.actions = Some(actions);
601 }
602 if let Some(ttl) = update.ttl {
603 meta.ttl = Some(ttl);
604 }
605
606 let has_conditions = meta
607 .conditions
608 .as_ref()
609 .map(|items| !items.is_empty())
610 .unwrap_or(false);
611
612 if has_conditions {
613 let external_id = meta
614 .external_id
615 .clone()
616 .unwrap_or_else(|| existing.rule.id.to_string());
617 let name = meta
618 .name
619 .clone()
620 .unwrap_or_else(|| existing.rule.description.clone());
621 let rule_type = meta
622 .rule_type
623 .clone()
624 .unwrap_or_else(|| default_rule_type(&existing.rule));
625 let enabled = meta.enabled.unwrap_or(true);
626 let priority = meta.priority.unwrap_or(100);
627 let conditions = meta.conditions.clone().unwrap_or_default();
628 let actions = meta
629 .actions
630 .clone()
631 .filter(|items| !items.is_empty())
632 .unwrap_or_else(|| default_actions(&existing.rule));
633
634 let custom = CustomRuleInput {
635 id: external_id,
636 name,
637 rule_type,
638 enabled,
639 priority,
640 conditions,
641 actions,
642 ttl: meta.ttl,
643 };
644
645 let mut stored = StoredRule::from_custom(custom)?;
646 stored.meta.created_at = meta.created_at.clone();
647 stored.meta.updated_at = meta.updated_at.clone();
648 stored.meta.hit_count = meta.hit_count;
649 stored.meta.last_hit = meta.last_hit.clone();
650 return Ok(stored);
651 }
652
653 let rule_type = meta
654 .rule_type
655 .clone()
656 .unwrap_or_else(|| default_rule_type(&existing.rule));
657 let actions = meta
658 .actions
659 .clone()
660 .filter(|items| !items.is_empty())
661 .unwrap_or_else(|| default_actions(&existing.rule));
662
663 let mut stored = existing.clone();
664 stored.meta = meta.clone();
665 stored.rule.description = meta
666 .name
667 .clone()
668 .unwrap_or_else(|| existing.rule.description.clone());
669 stored.rule.risk = Some(risk_for_type(&rule_type, &actions));
670 stored.rule.blocking = Some(blocking_for_rule(&rule_type, &actions));
671
672 Ok(stored)
673}
674
675pub fn rules_hash(rules: &[StoredRule]) -> String {
676 let mut views: Vec<RuleView> = rules.iter().map(RuleView::from_stored).collect();
677 views.sort_by(|a, b| a.id.cmp(&b.id));
678
679 let payload = serde_json::to_string(&views).unwrap_or_default();
680 let mut hasher = Sha256::new();
681 hasher.update(payload.as_bytes());
682 format!("{:x}", hasher.finalize())
683}