1pub mod cli;
2pub mod models;
3pub mod msf;
4pub mod planner;
5
6use std::collections::BTreeMap;
7use std::time::Duration;
8
9pub use models::*;
10
11use crate::db::models::now_utc;
12use crate::error::Result;
13
14pub fn execute_step(
18 framework: &mut dyn ExploitFramework,
19 step: &ExploitStep,
20 payload: Option<&str>,
21 run_timeout: Duration,
22) -> Result<ExploitOutcome> {
23 let start_time = now_utc();
24 let options = step.resolved_options();
25 let result = framework.run_module(&step.module_type, &step.module_name, &options, payload)?;
26
27 let mut session_id = None;
28 let mut loot = Vec::new();
29 let mut status = ExploitStatus::Failed;
30 let mut error = None;
31 let job_id = result.job_id.clone();
32
33 if !result.success {
34 error = Some(result.message.clone());
35 } else if let Some(job) = &job_id {
36 let job_status = framework.monitor_job(job, run_timeout)?;
37 if job_status.error.is_some() {
38 error = job_status.error.clone();
39 } else if !job_status.running {
40 status = ExploitStatus::Completed;
41 if let Ok(sessions) = framework.list_sessions() {
42 session_id = sessions.first().map(|s| s.id.clone());
43 }
44 if let Ok(loot_items) = framework.list_loot() {
45 loot = loot_items.iter().map(|l| l.id.clone()).collect();
46 }
47 } else {
48 status = ExploitStatus::Running;
49 }
50 } else {
51 status = ExploitStatus::Completed;
52 }
53
54 Ok(ExploitOutcome {
55 step: step.clone(),
56 status,
57 job_id,
58 session_id,
59 error,
60 loot,
61 start_time,
62 finish_time: Some(now_utc()),
63 })
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69 use crate::db::models::ExploitExecution;
70 use crate::db::Storage;
71
72 struct MockFramework {
74 workspace: String,
75 run_succeeds: bool,
76 job_id: String,
77 job_finishes: bool,
78 }
79
80 impl ExploitFramework for MockFramework {
81 fn name(&self) -> &'static str {
82 "mock"
83 }
84 fn workspace(&self) -> &str {
85 &self.workspace
86 }
87 fn set_workspace(&mut self, workspace: &str) {
88 self.workspace = workspace.to_string();
89 }
90 fn connect(&mut self) -> Result<()> {
91 Ok(())
92 }
93 fn disconnect(&mut self) {}
94 fn is_connected(&self) -> bool {
95 true
96 }
97 fn module_info(&mut self, _t: &str, _n: &str) -> Result<ModuleInfo> {
98 Ok(ModuleInfo::default())
99 }
100 fn run_module(
101 &mut self,
102 _t: &str,
103 _n: &str,
104 _o: &BTreeMap<String, String>,
105 _p: Option<&str>,
106 ) -> Result<ModuleRunResult> {
107 if self.run_succeeds {
108 Ok(ModuleRunResult {
109 job_id: Some(self.job_id.clone()),
110 success: true,
111 message: "started".to_string(),
112 raw: String::new(),
113 })
114 } else {
115 Ok(ModuleRunResult {
116 job_id: None,
117 success: false,
118 message: "boom".to_string(),
119 raw: String::new(),
120 })
121 }
122 }
123 fn monitor_job(&mut self, job_id: &str, _timeout: Duration) -> Result<JobStatus> {
124 Ok(JobStatus {
125 job_id: job_id.to_string(),
126 running: !self.job_finishes,
127 status: if self.job_finishes {
128 "completed".to_string()
129 } else {
130 "still_running".to_string()
131 },
132 error: None,
133 })
134 }
135 fn list_sessions(&mut self) -> Result<Vec<SessionInfo>> {
136 Ok(vec![SessionInfo {
137 id: "1".to_string(),
138 typ: "meterpreter".to_string(),
139 target_host: "10.0.0.5".to_string(),
140 platform: "linux".to_string(),
141 info: "test".to_string(),
142 }])
143 }
144 fn list_loot(&mut self) -> Result<Vec<LootInfo>> {
145 Ok(vec![LootInfo {
146 id: "loot-1".to_string(),
147 name: "shell".to_string(),
148 content_type: "text/plain".to_string(),
149 host: "10.0.0.5".to_string(),
150 path: "/tmp/loot".to_string(),
151 }])
152 }
153 }
154
155 fn sample_step() -> ExploitStep {
156 ExploitStep {
157 module_type: MODULE_EXPLOIT.to_string(),
158 module_name: "exploit/multi/http/apache_path_traversal".to_string(),
159 cve: Some("CVE-2021-41773".to_string()),
160 vulnerability: "Apache path traversal".to_string(),
161 rank: "excellent".to_string(),
162 asset_id: "asset-1".to_string(),
163 finding_id: "finding-1".to_string(),
164 host: "10.0.0.5".to_string(),
165 service_port: Some(80),
166 service_protocol: Some("tcp".to_string()),
167 inferred_options: BTreeMap::from([("RHOSTS".to_string(), "10.0.0.5".to_string())]),
168 missing_options: vec!["LHOST".to_string()],
169 payload_compatibility: "generic".to_string(),
170 confidence: "high".to_string(),
171 default_selected: true,
172 }
173 }
174
175 #[test]
176 fn execute_step_records_completed_outcome() {
177 let mut framework = MockFramework {
178 workspace: "default".to_string(),
179 run_succeeds: true,
180 job_id: "7".to_string(),
181 job_finishes: true,
182 };
183 let outcome =
184 execute_step(&mut framework, &sample_step(), None, Duration::from_secs(5)).unwrap();
185 assert_eq!(outcome.status, ExploitStatus::Completed);
186 assert_eq!(outcome.job_id.as_deref(), Some("7"));
187 assert_eq!(outcome.session_id.as_deref(), Some("1"));
188 assert!(outcome.loot.iter().any(|l| l == "loot-1"));
189 assert!(outcome.error.is_none());
190 }
191
192 #[test]
193 fn execute_step_marks_failure() {
194 let mut framework = MockFramework {
195 workspace: "default".to_string(),
196 run_succeeds: false,
197 job_id: String::new(),
198 job_finishes: true,
199 };
200 let outcome =
201 execute_step(&mut framework, &sample_step(), None, Duration::from_secs(5)).unwrap();
202 assert_eq!(outcome.status, ExploitStatus::Failed);
203 assert_eq!(outcome.error.as_deref(), Some("boom"));
204 }
205
206 #[test]
207 fn execution_status_roundtrips() {
208 for status in [
209 ExploitStatus::Planned,
210 ExploitStatus::Running,
211 ExploitStatus::Completed,
212 ExploitStatus::Failed,
213 ExploitStatus::Skipped,
214 ExploitStatus::DryRun,
215 ] {
216 assert_eq!(ExploitStatus::parse(status.as_str()), status);
217 }
218 }
219
220 #[test]
221 fn persist_execution_to_sqlite_roundtrips() {
222 let mut storage = crate::db::sqlite::SqliteStorage::open(":memory:").unwrap();
223 storage.ensure_schema().unwrap();
224
225 let mut exec =
226 ExploitExecution::new("run123", "asset-1", "finding-1", "exploit/multi/http/x");
227 exec.module_type = MODULE_EXPLOIT.to_string();
228 exec.host = "10.0.0.5".to_string();
229 exec.status = ExploitStatus::Completed.as_str().to_string();
230 exec.job_id = Some("9".to_string());
231 exec.session_id = Some("2".to_string());
232 exec.loot = vec!["loot-9".to_string()];
233 exec.selected = true;
234 storage.upsert_exploit_execution(&exec).unwrap();
235
236 let loaded = storage
237 .get_exploit_execution(&exec.execution_id)
238 .unwrap()
239 .unwrap();
240 assert_eq!(loaded.run_id, "run123");
241 assert_eq!(loaded.module, "exploit/multi/http/x");
242 assert_eq!(loaded.status, "completed");
243 assert_eq!(loaded.job_id.as_deref(), Some("9"));
244 assert_eq!(loaded.session_id.as_deref(), Some("2"));
245 assert_eq!(loaded.loot, vec!["loot-9".to_string()]);
246 assert!(loaded.selected);
247
248 let by_run = storage.list_exploit_executions_for_run("run123").unwrap();
249 assert_eq!(by_run.len(), 1);
250 let all = storage.list_exploit_executions().unwrap();
251 assert_eq!(all.len(), 1);
252 }
253}
254
255pub trait ExploitFramework: Send {
261 fn name(&self) -> &'static str;
262 fn workspace(&self) -> &str;
263 fn set_workspace(&mut self, workspace: &str);
264 fn connect(&mut self) -> Result<()>;
267 fn disconnect(&mut self);
268 fn is_connected(&self) -> bool;
269
270 fn module_info(&mut self, module_type: &str, name: &str) -> Result<ModuleInfo>;
274
275 fn run_module(
277 &mut self,
278 module_type: &str,
279 name: &str,
280 options: &BTreeMap<String, String>,
281 payload: Option<&str>,
282 ) -> Result<ModuleRunResult>;
283
284 fn monitor_job(&mut self, job_id: &str, timeout: Duration) -> Result<JobStatus>;
287
288 fn list_sessions(&mut self) -> Result<Vec<SessionInfo>>;
290
291 fn list_loot(&mut self) -> Result<Vec<LootInfo>>;
293}