Skip to main content

specfold_core/
guards.rs

1//! Guards: named checks over any file in the repository and over commit messages, such as the
2//! gate that keeps internal names out of a public package.
3
4mod cache;
5mod code_spans;
6mod compile;
7mod jslex;
8mod pattern;
9mod prefilter;
10mod prove;
11pub mod range;
12mod scan;
13pub mod secrets;
14pub mod terms;
15
16use crate::check::baseline;
17use crate::check::finding::Finding;
18use crate::config::{Config, GuardDef};
19use anyhow::{Result, anyhow, bail};
20pub use range::GuardRange;
21
22fn select<'a>(cfg: &'a Config, names: &[String]) -> Result<Vec<&'a GuardDef>> {
23    if names.is_empty() {
24        return Ok(cfg.raw.guards.iter().collect());
25    }
26    names
27        .iter()
28        .map(|n| {
29            cfg.raw.guards.iter().find(|g| &g.name == n).ok_or_else(|| {
30                // C216: the built-in check runs as `spec guard secrets` with no guard configured.
31                let mut have: Vec<&str> = cfg.raw.guards.iter().map(|g| g.name.as_str()).collect();
32                have.push("secrets (built-in)");
33                anyhow!("no guard named '{n}' (have: {}); next: spec guard secrets", have.join(", "))
34            })
35        })
36        .collect()
37}
38
39fn scan_one(cfg: &Config, g: &GuardDef, range: Option<&GuardRange>) -> Result<scan::Scanned> {
40    let c = compile::compile(cfg, g)?;
41    prove::prove(g, &c)?;
42    let r = range.cloned().unwrap_or_else(|| GuardRange::parse(&g.range));
43    scan::run(cfg, g, &c, &range::resolve(&cfg.root, &r, &c.bases)?)
44}
45
46/// Runs the named guards (all of them when `names` is empty). A guard without an explicit range
47/// uses its own default; a guard with a baseline reports only hits the baseline does not hold.
48pub fn run(cfg: &Config, names: &[String], range: Option<&GuardRange>) -> Result<Vec<Finding>> {
49    let mut out = vec![];
50    for g in select(cfg, names)? {
51        let found = scan_one(cfg, g, range)?.findings;
52        match &g.baseline {
53            Some(rel) => match baseline::load_file(&cfg.root, rel)? {
54                Some(b) => out.extend(baseline::hold_with(&b, found, &[]).findings),
55                None => out.extend(found),
56            },
57            None => out.extend(found),
58        }
59    }
60    Ok(out)
61}
62
63/// Measures each named guard over the whole tree and writes its baseline file.
64/// With `write` false, only measures: what `--write-baseline` would record, for its preview.
65pub fn write_baselines(cfg: &Config, names: &[String], write: bool) -> Result<Vec<(String, String, usize)>> {
66    let mut written = vec![];
67    for g in select(cfg, names)? {
68        let Some(rel) = &g.baseline else {
69            if !names.is_empty() {
70                bail!("guard {} has no baseline file; set baseline = \"…\" on it", g.name);
71            }
72            continue;
73        };
74        let scanned = scan_one(cfg, g, Some(&GuardRange::All))?;
75        let b = baseline::measure_with(&cfg.root, &scanned.findings, &[], |p| scanned.files.iter().any(|f| f == p))?;
76        if write {
77            baseline::write_file(&cfg.root, rel, &b)?;
78        }
79        written.push((g.name.clone(), rel.clone(), b.counts.values().sum()));
80    }
81    Ok(written)
82}
83
84mod blobs;
85#[cfg(test)]
86mod tests;
87pub use blobs::{builtin_secrets, reads_content, scan_blobs};