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 options = step.resolved_options();
let result = framework.run_module(&step.module_type, &step.module_name, &options, payload)?;
let mut session_id = None;
let mut loot = Vec::new();
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;
if let Ok(sessions) = framework.list_sessions() {
session_id = sessions.first().map(|s| s.id.clone());
}
if let Ok(loot_items) = framework.list_loot() {
loot = loot_items.iter().map(|l| l.id.clone()).collect();
}
} else {
status = ExploitStatus::Running;
}
} else {
status = ExploitStatus::Completed;
}
Ok(ExploitOutcome {
step: step.clone(),
status,
job_id,
session_id,
error,
loot,
start_time,
finish_time: Some(now_utc()),
})
}
#[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,
}
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_exists(&mut self, _t: &str, _n: &str) -> Result<bool> {
Ok(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> {
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>> {
Ok(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(),
}])
}
fn list_loot(&mut self) -> Result<Vec<LootInfo>> {
Ok(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(),
}])
}
}
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,
};
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_marks_failure() {
let mut framework = MockFramework {
workspace: "default".to_string(),
run_succeeds: false,
job_id: String::new(),
job_finishes: true,
};
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);
}
}
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_exists(&mut self, module_type: &str, name: &str) -> Result<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>>;
}