Skip to main content

guard_core_engine/semantic/
keywords.rs

1use std::collections::HashMap;
2use std::collections::HashSet;
3
4/// Keyword sets per attack category (xss, sql, command, path, template).
5///
6/// Used by [`super::analyze_attack_probability`] for base score calculation.
7/// Construct with `Default::default()` for the built-in keyword tables.
8pub struct AttackKeywords {
9    categories: HashMap<&'static str, HashSet<&'static str>>,
10}
11
12impl AttackKeywords {
13    /// All categories as `name -> keyword set` map.
14    #[must_use]
15    pub const fn all(&self) -> &HashMap<&'static str, HashSet<&'static str>> {
16        &self.categories
17    }
18}
19
20impl Default for AttackKeywords {
21    fn default() -> Self {
22        let categories = HashMap::from([
23            (
24                "xss",
25                HashSet::from([
26                    "script",
27                    "javascript",
28                    "onerror",
29                    "onload",
30                    "onclick",
31                    "onmouseover",
32                    "alert",
33                    "eval",
34                    "document",
35                    "cookie",
36                    "window",
37                    "location",
38                ]),
39            ),
40            (
41                "sql",
42                HashSet::from([
43                    "select",
44                    "union",
45                    "insert",
46                    "update",
47                    "delete",
48                    "drop",
49                    "from",
50                    "where",
51                    "order",
52                    "group",
53                    "having",
54                    "concat",
55                    "substring",
56                    "database",
57                    "table",
58                    "column",
59                ]),
60            ),
61            (
62                "command",
63                HashSet::from([
64                    "exec",
65                    "system",
66                    "shell",
67                    "cmd",
68                    "bash",
69                    "powershell",
70                    "wget",
71                    "curl",
72                    "nc",
73                    "netcat",
74                    "chmod",
75                    "chown",
76                    "sudo",
77                    "passwd",
78                ]),
79            ),
80            (
81                "path",
82                HashSet::from([
83                    "etc", "passwd", "shadow", "hosts", "proc", "boot", "win", "ini",
84                ]),
85            ),
86            (
87                "template",
88                HashSet::from([
89                    "render",
90                    "template",
91                    "jinja",
92                    "mustache",
93                    "handlebars",
94                    "ejs",
95                    "pug",
96                    "twig",
97                ]),
98            ),
99        ]);
100        Self { categories }
101    }
102}
103
104#[cfg(test)]
105mod empty_category_tests {
106    use super::*;
107
108    #[test]
109    fn an_empty_category_scores_zero_without_blocking_the_rest() {
110        // a caller can wire a category with no keywords yet: it scores 0.0
111        // while the populated categories still compute normally
112        let keywords = AttackKeywords {
113            categories: HashMap::from([
114                ("empty_kind", HashSet::new()),
115                ("path", HashSet::from(["etc", "passwd"])),
116            ]),
117        };
118        let probabilities = crate::semantic::attack_probability_with_tokens(
119            "/etc/passwd",
120            &[String::from("etc"), String::from("passwd")],
121            &keywords,
122        );
123        assert!(probabilities["empty_kind"].abs() < f64::EPSILON);
124        assert!(probabilities["path"] > 0.0, "path: {probabilities:?}");
125    }
126}