ploit-malper 0.1.3

Post-exploitation analysis and vulnerability intelligence toolkit
Documentation
use serde::{Deserialize, Serialize};
use serde_json::Value;
use sha2::{Digest, Sha256};
use time::format_description::well_known::Rfc3339;
use time::OffsetDateTime;

pub fn now_utc() -> String {
    OffsetDateTime::now_utc()
        .format(&Rfc3339)
        .unwrap_or_else(|_| format!("{:?}", std::time::SystemTime::now()))
}

pub fn sha256_hex(input: &str) -> String {
    let mut hasher = Sha256::new();
    hasher.update(input.as_bytes());
    format!("{:x}", hasher.finalize())
}

fn stable_id(prefix: &str, key: &str) -> String {
    sha256_hex(&format!("{}:{}", prefix, key))
}

pub const ASSET_ACTIVE: &str = "active";
pub const ASSET_REMOVED: &str = "removed";

/// A scannable target (IP, hostname, or root domain) with lifecycle state.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Asset {
    pub stable_id: String,
    pub asset_type: String,
    pub name: String,
    pub ip: Option<String>,
    pub fqdn: Option<String>,
    pub reverse_dns: Option<String>,
    pub first_seen: String,
    pub last_seen: String,
    pub status: String,
    #[serde(default)]
    pub metadata: Value,
}

impl Asset {
    pub fn new(name: &str, asset_type: &str) -> Self {
        let mut asset = Self {
            stable_id: stable_id("asset", name),
            asset_type: asset_type.to_string(),
            name: name.to_string(),
            ..Default::default()
        };
        asset.metadata = Value::Object(Default::default());
        let now = now_utc();
        asset.first_seen = now.clone();
        asset.last_seen = now;
        asset.status = ASSET_ACTIVE.to_string();
        asset
    }
}

/// A network service discovered on an asset (open port + protocol + product).
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Service {
    pub stable_id: String,
    pub asset_id: String,
    pub port: u16,
    pub protocol: String,
    pub service_name: String,
    pub product: Option<String>,
    pub version: Option<String>,
    pub version_str: Option<String>,
    pub banner: Option<String>,
    #[serde(default)]
    pub technologies: Vec<String>,
    #[serde(default)]
    pub cpes: Vec<String>,
    pub first_seen: String,
    pub last_seen: String,
    pub status: String,
    #[serde(default)]
    pub metadata: Value,
}

impl Service {
    pub fn new(asset_id: &str, port: u16, protocol: &str, service_name: &str) -> Self {
        let key = format!("{}:{}:{}:{}", asset_id, port, protocol, service_name);
        let mut service = Self {
            stable_id: stable_id("service", &key),
            asset_id: asset_id.to_string(),
            port,
            protocol: protocol.to_string(),
            service_name: service_name.to_string(),
            ..Default::default()
        };
        service.metadata = Value::Object(Default::default());
        let now = now_utc();
        service.first_seen = now.clone();
        service.last_seen = now;
        service.status = ASSET_ACTIVE.to_string();
        service
    }
}

/// A vulnerability / security finding on an asset with exploitability tracking.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Finding {
    pub stable_id: String,
    pub asset_id: String,
    pub service_id: Option<String>,
    pub title: String,
    pub severity: String,
    pub tool: String,
    pub target_url: Option<String>,
    pub detail: Option<String>,
    pub reference: Option<String>,
    #[serde(default)]
    pub cves: Vec<String>,
    #[serde(default)]
    pub endpoints: Vec<String>,
    #[serde(default)]
    pub technologies: Vec<String>,
    pub first_seen: String,
    pub last_seen: String,
    pub status: String,
    pub exploitability: Option<String>,
    #[serde(default)]
    pub metadata: Value,
}

impl Finding {
    pub fn new(asset_id: &str, tool: &str, title: &str) -> Self {
        let key = format!("{}:{}:{}", asset_id, tool, title);
        let mut finding = Self {
            stable_id: stable_id("finding", &key),
            asset_id: asset_id.to_string(),
            tool: tool.to_string(),
            title: title.to_string(),
            ..Default::default()
        };
        finding.metadata = Value::Object(Default::default());
        let now = now_utc();
        finding.first_seen = now.clone();
        finding.last_seen = now;
        finding.status = ASSET_ACTIVE.to_string();
        finding
    }
}

