Skip to main content

cargo_context_core/scrub/
config.rs

1//! YAML schema for `.cargo-context/scrub.yaml`.
2//!
3//! Full reference lives in `README.md` §10. This module mirrors that schema
4//! in serde-deserializable types so the parser itself is the source of truth
5//! about which fields are accepted.
6
7use serde::Deserialize;
8
9use crate::scrub::Pattern;
10use crate::scrub::entropy::EntropyConfigRaw;
11use crate::scrub::paths::PathRulesRaw;
12
13/// Root of `.cargo-context/scrub.yaml`.
14#[derive(Debug, Clone, Default, Deserialize)]
15#[serde(deny_unknown_fields, default)]
16pub struct ScrubConfig {
17    pub version: u32,
18    pub builtins: BuiltinsMode,
19    pub disable_builtins: Vec<String>,
20    pub patterns: Vec<Pattern>,
21    pub entropy: EntropyConfigRaw,
22    pub paths: PathRulesRaw,
23    pub allowlist: Vec<AllowlistEntry>,
24    pub report: ReportConfig,
25}
26
27/// How user-defined patterns combine with the built-in rule set.
28#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize)]
29#[serde(rename_all = "lowercase")]
30pub enum BuiltinsMode {
31    /// Add user patterns on top of built-ins (default).
32    #[default]
33    Extend,
34    /// Ignore built-ins entirely — user patterns are the whole rule set.
35    Replace,
36    /// Extend, but with the expectation that `disable_builtins` lists one or
37    /// more built-in ids to turn off. Behaviorally equivalent to [`Extend`]
38    /// since `disable_builtins` is honored in both modes.
39    Disable,
40}
41
42/// One allowlist entry. Either `exact` or `regex` is set.
43#[derive(Debug, Clone, Default, Deserialize)]
44pub struct AllowlistEntry {
45    #[serde(default)]
46    pub exact: Option<String>,
47    #[serde(default)]
48    pub regex: Option<String>,
49}
50
51#[derive(Debug, Clone, Default, Deserialize)]
52pub struct ReportConfig {
53    #[serde(default)]
54    pub stderr_summary: bool,
55    #[serde(default)]
56    pub fail_on_match: bool,
57    #[serde(default)]
58    pub log_file: Option<std::path::PathBuf>,
59    /// Cap the scrub audit log to the most recent N entries. When set, the
60    /// log file is truncated after each write to retain only the newest
61    /// `max_entries` lines. `None` (default) means unbounded growth.
62    #[serde(default)]
63    pub max_entries: Option<usize>,
64}
65
66#[cfg(test)]
67mod tests {
68    use super::*;
69
70    #[test]
71    fn parses_minimal_config() {
72        let yaml = "version: 1";
73        let cfg: ScrubConfig = serde_yaml::from_str(yaml).unwrap();
74        assert_eq!(cfg.version, 1);
75        assert_eq!(cfg.builtins, BuiltinsMode::Extend);
76    }
77
78    #[test]
79    fn parses_full_example() {
80        // Mirrors the example committed at .cargo-context/scrub.yaml.
81        let yaml = r#"
82version: 1
83builtins: extend
84disable_builtins:
85  - jwt
86patterns:
87  - id: acme
88    regex: 'ACME_[A-Z0-9]{32}'
89    category: api_key
90    severity: high
91entropy:
92  enabled: true
93  min_length: 20
94  threshold: 4.5
95  context_keys:
96    - key
97    - secret
98paths:
99  redact_whole:
100    - "**/.env"
101  exclude:
102    - "**/test_fixtures/**"
103allowlist:
104  - exact: "sk-ant-api03-PUBLIC-DEMO"
105  - regex: '^AKIAEXAMPLE[0-9]+$'
106report:
107  stderr_summary: true
108  fail_on_match: false
109"#;
110        let cfg: ScrubConfig = serde_yaml::from_str(yaml).unwrap();
111        assert_eq!(cfg.version, 1);
112        assert_eq!(cfg.disable_builtins, vec!["jwt".to_string()]);
113        assert_eq!(cfg.patterns.len(), 1);
114        assert_eq!(cfg.patterns[0].id, "acme");
115        assert_eq!(cfg.entropy.context_keys.len(), 2);
116        assert_eq!(cfg.paths.redact_whole.len(), 1);
117        assert_eq!(cfg.allowlist.len(), 2);
118        assert!(cfg.report.stderr_summary);
119    }
120
121    #[test]
122    fn unknown_fields_are_rejected() {
123        let yaml = "version: 1\nnonsense_field: true\n";
124        let err = serde_yaml::from_str::<ScrubConfig>(yaml);
125        assert!(err.is_err(), "unknown top-level fields should reject");
126    }
127
128    #[test]
129    fn builtins_modes_parse() {
130        for (s, expected) in [
131            ("builtins: extend", BuiltinsMode::Extend),
132            ("builtins: replace", BuiltinsMode::Replace),
133            ("builtins: disable", BuiltinsMode::Disable),
134        ] {
135            let cfg: ScrubConfig = serde_yaml::from_str(s).unwrap();
136            assert_eq!(cfg.builtins, expected);
137        }
138    }
139}