killer/klr/ast.rs
1//! The abstract syntax tree for the Killer Rule Language (`.klr`).
2//!
3//! A `.klr` file parses into a [`Program`]: an optional project name plus a set
4//! of [`Attack`] definitions (dynamic security tests) and [`KlrRule`]
5//! definitions (static code rules).
6
7use crate::analyzer::Severity;
8
9/// A parsed `.klr` program.
10#[derive(Debug, Clone, Default, PartialEq)]
11pub struct Program {
12 /// The `project "..."` declaration, if present.
13 pub project: Option<String>,
14 /// Top-level attack/test definitions (not inside a suite).
15 pub attacks: Vec<Attack>,
16 /// Named suites grouping attacks/tests.
17 pub suites: Vec<Suite>,
18 /// Static code-rule definitions.
19 pub rules: Vec<KlrRule>,
20}
21
22impl Program {
23 /// All attacks in the program: top-level ones plus every suite's attacks,
24 /// each tagged with its suite name.
25 pub fn all_attacks(&self) -> Vec<Attack> {
26 let mut out: Vec<Attack> = self.attacks.clone();
27 for suite in &self.suites {
28 for attack in &suite.attacks {
29 let mut a = attack.clone();
30 a.suite = Some(suite.name.clone());
31 out.push(a);
32 }
33 }
34 out
35 }
36}
37
38/// A named group of attacks/tests (`suite "Payment Security" { ... }`).
39#[derive(Debug, Clone, PartialEq)]
40pub struct Suite {
41 pub name: String,
42 pub attacks: Vec<Attack>,
43 pub line: usize,
44}
45
46/// A fuzz-mutation of a request field (`mutate amount { negative_numbers ... }`).
47#[derive(Debug, Clone, PartialEq)]
48pub struct Mutation {
49 /// The `send` field to mutate.
50 pub field: String,
51 /// Named value generators, e.g. `negative_numbers`, `huge_values`.
52 pub generators: Vec<String>,
53}
54
55/// A literal value in the source: string, number, boolean, or bare identifier.
56///
57/// Marked `#[non_exhaustive]`: the set of literal forms the language accepts
58/// grows with the language.
59#[derive(Debug, Clone, PartialEq)]
60#[non_exhaustive]
61pub enum Value {
62 Str(String),
63 Num(i64),
64 Bool(bool),
65 Ident(String),
66}
67
68impl Value {
69 /// Render the value as a plain string (for request bodies, reports, etc.).
70 pub fn as_string(&self) -> String {
71 match self {
72 Value::Str(s) => s.clone(),
73 Value::Num(n) => n.to_string(),
74 Value::Bool(b) => b.to_string(),
75 Value::Ident(s) => s.clone(),
76 }
77 }
78}
79
80/// A comparison operator used in `expect` conditions.
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum CompareOp {
83 Eq,
84 Ne,
85 Lt,
86 Gt,
87 Le,
88 Ge,
89}
90
91impl CompareOp {
92 /// Apply the operator to two integers.
93 pub fn apply(&self, lhs: i64, rhs: i64) -> bool {
94 match self {
95 CompareOp::Eq => lhs == rhs,
96 CompareOp::Ne => lhs != rhs,
97 CompareOp::Lt => lhs < rhs,
98 CompareOp::Gt => lhs > rhs,
99 CompareOp::Le => lhs <= rhs,
100 CompareOp::Ge => lhs >= rhs,
101 }
102 }
103
104 pub fn symbol(&self) -> &'static str {
105 match self {
106 CompareOp::Eq => "==",
107 CompareOp::Ne => "!=",
108 CompareOp::Lt => "<",
109 CompareOp::Gt => ">",
110 CompareOp::Le => "<=",
111 CompareOp::Ge => ">=",
112 }
113 }
114}
115
116/// A generic action statement, e.g. `login user "test"`, `steal cookie`,
117/// `attempt reuse`. The `verb` is the leading keyword; `args` are the rest.
118#[derive(Debug, Clone, PartialEq)]
119pub struct Action {
120 pub verb: String,
121 pub args: Vec<Value>,
122}
123
124/// A dynamic attack definition.
125///
126/// Marked `#[non_exhaustive]` because every new `.klr` clause adds a field
127/// here, and fourteen of them are already public. Start from
128/// [`Attack::empty`] and assign the fields you need; struct-literal syntax is
129/// unavailable outside this crate.
130#[derive(Debug, Clone, PartialEq)]
131#[non_exhaustive]
132pub struct Attack {
133 pub name: String,
134 /// The endpoint path or full URL to target.
135 pub target: Option<String>,
136 /// HTTP method (defaults applied by the interpreter).
137 pub method: Option<String>,
138 /// Request body fields (`send { ... }`).
139 pub send: Vec<(String, Value)>,
140 /// Request headers (`header { ... }`).
141 pub headers: Vec<(String, Value)>,
142 /// A raw payload string (`payload "..."`), e.g. a path-traversal string.
143 pub payload: Option<String>,
144 /// Number of times to repeat the request (`repeat N times`).
145 pub repeat: Option<usize>,
146 /// Generic action statements (login/steal/attempt/...).
147 pub actions: Vec<Action>,
148 /// Conditions the response is expected to satisfy.
149 pub expectations: Vec<Expectation>,
150 /// Built-in checks (`check authentication`), expanded by the interpreter.
151 pub checks: Vec<String>,
152 /// Fuzz mutations (`mutate <field> { ... }`), expanded into variants.
153 pub mutations: Vec<Mutation>,
154 pub severity: Severity,
155 pub message: Option<String>,
156 /// The suite this attack belongs to, if any (filled in by `all_attacks`).
157 pub suite: Option<String>,
158 /// 1-indexed source line of the `attack` keyword.
159 pub line: usize,
160}
161
162impl Attack {
163 /// A fresh attack with only a name and line set; all else empty/default.
164 ///
165 /// This is the constructor for [`Attack`]: every field stays public, so
166 /// callers build one by starting here and assigning what they need. Fields
167 /// added later default to empty, which keeps this signature stable.
168 pub fn empty(name: String, line: usize) -> Attack {
169 Attack {
170 name,
171 target: None,
172 method: None,
173 send: Vec::new(),
174 headers: Vec::new(),
175 payload: None,
176 repeat: None,
177 actions: Vec::new(),
178 expectations: Vec::new(),
179 checks: Vec::new(),
180 mutations: Vec::new(),
181 severity: Severity::High,
182 message: None,
183 suite: None,
184 line,
185 }
186 }
187}
188
189/// A single expectation inside an `expect` block.
190///
191/// Marked `#[non_exhaustive]`: adding an expectation form is the most common
192/// way the `.klr` language grows, and every one of them lands here.
193#[derive(Debug, Clone, PartialEq)]
194#[non_exhaustive]
195pub enum Expectation {
196 /// `status <op> <n>`
197 Status { op: CompareOp, value: i64 },
198 /// `response contains "..."`
199 ResponseContains(String),
200 /// `response does_not_contain "..."`
201 ResponseNotContains(String),
202 /// `blocked_after <n>`
203 BlockedAfter(usize),
204 /// A named boolean expectation, e.g. `session_invalidated true`,
205 /// `file_not_exposed true`.
206 Named { name: String, expected: bool },
207}
208
209/// A static code rule (the `rule "..."` construct).
210#[derive(Debug, Clone, PartialEq)]
211pub struct KlrRule {
212 /// The rule description / name from `rule "..."`.
213 pub name: String,
214 /// Substrings a line must contain to match (`when function contains "X"`).
215 pub contains: Vec<String>,
216 /// If set, the line must also reference an input source (`input reaches X`).
217 pub reaches: Option<String>,
218 /// Protections whose absence is required (`without sanitization`).
219 pub without: Vec<String>,
220 pub severity: Severity,
221 /// The `report: "..."` message.
222 pub report: Option<String>,
223 /// 1-indexed source line of the `rule` keyword.
224 pub line: usize,
225}