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(
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 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}