ruso_runtime/runtime/
context.rs1use std::collections::HashMap;
2
3use crate::runtime::report::{Finding, Report};
4use crate::runtime::response::ProbeResponse;
5use crate::runtime::session::ProbeSession;
6use crate::runtime::spec::{CheckMetadata, ProgramSpec};
7
8#[derive(Debug, Clone, PartialEq)]
9pub enum VariableValue {
10 String(String),
11 List(Vec<String>),
12}
13
14impl VariableValue {
15 pub fn as_string(&self) -> Option<&str> {
16 match self {
17 Self::String(value) => Some(value),
18 Self::List(_) => None,
19 }
20 }
21}
22
23#[derive(Debug)]
24pub enum LoopState {
25 ForEach {
26 item: String,
27 values: Vec<String>,
28 index: usize,
29 previous: Option<VariableValue>,
30 },
31}
32
33#[derive(Debug)]
34pub struct LoopFrame {
35 pub state: LoopState,
36 pub head_pc: usize,
37 pub continue_pc: usize,
38 pub end_pc: usize,
39}
40
41#[derive(Debug)]
42pub struct Context {
43 pub variables: HashMap<String, VariableValue>,
44 pub responses: HashMap<String, ProbeResponse>,
45 pub sessions: HashMap<String, ProbeSession>,
46 pub metadata: CheckMetadata,
47 pub report: Report,
48 pub evidence: Vec<String>,
49 pub retry_delay: Option<std::time::Duration>,
50 pub failed: bool,
51 pub matched: bool,
52 pub emit_finding: bool,
54 pub loop_stack: Vec<LoopFrame>,
55}
56
57impl Default for Context {
58 fn default() -> Self {
59 Self {
60 variables: HashMap::new(),
61 responses: HashMap::new(),
62 sessions: HashMap::new(),
63 metadata: CheckMetadata::default(),
64 report: Report::default(),
65 evidence: Vec::new(),
66 retry_delay: None,
67 failed: false,
68 matched: true,
69 emit_finding: true,
70 loop_stack: Vec::new(),
71 }
72 }
73}
74
75impl Context {
76 pub fn from_spec(spec: &ProgramSpec) -> Self {
77 Self {
78 metadata: spec.metadata.clone(),
79 ..Default::default()
80 }
81 }
82
83 pub fn set_variable(&mut self, name: impl Into<String>, value: impl Into<String>) {
84 self.variables
85 .insert(name.into(), VariableValue::String(value.into()));
86 }
87
88 pub fn set_list_variable(&mut self, name: impl Into<String>, values: Vec<String>) {
89 self.variables
90 .insert(name.into(), VariableValue::List(values));
91 }
92
93 pub fn restore_or_remove_variable(
94 &mut self,
95 name: impl Into<String>,
96 value: Option<VariableValue>,
97 ) {
98 let name = name.into();
99 match value {
100 Some(value) => {
101 self.variables.insert(name, value);
102 }
103 None => {
104 self.variables.remove(&name);
105 }
106 }
107 }
108
109 pub fn response(&self, name: &str) -> Option<&ProbeResponse> {
110 self.responses.get(name)
111 }
112
113 pub fn store_response(&mut self, name: impl Into<String>, response: ProbeResponse) {
114 self.responses.insert(name.into(), response);
115 }
116
117 pub fn alias_response(&mut self, from: &str, alias: impl Into<String>) {
118 if let Some(response) = self.responses.get(from).cloned() {
119 self.responses.insert(alias.into(), response);
120 }
121 }
122
123 pub fn close_sessions(&mut self) {
124 self.sessions.clear();
125 }
126
127 pub fn finalize_finding(&mut self) {
128 if !self.matched || !self.emit_finding {
129 return;
130 }
131 let evidence = std::mem::take(&mut self.evidence);
132 if let Some(finding) = Finding::from_metadata(&self.metadata, evidence) {
133 self.report.set_finding(finding);
134 }
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use crate::contract::Severity;
141
142 use super::Context;
143
144 #[test]
145 fn finalize_emits_finding_when_matched() {
146 let mut ctx = Context {
147 matched: true,
148 ..Context::default()
149 };
150 ctx.metadata.name = Some("Exposed .env".into());
151 ctx.metadata.severity = Some(Severity::High);
152 ctx.evidence.push("DB_PASSWORD=secret".into());
153 ctx.finalize_finding();
154 assert_eq!(ctx.report.findings.len(), 1);
155 assert_eq!(ctx.report.findings[0].name, "Exposed .env");
156 assert_eq!(ctx.report.findings[0].severity, Severity::High);
157 }
158
159 #[test]
160 fn finalize_skips_when_match_chain_failed() {
161 let mut ctx = Context {
162 matched: false,
163 ..Context::default()
164 };
165 ctx.metadata.name = Some("Should not emit".into());
166 ctx.finalize_finding();
167 assert!(ctx.report.findings.is_empty());
168 }
169
170 #[test]
171 fn finalize_skips_after_stop() {
172 let mut ctx = Context {
173 matched: true,
174 emit_finding: false,
175 ..Context::default()
176 };
177 ctx.metadata.name = Some("Stopped".into());
178 ctx.finalize_finding();
179 assert!(ctx.report.findings.is_empty());
180 }
181}