blazehash 0.2.5

Forensic file hasher — hashdeep for the modern era, BLAKE3 by default
Documentation
use crate::algorithm::Algorithm;
use crate::hash::FileHashResult;
use anyhow::Result;
use serde_json::{json, Value};
use std::io::Write;
use uuid::Uuid;

// STIX SCO namespace: 00abedb4-aa42-466c-9c01-fed23315a9b7
const STIX_SCO_NAMESPACE: Uuid = Uuid::from_bytes([
    0x00, 0xab, 0xed, 0xb4, 0xaa, 0x42, 0x46, 0x6c, 0x9c, 0x01, 0xfe, 0xd2, 0x33, 0x15, 0xa9, 0xb7,
]);

fn stix_hash_name(algo: &Algorithm) -> &'static str {
    match algo {
        Algorithm::Blake3 => "BLAKE3",
        Algorithm::Sha256 => "SHA-256",
        Algorithm::Sha512 => "SHA-512",
        Algorithm::Sha3_256 => "SHA3-256",
        Algorithm::Sha1 => "SHA-1",
        Algorithm::Md5 => "MD5",
        Algorithm::Tiger => "TIGER",
        Algorithm::Whirlpool => "WHIRLPOOL",
        Algorithm::Ssdeep => "SSDEEP",
        Algorithm::Tlsh => "TLSH",
        Algorithm::Crc32c => "CRC32C",
        Algorithm::Xxh3 => "XXH3",
        Algorithm::Shake128 => "SHAKE128",
        Algorithm::Shake256 => "SHAKE256",
        Algorithm::Blake2b => "BLAKE2B",
        Algorithm::Blake2s => "BLAKE2S",
        Algorithm::Sm3 => "SM3",
        Algorithm::Streebog256 => "STREEBOG-256",
        Algorithm::Streebog512 => "STREEBOG-512",
        Algorithm::Ripemd160 => "RIPEMD-160",
        Algorithm::Sha512_256 => "SHA-512/256",
        Algorithm::Sha512_224 => "SHA-512/224",
        Algorithm::K12 => "K12",
        Algorithm::Adler32 => "ADLER32",
        Algorithm::Crc64 => "CRC64",
    }
}

fn result_to_stix_sco(result: &FileHashResult, algorithms: &[Algorithm]) -> Value {
    let mut hashes = serde_json::Map::new();
    for algo in algorithms {
        if let Some(hash) = result.hashes.get(algo) {
            hashes.insert(stix_hash_name(algo).to_string(), json!(hash));
        }
    }
    let hashes_json = serde_json::to_string(&hashes).unwrap_or_default();
    let id_input = format!("{{\"hashes\":{hashes_json}}}");
    let uuid = Uuid::new_v5(&STIX_SCO_NAMESPACE, id_input.as_bytes());
    let stix_id = format!("file--{uuid}");
    let file_name = result
        .path
        .file_name()
        .and_then(|n| n.to_str())
        .unwrap_or("");
    json!({
        "type": "file",
        "spec_version": "2.1",
        "id": stix_id,
        "hashes": hashes,
        "size": result.size,
        "name": file_name,
    })
}

pub fn write_stix<W: Write>(
    w: &mut W,
    results: &[FileHashResult],
    algorithms: &[Algorithm],
) -> Result<()> {
    #[cfg_attr(not(feature = "yara"), allow(unused_mut))]
    let mut objects: Vec<Value> = results
        .iter()
        .map(|r| result_to_stix_sco(r, algorithms))
        .collect();

    // Enrich with x-mitre-attack extension objects for any YARA matches.
    #[cfg(feature = "yara")]
    {
        use crate::attack::lookup_attack_for_match;
        for result in results {
            if let Some(ref matches) = result.yara_matches {
                let file_name = result
                    .path
                    .file_name()
                    .and_then(|n| n.to_str())
                    .unwrap_or("");
                let file_path = result.path.to_string_lossy();
                for rule_match in matches {
                    if let Some(technique) = lookup_attack_for_match(rule_match) {
                        let id_input = format!("{}-{}-{}", file_path, technique.technique_id, rule_match.rule_name);
                        let uuid = Uuid::new_v5(&Uuid::NAMESPACE_URL, id_input.as_bytes());
                        objects.push(json!({
                            "type": "x-mitre-attack",
                            "id": format!("x-mitre-attack--{uuid}"),
                            "spec_version": "2.1",
                            "technique_id": technique.technique_id,
                            "tactic": technique.tactic,
                            "technique_name": technique.name,
                            "yara_rule": rule_match.rule_name,
                            "related_file": file_name,
                        }));
                    }
                }
            }
        }
    }

    let bundle_uuid = Uuid::new_v5(
        &STIX_SCO_NAMESPACE,
        format!("blazehash-bundle-{}", objects.len()).as_bytes(),
    );
    let bundle = json!({
        "type": "bundle",
        "id": format!("bundle--{bundle_uuid}"),
        "spec_version": "2.1",
        "objects": objects,
    });
    serde_json::to_writer_pretty(&mut *w, &bundle)?;
    writeln!(w)?;
    Ok(())
}