use std::env;
use std::io::{self, Write};
use std::path::PathBuf;
use crate::config::ConfigManager;
use crate::db::Storage;
use crate::engine::msfrpc;
use crate::error::{AppError, Result};
use crate::models::PayloadRecipe;
use crate::recipe;
use crate::report;
pub fn main() {
if let Err(err) = run() {
eprintln!("[!] {}", err);
std::process::exit(1);
}
}
pub fn run() -> Result<()> {
let mut config_mgr = ConfigManager::new();
let config_loaded = config_mgr.load();
let args = env::args().skip(1).collect::<Vec<_>>();
if args.is_empty() {
print_banner();
return Ok(());
}
match args[0].as_str() {
"process" => {
cmd_process(&args[1..], &mut config_mgr, config_loaded)?;
}
"reset-config" => {
cmd_reset_config(&mut config_mgr)?;
}
"setup" => {
cmd_setup(&mut config_mgr)?;
}
"db_setup" => {
cmd_db_setup(&mut config_mgr)?;
}
"ingest" => {
cmd_ingest(&args[1..], &mut config_mgr)?;
}
"assets" => {
crate::frontend::assets::cmd_assets(&args[1..], &mut config_mgr)?;
}
"services" => {
crate::frontend::services::cmd_services(&args[1..], &mut config_mgr)?;
}
"findings" => {
crate::frontend::findings::cmd_findings(&args[1..], &mut config_mgr)?;
}
"history" => {
crate::frontend::history::cmd_history(&args[1..], &mut config_mgr)?;
}
"runs" => {
crate::frontend::runs::cmd_runs(&args[1..], &mut config_mgr)?;
}
"diff" => {
crate::frontend::diff::cmd_diff(&args[1..], &mut config_mgr)?;
}
"del" => {
cmd_del(&args[1..], &mut config_mgr)?;
}
"exploit" => {
crate::exploit::cli::cmd_exploit(&args[1..], &mut config_mgr)?;
}
"-h" | "--help" => {
print_banner();
}
other => {
eprintln!("[!] Unknown command: {}", other);
print_banner();
}
}
Ok(())
}
fn cmd_setup(config_mgr: &mut ConfigManager) -> Result<()> {
println!("PloitMalper Configuration Setup");
println!();
println!("--- MSF-RPC Configuration ---");
println!("Enter your Metasploit RPC server details.");
let current = config_mgr.get_msfrpc_config().clone();
let host = prompt_text("MSF-RPC Host", ¤t.host)?;
let port = prompt_u16("MSF-RPC Port", current.port)?;
let username = prompt_text("MSF-RPC Username", ¤t.username)?;
let password = prompt_text("MSF-RPC Password", ¤t.password)?;
let ssl = prompt_bool("Use SSL?", current.ssl)?;
let workspace = prompt_text("Workspace", ¤t.workspace)?;
config_mgr.config.msfrpc.host = host;
config_mgr.config.msfrpc.port = port;
config_mgr.config.msfrpc.username = username;
config_mgr.config.msfrpc.password = password;
config_mgr.config.msfrpc.ssl = ssl;
config_mgr.config.msfrpc.workspace = workspace;
println!();
println!("--- NVD API Configuration ---");
println!("Enter your NVD API key for CVE enrichment (optional but recommended).");
println!("Get one at: https://nvd.nist.gov/developers/request-an-api-key");
let current_nvd = config_mgr.get_nvd_config().clone();
let nvd_key = prompt_text("NVD API Key", ¤t_nvd.api_key)?;
config_mgr.config.nvd.api_key = nvd_key;
config_mgr.save()?;
println!();
println!(
"[+] Configuration saved to {}",
crate::config::config_file().display()
);
Ok(())
}
fn cmd_process(args: &[String], config_mgr: &mut ConfigManager, config_loaded: bool) -> Result<()> {
let verbose = args.iter().any(|a| a == "--verbose" || a == "-v");
let input_file = match args.iter().find(|a| !a.starts_with('-')) {
Some(path) => path,
None => {
eprintln!("[!] No input file specified. Use: ploit-malper process <scan.json>");
return Ok(());
}
};
let input_path = PathBuf::from(input_file);
if !input_path.exists() {
eprintln!("[!] File not found: {}", input_path.display());
return Ok(());
}
let process_opts = crate::process::ProcessOptions {
verbose,
print_console: true,
};
let processed = crate::process::process_scan_file(&input_path, config_mgr, &process_opts)?;
let records = processed.records;
let injectable = processed.injectable;
let all_suggestions = processed.suggestions;
let dedup_stats = processed.dedup_stats;
let mut msf_connected = false;
let mut msf_client = None;
if !config_loaded || !config_mgr.is_configured() {
println!("[?] No MSF-RPC configuration found.");
if prompt_bool_with_default("Configure MSF-RPC and NVD API now?", true)? {
cmd_setup(config_mgr)?;
} else {
println!("[?] Continuing without MSF-RPC integration.");
println!();
}
}
if config_mgr.is_configured() {
let msf_cfg = config_mgr.get_msfrpc_config().clone();
println!();
println!("[+] Connecting to MSF-RPC for workspace verification...");
let mut client = msfrpc::MsfRpcClient::new(
&msf_cfg.host,
msf_cfg.port,
&msf_cfg.username,
&msf_cfg.password,
msf_cfg.ssl,
);
let login_result = client.login()?;
if login_result.success {
msf_connected = true;
println!(
"[+] Authenticated to MSF-RPC at {}:{}",
msf_cfg.host, msf_cfg.port
);
if verbose {
println!("[dbg] fetching available workspaces...");
}
let ws_result = client.get_workspaces()?;
if !ws_result.workspaces.is_empty() {
println!();
println!("MSF Workspaces");
println!("{:<24} {:<16} {:>8}", "Name", "Scope", "Hosts");
println!("{}", "-".repeat(52));
for workspace in &ws_result.workspaces {
println!(
"{:<24} {:<16} {:>8}",
truncate(&workspace.name, 24),
truncate(&workspace.scope, 16),
workspace.host_count
);
}
}
for record in records.iter().take(5) {
if let Some(target) = record.target.as_str().split_whitespace().next() {
if verbose {
println!(
"[dbg] checking host {} against workspace '{}'...",
target, msf_cfg.workspace
);
}
let check_result =
client.check_host_exists(target, Some(&msf_cfg.workspace))?;
if check_result.exists {
println!(
"[?] Host {} already exists in workspace '{}'",
target, msf_cfg.workspace
);
}
}
}
} else {
println!(
"[!] Remote MSF-RPC unreachable. Switched entirely to offline matching matrix."
);
println!(" Error: {}", login_result.error);
}
msf_client = Some(client);
}
let mut recipes: Vec<PayloadRecipe> = Vec::new();
if prompt_bool_with_default("\nGenerate msfvenom payload recipes?", false)? {
println!();
println!("Payload Recipe Builder");
let platforms = recipe::get_available_platforms();
println!("Available platforms: {}", platforms.join(", "));
let platform = prompt_choice("Platform", &platforms, "windows")?;
let arches = recipe::get_available_arches(&platform);
println!("Available architectures: {}", arches.join(", "));
let arch = prompt_choice("Architecture", &arches, "x64")?;
let lhost = prompt_text("LHOST", "10.0.0.1")?;
let lport = prompt_u16("LPORT", 4444)?;
let output = prompt_text("Output path", "/tmp/payload")?;
let recipe = recipe::build_recipe(&platform, &arch, &lhost, lport, &output, None, None);
println!();
println!("Generated recipe:");
println!("{}", recipe.command);
recipes.push(recipe);
}
let report_path = report::derive_report_path(&input_path);
if prompt_bool_with_default(
&format!(
"\nWrite findings to Markdown report ({})?",
report_path.display()
),
true,
)? {
let output_path = report::generate_markdown_report(
&records,
&injectable,
&all_suggestions,
&recipes,
&dedup_stats,
report_path.to_str().unwrap_or("report.md"),
)?;
println!();
println!("[+] Report written to: {}", output_path.display());
}
if msf_connected {
if let Some(mut client) = msf_client {
client.logout();
}
}
config_mgr.config.last_scan_file = Some(input_path.to_string_lossy().to_string());
config_mgr.save()?;
Ok(())
}
fn cmd_reset_config(config_mgr: &mut ConfigManager) -> Result<()> {
println!("[?] Resetting all stored configuration...");
config_mgr.reset()?;
println!("[+] Configuration reset. Run ploit-malper to set up credentials.");
Ok(())
}
fn cmd_db_setup(config_mgr: &mut ConfigManager) -> Result<()> {
let _verbose = std::env::args().any(|a| a == "--verbose" || a == "-v");
println!("PloitMalper Database Setup");
println!();
println!("The intelligence database stores scan artifacts (NetMalper, VulnMalper,");
println!("PloitMalper) with history tracking. PocketBase is the primary backend with");
println!("SQLite as the local fallback.");
println!();
let current = config_mgr.get_database_config().clone();
let backend_choices = vec!["pocketbase".to_string(), "sqlite".to_string()];
let backend = prompt_choice("Backend", &backend_choices, ¤t.backend)?;
let mut db = current.clone();
db.backend = backend;
if db.backend == "pocketbase" {
println!();
println!("--- PocketBase Configuration ---");
db.pocketbase_url = prompt_text("PocketBase URL", &db.pocketbase_url)?;
db.pocketbase_admin_email = prompt_text("Admin email", &db.pocketbase_admin_email)?;
db.pocketbase_admin_password =
prompt_text("Admin password", &db.pocketbase_admin_password)?;
println!();
println!("[+] Writing schema reference file...");
crate::db::schema::write_schema_file()?;
println!(
"[+] Schema reference written to {}",
crate::config::db_schema_file().display()
);
let mut storage = crate::db::pocketbase::PocketBaseStorage::new(&db)?;
if !storage.ping()? {
return Err(AppError::Message(format!(
"PocketBase is not reachable at {}",
db.pocketbase_url
)));
}
println!("[+] Connected to PocketBase at {}", db.pocketbase_url);
println!("[+] Authenticating as superuser...");
storage.login()?;
println!("[+] Authenticated.");
let existing = storage.list_collection_names()?;
let missing: Vec<&str> = crate::db::collections::ALL
.iter()
.filter(|name| !existing.iter().any(|e| e == *name))
.cloned()
.collect();
if missing.is_empty() {
println!(
"[+] All {} required collections already present.",
crate::db::collections::ALL.len()
);
} else {
println!("[+] Missing collections (will be created):");
for name in &missing {
println!(" - {}", name);
}
}
if !missing.is_empty() && prompt_bool_with_default("Create missing collections?", true)? {
storage.ensure_schema()?;
println!("[+] Schema applied.");
} else {
println!("[!] No collections created.");
}
db.pocketbase_token = storage.token();
} else {
println!();
println!("--- SQLite Configuration ---");
db.sqlite_path = prompt_text("SQLite database path", &db.sqlite_path)?;
println!("[+] Initializing SQLite database...");
let mut storage = crate::db::open_storage(&db)?;
storage.ensure_schema()?;
if !storage.ping()? {
return Err(AppError::Message(
"SQLite database could not be opened".to_string(),
));
}
println!("[+] SQLite database ready.");
}
db.configured = true;
config_mgr.config.database = db;
config_mgr.save()?;
println!();
println!(
"[+] Database configuration saved to {}",
crate::config::config_file().display()
);
Ok(())
}
fn cmd_ingest(args: &[String], config_mgr: &mut ConfigManager) -> Result<()> {
let mut folder: Option<PathBuf> = None;
let mut opts = crate::ingest::importer::IngestOptions::default();
let mut index = 0usize;
while index < args.len() {
match args[index].as_str() {
"--verbose" | "-v" => opts.verbose = true,
"--dry-run" => opts.dry_run = true,
"--force" | "-f" => opts.force = true,
"--process" | "-p" => opts.process = true,
"--backend" | "-b" => {
if let Some(value) = args.get(index + 1) {
if !["pocketbase", "sqlite", "auto"].contains(&value.as_str()) {
return Err(AppError::Message(format!(
"invalid backend '{}' (use pocketbase, sqlite or auto)",
value
)));
}
opts.backend = value.clone();
index += 1;
}
}
"--pocketbase-url" => {
if let Some(value) = args.get(index + 1) {
opts.pocketbase_url = Some(value.clone());
index += 1;
}
}
"--sqlite-path" => {
if let Some(value) = args.get(index + 1) {
opts.sqlite_path = Some(value.clone());
index += 1;
}
}
"-h" | "--help" => {
println!(
"Usage: ploit-malper ingest <folder> [--verbose] [--dry-run] [--force] [--process] \
[--backend pocketbase|sqlite|auto] [--pocketbase-url URL] [--sqlite-path PATH]"
);
return Ok(());
}
other if folder.is_none() && !other.starts_with('-') => {
folder = Some(PathBuf::from(other));
}
other => {
return Err(AppError::Message(format!(
"unknown ingest option: {}",
other
)));
}
}
index += 1;
}
let folder = folder.ok_or_else(|| {
AppError::Message(
"ingest requires a folder path (e.g. ploit-malper ingest ./results)".to_string(),
)
})?;
if !config_mgr.database_configured() {
return Err(AppError::Message(
"no database configured; run 'ploit-malper db_setup' first".to_string(),
));
}
if opts.process && !opts.dry_run {
let processed = crate::process::process_folder(&folder, config_mgr, opts.verbose)?;
println!(
"[+] Pre-processed {} VulnMalper JSON file(s) into PloitMalper reports.",
processed
);
println!();
}
let summary =
crate::ingest::importer::ingest_folder(&folder, config_mgr.get_database_config(), &opts)?;
if opts.dry_run || summary.skipped_already_imported {
return Ok(());
}
println!();
println!("[+] Import summary:");
println!(" run_id: {}", summary.run_id);
println!(" target: {}", summary.target);
println!(
" artifacts: {} accepted, {} rejected",
summary.artifacts_accepted, summary.artifacts_rejected
);
println!(" assets: {}", summary.assets);
println!(" services: {}", summary.services);
println!(" findings: {}", summary.findings);
println!(" observations: {}", summary.observations);
Ok(())
}
fn cmd_del(args: &[String], config_mgr: &mut ConfigManager) -> Result<()> {
let parsed = crate::frontend::Args::parse(args)?;
if parsed.has("help") {
println!(
"Usage: ploit-malper del <run_id> [--yes] [--backend pocketbase|sqlite] \
[--pocketbase-url URL] [--sqlite-path PATH]"
);
return Ok(());
}
let run_ref = parsed.positionals().first().cloned().ok_or_else(|| {
AppError::Message("del requires a run id (e.g. ploit-malper del <run_id>)".to_string())
})?;
let mut storage = crate::frontend::open_backend(config_mgr, &parsed)?;
let run = crate::exploit::planner::Planner::resolve_run(storage.as_mut(), &run_ref)?;
println!(
"[+] Run: {} (target={})",
crate::frontend::truncate(&run.stable_id, 16),
run.target
);
let confirmed = parsed.has("yes")
|| prompt_bool_with_default(
&format!("Delete run '{}' and its observations?", run.stable_id),
false,
)?;
if !confirmed {
println!("[!] Aborted.");
return Ok(());
}
storage.delete_scan_run(&run.stable_id)?;
println!("[+] Deleted run {} (target={}).", run.stable_id, run.target);
Ok(())
}
fn prompt_text(prompt: &str, default: &str) -> Result<String> {
print!(" {} [{}]: ", prompt, default);
io::stdout().flush()?;
let mut buffer = String::new();
io::stdin().read_line(&mut buffer)?;
let value = buffer.trim();
Ok(if value.is_empty() {
default.to_string()
} else {
value.to_string()
})
}
fn prompt_u16(prompt: &str, default: u16) -> Result<u16> {
loop {
let value = prompt_text(prompt, &default.to_string())?;
if value.is_empty() {
return Ok(default);
}
match value.parse::<u16>() {
Ok(parsed) => return Ok(parsed),
Err(_) => println!(" Please enter a valid number."),
}
}
}
fn prompt_bool(prompt: &str, default: bool) -> Result<bool> {
prompt_bool_with_default(prompt, default)
}
fn prompt_bool_with_default(prompt: &str, default: bool) -> Result<bool> {
let suffix = if default { "[Y/n]" } else { "[y/N]" };
loop {
print!(" {} {}: ", prompt, suffix);
io::stdout().flush()?;
let mut buffer = String::new();
io::stdin().read_line(&mut buffer)?;
let value = buffer.trim().to_lowercase();
if value.is_empty() {
return Ok(default);
}
match value.as_str() {
"y" | "yes" | "true" => return Ok(true),
"n" | "no" | "false" => return Ok(false),
_ => println!(" Please answer yes or no."),
}
}
}
fn prompt_choice(prompt: &str, choices: &[String], default: &str) -> Result<String> {
let resolved_default = if choices.iter().any(|c| c == default) {
default.to_string()
} else {
choices.first().cloned().unwrap_or_default()
};
loop {
print!(" {} [{}]: ", prompt, resolved_default);
io::stdout().flush()?;
let mut buffer = String::new();
io::stdin().read_line(&mut buffer)?;
let value = buffer.trim();
if value.is_empty() {
return Ok(resolved_default);
}
if choices.iter().any(|choice| choice == value) {
return Ok(value.to_string());
}
println!(" Please choose one of: {}", choices.join(", "));
}
}
fn truncate(value: &str, width: usize) -> String {
let count = value.chars().count();
if count <= width {
return value.to_string();
}
if width <= 1 {
return "…".to_string();
}
let mut truncated = value.chars().take(width - 1).collect::<String>();
truncated.push('…');
truncated
}
fn print_banner() {
println!("PloitMalper v{}", env!("CARGO_PKG_VERSION"));
println!("Vulnerability Post-Processing and Analysis Toolkit");
println!();
println!("Commands:");
println!(" process <file> Process and deduplicate scan results");
println!(" db_setup Configure the intelligence database (PocketBase/SQLite)");
println!(" ingest <folder> Import Malper scan artifacts into the database");
println!(" assets List assets with lifecycle and service summary");
println!(" services List discovered services across assets");
println!(" findings List vulnerability findings");
println!(" history <id> Show the observation timeline for an asset/service/finding");
println!(" runs List all scan runs");
println!(" diff [a] [b] Compare two scan runs (latest two by default)");
println!(" exploit <run_id> Plan and run exploits from a scan run against a framework");
println!(" del <run_id> Delete a scan run and its observations");
println!(" setup Configure MSF-RPC and NVD API credentials");
println!(" reset-config Reset stored configuration");
println!();
println!("Frontend commands accept --verbose, --json, and --backend pocketbase|sqlite.");
}