ploit-malper 0.2.6

Post-exploitation analysis and vulnerability intelligence toolkit
Documentation
use crate::config::ConfigManager;
use crate::db::models::Asset;
use crate::error::{AppError, Result};
use crate::frontend::{
    bold, fmt_ts, open_backend, parse_ts, print_json, ts_since, Args, Cell, IdMaps, Lifecycle,
    LifecycleQuery, ObsIndex, Table,
};

pub fn usage() {
    println!(
        "Usage: ploit-malper assets [--lifecycle STATE|--all] [--new] [--changed] [--since DATE] \
         [--target TERM] [--verbose] [--json] [--backend pocketbase|sqlite]"
    );
}

pub fn cmd_assets(args: &[String], config_mgr: &mut ConfigManager) -> Result<()> {
    let parsed = Args::parse(args)?;
    if parsed.has("help") {
        usage();
        return Ok(());
    }

    let mut storage = open_backend(config_mgr, &parsed)?;

    let assets = storage.list_assets()?;
    let observations = storage.list_all_observations()?;
    let obs_index = ObsIndex::build(&observations);
    let maps = IdMaps::load(&mut storage)?;

    let since = match parsed.get("since") {
        Some(raw) => Some(
            parse_ts(raw)
                .ok_or_else(|| AppError::Message(format!("invalid --since date: '{}'", raw)))?,
        ),
        None => None,
    };

    let lifecycle_query = LifecycleQuery::parse(&parsed, true, false)?;

    let mut selected: Vec<&Asset> = Vec::new();
    for asset in &assets {
        if let Some(target) = parsed.get("target") {
            let needle = target.to_lowercase();
            let hit = asset.name.to_lowercase().contains(&needle)
                || asset
                    .ip
                    .as_deref()
                    .is_some_and(|i| i.to_lowercase().contains(&needle))
                || asset
                    .fqdn
                    .as_deref()
                    .is_some_and(|f| f.to_lowercase().contains(&needle))
                || asset
                    .reverse_dns
                    .as_deref()
                    .is_some_and(|r| r.to_lowercase().contains(&needle));
            if !hit {
                continue;
            }
        }
        if let Some(since) = since {
            if !ts_since(&asset.last_seen, since) {
                continue;
            }
        }
        let lifecycle = obs_index.lifecycle(&asset.stable_id, &asset.status);
        if !lifecycle_query.matches(lifecycle) {
            continue;
        }
        selected.push(asset);
    }

    if parsed.has("json") {
        let data: Vec<Asset> = selected.into_iter().cloned().collect();
        return print_json(&data);
    }

    if selected.is_empty() {
        crate::frontend::empty_result("assets");
        return Ok(());
    }

    let mut table = Table::new(vec![
        "NAME".to_string(),
        "TYPE".to_string(),
        "SERVICES".to_string(),
        "FIRST SEEN".to_string(),
        "LAST SEEN".to_string(),
        "CHANGE".to_string(),
    ]);
    for asset in &selected {
        let lifecycle = obs_index.lifecycle(&asset.stable_id, &asset.status);
        table.add_row(vec![
            Cell::plain(asset.name.clone()),
            Cell::plain(asset.asset_type.clone()),
            Cell::plain(maps.service_summary(&asset.stable_id)),
            Cell::plain(fmt_ts(&asset.first_seen)),
            Cell::plain(fmt_ts(&asset.last_seen)),
            Cell::styled(lifecycle.label().to_string(), lifecycle_style(lifecycle)),
        ]);
    }
    println!("{}", table.render());

    if parsed.has("verbose") {
        println!();
        for asset in &selected {
            println!(
                "{} ({})  [{}]",
                bold(&asset.name),
                asset.asset_type,
                asset.stable_id
            );
            println!(
                "    ip={} fqdn={} reverse_dns={} status={}",
                asset.ip.as_deref().unwrap_or("-"),
                asset.fqdn.as_deref().unwrap_or("-"),
                asset.reverse_dns.as_deref().unwrap_or("-"),
                asset.status
            );
            if !asset.metadata.is_null() && asset.metadata.as_object().is_some() {
                println!("    metadata={}", asset.metadata);
            }
            let observations = obs_index.observations_for(&asset.stable_id);
            if !observations.is_empty() {
                let last = observations.last().unwrap();
                println!("    last change: {} ({})", last.kind, last.observed_at);
            }
        }
    }

    Ok(())
}

fn lifecycle_style(lifecycle: Lifecycle) -> &'static str {
    match lifecycle {
        Lifecycle::New => "32",
        Lifecycle::Changed => "33",
        Lifecycle::Removed => "31",
        Lifecycle::Active => "2",
    }
}