1use 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 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}