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_exists(&mut self, _t: &str, _n: &str) -> Result<bool> {
98 Ok(true)
99 }
100 fn module_info(&mut self, _t: &str, _n: &str) -> Result<ModuleInfo> {
101 Ok(ModuleInfo::default())
102 }
103 fn run_module(
104 &mut self,
105 _t: &str,
106 _n: &str,
107 _o: &BTreeMap<String, String>,
108 _p: Option<&str>,
109 ) -> Result<ModuleRunResult> {
110 if self.run_succeeds {
111 Ok(ModuleRunResult {
112 job_id: Some(self.job_id.clone()),
113 success: true,
114 message: "started".to_string(),
115 raw: String::new(),
116 })
117 } else {
118 Ok(ModuleRunResult {
119 job_id: None,
120 success: false,
121 message: "boom".to_string(),
122 raw: String::new(),
123 })
124 }
125 }
126 fn monitor_job(&mut self, job_id: &str, _timeout: Duration) -> Result<JobStatus> {
127 Ok(JobStatus {
128 job_id: job_id.to_string(),
129 running: !self.job_finishes,
130 status: if self.job_finishes {
131 "completed".to_string()
132 } else {
133 "still_running".to_string()
134 },
135 error: None,
136 })
137 }
138 fn list_sessions(&mut self) -> Result<Vec<SessionInfo>> {
139 Ok(vec![SessionInfo {
140 id: "1".to_string(),
141 typ: "meterpreter".to_string(),
142 target_host: "10.0.0.5".to_string(),
143 platform: "linux".to_string(),
144 info: "test".to_string(),
145 }])
146 }
147 fn list_loot(&mut self) -> Result<Vec<LootInfo>> {
148 Ok(vec![LootInfo {
149 id: "loot-1".to_string(),
150 name: "shell".to_string(),
151 content_type: "text/plain".to_string(),
152 host: "10.0.0.5".to_string(),
153 path: "/tmp/loot".to_string(),
154 }])
155 }
156 }
157
158 fn sample_step() -> ExploitStep {
159 ExploitStep {
160 module_type: MODULE_EXPLOIT.to_string(),
161 module_name: "exploit/multi/http/apache_path_traversal".to_string(),
162 cve: Some("CVE-2021-41773".to_string()),
163 vulnerability: "Apache path traversal".to_string(),
164 rank: "excellent".to_string(),
165 asset_id: "asset-1".to_string(),
166 finding_id: "finding-1".to_string(),
167 host: "10.0.0.5".to_string(),
168 service_port: Some(80),
169 service_protocol: Some("tcp".to_string()),
170 inferred_options: BTreeMap::from([("RHOSTS".to_string(), "10.0.0.5".to_string())]),
171 missing_options: vec!["LHOST".to_string()],
172 payload_compatibility: "generic".to_string(),
173 confidence: "high".to_string(),
174 default_selected: true,
175 }
176 }
177
178 #[test]
179 fn execute_step_records_completed_outcome() {
180 let mut framework = MockFramework {
181 workspace: "default".to_string(),
182 run_succeeds: true,
183 job_id: "7".to_string(),
184 job_finishes: true,
185 };
186 let outcome =
187 execute_step(&mut framework, &sample_step(), None, Duration::from_secs(5)).unwrap();
188 assert_eq!(outcome.status, ExploitStatus::Completed);
189 assert_eq!(outcome.job_id.as_deref(), Some("7"));
190 assert_eq!(outcome.session_id.as_deref(), Some("1"));
191 assert!(outcome.loot.iter().any(|l| l == "loot-1"));
192 assert!(outcome.error.is_none());
193 }
194
195 #[test]
196 fn execute_step_marks_failure() {
197 let mut framework = MockFramework {
198 workspace: "default".to_string(),
199 run_succeeds: false,
200 job_id: String::new(),
201 job_finishes: true,
202 };
203 let outcome =
204 execute_step(&mut framework, &sample_step(), None, Duration::from_secs(5)).unwrap();
205 assert_eq!(outcome.status, ExploitStatus::Failed);
206 assert_eq!(outcome.error.as_deref(), Some("boom"));
207 }
208
209 #[test]
210 fn execution_status_roundtrips() {
211 for status in [
212 ExploitStatus::Planned,
213 ExploitStatus::Running,
214 ExploitStatus::Completed,
215 ExploitStatus::Failed,
216 ExploitStatus::Skipped,
217 ExploitStatus::DryRun,
218 ] {
219 assert_eq!(ExploitStatus::parse(status.as_str()), status);
220 }
221 }
222
223 #[test]
224 fn persist_execution_to_sqlite_roundtrips() {
225 let mut storage = crate::db::sqlite::SqliteStorage::open(":memory:").unwrap();
226 storage.ensure_schema().unwrap();
227
228 let mut exec =
229 ExploitExecution::new("run123", "asset-1", "finding-1", "exploit/multi/http/x");
230 exec.module_type = MODULE_EXPLOIT.to_string();
231 exec.host = "10.0.0.5".to_string();
232 exec.status = ExploitStatus::Completed.as_str().to_string();
233 exec.job_id = Some("9".to_string());
234 exec.session_id = Some("2".to_string());
235 exec.loot = vec!["loot-9".to_string()];
236 exec.selected = true;
237 storage.upsert_exploit_execution(&exec).unwrap();
238
239 let loaded = storage
240 .get_exploit_execution(&exec.execution_id)
241 .unwrap()
242 .unwrap();
243 assert_eq!(loaded.run_id, "run123");
244 assert_eq!(loaded.module, "exploit/multi/http/x");
245 assert_eq!(loaded.status, "completed");
246 assert_eq!(loaded.job_id.as_deref(), Some("9"));
247 assert_eq!(loaded.session_id.as_deref(), Some("2"));
248 assert_eq!(loaded.loot, vec!["loot-9".to_string()]);
249 assert!(loaded.selected);
250
251 let by_run = storage.list_exploit_executions_for_run("run123").unwrap();
252 assert_eq!(by_run.len(), 1);
253 let all = storage.list_exploit_executions().unwrap();
254 assert_eq!(all.len(), 1);
255 }
256}
257
258pub trait ExploitFramework: Send {
264 fn name(&self) -> &'static str;
265 fn workspace(&self) -> &str;
266 fn set_workspace(&mut self, workspace: &str);
267 fn connect(&mut self) -> Result<()>;
270 fn disconnect(&mut self);
271 fn is_connected(&self) -> bool;
272
273 fn module_exists(&mut self, module_type: &str, name: &str) -> Result<bool>;
275
276 fn module_info(&mut self, module_type: &str, name: &str) -> Result<ModuleInfo>;
278
279 fn run_module(
281 &mut self,
282 module_type: &str,
283 name: &str,
284 options: &BTreeMap<String, String>,
285 payload: Option<&str>,
286 ) -> Result<ModuleRunResult>;
287
288 fn monitor_job(&mut self, job_id: &str, timeout: Duration) -> Result<JobStatus>;
291
292 fn list_sessions(&mut self) -> Result<Vec<SessionInfo>>;
294
295 fn list_loot(&mut self) -> Result<Vec<LootInfo>>;
297}