holger-intel 2.0.0

Holger offline vulnerability intelligence: OSV / NVD / CISA KEV normalized into one advisory set, carried across the airgap as a signed holger.update-bundle/v1 (product holger.intel/v1), verified with anti-rollback, and kept in a persistent local advisory database the scanners read. Network-free unless the `fetch` feature is on.
Documentation
//! How old an advisory database may get before a scan against it says so, and
//! before it stops being an answer at all.

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};

/// Warn after `warn_after_days`, block after `block_after_days`, both counted
/// from the snapshot date the bundle carries (its `created_at`), not from the
/// day it was imported: a month-old bundle carried in today is a month old.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct StalenessRule {
    pub warn_after_days: u32,
    pub block_after_days: u32,
}

impl Default for StalenessRule {
    fn default() -> Self {
        Self { warn_after_days: 7, block_after_days: 30 }
    }
}

/// The verdict on one database's age.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Staleness {
    Fresh,
    Warn,
    Block,
}

impl Staleness {
    pub fn as_str(self) -> &'static str {
        match self {
            Staleness::Fresh => "fresh",
            Staleness::Warn => "warn",
            Staleness::Block => "block",
        }
    }
}

impl StalenessRule {
    /// Refuse a rule whose block comes before its warning.
    pub fn validate(&self) -> anyhow::Result<()> {
        anyhow::ensure!(
            self.block_after_days >= self.warn_after_days,
            "intel staleness: block_after_days ({}) is before warn_after_days ({})",
            self.block_after_days,
            self.warn_after_days
        );
        Ok(())
    }

    /// Whole days from `snapshot` to `now`. A snapshot dated in the future (a
    /// skewed builder clock) counts as age 0, not as negative.
    pub fn age_days(snapshot: DateTime<Utc>, now: DateTime<Utc>) -> i64 {
        (now - snapshot).num_days().max(0)
    }

    pub fn judge(&self, snapshot: DateTime<Utc>, now: DateTime<Utc>) -> Staleness {
        let age = Self::age_days(snapshot, now);
        if age >= i64::from(self.block_after_days) {
            Staleness::Block
        } else if age >= i64::from(self.warn_after_days) {
            Staleness::Warn
        } else {
            Staleness::Fresh
        }
    }
}