Skip to main content

redact_core/policy/
mod.rs

1// Copyright (c) 2026 Censgate LLC.
2// Licensed under the Business Source License 1.1 (BUSL-1.1).
3// See the LICENSE file in the project root for license details,
4// including the Additional Use Grant, Change Date, and Change License.
5
6use crate::types::{EntityType, RecognizerResult};
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10/// Policy for PII detection and anonymization
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Policy {
13    pub id: String,
14    pub name: String,
15    pub display_name: String,
16    pub organization_id: String,
17    pub status: PolicyStatus,
18    pub priority: u32,
19    pub description: String,
20    pub conditions: Vec<PolicyCondition>,
21    pub pattern_rules: Vec<PatternRule>,
22    pub redaction_config: RedactionConfig,
23    pub actions: PolicyActions,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "lowercase")]
28pub enum PolicyStatus {
29    Active,
30    Inactive,
31    Draft,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct PolicyCondition {
36    pub field: String,
37    pub operator: ConditionOperator,
38    pub value: serde_json::Value,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
42#[serde(rename_all = "lowercase")]
43pub enum ConditionOperator {
44    Equals,
45    NotEquals,
46    Contains,
47    NotContains,
48    GreaterThan,
49    LessThan,
50    Regex,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize)]
54pub struct PatternRule {
55    pub pattern_id: String,
56    pub name: String,
57    pub enabled: bool,
58    pub mode: String,
59    pub strategy: String,
60    pub confidence: f32,
61    pub replacement: String,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct RedactionConfig {
66    pub default_mode: String,
67    pub enabled_categories: Vec<String>,
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct PolicyActions {
72    pub action: String,
73    pub redact_fields: Vec<String>,
74}
75
76impl Policy {
77    /// Apply policy to filter and modify recognizer results
78    pub fn apply(&self, results: Vec<RecognizerResult>) -> Vec<RecognizerResult> {
79        if self.status != PolicyStatus::Active {
80            return results;
81        }
82
83        // Create a lookup map for pattern rules
84        let mut rule_map: HashMap<String, &PatternRule> = HashMap::new();
85        for rule in &self.pattern_rules {
86            if rule.enabled {
87                rule_map.insert(rule.pattern_id.clone(), rule);
88            }
89        }
90
91        // Filter and adjust results based on policy
92        results
93            .into_iter()
94            .filter_map(|mut result| {
95                let entity_id = result.entity_type.as_str();
96
97                // Check if this entity type has a rule
98                if let Some(rule) = rule_map.get(entity_id) {
99                    // Apply confidence threshold
100                    if result.score >= rule.confidence {
101                        // Update score based on policy
102                        result.score = result.score.max(rule.confidence);
103                        Some(result)
104                    } else {
105                        None
106                    }
107                } else {
108                    // No specific rule, keep result if above general threshold
109                    Some(result)
110                }
111            })
112            .collect()
113    }
114
115    /// Get entity types that should be detected based on policy
116    pub fn enabled_entity_types(&self) -> Vec<EntityType> {
117        self.pattern_rules
118            .iter()
119            .filter(|rule| rule.enabled)
120            .map(|rule| {
121                // Parse entity type from pattern_id
122                EntityType::from(rule.pattern_id.clone())
123            })
124            .collect()
125    }
126}
127
128#[cfg(test)]
129mod tests {
130    use super::*;
131
132    #[test]
133    fn test_policy_apply() {
134        let policy = Policy {
135            id: "test".to_string(),
136            name: "test".to_string(),
137            display_name: "Test Policy".to_string(),
138            organization_id: "org1".to_string(),
139            status: PolicyStatus::Active,
140            priority: 100,
141            description: "Test policy".to_string(),
142            conditions: vec![],
143            pattern_rules: vec![PatternRule {
144                pattern_id: "EMAIL_ADDRESS".to_string(),
145                name: "email".to_string(),
146                enabled: true,
147                mode: "replace".to_string(),
148                strategy: "semantic".to_string(),
149                confidence: 0.8,
150                replacement: "[EMAIL]".to_string(),
151            }],
152            redaction_config: RedactionConfig {
153                default_mode: "replace".to_string(),
154                enabled_categories: vec!["contact_info".to_string()],
155            },
156            actions: PolicyActions {
157                action: "redact".to_string(),
158                redact_fields: vec!["content".to_string()],
159            },
160        };
161
162        let results = vec![
163            RecognizerResult::new(EntityType::EmailAddress, 0, 10, 0.9, "test"),
164            RecognizerResult::new(EntityType::EmailAddress, 10, 20, 0.7, "test"),
165        ];
166
167        let filtered = policy.apply(results);
168
169        // Only the result with score >= 0.8 should remain
170        assert_eq!(filtered.len(), 1);
171        assert!(filtered[0].score >= 0.8);
172    }
173
174    #[test]
175    fn test_inactive_policy() {
176        let policy = Policy {
177            id: "test".to_string(),
178            name: "test".to_string(),
179            display_name: "Test Policy".to_string(),
180            organization_id: "org1".to_string(),
181            status: PolicyStatus::Inactive,
182            priority: 100,
183            description: "Test policy".to_string(),
184            conditions: vec![],
185            pattern_rules: vec![],
186            redaction_config: RedactionConfig {
187                default_mode: "replace".to_string(),
188                enabled_categories: vec![],
189            },
190            actions: PolicyActions {
191                action: "redact".to_string(),
192                redact_fields: vec![],
193            },
194        };
195
196        let results = vec![RecognizerResult::new(
197            EntityType::EmailAddress,
198            0,
199            10,
200            0.9,
201            "test",
202        )];
203
204        let filtered = policy.apply(results.clone());
205
206        // Inactive policy should not filter
207        assert_eq!(filtered.len(), results.len());
208    }
209}