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;
use std::io::Write;

fn ecs_hash_name(algo: &Algorithm) -> &'static str {
    match algo {
        Algorithm::Blake3 => "blake3",
        Algorithm::Sha256 => "sha256",
        Algorithm::Sha512 => "sha512",
        Algorithm::Sha3_256 => "sha3_256",
        Algorithm::Sha1 => "sha1",
        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 => "streebog256",
        Algorithm::Streebog512 => "streebog512",
        Algorithm::Ripemd160 => "ripemd160",
        Algorithm::Sha512_256 => "sha512_256",
        Algorithm::Sha512_224 => "sha512_224",
        Algorithm::K12 => "k12",
        Algorithm::Adler32 => "adler32",
        Algorithm::Crc64 => "crc64",
    }
}

fn now_iso8601() -> String {
    let secs = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .unwrap_or_default()
        .as_secs();
    let s = secs % 60;
    let m = (secs / 60) % 60;
    let h = (secs / 3600) % 24;
    let days = secs / 86400;
    let mut y = 1970u32;
    let mut d = days as u32;
    loop {
        let leap = y % 4 == 0 && (y % 100 != 0 || y % 400 == 0);
        let days_in_year = if leap { 366 } else { 365 };
        if d < days_in_year {
            break;
        }
        d -= days_in_year;
        y += 1;
    }
    let leap = y % 4 == 0 && (y % 100 != 0 || y % 400 == 0);
    let months = [
        31u32,
        if leap { 29 } else { 28 },
        31,
        30,
        31,
        30,
        31,
        31,
        30,
        31,
        30,
        31,
    ];
    let mut mo = 1u32;
    for dim in &months {
        if d < *dim {
            break;
        }
        d -= dim;
        mo += 1;
    }
    format!("{y:04}-{mo:02}-{:02}T{h:02}:{m:02}:{s:02}.000Z", d + 1)
}

pub fn write_ecs<W: Write>(
    w: &mut W,
    results: &[FileHashResult],
    algorithms: &[Algorithm],
) -> Result<()> {
    let timestamp = now_iso8601();
    for result in results {
        let mut hash_obj = serde_json::Map::new();
        for algo in algorithms {
            if let Some(hash) = result.hashes.get(algo) {
                hash_obj.insert(ecs_hash_name(algo).to_string(), json!(hash));
            }
        }
        let file_name = result
            .path
            .file_name()
            .and_then(|n| n.to_str())
            .unwrap_or("");
        let mut file_obj = serde_json::Map::new();
        file_obj.insert("path".to_string(), json!(result.path.display().to_string()));
        file_obj.insert("name".to_string(), json!(file_name));
        file_obj.insert("size".to_string(), json!(result.size));
        file_obj.insert("hash".to_string(), serde_json::Value::Object(hash_obj));
        if let Some(entropy) = result.entropy {
            file_obj.insert("entropy".to_string(), json!(entropy));
        }
        let doc = json!({
            "@timestamp": timestamp,
            "event": {
                "kind": "enrichment",
                "category": ["file"],
                "type": ["info"],
                "module": "blazehash"
            },
            "file": file_obj,
        });
        serde_json::to_writer(&mut *w, &doc)?;
        writeln!(w)?;
    }
    Ok(())
}