Skip to main content

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}