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}
60
61#[cfg(test)]
62mod tests {
63    use super::*;
64
65    #[test]
66    fn parses_minimal_config() {
67        let yaml = "version: 1";
68        let cfg: ScrubConfig = serde_yaml::from_str(yaml).unwrap();
69        assert_eq!(cfg.version, 1);
70        assert_eq!(cfg.builtins, BuiltinsMode::Extend);
71    }
72
73    #[test]
74    fn parses_full_example() {
75        // Mirrors the example committed at .cargo-context/scrub.yaml.
76        let yaml = r#"
77version: 1
78builtins: extend
79disable_builtins:
80  - jwt
81patterns:
82  - id: acme
83    regex: 'ACME_[A-Z0-9]{32}'
84    category: api_key
85    severity: high
86entropy:
87  enabled: true
88  min_length: 20
89  threshold: 4.5
90  context_keys:
91    - key
92    - secret
93paths:
94  redact_whole:
95    - "**/.env"
96  exclude:
97    - "**/test_fixtures/**"
98allowlist:
99  - exact: "sk-ant-api03-PUBLIC-DEMO"
100  - regex: '^AKIAEXAMPLE[0-9]+$'
101report:
102  stderr_summary: true
103  fail_on_match: false
104"#;
105        let cfg: ScrubConfig = serde_yaml::from_str(yaml).unwrap();
106        assert_eq!(cfg.version, 1);
107        assert_eq!(cfg.disable_builtins, vec!["jwt".to_string()]);
108        assert_eq!(cfg.patterns.len(), 1);
109        assert_eq!(cfg.patterns[0].id, "acme");
110        assert_eq!(cfg.entropy.context_keys.len(), 2);
111        assert_eq!(cfg.paths.redact_whole.len(), 1);
112        assert_eq!(cfg.allowlist.len(), 2);
113        assert!(cfg.report.stderr_summary);
114    }
115
116    #[test]
117    fn unknown_fields_are_rejected() {
118        let yaml = "version: 1\nnonsense_field: true\n";
119        let err = serde_yaml::from_str::<ScrubConfig>(yaml);
120        assert!(err.is_err(), "unknown top-level fields should reject");
121    }
122
123    #[test]
124    fn builtins_modes_parse() {
125        for (s, expected) in [
126            ("builtins: extend", BuiltinsMode::Extend),
127            ("builtins: replace", BuiltinsMode::Replace),
128            ("builtins: disable", BuiltinsMode::Disable),
129        ] {
130            let cfg: ScrubConfig = serde_yaml::from_str(s).unwrap();
131            assert_eq!(cfg.builtins, expected);
132        }
133    }
134}