use std::collections::BTreeMap;
use std::time::Duration;
use crate::engine::msfrpc::MsfRpcClient;
use crate::error::{AppError, Result};
use crate::msgpack::MsgValue;
use super::models::*;
use super::ExploitFramework;
pub struct MsfFramework {
client: Option<MsfRpcClient>,
host: String,
port: u16,
ssl: bool,
username: String,
password: String,
workspace: String,
}
impl MsfFramework {
pub fn new(host: &str, port: u16, username: &str, password: &str, ssl: bool) -> Self {
Self {
client: None,
host: host.to_string(),
port,
ssl,
username: username.to_string(),
password: password.to_string(),
workspace: "default".to_string(),
}
}
}
impl ExploitFramework for MsfFramework {
fn name(&self) -> &'static str {
"metasploit"
}
fn workspace(&self) -> &str {
&self.workspace
}
fn set_workspace(&mut self, workspace: &str) {
self.workspace = workspace.to_string();
}
fn connect(&mut self) -> Result<()> {
let mut client = MsfRpcClient::new(
&self.host,
self.port,
&self.username,
&self.password,
self.ssl,
);
let login = client
.login()
.map_err(|e| AppError::Network(format!("MSF-RPC login transport failure: {e}")))?;
if !login.success {
return Err(AppError::Message(format!(
"MSF-RPC authentication failed: {}",
login.error
)));
}
self.client = Some(client);
Ok(())
}
fn disconnect(&mut self) {
if let Some(client) = self.client.as_mut() {
client.logout();
}
self.client = None;
}
fn is_connected(&self) -> bool {
self.client
.as_ref()
.is_some_and(MsfRpcClient::is_authenticated)
}
fn module_exists(&mut self, module_type: &str, name: &str) -> Result<bool> {
let client = self.client()?;
client
.module_exists(module_type, name)
.map_err(|e| AppError::Network(e.to_string()))
}
fn module_info(&mut self, module_type: &str, name: &str) -> Result<ModuleInfo> {
let client = self.client()?;
let raw = client
.module_info(module_type, name)
.map_err(|e| AppError::Network(e.to_string()))?;
parse_module_info(module_type, name, &raw)
}
fn run_module(
&mut self,
module_type: &str,
name: &str,
options: &BTreeMap<String, String>,
payload: Option<&str>,
) -> Result<ModuleRunResult> {
let client = self.client()?;
let raw = client
.module_execute(module_type, name, options, payload)
.map_err(|e| AppError::Network(e.to_string()))?;
parse_run_result(&raw)
}
fn monitor_job(&mut self, job_id: &str, timeout: Duration) -> Result<JobStatus> {
let client = self.client()?;
let started = std::time::Instant::now();
loop {
match client.job_list() {
Ok(list) => {
let present = job_present(&list, job_id);
if !present {
return Ok(JobStatus {
job_id: job_id.to_string(),
running: false,
status: "completed".to_string(),
error: None,
});
}
}
Err(e) => {
return Ok(JobStatus {
job_id: job_id.to_string(),
running: false,
status: "failed".to_string(),
error: Some(e.to_string()),
});
}
}
if started.elapsed() >= timeout {
return Ok(JobStatus {
job_id: job_id.to_string(),
running: true,
status: "still_running".to_string(),
error: None,
});
}
std::thread::sleep(Duration::from_millis(1000));
}
}
fn list_sessions(&mut self) -> Result<Vec<SessionInfo>> {
let client = self.client()?;
let raw = client
.session_list()
.map_err(|e| AppError::Network(e.to_string()))?;
Ok(parse_sessions(&raw))
}
fn list_loot(&mut self) -> Result<Vec<LootInfo>> {
let workspace = self.workspace.clone();
let client = self.client()?;
let raw = client
.db_loots(&workspace)
.map_err(|e| AppError::Network(e.to_string()))?;
Ok(parse_loot(&raw))
}
}
impl MsfFramework {
fn client(&mut self) -> Result<&mut MsfRpcClient> {
self.client.as_mut().ok_or_else(|| {
AppError::Message("MSF-RPC not connected; call connect() first".to_string())
})
}
}
fn parse_module_info(module_type: &str, name: &str, raw: &MsgValue) -> Result<ModuleInfo> {
if raw.get("error").and_then(MsgValue::as_str) == Some("success") {
return Err(AppError::Message(format!(
"module not found on the connected instance: {module_type}/{name}"
)));
}
let rank = raw
.get("rank")
.and_then(MsgValue::as_str)
.unwrap_or("")
.to_string();
let description = raw
.get("description")
.and_then(MsgValue::as_str)
.unwrap_or("")
.to_string();
let mut platforms = parse_strings(raw, "platforms");
if platforms.is_empty() {
if let Some(single) = raw.get("platform").and_then(MsgValue::as_str) {
platforms.push(single.to_string());
}
}
let targets = parse_strings(raw, "targets");
let options = parse_options(raw);
let mut required_options = parse_strings(raw, "required_options");
if required_options.is_empty() {
required_options = options
.iter()
.filter(|opt| opt.required && opt.default.is_none())
.map(|opt| opt.name.clone())
.collect();
}
Ok(ModuleInfo {
name: name.to_string(),
module_type: module_type.to_string(),
rank,
description,
platforms,
targets,
options,
required_options,
})
}
fn parse_options(raw: &MsgValue) -> Vec<ModuleOption> {
let mut options = Vec::new();
if let Some(map) = raw.get("options").and_then(MsgValue::as_map) {
for (name, opt_val) in map {
let required = opt_val.get("required").is_some_and(|v| match v {
MsgValue::Bool(b) => *b,
MsgValue::String(s) => s == "true",
MsgValue::UInt(n) => *n != 0,
MsgValue::Int(n) => *n != 0,
_ => false,
});
let default = opt_val
.get("default")
.and_then(MsgValue::as_str)
.map(str::to_string);
let description = opt_val
.get("desc")
.and_then(MsgValue::as_str)
.unwrap_or("")
.to_string();
options.push(ModuleOption {
name: name.clone(),
required,
default,
description,
});
}
}
options
}
fn parse_run_result(raw: &MsgValue) -> Result<ModuleRunResult> {
let raw_text = format!("{raw:?}");
let error_flag = match raw.get("error") {
Some(MsgValue::Bool(b)) => *b,
Some(MsgValue::String(s)) => s == "true",
Some(MsgValue::UInt(n)) => *n != 0,
_ => false,
};
if error_flag {
let message = raw
.get("error_message")
.or_else(|| raw.get("error_string"))
.and_then(MsgValue::as_str)
.unwrap_or("module execution failed")
.to_string();
return Ok(ModuleRunResult {
job_id: None,
success: false,
message,
raw: raw_text,
});
}
let job_id = raw
.get("job_id")
.and_then(MsgValue::as_u64)
.map(|id| id.to_string());
let message = raw
.get("message")
.and_then(MsgValue::as_str)
.unwrap_or("module started")
.to_string();
Ok(ModuleRunResult {
job_id,
success: true,
message,
raw: raw_text,
})
}
fn job_present(list: &MsgValue, job_id: &str) -> bool {
if let Some(map) = list.get("jobs").and_then(MsgValue::as_map) {
map.keys().any(|id| id == job_id)
} else if let Some(map) = list.as_map() {
map.get("jobs").is_none() && map.keys().any(|id| id == job_id)
} else {
false
}
}
fn parse_sessions(raw: &MsgValue) -> Vec<SessionInfo> {
let mut sessions = Vec::new();
if let Some(map) = raw.get("sessions").and_then(MsgValue::as_map) {
for (id, val) in map {
let typ = val
.get("type")
.or_else(|| val.get("session_type"))
.and_then(MsgValue::as_str)
.unwrap_or("")
.to_string();
let target_host = val
.get("target_host")
.or_else(|| val.get("host"))
.and_then(MsgValue::as_str)
.unwrap_or("")
.to_string();
let platform = val
.get("platform")
.and_then(MsgValue::as_str)
.unwrap_or("")
.to_string();
let info = val
.get("info")
.and_then(MsgValue::as_str)
.unwrap_or("")
.to_string();
sessions.push(SessionInfo {
id: id.clone(),
typ,
target_host,
platform,
info,
});
}
}
sessions
}
fn parse_loot(raw: &MsgValue) -> Vec<LootInfo> {
let mut loot = Vec::new();
if let Some(array) = raw.get("loot").and_then(MsgValue::as_array) {
for val in array {
let id = val
.get("loot_id")
.or_else(|| val.get("id"))
.and_then(MsgValue::as_str)
.unwrap_or("")
.to_string();
let name = val
.get("name")
.and_then(MsgValue::as_str)
.unwrap_or("")
.to_string();
let content_type = val
.get("content_type")
.and_then(MsgValue::as_str)
.unwrap_or("")
.to_string();
let host = val
.get("host")
.and_then(MsgValue::as_str)
.unwrap_or("")
.to_string();
let path = val
.get("path")
.and_then(MsgValue::as_str)
.unwrap_or("")
.to_string();
loot.push(LootInfo {
id,
name,
content_type,
host,
path,
});
}
}
loot
}
fn parse_strings(raw: &MsgValue, key: &str) -> Vec<String> {
match raw.get(key) {
Some(MsgValue::Array(arr)) => arr
.iter()
.filter_map(MsgValue::as_str)
.map(str::to_string)
.collect(),
_ => Vec::new(),
}
}