/// A historical observation recording how intelligence changed across runs.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Observation {
    pub stable_id: String,
    pub run_id: String,
    pub subject_type: String,
    pub subject_id: String,
    pub kind: String,
    #[serde(default)]
    pub before: Value,
    #[serde(default)]
    pub after: Value,
    pub detail: String,
    pub observed_at: String,
}

impl Observation {
    pub fn new(
        run_id: &str,
        subject_type: &str,
        subject_id: &str,
        kind: &str,
        before: Value,
        after: Value,
        detail: &str,
    ) -> Self {
        let before_s = before.to_string();
        let after_s = after.to_string();
        let key = format!(
            "{}:{}:{}:{}:{}:{}",
            run_id, subject_type, subject_id, kind, before_s, after_s
        );
        Self {
            stable_id: stable_id("observation", &key),
            run_id: run_id.to_string(),
            subject_type: subject_type.to_string(),
            subject_id: subject_id.to_string(),
            kind: kind.to_string(),
            before,
            after,
            detail: detail.to_string(),
            observed_at: now_utc(),
        }
    }
}

/// A typed relationship between two records (asset hosts service, service maps
/// to finding, etc.).
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Relationship {
    pub stable_id: String,
    pub run_id: String,
    pub subject_type: String,
    pub subject_id: String,
    pub object_type: String,
    pub object_id: String,
    pub kind: String,
    pub detail: String,
}

impl Relationship {
    pub fn new(
        run_id: &str,
        subject_type: &str,
        subject_id: &str,
        object_type: &str,
        object_id: &str,
        kind: &str,
        detail: &str,
    ) -> Self {
        let key = format!(
            "{}:{}:{}:{}:{}",
            subject_type, subject_id, object_type, object_id, kind
        );
        Self {
            stable_id: stable_id("relationship", &key),
            run_id: run_id.to_string(),
            subject_type: subject_type.to_string(),
            subject_id: subject_id.to_string(),
            object_type: object_type.to_string(),
            object_id: object_id.to_string(),
            kind: kind.to_string(),
            detail: detail.to_string(),
        }
    }
}

/// Metadata describing one logical scan run over a target.
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ScanRun {
    pub stable_id: String,
    pub target: String,
    pub folder: String,
    pub started_at: Option<String>,
    pub finished_at: Option<String>,
    #[serde(default)]
    pub tools: Vec<String>,
    #[serde(default)]
    pub artifacts: Vec<Value>,
    pub content_hash: String,
    pub imported_at: String,
    #[serde(default)]
    pub stats: Value,
}

impl ScanRun {
    pub fn new(
        run_id: &str,
        target: &str,
        folder: &str,
        content_hash: &str,
        started_at: Option<String>,
    ) -> Self {
        Self {
            stable_id: run_id.to_string(),
            target: target.to_string(),
            folder: folder.to_string(),
            started_at,
            finished_at: None,
            content_hash: content_hash.to_string(),
            imported_at: now_utc(),
            stats: Value::Object(Default::default()),
            ..Default::default()
        }
    }
}

/// A stored report artifact (vulnmalper markdown, ploitmalper report, ...).
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct Report {
    pub stable_id: String,
    pub run_id: String,
    pub tool: String,
    pub format: String,
    pub title: String,
    pub source_path: String,
    pub content: String,
    pub content_hash: String,
    pub imported_at: String,
}

impl Report {
    pub fn new(
        run_id: &str,
        tool: &str,
        format: &str,
        title: &str,
        source_path: &str,
        content: &str,
        content_hash: &str,
    ) -> Self {
        let key = format!("{}:{}:{}:{}", run_id, tool, format, content_hash);
        Self {
            stable_id: stable_id("report", &key),
            run_id: run_id.to_string(),
            tool: tool.to_string(),
            format: format.to_string(),
            title: title.to_string(),
            source_path: source_path.to_string(),
            content: content.to_string(),
            content_hash: content_hash.to_string(),
            imported_at: now_utc(),
        }
    }
}