Skip to main content

faucet_core/policy/
compile.rs

1//! Compiled form of a [`PolicySpec`]: regexes built once, labels resolvable
2//! by column name and by value.
3
4use super::spec::{PolicyRule, PolicySpec};
5use crate::masking::{Detector, detect};
6use regex::Regex;
7use std::collections::{BTreeSet, HashSet};
8
9/// One compiled classification.
10#[derive(Debug, Clone)]
11pub struct CompiledClassification {
12    pub label: String,
13    pub fields: HashSet<String>,
14    pub field_pattern: Option<Regex>,
15    pub value_detector: Option<Detector>,
16}
17
18/// A validated, compiled policy.
19#[derive(Debug, Clone)]
20pub struct CompiledPolicy {
21    classifications: Vec<CompiledClassification>,
22    rules: Vec<PolicyRule>,
23}
24
25impl CompiledPolicy {
26    /// Validate and compile. Every defect surfaces here, at config load.
27    pub fn compile(spec: &PolicySpec) -> Result<Self, String> {
28        spec.validate()?;
29        let classifications = spec
30            .classifications
31            .iter()
32            .map(|c| {
33                Ok(CompiledClassification {
34                    label: c.label.clone(),
35                    fields: c.fields.iter().cloned().collect(),
36                    field_pattern: match &c.field_pattern {
37                        Some(p) => Some(Regex::new(p).map_err(|e| e.to_string())?),
38                        None => None,
39                    },
40                    value_detector: c.value_detector,
41                })
42            })
43            .collect::<Result<Vec<_>, String>>()?;
44        Ok(Self {
45            classifications,
46            rules: spec.rules.clone(),
47        })
48    }
49
50    pub fn rules(&self) -> &[PolicyRule] {
51        &self.rules
52    }
53
54    pub fn classifications(&self) -> &[CompiledClassification] {
55        &self.classifications
56    }
57
58    /// Labels a column earns from its **name**: an exact `fields` entry
59    /// matches the full dot-path or its leaf key (`ssn` covers `user.ssn`),
60    /// `field_pattern` is tested against the full dot-path.
61    pub fn labels_for_name(&self, name: &str) -> BTreeSet<String> {
62        let leaf = name.rsplit('.').next().unwrap_or(name);
63        self.classifications
64            .iter()
65            .filter(|c| {
66                c.fields.contains(name)
67                    || c.fields.contains(leaf)
68                    || c.field_pattern.as_ref().is_some_and(|re| re.is_match(name))
69            })
70            .map(|c| c.label.clone())
71            .collect()
72    }
73
74    /// Labels a scalar string **value** earns from the value detectors.
75    pub fn labels_for_value(&self, value: &str) -> BTreeSet<String> {
76        self.classifications
77            .iter()
78            .filter(|c| c.value_detector.is_some_and(|d| detect::detects(d, value)))
79            .map(|c| c.label.clone())
80            .collect()
81    }
82
83    /// [`labels_for_value`](Self::labels_for_value) over any JSON scalar: a
84    /// number is classified by the card detector too, as masking does.
85    pub fn labels_for_scalar(&self, value: &serde_json::Value) -> BTreeSet<String> {
86        self.classifications
87            .iter()
88            .filter(|c| {
89                c.value_detector
90                    .is_some_and(|d| detect::detects_value(d, value))
91            })
92            .map(|c| c.label.clone())
93            .collect()
94    }
95
96    /// The value-detector classifications (the runtime backstop's work list).
97    pub fn value_detectors(&self) -> impl Iterator<Item = (&str, Detector)> {
98        self.classifications
99            .iter()
100            .filter_map(|c| c.value_detector.map(|d| (c.label.as_str(), d)))
101    }
102
103    /// Whether any rule governs `label` at all.
104    pub fn governs(&self, label: &str) -> bool {
105        self.rules.iter().any(|r| r.when.label == label)
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use serde_json::json;
113
114    fn policy() -> CompiledPolicy {
115        let spec: PolicySpec = serde_json::from_value(json!({
116            "classifications": [
117                {"label": "pii", "fields": ["email", "user.phone"], "value_detector": "email"},
118                {"label": "pii", "field_pattern": "(?i)ssn$", "value_detector": "ssn"},
119                {"label": "finance", "field_pattern": "^amount"}
120            ],
121            "rules": [
122                {"name": "r", "when": {"label": "pii"}, "deny": true}
123            ]
124        }))
125        .unwrap();
126        CompiledPolicy::compile(&spec).unwrap()
127    }
128
129    #[test]
130    fn labels_by_name_and_pattern() {
131        let p = policy();
132        assert_eq!(p.labels_for_name("email"), ["pii".to_string()].into());
133        assert_eq!(p.labels_for_name("user.phone"), ["pii".to_string()].into());
134        assert_eq!(
135            p.labels_for_name("contact.email"),
136            ["pii".to_string()].into(),
137            "a bare field name covers the leaf key"
138        );
139        assert_eq!(
140            p.labels_for_name("customer_SSN"),
141            ["pii".to_string()].into()
142        );
143        assert_eq!(
144            p.labels_for_name("amount_cents"),
145            ["finance".to_string()].into()
146        );
147        assert!(p.labels_for_name("id").is_empty());
148    }
149
150    #[test]
151    fn labels_by_value_use_the_masking_detectors() {
152        let p = policy();
153        assert_eq!(p.labels_for_value("a@b.io"), ["pii".to_string()].into());
154        assert_eq!(
155            p.labels_for_value("123-45-6789"),
156            ["pii".to_string()].into()
157        );
158        assert!(p.labels_for_value("hello").is_empty());
159        assert_eq!(p.value_detectors().count(), 2);
160        assert!(p.governs("pii") && !p.governs("finance"));
161        assert_eq!(p.rules().len(), 1);
162        assert_eq!(p.classifications().len(), 3);
163    }
164
165    #[test]
166    fn compile_rejects_an_invalid_spec() {
167        let spec: PolicySpec = serde_json::from_value(json!({
168            "classifications": [{"label": "x", "field_pattern": "("}],
169            "rules": []
170        }))
171        .unwrap();
172        assert!(CompiledPolicy::compile(&spec).is_err());
173    }
174}