Skip to main content

provide_telemetry/
classification.rs

1// SPDX-FileCopyrightText: Copyright (C) 2026 provide.io llc
2// SPDX-License-Identifier: Apache-2.0
3// SPDX-Comment: Part of provide-telemetry.
4//
5
6use std::sync::{Mutex, OnceLock};
7
8#[derive(Clone, Debug, PartialEq, Eq)]
9pub enum DataClass {
10    Public,
11    Internal,
12    Pii,
13    Phi,
14    Pci,
15    Secret,
16}
17
18impl DataClass {
19    pub fn as_str(&self) -> &'static str {
20        match self {
21            Self::Public => "PUBLIC",
22            Self::Internal => "INTERNAL",
23            Self::Pii => "PII",
24            Self::Phi => "PHI",
25            Self::Pci => "PCI",
26            Self::Secret => "SECRET", // pragma: allowlist secret
27        }
28    }
29}
30
31#[derive(Clone, Debug, PartialEq, Eq)]
32pub struct ClassificationRule {
33    pub pattern: String,
34    pub classification: DataClass,
35}
36
37impl ClassificationRule {
38    pub fn new(pattern: impl Into<String>, classification: DataClass) -> Self {
39        Self {
40            pattern: pattern.into(),
41            classification,
42        }
43    }
44}
45
46#[derive(Clone, Debug, PartialEq, Eq)]
47pub struct ClassificationPolicy {
48    pub public: String,
49    pub internal: String,
50    pub pii: String,
51    pub phi: String,
52    pub pci: String,
53    pub secret: String,
54}
55
56impl Default for ClassificationPolicy {
57    fn default() -> Self {
58        Self {
59            public: "pass".to_string(),
60            internal: "pass".to_string(),
61            pii: "redact".to_string(),
62            phi: "drop".to_string(),
63            pci: "hash".to_string(),
64            secret: "drop".to_string(), // pragma: allowlist secret
65        }
66    }
67}
68
69static POLICY: OnceLock<Mutex<ClassificationPolicy>> = OnceLock::new();
70
71fn policy() -> &'static Mutex<ClassificationPolicy> {
72    POLICY.get_or_init(|| Mutex::new(ClassificationPolicy::default()))
73}
74
75pub fn set_classification_policy(p: ClassificationPolicy) {
76    *policy()
77        .lock()
78        .expect("classification policy lock poisoned") = p;
79}
80
81pub fn get_classification_policy() -> ClassificationPolicy {
82    policy()
83        .lock()
84        .expect("classification policy lock poisoned")
85        .clone()
86}
87
88static RULES: OnceLock<Mutex<Vec<ClassificationRule>>> = OnceLock::new();
89
90fn rules() -> &'static Mutex<Vec<ClassificationRule>> {
91    RULES.get_or_init(|| Mutex::new(Vec::new()))
92}
93
94fn match_glob(pattern: &str, key: &str) -> bool {
95    if let Some((prefix, "")) = pattern.split_once('*') {
96        return key.starts_with(prefix);
97    }
98    pattern == key
99}
100
101pub fn register_classification_rule(rule: ClassificationRule) {
102    rules()
103        .lock()
104        .expect("classification lock poisoned")
105        .push(rule);
106}
107
108pub fn register_classification_rules(next: Vec<ClassificationRule>) {
109    rules()
110        .lock()
111        .expect("classification lock poisoned")
112        .extend(next);
113}
114
115pub fn clear_classification_rules() {
116    rules()
117        .lock()
118        .expect("classification lock poisoned")
119        .clear();
120}
121
122pub fn classify_key(key: &str) -> Option<String> {
123    rules()
124        .lock()
125        .expect("classification lock poisoned")
126        .iter()
127        .find(|rule| match_glob(&rule.pattern, key))
128        .map(|rule| rule.classification.as_str().to_string())
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134    use crate::testing::acquire_test_state_lock;
135
136    #[test]
137    fn classification_test_clear_rules_removes_registered_matches() {
138        let _guard = acquire_test_state_lock();
139        clear_classification_rules();
140        register_classification_rule(ClassificationRule::new("email*", DataClass::Pii));
141        assert_eq!(classify_key("email_address").as_deref(), Some("PII"));
142
143        clear_classification_rules();
144
145        assert_eq!(classify_key("email_address"), None);
146    }
147}