1use std::path::Path;
7use std::process::Command;
8
9use fleetreach_core::semver::Version;
10use fleetreach_core::{FleetReport, Provenance};
11#[cfg(feature = "network")]
13use fleetreach_core::Severity;
14use fleetreach_scan::{AdvisoryDb, DatabaseMeta, RUSTSEC_VERSION};
15
16use crate::enrich::Enrichment;
17use crate::orchestrate::Toolchain;
18
19pub(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#[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#[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#[cfg(feature = "network")]
62pub(crate) fn fetch_enrichment(report: &FleetReport) -> Result<Enrichment, String> {
63 use std::collections::BTreeSet;
64 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 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#[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
119fn 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
136pub(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)?; 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}