Skip to main content

fleetreach_cli/
db.rs

1//! Advisory-DB loading, freshness, provenance, and enrichment fetch — the
2//! binary's I/O wiring, kept out of the command runners. Everything here is
3//! `Args`-agnostic (it takes primitives), so `scan` and the `vex` subcommands
4//! share it without coupling to a particular clap struct.
5
6use std::path::Path;
7use std::process::Command;
8
9use fleetreach_core::semver::Version;
10use fleetreach_core::{FleetReport, Provenance};
11// `Severity` only appears in the network-only enrichment fetch.
12#[cfg(feature = "network")]
13use fleetreach_core::Severity;
14use fleetreach_scan::{AdvisoryDb, DatabaseMeta, RUSTSEC_VERSION};
15
16use crate::enrich::Enrichment;
17use crate::orchestrate::Toolchain;
18
19/// Load the advisory DB from explicit options, shared by `scan` and `vex check`.
20pub(crate) fn load_db_from(
21    db: Option<&Path>,
22    db_rev: Option<&str>,
23    offline: bool,
24) -> Result<AdvisoryDb, String> {
25    if let Some(path) = db {
26        if let Some(rev) = db_rev {
27            checkout_rev(path, rev)?;
28        }
29        return AdvisoryDb::open(path).map_err(|e| e.to_string());
30    }
31    if db_rev.is_some() {
32        return Err("--db-rev requires --db <PATH> (a local advisory-db git clone)".to_string());
33    }
34    load_db_remote(offline)
35}
36
37/// Load the DB from the network (default cache when `offline`, else fetch). Only
38/// exists in a `network` build; the pure-Rust build directs the user to `--db`.
39#[cfg(feature = "network")]
40fn load_db_remote(offline: bool) -> Result<AdvisoryDb, String> {
41    if offline {
42        AdvisoryDb::open_default_cache().map_err(|e| e.to_string())
43    } else {
44        AdvisoryDb::fetch().map_err(|e| e.to_string())
45    }
46}
47
48/// Pure-Rust build: no git/network backend, so the advisory DB must be supplied
49/// explicitly with `--db`.
50#[cfg(not(feature = "network"))]
51fn load_db_remote(_offline: bool) -> Result<AdvisoryDb, String> {
52    Err(
53        "this build has no network support: pass --db <PATH> to a local advisory-db \
54         clone, or rebuild with --features network to fetch the DB"
55            .to_string(),
56    )
57}
58
59/// Compute the (deduped) CVE lists and fetch KEV/EPSS/NVD enrichment. Network
60/// build only; the pure-Rust build directs the user to `--kev-file`/`--epss-file`.
61#[cfg(feature = "network")]
62pub(crate) fn fetch_enrichment(report: &FleetReport) -> Result<Enrichment, String> {
63    use std::collections::BTreeSet;
64    // Dedup so a CVE shared across advisories doesn't multiply rate-limited lookups.
65    let cves: Vec<String> = report
66        .vulnerabilities
67        .iter()
68        .flat_map(|v| v.aliases.iter().filter(|a| a.starts_with("CVE-")).cloned())
69        .collect::<BTreeSet<String>>()
70        .into_iter()
71        .collect();
72    // NVD CVSS backfill only targets unknown-severity findings (the few that need it).
73    let backfill_cves: Vec<String> = report
74        .vulnerabilities
75        .iter()
76        .filter(|v| v.severity == Severity::Unknown)
77        .flat_map(|v| v.aliases.iter().filter(|a| a.starts_with("CVE-")).cloned())
78        .collect::<BTreeSet<String>>()
79        .into_iter()
80        .collect();
81    Enrichment::fetch(&cves, &backfill_cves)
82}
83
84/// Pure-Rust build: enrichment fetch is unavailable; use the local `*_file` paths.
85#[cfg(not(feature = "network"))]
86pub(crate) fn fetch_enrichment(_report: &FleetReport) -> Result<Enrichment, String> {
87    Err(
88        "enrichment fetch needs the `network` feature; supply --kev-file / --epss-file, \
89         or rebuild with --features network"
90            .to_string(),
91    )
92}
93
94fn checkout_rev(path: &Path, rev: &str) -> Result<(), String> {
95    let status = Command::new("git")
96        .arg("-C")
97        .arg(path)
98        .args(["checkout", "--quiet", rev])
99        .status()
100        .map_err(|e| format!("running git: {e}"))?;
101    if status.success() {
102        Ok(())
103    } else {
104        Err(format!("git checkout {rev} failed in {}", path.display()))
105    }
106}
107
108pub(crate) fn check_db_age(db: &AdvisoryDb, spec: &str) -> Result<(), String> {
109    let limit = parse_duration_secs(spec)?;
110    match db.age_seconds() {
111        Some(age) if age <= limit => Ok(()),
112        Some(age) => Err(format!(
113            "advisory DB is {age}s old, older than --max-db-age {limit}s"
114        )),
115        None => Err("cannot determine advisory DB age; refusing under --max-db-age".to_string()),
116    }
117}
118
119/// Parse a duration like `7d`, `24h`, `30m`, `90s`, or a bare seconds count.
120fn parse_duration_secs(spec: &str) -> Result<i64, String> {
121    let spec = spec.trim();
122    let (digits, mult) = match spec.chars().last() {
123        Some('d') => (&spec[..spec.len() - 1], 86_400),
124        Some('h') => (&spec[..spec.len() - 1], 3_600),
125        Some('m') => (&spec[..spec.len() - 1], 60),
126        Some('s') => (&spec[..spec.len() - 1], 1),
127        _ => (spec, 1),
128    };
129    digits
130        .trim()
131        .parse::<i64>()
132        .map(|n| n * mult)
133        .map_err(|_| format!("invalid duration `{spec}`"))
134}
135
136/// Best-effort toolchain detection via `rustc --version`. A missing or
137/// unparseable rustc simply skips the toolchain scan (not an error).
138pub(crate) fn detect_toolchain() -> Option<Toolchain> {
139    let output = Command::new("rustc").arg("--version").output().ok()?;
140    if !output.status.success() {
141        return None;
142    }
143    let text = String::from_utf8(output.stdout).ok()?;
144    let token = text.split_whitespace().nth(1)?; // "rustc <token> (...)"
145    let version = Version::parse(token).ok()?;
146    Some(Toolchain {
147        channel: format!("rustc {token}"),
148        version,
149    })
150}
151
152pub(crate) fn build_provenance(meta: &DatabaseMeta) -> Provenance {
153    Provenance {
154        tool_version: env!("CARGO_PKG_VERSION").to_string(),
155        rustsec_crate_version: RUSTSEC_VERSION.to_string(),
156        db_commit: meta.commit.clone(),
157        db_timestamp: meta.timestamp.clone(),
158        host_os: std::env::consts::OS.to_string(),
159        host_arch: std::env::consts::ARCH.to_string(),
160        generated_at: now_rfc3339(),
161    }
162}
163
164fn now_rfc3339() -> String {
165    time::OffsetDateTime::now_utc()
166        .format(&time::format_description::well_known::Rfc3339)
167        .unwrap_or_default()
168}