Skip to main content

ruso_script/
compile.rs

1//! Lower script AST to bytecode for `ruso-runtime`.
2
3use std::collections::HashMap;
4
5use ruso_runtime::opcode::Opcode as Instr;
6use ruso_runtime::{BytecodeProgram, EvidenceKind, ExtractSource, QualifiedMatch};
7
8use crate::script::Program;
9use crate::script::ast::{ListSource, Stmt, Value};
10use crate::spec_build::build_program_spec;
11
12#[derive(Debug, thiserror::Error)]
13pub enum CompileError {
14    #[error("script has match/evidence logic but no `name` metadata for the finding title")]
15    MissingFindingTitle,
16    #[error("`mitigation` may appear at most once; it is a single free-text field, not a list")]
17    DuplicateMitigation,
18}
19
20pub fn compile(program: &Program) -> Result<BytecodeProgram, CompileError> {
21    // `mitigation` is a single free-text field (unlike cve/cwe/references/tags,
22    // which accumulate). Reject a script that declares it more than once rather
23    // than silently keeping the last one.
24    if program
25        .statements
26        .iter()
27        .filter(|s| matches!(s, Stmt::Mitigation(_)))
28        .count()
29        > 1
30    {
31        return Err(CompileError::DuplicateMitigation);
32    }
33    let spec = build_program_spec(&program.statements);
34    validate_finding_metadata(&spec.metadata, &program.statements)?;
35    let mut compiler = Compiler::new(spec);
36    compiler.emit_program(&program.statements);
37    Ok(compiler.finish())
38}
39
40fn validate_finding_metadata(
41    metadata: &ruso_runtime::CheckMetadata,
42    statements: &[Stmt],
43) -> Result<(), CompileError> {
44    if !statements.iter().any(needs_finding_title) {
45        return Ok(());
46    }
47    if metadata.name.is_some() {
48        return Ok(());
49    }
50    Err(CompileError::MissingFindingTitle)
51}
52
53fn needs_finding_title(stmt: &Stmt) -> bool {
54    match stmt {
55        Stmt::Match(_)
56        | Stmt::MatchAll(_)
57        | Stmt::MatchAny(_)
58        | Stmt::Assert(_)
59        | Stmt::Evidence(_) => true,
60        Stmt::If { body, .. } => body.iter().any(needs_finding_title),
61        Stmt::ForIn { body, .. } => body.iter().any(needs_finding_title),
62        _ => false,
63    }
64}
65
66struct Compiler {
67    spec: ruso_runtime::ProgramSpec,
68    code: Vec<Instr>,
69    strings: Vec<String>,
70    string_ids: HashMap<String, u32>,
71    payloads: Vec<Vec<u8>>,
72    payload_ids: HashMap<Vec<u8>, u32>,
73    matchers: Vec<QualifiedMatch>,
74    extracts: Vec<ExtractSource>,
75    evidence: Vec<EvidenceKind>,
76}
77
78impl Compiler {
79    fn new(spec: ruso_runtime::ProgramSpec) -> Self {
80        Self {
81            spec,
82            code: Vec::new(),
83            strings: Vec::new(),
84            string_ids: HashMap::new(),
85            payloads: Vec::new(),
86            payload_ids: HashMap::new(),
87            matchers: Vec::new(),
88            extracts: Vec::new(),
89            evidence: Vec::new(),
90        }
91    }
92
93    fn finish(self) -> BytecodeProgram {
94        BytecodeProgram {
95            spec: self.spec,
96            code: self.code,
97            strings: self.strings,
98            payloads: self.payloads,
99            matchers: self.matchers,
100            extracts: self.extracts,
101            evidence: self.evidence,
102        }
103    }
104
105    fn str_id(&mut self, value: impl Into<String>) -> u32 {
106        let value = value.into();
107        if let Some(&id) = self.string_ids.get(&value) {
108            return id;
109        }
110        let id = self.strings.len() as u32;
111        self.string_ids.insert(value.clone(), id);
112        self.strings.push(value);
113        id
114    }
115
116    fn string_span(&mut self, values: &[String]) -> (u32, u16) {
117        let start = self.strings.len() as u32;
118        for value in values {
119            self.strings.push(value.clone());
120        }
121        (start, values.len() as u16)
122    }
123
124    fn payload_id(&mut self, bytes: Vec<u8>) -> u32 {
125        if let Some(&id) = self.payload_ids.get(&bytes) {
126            return id;
127        }
128        let id = self.payloads.len() as u32;
129        self.payload_ids.insert(bytes.clone(), id);
130        self.payloads.push(bytes);
131        id
132    }
133
134    fn matcher_id(&mut self, matcher: QualifiedMatch) -> u32 {
135        let id = self.matchers.len() as u32;
136        self.matchers.push(matcher);
137        id
138    }
139
140    fn extract_id(&mut self, source: ExtractSource) -> u32 {
141        let id = self.extracts.len() as u32;
142        self.extracts.push(source);
143        id
144    }
145
146    fn evidence_id(&mut self, kind: EvidenceKind) -> u32 {
147        let id = self.evidence.len() as u32;
148        self.evidence.push(kind);
149        id
150    }
151
152    fn emit(&mut self, instr: Instr) -> usize {
153        let pc = self.code.len();
154        self.code.push(instr);
155        pc
156    }
157
158    fn emit_program(&mut self, statements: &[Stmt]) {
159        for stmt in statements {
160            self.emit_stmt(stmt);
161        }
162    }
163
164    fn emit_stmt(&mut self, stmt: &Stmt) {
165        match stmt {
166            Stmt::Set { name, value } => {
167                let name = self.str_id(name);
168                match value {
169                    Value::String(value) => {
170                        let value = self.str_id(value);
171                        self.emit(Instr::Set { name, value });
172                    }
173                    Value::List(values) => {
174                        let (start, len) = self.string_span(values);
175                        self.emit(Instr::SetList { name, start, len });
176                    }
177                }
178            }
179            Stmt::Send { probe, payload } => {
180                let probe = self.str_id(probe);
181                let payload = payload.as_ref().map(|bytes| self.payload_id(bytes.clone()));
182                self.emit(Instr::Send { probe, payload });
183            }
184            Stmt::Match(matcher) => {
185                let id = self.matcher_id(matcher.clone());
186                self.emit(Instr::Match(id));
187            }
188            Stmt::MatchAll(matchers) => {
189                let start = self.matchers.len() as u32;
190                for matcher in matchers {
191                    self.matchers.push(matcher.clone());
192                }
193                let len = (self.matchers.len() as u32 - start) as u16;
194                self.emit(Instr::MatchAll { start, len });
195            }
196            Stmt::MatchAny(matchers) => {
197                let start = self.matchers.len() as u32;
198                for matcher in matchers {
199                    self.matchers.push(matcher.clone());
200                }
201                let len = (self.matchers.len() as u32 - start) as u16;
202                self.emit(Instr::MatchAny { start, len });
203            }
204            Stmt::Assert(matcher) => {
205                let id = self.matcher_id(matcher.clone());
206                self.emit(Instr::Assert(id));
207            }
208            Stmt::Extract { name, source } => {
209                let name = self.str_id(name);
210                let source = self.extract_id(source.clone());
211                self.emit(Instr::Extract { name, source });
212            }
213            Stmt::If { condition, body } => {
214                let matcher = self.matcher_id(condition.clone());
215                let if_pc = self.emit(Instr::IfMatch {
216                    matcher,
217                    else_pc: 0,
218                });
219                self.emit_program(body);
220                let else_pc = self.code.len() as u32;
221                self.code[if_pc] = Instr::IfMatch { matcher, else_pc };
222            }
223            Stmt::ForIn { item, list, body } => {
224                let item = self.str_id(item);
225                let enter = match list {
226                    ListSource::Literal(values) => {
227                        let (start, len) = self.string_span(values);
228                        Instr::ForList {
229                            item,
230                            start,
231                            len,
232                            end_pc: 0,
233                        }
234                    }
235                    ListSource::Variable(name) => {
236                        let list = self.str_id(name);
237                        Instr::ForVar {
238                            item,
239                            list,
240                            end_pc: 0,
241                        }
242                    }
243                };
244                let for_pc = self.emit(enter);
245                self.emit_program(body);
246                self.emit(Instr::LoopBack);
247                let end_pc = self.code.len() as u32;
248                self.code[for_pc] = match self.code[for_pc].clone() {
249                    Instr::ForList {
250                        item, start, len, ..
251                    } => Instr::ForList {
252                        item,
253                        start,
254                        len,
255                        end_pc,
256                    },
257                    Instr::ForVar { item, list, .. } => Instr::ForVar { item, list, end_pc },
258                    other => other,
259                };
260            }
261            Stmt::Break => {
262                self.emit(Instr::Break);
263            }
264            Stmt::Save { request, alias } => {
265                let from = self.str_id(request);
266                let to = self.str_id(alias);
267                self.emit(Instr::Save { from, to });
268            }
269            Stmt::Evidence(kind) => {
270                let id = self.evidence_id(kind.clone());
271                self.emit(Instr::Evidence(id));
272            }
273            Stmt::Retry { request, count } => {
274                let probe = self.str_id(request);
275                self.emit(Instr::Retry {
276                    probe,
277                    count: *count,
278                });
279            }
280            Stmt::RetryDelay(value) => {
281                let id = self.str_id(value);
282                self.emit(Instr::RetryDelay(id));
283            }
284            Stmt::Sleep(value) => {
285                let id = self.str_id(value);
286                self.emit(Instr::Sleep(id));
287            }
288            Stmt::Stop => {
289                self.emit(Instr::Stop);
290            }
291            Stmt::Fail => {
292                self.emit(Instr::Fail);
293            }
294            Stmt::Continue => {
295                self.emit(Instr::Continue);
296            }
297            Stmt::Exit => {
298                self.emit(Instr::Exit);
299            }
300            Stmt::Name(_)
301            | Stmt::Description(_)
302            | Stmt::Impact(_)
303            | Stmt::Severity(_)
304            | Stmt::Author(_)
305            | Stmt::Cve(_)
306            | Stmt::Cwe(_)
307            | Stmt::Reference(_)
308            | Stmt::Cvss(_)
309            | Stmt::CvssScore(_)
310            | Stmt::Mitigation(_)
311            | Stmt::Tag(_)
312            | Stmt::Version(_)
313            | Stmt::Family(_)
314            | Stmt::Http { .. }
315            | Stmt::Dns(_)
316            | Stmt::Tcp(_)
317            | Stmt::Udp(_) => {}
318        }
319    }
320}
321
322#[cfg(test)]
323mod tests {
324    use ruso_runtime::opcode::Opcode as Instr;
325
326    use crate::script::Program;
327    use crate::script::ast::{
328        CmpOp, CmpValue, FieldKind, MatchPredicate, QualifiedField, QualifiedMatch, Stmt,
329    };
330
331    use super::{CompileError, compile};
332
333    #[test]
334    fn compile_skips_metadata_and_probe_definitions() {
335        let program = Program {
336            statements: vec![
337                Stmt::Name("Check".into()),
338                Stmt::Http {
339                    name: "home".into(),
340                    items: vec![],
341                },
342                Stmt::Send {
343                    probe: "home".into(),
344                    payload: None,
345                },
346                Stmt::Match(QualifiedMatch {
347                    field: QualifiedField {
348                        target: "home".into(),
349                        kind: FieldKind::Status,
350                    },
351                    predicate: MatchPredicate::Compare {
352                        op: CmpOp::Eq,
353                        value: CmpValue::Number(200),
354                    },
355                }),
356            ],
357        };
358        let bytecode = compile(&program).unwrap();
359        assert_eq!(bytecode.code.len(), 2);
360        assert!(matches!(bytecode.code[0], Instr::Send { .. }));
361        assert!(matches!(bytecode.code[1], Instr::Match(_)));
362    }
363
364    #[test]
365    fn compile_rejects_match_without_finding_title() {
366        let program = Program {
367            statements: vec![
368                Stmt::Send {
369                    probe: "home".into(),
370                    payload: None,
371                },
372                Stmt::Match(QualifiedMatch {
373                    field: QualifiedField {
374                        target: "home".into(),
375                        kind: FieldKind::Status,
376                    },
377                    predicate: MatchPredicate::Compare {
378                        op: CmpOp::Eq,
379                        value: CmpValue::Number(200),
380                    },
381                }),
382            ],
383        };
384        assert!(matches!(
385            compile(&program),
386            Err(CompileError::MissingFindingTitle)
387        ));
388    }
389
390    #[test]
391    fn compile_rejects_duplicate_mitigation() {
392        let program = Program {
393            statements: vec![
394                Stmt::Name("Dup".into()),
395                Stmt::Mitigation("first".into()),
396                Stmt::Mitigation("second".into()),
397            ],
398        };
399        assert!(matches!(
400            compile(&program),
401            Err(CompileError::DuplicateMitigation)
402        ));
403    }
404
405    #[test]
406    fn compile_accepts_single_mitigation() {
407        let program = Program {
408            statements: vec![
409                Stmt::Name("Single".into()),
410                Stmt::Mitigation("patch it".into()),
411            ],
412        };
413        assert!(compile(&program).is_ok());
414    }
415}