1use std::collections::BTreeMap;
2use std::time::Duration;
3
4use crate::engine::msfrpc::MsfRpcClient;
5use crate::error::{AppError, Result};
6use crate::msgpack::MsgValue;
7
8use super::models::*;
9use super::ExploitFramework;
10
11pub struct MsfFramework {
16 client: Option<MsfRpcClient>,
17 host: String,
18 port: u16,
19 ssl: bool,
20 username: String,
21 password: String,
22 workspace: String,
23}
24
25impl MsfFramework {
26 pub fn new(host: &str, port: u16, username: &str, password: &str, ssl: bool) -> Self {
27 Self {
28 client: None,
29 host: host.to_string(),
30 port,
31 ssl,
32 username: username.to_string(),
33 password: password.to_string(),
34 workspace: "default".to_string(),
35 }
36 }
37}
38
39impl ExploitFramework for MsfFramework {
40 fn name(&self) -> &'static str {
41 "metasploit"
42 }
43
44 fn workspace(&self) -> &str {
45 &self.workspace
46 }
47
48 fn set_workspace(&mut self, workspace: &str) {
49 self.workspace = workspace.to_string();
50 }
51
52 fn connect(&mut self) -> Result<()> {
53 let mut client = MsfRpcClient::new(
54 &self.host,
55 self.port,
56 &self.username,
57 &self.password,
58 self.ssl,
59 );
60 let login = client
61 .login()
62 .map_err(|e| AppError::Network(format!("MSF-RPC login transport failure: {e}")))?;
63 if !login.success {
64 return Err(AppError::Message(format!(
65 "MSF-RPC authentication failed: {}",
66 login.error
67 )));
68 }
69 self.client = Some(client);
70 Ok(())
71 }
72
73 fn disconnect(&mut self) {
74 if let Some(client) = self.client.as_mut() {
75 client.logout();
76 }
77 self.client = None;
78 }
79
80 fn is_connected(&self) -> bool {
81 self.client
82 .as_ref()
83 .is_some_and(MsfRpcClient::is_authenticated)
84 }
85
86 fn module_info(&mut self, module_type: &str, name: &str) -> Result<ModuleInfo> {
87 let client = self.client()?;
88 let raw = client.module_info(module_type, name)?;
89 let mut info = parse_module_info(module_type, name, &raw)?;
90 if let Ok(options_raw) = client.module_options(module_type, name) {
94 let options = parse_options(&options_raw);
95 info.options = options.clone();
96 let required: Vec<String> = options
97 .iter()
98 .filter(|opt| opt.required)
99 .map(|opt| opt.name.clone())
100 .collect();
101 if !required.is_empty() {
102 info.required_options = required;
103 }
104 }
105 Ok(info)
106 }
107
108 fn run_module(
109 &mut self,
110 module_type: &str,
111 name: &str,
112 options: &BTreeMap<String, String>,
113 payload: Option<&str>,
114 ) -> Result<ModuleRunResult> {
115 let client = self.client()?;
116 let raw = client.module_execute(module_type, name, options, payload)?;
117 parse_run_result(&raw)
118 }
119
120 fn monitor_job(&mut self, job_id: &str, timeout: Duration) -> Result<JobStatus> {
121 let client = self.client()?;
122 let started = std::time::Instant::now();
123 loop {
124 match client.job_list() {
125 Ok(list) => {
126 let present = job_present(&list, job_id);
127 if !present {
128 return Ok(JobStatus {
129 job_id: job_id.to_string(),
130 running: false,
131 status: "completed".to_string(),
132 error: None,
133 });
134 }
135 }
136 Err(e) => {
137 return Ok(JobStatus {
138 job_id: job_id.to_string(),
139 running: false,
140 status: "failed".to_string(),
141 error: Some(e.to_string()),
142 });
143 }
144 }
145 if started.elapsed() >= timeout {
146 return Ok(JobStatus {
147 job_id: job_id.to_string(),
148 running: true,
149 status: "still_running".to_string(),
150 error: None,
151 });
152 }
153 std::thread::sleep(Duration::from_millis(1000));
154 }
155 }
156
157 fn list_sessions(&mut self) -> Result<Vec<SessionInfo>> {
158 let client = self.client()?;
159 let raw = client
160 .session_list()
161 .map_err(|e| AppError::Network(e.to_string()))?;
162 Ok(parse_sessions(&raw))
163 }
164
165 fn list_loot(&mut self) -> Result<Vec<LootInfo>> {
166 let workspace = self.workspace.clone();
167 let client = self.client()?;
168 let raw = client
169 .db_loots(&workspace)
170 .map_err(|e| AppError::Network(e.to_string()))?;
171 Ok(parse_loot(&raw))
172 }
173}
174
175impl MsfFramework {
176 fn client(&mut self) -> Result<&mut MsfRpcClient> {
177 self.client.as_mut().ok_or_else(|| {
178 AppError::Message("MSF-RPC not connected; call connect() first".to_string())
179 })
180 }
181}
182
183fn parse_module_info(module_type: &str, name: &str, raw: &MsgValue) -> Result<ModuleInfo> {
184 let rank = raw
185 .get("rank")
186 .and_then(MsgValue::as_str)
187 .map(str::to_string)
188 .or_else(|| {
189 raw.get("rank").and_then(MsgValue::as_u64).map(rank_label)
192 })
193 .unwrap_or_default();
194 let description = raw
195 .get("description")
196 .and_then(MsgValue::as_str)
197 .unwrap_or("")
198 .to_string();
199
200 let mut platforms = parse_strings(raw, "platforms");
201 if platforms.is_empty() {
202 if let Some(single) = raw.get("platform").and_then(MsgValue::as_str) {
203 platforms.push(single.to_string());
204 }
205 }
206 let targets = parse_strings(raw, "targets");
207
208 let options = parse_options(raw);
209 let mut required_options = parse_strings(raw, "required_options");
210 if required_options.is_empty() {
211 required_options = options
212 .iter()
213 .filter(|opt| opt.required && opt.default.is_none())
214 .map(|opt| opt.name.clone())
215 .collect();
216 }
217
218 Ok(ModuleInfo {
219 name: name.to_string(),
220 module_type: module_type.to_string(),
221 rank,
222 description,
223 platforms,
224 targets,
225 options,
226 required_options,
227 })
228}
229
230fn rank_label(rank: u64) -> String {
232 match rank {
233 0 => "manual".to_string(),
234 100 => "low".to_string(),
235 200 => "average".to_string(),
236 300 => "normal".to_string(),
237 400 => "good".to_string(),
238 500 => "great".to_string(),
239 600 => "excellent".to_string(),
240 other => format!("rank-{other}"),
241 }
242}
243
244fn parse_options(raw: &MsgValue) -> Vec<ModuleOption> {
245 let mut options = Vec::new();
246 if let Some(map) = raw.get("options").and_then(MsgValue::as_map) {
247 for (name, opt_val) in map {
248 let required = opt_val.get("required").is_some_and(|v| match v {
249 MsgValue::Bool(b) => *b,
250 MsgValue::String(s) => s == "true",
251 MsgValue::UInt(n) => *n != 0,
252 MsgValue::Int(n) => *n != 0,
253 _ => false,
254 });
255 let default = opt_val
256 .get("default")
257 .and_then(MsgValue::as_str)
258 .map(str::to_string);
259 let description = opt_val
260 .get("desc")
261 .and_then(MsgValue::as_str)
262 .unwrap_or("")
263 .to_string();
264 options.push(ModuleOption {
265 name: name.clone(),
266 required,
267 default,
268 description,
269 });
270 }
271 }
272 options
273}
274
275fn parse_run_result(raw: &MsgValue) -> Result<ModuleRunResult> {
276 let raw_text = format!("{raw:?}");
277
278 let error_flag = match raw.get("error") {
279 Some(MsgValue::Bool(b)) => *b,
280 Some(MsgValue::String(s)) => s == "true",
281 Some(MsgValue::UInt(n)) => *n != 0,
282 _ => false,
283 };
284 if error_flag {
285 let message = raw
286 .get("error_message")
287 .or_else(|| raw.get("error_string"))
288 .and_then(MsgValue::as_str)
289 .unwrap_or("module execution failed")
290 .to_string();
291 return Ok(ModuleRunResult {
292 job_id: None,
293 success: false,
294 message,
295 raw: raw_text,
296 });
297 }
298
299 let job_id = raw
300 .get("job_id")
301 .and_then(MsgValue::as_u64)
302 .map(|id| id.to_string());
303 let message = raw
304 .get("message")
305 .and_then(MsgValue::as_str)
306 .unwrap_or("module started")
307 .to_string();
308 Ok(ModuleRunResult {
309 job_id,
310 success: true,
311 message,
312 raw: raw_text,
313 })
314}
315
316fn job_present(list: &MsgValue, job_id: &str) -> bool {
317 if let Some(map) = list.get("jobs").and_then(MsgValue::as_map) {
318 map.keys().any(|id| id == job_id)
319 } else if let Some(map) = list.as_map() {
320 map.get("jobs").is_none() && map.keys().any(|id| id == job_id)
321 } else {
322 false
323 }
324}
325
326fn parse_sessions(raw: &MsgValue) -> Vec<SessionInfo> {
327 let mut sessions = Vec::new();
328 if let Some(map) = raw.get("sessions").and_then(MsgValue::as_map) {
329 for (id, val) in map {
330 let typ = val
331 .get("type")
332 .or_else(|| val.get("session_type"))
333 .and_then(MsgValue::as_str)
334 .unwrap_or("")
335 .to_string();
336 let target_host = val
337 .get("target_host")
338 .or_else(|| val.get("host"))
339 .and_then(MsgValue::as_str)
340 .unwrap_or("")
341 .to_string();
342 let platform = val
343 .get("platform")
344 .and_then(MsgValue::as_str)
345 .unwrap_or("")
346 .to_string();
347 let info = val
348 .get("info")
349 .and_then(MsgValue::as_str)
350 .unwrap_or("")
351 .to_string();
352 sessions.push(SessionInfo {
353 id: id.clone(),
354 typ,
355 target_host,
356 platform,
357 info,
358 });
359 }
360 }
361 sessions
362}
363
364fn parse_loot(raw: &MsgValue) -> Vec<LootInfo> {
365 let mut loot = Vec::new();
366 if let Some(array) = raw.get("loot").and_then(MsgValue::as_array) {
367 for val in array {
368 let id = val
369 .get("loot_id")
370 .or_else(|| val.get("id"))
371 .and_then(MsgValue::as_str)
372 .unwrap_or("")
373 .to_string();
374 let name = val
375 .get("name")
376 .and_then(MsgValue::as_str)
377 .unwrap_or("")
378 .to_string();
379 let content_type = val
380 .get("content_type")
381 .and_then(MsgValue::as_str)
382 .unwrap_or("")
383 .to_string();
384 let host = val
385 .get("host")
386 .and_then(MsgValue::as_str)
387 .unwrap_or("")
388 .to_string();
389 let path = val
390 .get("path")
391 .and_then(MsgValue::as_str)
392 .unwrap_or("")
393 .to_string();
394 loot.push(LootInfo {
395 id,
396 name,
397 content_type,
398 host,
399 path,
400 });
401 }
402 }
403 loot
404}
405
406fn parse_strings(raw: &MsgValue, key: &str) -> Vec<String> {
407 match raw.get(key) {
408 Some(MsgValue::Array(arr)) => arr
409 .iter()
410 .filter_map(MsgValue::as_str)
411 .map(str::to_string)
412 .collect(),
413 Some(MsgValue::Map(map)) => {
417 let mut items: Vec<&String> = map.keys().collect();
418 items.sort();
419 items
420 .into_iter()
421 .filter_map(|k| map.get(k))
422 .filter_map(MsgValue::as_str)
423 .map(str::to_string)
424 .collect()
425 }
426 _ => Vec::new(),
427 }
428}
429
430#[cfg(test)]
431mod tests {
432 use super::*;
433 use std::collections::BTreeMap;
434
435 fn raw_map(pairs: &[(&str, MsgValue)]) -> MsgValue {
436 MsgValue::Map(
437 pairs
438 .iter()
439 .map(|(k, v)| (k.to_string(), v.clone()))
440 .collect(),
441 )
442 }
443
444 #[test]
445 fn parse_strings_reads_array_form() {
446 let raw = raw_map(&[(
447 "targets",
448 MsgValue::Array(vec![
449 MsgValue::String("Linux x86".to_string()),
450 MsgValue::String("Linux x86_64".to_string()),
451 ]),
452 )]);
453 assert_eq!(
454 parse_strings(&raw, "targets"),
455 vec!["Linux x86", "Linux x86_64"]
456 );
457 }
458
459 #[test]
460 fn parse_strings_reads_indexed_map_form() {
461 let mut targets = BTreeMap::new();
464 targets.insert("1".to_string(), MsgValue::String("Windows 7".to_string()));
465 targets.insert(
466 "0".to_string(),
467 MsgValue::String("Automatic Target".to_string()),
468 );
469 targets.insert("2".to_string(), MsgValue::String("Windows 8".to_string()));
470 let raw = raw_map(&[("targets", MsgValue::Map(targets))]);
471 assert_eq!(
472 parse_strings(&raw, "targets"),
473 vec!["Automatic Target", "Windows 7", "Windows 8"]
474 );
475 }
476
477 #[test]
478 fn parse_module_info_extracts_indexed_targets() {
479 let mut targets = BTreeMap::new();
482 targets.insert(
483 "0".to_string(),
484 MsgValue::String("Automatic Target".to_string()),
485 );
486 targets.insert("1".to_string(), MsgValue::String("Windows 7".to_string()));
487 let raw = raw_map(&[
488 ("name", MsgValue::String("eternalblue".to_string())),
489 ("description", MsgValue::String("MS17-010".to_string())),
490 ("targets", MsgValue::Map(targets)),
491 ("options", MsgValue::Map(BTreeMap::new())),
492 ]);
493 let info = parse_module_info("exploit", "windows/smb/ms17_010_eternalblue", &raw).unwrap();
494 assert_eq!(info.targets, vec!["Automatic Target", "Windows 7"]);
495 }
496}