ploit-malper 0.1.1

Post-exploitation analysis and vulnerability intelligence toolkit
Documentation
use std::collections::HashSet;

use regex::Regex;
use sha2::{Digest, Sha256};

use crate::models::{DedupResult, ScanRecord};

fn normalize_title(title: &str) -> String {
    let cleaned = title
        .trim()
        .to_lowercase()
        .split_whitespace()
        .collect::<Vec<&str>>()
        .join(" ");

    let re = Regex::new(r"\s*[/\\]\s*$").expect("valid regex");
    re.replace_all(&cleaned, "").to_string()
}

fn composite_key(record: &ScanRecord) -> String {
    let normalized = normalize_title(&record.title);
    let raw = format!("{}|{}|{}", record.target, record.tool, normalized);
    let mut hasher = Sha256::new();
    hasher.update(raw.as_bytes());
    format!("{:x}", hasher.finalize())
}

pub fn deduplicate_records(records: Vec<ScanRecord>) -> DedupResult {
    let total_count = records.len();
    let mut seen: HashSet<String> = HashSet::new();
    let mut unique_records = Vec::new();

    for record in records {
        let key = composite_key(&record);
        if seen.insert(key) {
            unique_records.push(record);
        }
    }

    let unique_count = unique_records.len();
    let removed_count = total_count.saturating_sub(unique_count);

    DedupResult {
        unique_count,
        removed_count,
        records: unique_records,
    }
}

pub fn normalize_title_text(title: &str) -> String {
    normalize_title(title)
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::models::ScanRecord;
    use serde_json::json;

    #[test]
    fn deduplicates_on_normalized_title() {
        let records = vec![
            ScanRecord {
                target: "10.0.0.1".into(),
                tool: "nmap".into(),
                title: "Apache Version /".into(),
                severity: "high".into(),
                port: Some(80),
                cve: None,
                service: Some("apache".into()),
                raw: json!({}),
                ..Default::default()
            },
            ScanRecord {
                target: "10.0.0.1".into(),
                tool: "nmap".into(),
                title: "apache version".into(),
                severity: "high".into(),
                port: Some(80),
                cve: None,
                service: Some("apache".into()),
                raw: json!({}),
                ..Default::default()
            },
        ];

        let result = deduplicate_records(records);
        assert_eq!(result.unique_count, 1);
        assert_eq!(result.removed_count, 1);
    }
}