pub mod cli;
pub mod models;
pub mod msf;
pub mod planner;
use std::collections::BTreeMap;
use std::time::Duration;
pub use models::*;
use crate::db::models::now_utc;
use crate::error::Result;
pub fn execute_step(
framework: &mut dyn ExploitFramework,
step: &ExploitStep,
payload: Option<&str>,
run_timeout: Duration,
) -> Result<ExploitOutcome> {
let start_time = now_utc();
let baseline_sessions: Vec<String> = framework
.list_sessions()
.unwrap_or_default()
.into_iter()
.map(|s| s.id)
.collect();
let baseline_loot: Vec<String> = framework
.list_loot()
.unwrap_or_default()
.into_iter()
.map(|l| l.id)
.collect();
let options = step.resolved_options();
let result = framework.run_module(&step.module_type, &step.module_name, &options, payload)?;
let mut session_id = None;
let mut status = ExploitStatus::Failed;
let mut error = None;
let job_id = result.job_id.clone();
if !result.success {
error = Some(result.message.clone());
} else if let Some(job) = &job_id {
let job_status = framework.monitor_job(job, run_timeout)?;
if job_status.error.is_some() {
error = job_status.error.clone();
} else if !job_status.running {
status = ExploitStatus::Completed;
} else {
status = ExploitStatus::Running;
}
} else {
status = ExploitStatus::Completed;
}
let after_sessions = framework.list_sessions().unwrap_or_default();
let after_loot = framework.list_loot().unwrap_or_default();
for session in &after_sessions {
if !baseline_sessions.contains(&session.id) {
session_id = Some(session.id.clone());
break;
}
}
let loot: Vec<String> = after_loot
.iter()
.filter(|l| !baseline_loot.contains(&l.id))
.map(|l| l.id.clone())
.collect();
Ok(ExploitOutcome {
step: step.clone(),
status,
job_id,
session_id,
error,
loot,
start_time,
finish_time: Some(now_utc()),
})
}
pub trait ExploitFramework: Send {
fn name(&self) -> &'static str;
fn workspace(&self) -> &str;
fn set_workspace(&mut self, workspace: &str);
fn connect(&mut self) -> Result<()>;
fn disconnect(&mut self);
fn is_connected(&self) -> bool;
fn module_info(&mut self, module_type: &str, name: &str) -> Result<ModuleInfo>;
fn run_module(
&mut self,
module_type: &str,
name: &str,
options: &BTreeMap<String, String>,
payload: Option<&str>,
) -> Result<ModuleRunResult>;
fn monitor_job(&mut self, job_id: &str, timeout: Duration) -> Result<JobStatus>;
fn list_sessions(&mut self) -> Result<Vec<SessionInfo>>;
fn list_loot(&mut self) -> Result<Vec<LootInfo>>;
}
#[cfg(test)]
mod tests {
use super::*;
use crate::db::models::ExploitExecution;
use crate::db::Storage;
struct MockFramework {
workspace: String,
run_succeeds: bool,
job_id: String,
job_finishes: bool,
preexisting_sessions: Vec<SessionInfo>,
preexisting_loot: Vec<LootInfo>,
new_sessions: Vec<SessionInfo>,
new_loot: Vec<LootInfo>,
ran: bool,
}
impl ExploitFramework for MockFramework {
fn name(&self) -> &'static str {
"mock"
}
fn workspace(&self) -> &str {
&self.workspace
}
fn set_workspace(&mut self, workspace: &str) {
self.workspace = workspace.to_string();
}
fn connect(&mut self) -> Result<()> {
Ok(())
}
fn disconnect(&mut self) {}
fn is_connected(&self) -> bool {
true
}
fn module_info(&mut self, _t: &str, _n: &str) -> Result<ModuleInfo> {
Ok(ModuleInfo::default())
}
fn run_module(
&mut self,
_t: &str,
_n: &str,
_o: &BTreeMap<String, String>,
_p: Option<&str>,
) -> Result<ModuleRunResult> {
self.ran = true;
if self.run_succeeds {
Ok(ModuleRunResult {
job_id: Some(self.job_id.clone()),
success: true,
message: "started".to_string(),
raw: String::new(),
})
} else {
Ok(ModuleRunResult {
job_id: None,
success: false,
message: "boom".to_string(),
raw: String::new(),
})
}
}
fn monitor_job(&mut self, job_id: &str, _timeout: Duration) -> Result<JobStatus> {
Ok(JobStatus {
job_id: job_id.to_string(),
running: !self.job_finishes,
status: if self.job_finishes {
"completed".to_string()
} else {
"still_running".to_string()
},
error: None,
})
}
fn list_sessions(&mut self) -> Result<Vec<SessionInfo>> {
let mut all = self.preexisting_sessions.clone();
if self.ran {
all.extend(self.new_sessions.clone());
}
Ok(all)
}
fn list_loot(&mut self) -> Result<Vec<LootInfo>> {
let mut all = self.preexisting_loot.clone();
if self.ran {
all.extend(self.new_loot.clone());
}
Ok(all)
}
}
fn sample_step() -> ExploitStep {
ExploitStep {
module_type: MODULE_EXPLOIT.to_string(),
module_name: "exploit/multi/http/apache_path_traversal".to_string(),
cve: Some("CVE-2021-41773".to_string()),
vulnerability: "Apache path traversal".to_string(),
rank: "excellent".to_string(),
asset_id: "asset-1".to_string(),
finding_id: "finding-1".to_string(),
host: "10.0.0.5".to_string(),
service_port: Some(80),
service_protocol: Some("tcp".to_string()),
inferred_options: BTreeMap::from([("RHOSTS".to_string(), "10.0.0.5".to_string())]),
missing_options: vec!["LHOST".to_string()],
payload_compatibility: "generic".to_string(),
confidence: "high".to_string(),
default_selected: true,
}
}
#[test]
fn execute_step_records_completed_outcome() {
let mut framework = MockFramework {
workspace: "default".to_string(),
run_succeeds: true,
job_id: "7".to_string(),
job_finishes: true,
preexisting_sessions: Vec::new(),
preexisting_loot: Vec::new(),
new_sessions: vec![SessionInfo {
id: "1".to_string(),
typ: "meterpreter".to_string(),
target_host: "10.0.0.5".to_string(),
platform: "linux".to_string(),
info: "test".to_string(),
}],
new_loot: vec![LootInfo {
id: "loot-1".to_string(),
name: "shell".to_string(),
content_type: "text/plain".to_string(),
host: "10.0.0.5".to_string(),
path: "/tmp/loot".to_string(),
}],
ran: false,
};
let outcome =
execute_step(&mut framework, &sample_step(), None, Duration::from_secs(5)).unwrap();
assert_eq!(outcome.status, ExploitStatus::Completed);
assert_eq!(outcome.job_id.as_deref(), Some("7"));
assert_eq!(outcome.session_id.as_deref(), Some("1"));
assert!(outcome.loot.iter().any(|l| l == "loot-1"));
assert!(outcome.error.is_none());
}
#[test]
fn execute_step_does_not_credit_preexisting_sessions() {
let mut framework = MockFramework {
workspace: "default".to_string(),
run_succeeds: true,
job_id: "7".to_string(),
job_finishes: true,
preexisting_sessions: vec![SessionInfo {
id: "old-1".to_string(),
typ: "shell".to_string(),
target_host: "10.0.0.9".to_string(),
platform: "linux".to_string(),
info: "pre-existing".to_string(),
}],
preexisting_loot: vec![LootInfo {
id: "loot-old".to_string(),
name: "shell".to_string(),
content_type: "text/plain".to_string(),
host: "10.0.0.9".to_string(),
path: "/tmp/loot-old".to_string(),
}],
new_sessions: vec![SessionInfo {
id: "new-2".to_string(),
typ: "meterpreter".to_string(),
target_host: "10.0.0.5".to_string(),
platform: "linux".to_string(),
info: "from this run".to_string(),
}],
new_loot: vec![LootInfo {
id: "loot-2".to_string(),
name: "shell".to_string(),
content_type: "text/plain".to_string(),
host: "10.0.0.5".to_string(),
path: "/tmp/loot-2".to_string(),
}],
ran: false,
};
let outcome =
execute_step(&mut framework, &sample_step(), None, Duration::from_secs(5)).unwrap();
assert_eq!(outcome.session_id.as_deref(), Some("new-2"));
assert!(!outcome.loot.iter().any(|l| l == "loot-old"));
assert_eq!(outcome.loot, vec!["loot-2".to_string()]);
}
#[test]
fn execute_step_marks_failure() {
let mut framework = MockFramework {
workspace: "default".to_string(),
run_succeeds: false,
job_id: String::new(),
job_finishes: true,
preexisting_sessions: Vec::new(),
preexisting_loot: Vec::new(),
new_sessions: Vec::new(),
new_loot: Vec::new(),
ran: false,
};
let outcome =
execute_step(&mut framework, &sample_step(), None, Duration::from_secs(5)).unwrap();
assert_eq!(outcome.status, ExploitStatus::Failed);
assert_eq!(outcome.error.as_deref(), Some("boom"));
}
#[test]
fn execution_status_roundtrips() {
for status in [
ExploitStatus::Planned,
ExploitStatus::Running,
ExploitStatus::Completed,
ExploitStatus::Failed,
ExploitStatus::Skipped,
ExploitStatus::DryRun,
] {
assert_eq!(ExploitStatus::parse(status.as_str()), status);
}
}
#[test]
fn persist_execution_to_sqlite_roundtrips() {
let mut storage = crate::db::sqlite::SqliteStorage::open(":memory:").unwrap();
storage.ensure_schema().unwrap();
let mut exec =
ExploitExecution::new("run123", "asset-1", "finding-1", "exploit/multi/http/x");
exec.module_type = MODULE_EXPLOIT.to_string();
exec.host = "10.0.0.5".to_string();
exec.status = ExploitStatus::Completed.as_str().to_string();
exec.job_id = Some("9".to_string());
exec.session_id = Some("2".to_string());
exec.loot = vec!["loot-9".to_string()];
exec.selected = true;
storage.upsert_exploit_execution(&exec).unwrap();
let loaded = storage
.get_exploit_execution(&exec.execution_id)
.unwrap()
.unwrap();
assert_eq!(loaded.run_id, "run123");
assert_eq!(loaded.module, "exploit/multi/http/x");
assert_eq!(loaded.status, "completed");
assert_eq!(loaded.job_id.as_deref(), Some("9"));
assert_eq!(loaded.session_id.as_deref(), Some("2"));
assert_eq!(loaded.loot, vec!["loot-9".to_string()]);
assert!(loaded.selected);
let by_run = storage.list_exploit_executions_for_run("run123").unwrap();
assert_eq!(by_run.len(), 1);
let all = storage.list_exploit_executions().unwrap();
assert_eq!(all.len(), 1);
}
}