Skip to main content

redact_core/policy/
mod.rs

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