Skip to main content

ruso_runtime/runtime/
context.rs

1use 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    /// When false, `stop` was hit — do not emit a finding even if matchers passed.
53    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}