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 info.required_options = options
101 .iter()
102 .filter(|opt| opt.required && opt.default.is_none())
103 .map(|opt| opt.name.clone())
104 .collect();
105 }
106 Ok(info)
107 }
108
109 fn run_module(
110 &mut self,
111 module_type: &str,
112 name: &str,
113 options: &BTreeMap<String, String>,
114 payload: Option<&str>,
115 ) -> Result<ModuleRunResult> {
116 let client = self.client()?;
117 let raw = client.module_execute(module_type, name, options, payload)?;
118 parse_run_result(&raw)
119 }
120
121 fn monitor_job(&mut self, job_id: &str, timeout: Duration) -> Result<JobStatus> {
122 let client = self.client()?;
123 let started = std::time::Instant::now();
124 loop {
125 match client.job_list() {
126 Ok(list) => {
127 let present = job_present(&list, job_id);
128 if !present {
129 return Ok(JobStatus {
130 job_id: job_id.to_string(),
131 running: false,
132 status: "completed".to_string(),
133 error: None,
134 });
135 }
136 }
137 Err(e) => {
138 return Ok(JobStatus {
139 job_id: job_id.to_string(),
140 running: false,
141 status: "failed".to_string(),
142 error: Some(e.to_string()),
143 });
144 }
145 }
146 if started.elapsed() >= timeout {
147 return Ok(JobStatus {
148 job_id: job_id.to_string(),
149 running: true,
150 status: "still_running".to_string(),
151 error: None,
152 });
153 }
154 std::thread::sleep(Duration::from_millis(1000));
155 }
156 }
157
158 fn list_sessions(&mut self) -> Result<Vec<SessionInfo>> {
159 let client = self.client()?;
160 let raw = client
161 .session_list()
162 .map_err(|e| AppError::Network(e.to_string()))?;
163 Ok(parse_sessions(&raw))
164 }
165
166 fn list_loot(&mut self) -> Result<Vec<LootInfo>> {
167 let workspace = self.workspace.clone();
168 let client = self.client()?;
169 let raw = client
170 .db_loots(&workspace)
171 .map_err(|e| AppError::Network(e.to_string()))?;
172 Ok(parse_loot(&raw))
173 }
174}
175
176impl MsfFramework {
177 fn client(&mut self) -> Result<&mut MsfRpcClient> {
178 self.client.as_mut().ok_or_else(|| {
179 AppError::Message("MSF-RPC not connected; call connect() first".to_string())
180 })
181 }
182}
183
184fn parse_module_info(module_type: &str, name: &str, raw: &MsgValue) -> Result<ModuleInfo> {
185 let rank = raw
186 .get("rank")
187 .and_then(MsgValue::as_str)
188 .map(str::to_string)
189 .or_else(|| {
190 raw.get("rank").and_then(MsgValue::as_u64).map(rank_label)
193 })
194 .unwrap_or_default();
195 let description = raw
196 .get("description")
197 .and_then(MsgValue::as_str)
198 .unwrap_or("")
199 .to_string();
200
201 let mut platforms = parse_strings(raw, "platforms");
202 if platforms.is_empty() {
203 if let Some(single) = raw.get("platform").and_then(MsgValue::as_str) {
204 platforms.push(single.to_string());
205 }
206 }
207 let targets = parse_strings(raw, "targets");
208
209 let options = parse_options(raw);
210 let required_options = options
215 .iter()
216 .filter(|opt| opt.required && opt.default.is_none())
217 .map(|opt| opt.name.clone())
218 .collect();
219
220 Ok(ModuleInfo {
221 name: name.to_string(),
222 module_type: module_type.to_string(),
223 rank,
224 description,
225 platforms,
226 targets,
227 options,
228 required_options,
229 })
230}
231
232fn rank_label(rank: u64) -> String {
234 match rank {
235 0 => "manual".to_string(),
236 100 => "low".to_string(),
237 200 => "average".to_string(),
238 300 => "normal".to_string(),
239 400 => "good".to_string(),
240 500 => "great".to_string(),
241 600 => "excellent".to_string(),
242 other => format!("rank-{other}"),
243 }
244}
245
246fn parse_options(raw: &MsgValue) -> Vec<ModuleOption> {
247 let mut options = Vec::new();
248 if let Some(map) = raw.get("options").and_then(MsgValue::as_map) {
249 for (name, opt_val) in map {
250 let required = opt_val.get("required").is_some_and(|v| match v {
251 MsgValue::Bool(b) => *b,
252 MsgValue::String(s) => s == "true",
253 MsgValue::UInt(n) => *n != 0,
254 MsgValue::Int(n) => *n != 0,
255 _ => false,
256 });
257 let default = opt_val.get("default").and_then(parse_default_value);
258 let description = opt_val
259 .get("desc")
260 .and_then(MsgValue::as_str)
261 .unwrap_or("")
262 .to_string();
263 options.push(ModuleOption {
264 name: name.clone(),
265 required,
266 default,
267 description,
268 });
269 }
270 }
271 options
272}
273
274fn parse_default_value(value: &MsgValue) -> Option<String> {
279 match value {
280 MsgValue::Bool(b) => Some(b.to_string()),
281 MsgValue::Int(n) => Some(n.to_string()),
282 MsgValue::UInt(n) => Some(n.to_string()),
283 MsgValue::Float(f) => Some(format_float(f)),
284 MsgValue::String(s) => Some(s.clone()),
285 _ => None,
286 }
287}
288
289fn format_float(value: &f64) -> String {
293 value.to_string()
294}
295
296fn parse_run_result(raw: &MsgValue) -> Result<ModuleRunResult> {
297 let raw_text = format!("{raw:?}");
298
299 let error_flag = match raw.get("error") {
300 Some(MsgValue::Bool(b)) => *b,
301 Some(MsgValue::String(s)) => s == "true",
302 Some(MsgValue::UInt(n)) => *n != 0,
303 _ => false,
304 };
305 if error_flag {
306 let message = raw
307 .get("error_message")
308 .or_else(|| raw.get("error_string"))
309 .and_then(MsgValue::as_str)
310 .unwrap_or("module execution failed")
311 .to_string();
312 return Ok(ModuleRunResult {
313 job_id: None,
314 success: false,
315 message,
316 raw: raw_text,
317 });
318 }
319
320 let job_id = raw
321 .get("job_id")
322 .and_then(MsgValue::as_u64)
323 .map(|id| id.to_string());
324 let message = raw
325 .get("message")
326 .and_then(MsgValue::as_str)
327 .unwrap_or("module started")
328 .to_string();
329 Ok(ModuleRunResult {
330 job_id,
331 success: true,
332 message,
333 raw: raw_text,
334 })
335}
336
337fn job_present(list: &MsgValue, job_id: &str) -> bool {
338 if let Some(map) = list.get("jobs").and_then(MsgValue::as_map) {
339 map.keys().any(|id| id == job_id)
340 } else if let Some(map) = list.as_map() {
341 map.get("jobs").is_none() && map.keys().any(|id| id == job_id)
342 } else {
343 false
344 }
345}
346
347fn parse_sessions(raw: &MsgValue) -> Vec<SessionInfo> {
348 let mut sessions = Vec::new();
349 if let Some(map) = raw.get("sessions").and_then(MsgValue::as_map) {
350 for (id, val) in map {
351 let typ = val
352 .get("type")
353 .or_else(|| val.get("session_type"))
354 .and_then(MsgValue::as_str)
355 .unwrap_or("")
356 .to_string();
357 let target_host = val
358 .get("target_host")
359 .or_else(|| val.get("host"))
360 .and_then(MsgValue::as_str)
361 .unwrap_or("")
362 .to_string();
363 let platform = val
364 .get("platform")
365 .and_then(MsgValue::as_str)
366 .unwrap_or("")
367 .to_string();
368 let info = val
369 .get("info")
370 .and_then(MsgValue::as_str)
371 .unwrap_or("")
372 .to_string();
373 sessions.push(SessionInfo {
374 id: id.clone(),
375 typ,
376 target_host,
377 platform,
378 info,
379 });
380 }
381 }
382 sessions
383}
384
385fn parse_loot(raw: &MsgValue) -> Vec<LootInfo> {
386 let mut loot = Vec::new();
387 if let Some(array) = raw.get("loot").and_then(MsgValue::as_array) {
388 for val in array {
389 let id = val
390 .get("loot_id")
391 .or_else(|| val.get("id"))
392 .and_then(MsgValue::as_str)
393 .unwrap_or("")
394 .to_string();
395 let name = val
396 .get("name")
397 .and_then(MsgValue::as_str)
398 .unwrap_or("")
399 .to_string();
400 let content_type = val
401 .get("content_type")
402 .and_then(MsgValue::as_str)
403 .unwrap_or("")
404 .to_string();
405 let host = val
406 .get("host")
407 .and_then(MsgValue::as_str)
408 .unwrap_or("")
409 .to_string();
410 let path = val
411 .get("path")
412 .and_then(MsgValue::as_str)
413 .unwrap_or("")
414 .to_string();
415 loot.push(LootInfo {
416 id,
417 name,
418 content_type,
419 host,
420 path,
421 });
422 }
423 }
424 loot
425}
426
427fn parse_strings(raw: &MsgValue, key: &str) -> Vec<String> {
428 match raw.get(key) {
429 Some(MsgValue::Array(arr)) => arr
430 .iter()
431 .filter_map(MsgValue::as_str)
432 .map(str::to_string)
433 .collect(),
434 Some(MsgValue::Map(map)) => {
438 let mut items: Vec<&String> = map.keys().collect();
439 items.sort();
440 items
441 .into_iter()
442 .filter_map(|k| map.get(k))
443 .filter_map(MsgValue::as_str)
444 .map(str::to_string)
445 .collect()
446 }
447 _ => Vec::new(),
448 }
449}
450
451#[cfg(test)]
452mod tests {
453 use super::*;
454 use std::collections::BTreeMap;
455
456 fn raw_map(pairs: &[(&str, MsgValue)]) -> MsgValue {
457 MsgValue::Map(
458 pairs
459 .iter()
460 .map(|(k, v)| (k.to_string(), v.clone()))
461 .collect(),
462 )
463 }
464
465 #[test]
466 fn parse_strings_reads_array_form() {
467 let raw = raw_map(&[(
468 "targets",
469 MsgValue::Array(vec![
470 MsgValue::String("Linux x86".to_string()),
471 MsgValue::String("Linux x86_64".to_string()),
472 ]),
473 )]);
474 assert_eq!(
475 parse_strings(&raw, "targets"),
476 vec!["Linux x86", "Linux x86_64"]
477 );
478 }
479
480 #[test]
481 fn parse_strings_reads_indexed_map_form() {
482 let mut targets = BTreeMap::new();
485 targets.insert("1".to_string(), MsgValue::String("Windows 7".to_string()));
486 targets.insert(
487 "0".to_string(),
488 MsgValue::String("Automatic Target".to_string()),
489 );
490 targets.insert("2".to_string(), MsgValue::String("Windows 8".to_string()));
491 let raw = raw_map(&[("targets", MsgValue::Map(targets))]);
492 assert_eq!(
493 parse_strings(&raw, "targets"),
494 vec!["Automatic Target", "Windows 7", "Windows 8"]
495 );
496 }
497
498 #[test]
499 fn parse_module_info_extracts_indexed_targets() {
500 let mut targets = BTreeMap::new();
503 targets.insert(
504 "0".to_string(),
505 MsgValue::String("Automatic Target".to_string()),
506 );
507 targets.insert("1".to_string(), MsgValue::String("Windows 7".to_string()));
508 let raw = raw_map(&[
509 ("name", MsgValue::String("eternalblue".to_string())),
510 ("description", MsgValue::String("MS17-010".to_string())),
511 ("targets", MsgValue::Map(targets)),
512 ("options", MsgValue::Map(BTreeMap::new())),
513 ]);
514 let info = parse_module_info("exploit", "windows/smb/ms17_010_eternalblue", &raw).unwrap();
515 assert_eq!(info.targets, vec!["Automatic Target", "Windows 7"]);
516 }
517
518 #[test]
519 fn parse_default_value_stringifies_all_scalars() {
520 assert_eq!(
521 parse_default_value(&MsgValue::Bool(true)),
522 Some("true".to_string())
523 );
524 assert_eq!(
525 parse_default_value(&MsgValue::Bool(false)),
526 Some("false".to_string())
527 );
528 assert_eq!(
529 parse_default_value(&MsgValue::Int(-3)),
530 Some("-3".to_string())
531 );
532 assert_eq!(
533 parse_default_value(&MsgValue::UInt(2)),
534 Some("2".to_string())
535 );
536 assert_eq!(
537 parse_default_value(&MsgValue::Float(2.0)),
538 Some("2".to_string())
539 );
540 assert_eq!(
541 parse_default_value(&MsgValue::Float(2.5)),
542 Some("2.5".to_string())
543 );
544 assert_eq!(
545 parse_default_value(&MsgValue::String("eth0".to_string())),
546 Some("eth0".to_string())
547 );
548 assert_eq!(parse_default_value(&MsgValue::Nil), None);
549 assert_eq!(parse_default_value(&MsgValue::Map(BTreeMap::new())), None);
550 }
551
552 #[test]
553 fn required_with_default_is_not_in_required_options() {
554 let mut options = BTreeMap::new();
558 options.insert(
559 "ShowProgress".to_string(),
560 MsgValue::Map(BTreeMap::from([
561 ("required".to_string(), MsgValue::Bool(true)),
562 ("default".to_string(), MsgValue::Bool(false)),
563 (
564 "desc".to_string(),
565 MsgValue::String("show progress".to_string()),
566 ),
567 ])),
568 );
569 options.insert(
570 "THREADS".to_string(),
571 MsgValue::Map(BTreeMap::from([
572 ("required".to_string(), MsgValue::Bool(true)),
573 ("default".to_string(), MsgValue::UInt(2)),
574 ("desc".to_string(), MsgValue::String("threads".to_string())),
575 ])),
576 );
577 options.insert(
578 "RPORT".to_string(),
579 MsgValue::Map(BTreeMap::from([
580 ("required".to_string(), MsgValue::Bool(true)),
581 ("desc".to_string(), MsgValue::String("port".to_string())),
582 ])),
583 );
584
585 let raw = raw_map(&[
586 ("description", MsgValue::String("robots".to_string())),
587 ("options", MsgValue::Map(options)),
588 ]);
589 let info = parse_module_info("auxiliary", "scanner/http/robots_txt", &raw).unwrap();
590
591 assert!(info.required_options.contains(&"RPORT".to_string()));
594 assert!(!info.required_options.contains(&"ShowProgress".to_string()));
595 assert!(!info.required_options.contains(&"THREADS".to_string()));
596
597 let progress = info
599 .options
600 .iter()
601 .find(|o| o.name == "ShowProgress")
602 .unwrap();
603 assert_eq!(progress.default.as_deref(), Some("false"));
604 let threads = info.options.iter().find(|o| o.name == "THREADS").unwrap();
605 assert_eq!(threads.default.as_deref(), Some("2"));
606 let rport = info.options.iter().find(|o| o.name == "RPORT").unwrap();
607 assert_eq!(rport.default, None);
608 }
609}