Skip to main content

chio_guards/response_sanitization/
types.rs

1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4
5/// Category of a sensitive data finding.
6#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
7#[serde(rename_all = "snake_case")]
8pub enum SensitiveCategory {
9    Secret,
10    Pii,
11    Internal,
12    Custom(String),
13}
14
15/// Redaction strategy applied to a finding.
16#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
17#[serde(rename_all = "snake_case")]
18pub enum RedactionStrategy {
19    /// Replace the match with a constant mask (`****`).
20    Mask,
21    /// Replace the match with a stable fingerprint (sha256 prefix).
22    Fingerprint,
23    /// Drop the match entirely (replace with empty text; at the JSON-field
24    /// level the whole field is replaced with `null`).
25    Drop,
26    /// Replace the match with an opaque token id and record the mapping.
27    Tokenize,
28    /// Keep a small prefix/suffix, redact the middle.
29    Partial,
30    /// Replace with a typed label (`[REDACTED:email]`).
31    TypeLabel,
32    /// Do not redact.
33    Keep,
34}
35
36/// Byte span in the sanitized text.
37#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
38pub struct Span {
39    pub start: usize,
40    pub end: usize,
41}
42
43/// A single sensitive-data finding.
44#[derive(Clone, Debug, Serialize, Deserialize)]
45pub struct SensitiveDataFinding {
46    pub id: String,
47    pub category: SensitiveCategory,
48    pub data_type: String,
49    pub confidence: f32,
50    pub span: Span,
51    pub preview: String,
52    pub detector: String,
53    pub recommended_action: RedactionStrategy,
54}
55
56/// Record of a redaction that was actually applied.
57#[derive(Clone, Debug, Serialize, Deserialize)]
58pub struct Redaction {
59    pub finding_id: String,
60    pub strategy: RedactionStrategy,
61    pub original_span: Span,
62    pub replacement: String,
63}
64
65/// Processing statistics for a single sanitization run.
66#[derive(Clone, Debug, Default, Serialize, Deserialize)]
67pub struct ProcessingStats {
68    pub input_length: usize,
69    pub output_length: usize,
70    pub findings_count: usize,
71    pub redactions_count: usize,
72}
73
74/// Result of sanitizing a single string.
75#[derive(Clone, Debug, Serialize, Deserialize)]
76pub struct SanitizationResult {
77    pub sanitized: String,
78    pub was_redacted: bool,
79    pub findings: Vec<SensitiveDataFinding>,
80    pub redactions: Vec<Redaction>,
81    pub stats: ProcessingStats,
82}
83
84/// Category enable/disable toggles.
85#[derive(Clone, Debug, Serialize, Deserialize)]
86pub struct CategoryConfig {
87    pub secrets: bool,
88    pub pii: bool,
89    pub internal: bool,
90}
91
92impl Default for CategoryConfig {
93    fn default() -> Self {
94        Self {
95            secrets: true,
96            pii: true,
97            internal: true,
98        }
99    }
100}
101
102/// High-entropy token detector configuration.
103#[derive(Clone, Debug, Serialize, Deserialize)]
104pub struct EntropyConfig {
105    pub enabled: bool,
106    pub threshold: f64,
107    pub min_token_len: usize,
108}
109
110impl Default for EntropyConfig {
111    fn default() -> Self {
112        Self {
113            enabled: true,
114            threshold: 4.5,
115            min_token_len: 16,
116        }
117    }
118}
119
120/// Allowlist configuration (false-positive reduction).
121#[derive(Clone, Debug, Default, Serialize, Deserialize)]
122pub struct AllowlistConfig {
123    pub exact: Vec<String>,
124    pub patterns: Vec<String>,
125}
126
127/// Denylist configuration (forced redaction).
128#[derive(Clone, Debug, Default, Serialize, Deserialize)]
129pub struct DenylistConfig {
130    pub exact: Vec<String>,
131    pub patterns: Vec<String>,
132}
133
134/// Output sanitizer configuration.
135#[derive(Clone, Debug, Serialize, Deserialize)]
136pub struct OutputSanitizerConfig {
137    pub categories: CategoryConfig,
138    pub redaction_strategies: HashMap<SensitiveCategory, RedactionStrategy>,
139    pub entropy: EntropyConfig,
140    pub allowlist: AllowlistConfig,
141    pub denylist: DenylistConfig,
142    pub max_input_bytes: usize,
143    pub include_findings: bool,
144}
145
146impl Default for OutputSanitizerConfig {
147    fn default() -> Self {
148        let mut redaction_strategies = HashMap::new();
149        redaction_strategies.insert(SensitiveCategory::Secret, RedactionStrategy::Mask);
150        redaction_strategies.insert(SensitiveCategory::Pii, RedactionStrategy::Partial);
151        redaction_strategies.insert(SensitiveCategory::Internal, RedactionStrategy::TypeLabel);
152        Self {
153            categories: CategoryConfig::default(),
154            redaction_strategies,
155            entropy: EntropyConfig::default(),
156            allowlist: AllowlistConfig::default(),
157            denylist: DenylistConfig::default(),
158            max_input_bytes: 1_000_000,
159            include_findings: true,
160        }
161    }
162}
163
164#[derive(Debug, thiserror::Error)]
165pub enum OutputSanitizerConfigError {
166    #[error("invalid {list_name} regex `{pattern}`: {source}")]
167    InvalidPattern {
168        list_name: &'static str,
169        pattern: String,
170        #[source]
171        source: regex::Error,
172    },
173}
174
175/// Output of `OutputSanitizer::sanitize_value`.
176#[derive(Debug, Clone)]
177pub struct SanitizedValue {
178    pub value: serde_json::Value,
179    pub findings: Vec<SensitiveDataFinding>,
180    pub redactions: Vec<Redaction>,
181    pub was_redacted: bool,
182}