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