Skip to main content

cloudiful_redactor/
types.rs

1use serde::{Deserialize, Serialize};
2use std::ops::Range;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
5#[serde(rename_all = "snake_case")]
6pub enum FindingKind {
7    Secret,
8    Domain,
9    Url,
10    Email,
11    Ip,
12    Cidr,
13    Phone,
14    Person,
15    Organization,
16    CustomString,
17    CustomFile,
18}
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(default)]
22pub struct RedactionRules {
23    pub secret: bool,
24    pub domain: bool,
25    pub url: bool,
26    pub email: bool,
27    pub ip: bool,
28    pub cidr: bool,
29    pub phone: bool,
30    pub person: bool,
31    pub organization: bool,
32}
33
34impl Default for RedactionRules {
35    fn default() -> Self {
36        Self {
37            secret: false,
38            domain: false,
39            url: false,
40            email: true,
41            ip: true,
42            cidr: true,
43            phone: false,
44            person: false,
45            organization: false,
46        }
47    }
48}
49
50impl RedactionRules {
51    pub fn with_kind(mut self, kind: FindingKind, enabled: bool) -> Self {
52        self.set_kind(kind, enabled);
53        self
54    }
55
56    pub fn set_kind(&mut self, kind: FindingKind, enabled: bool) {
57        match kind {
58            FindingKind::Secret => self.secret = enabled,
59            FindingKind::Domain => self.domain = enabled,
60            FindingKind::Url => self.url = enabled,
61            FindingKind::Email => self.email = enabled,
62            FindingKind::Ip => self.ip = enabled,
63            FindingKind::Cidr => self.cidr = enabled,
64            FindingKind::Phone => self.phone = enabled,
65            FindingKind::Person => self.person = enabled,
66            FindingKind::Organization => self.organization = enabled,
67            FindingKind::CustomString | FindingKind::CustomFile => {}
68        }
69    }
70
71    pub fn is_enabled(self, kind: FindingKind) -> bool {
72        match kind {
73            FindingKind::Secret => self.secret,
74            FindingKind::Domain => self.domain,
75            FindingKind::Url => self.url,
76            FindingKind::Email => self.email,
77            FindingKind::Ip => self.ip,
78            FindingKind::Cidr => self.cidr,
79            FindingKind::Phone => self.phone,
80            FindingKind::Person => self.person,
81            FindingKind::Organization => self.organization,
82            FindingKind::CustomString | FindingKind::CustomFile => true,
83        }
84    }
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
88#[serde(rename_all = "snake_case")]
89pub enum CustomStringMatch {
90    #[default]
91    Exact,
92    Contains,
93    Regex,
94}
95
96#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
97#[serde(rename_all = "snake_case")]
98pub enum CustomStringScope {
99    #[default]
100    Text,
101    Line,
102}
103
104#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
105pub struct CustomStringRule {
106    pub pattern: String,
107    #[serde(default)]
108    pub match_type: CustomStringMatch,
109    #[serde(default)]
110    pub scope: CustomStringScope,
111}
112
113#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
114pub struct CustomFileRule {
115    pub path: String,
116}
117
118#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
119pub struct RedactionPolicy {
120    #[serde(flatten)]
121    pub rules: RedactionRules,
122    #[serde(default)]
123    pub custom_strings: Vec<CustomStringRule>,
124    #[serde(default)]
125    pub custom_files: Vec<CustomFileRule>,
126}
127
128impl RedactionPolicy {
129    pub fn with_kind(mut self, kind: FindingKind, enabled: bool) -> Self {
130        self.rules.set_kind(kind, enabled);
131        self
132    }
133
134    pub fn with_custom_string(mut self, rule: CustomStringRule) -> Self {
135        self.custom_strings.push(rule);
136        self
137    }
138
139    pub fn with_custom_file(mut self, rule: CustomFileRule) -> Self {
140        self.custom_files.push(rule);
141        self
142    }
143
144    pub fn with_custom_strings<I: IntoIterator<Item = CustomStringRule>>(
145        mut self,
146        rules: I,
147    ) -> Self {
148        self.custom_strings.extend(rules);
149        self
150    }
151
152    pub fn with_custom_files<I: IntoIterator<Item = CustomFileRule>>(mut self, rules: I) -> Self {
153        self.custom_files.extend(rules);
154        self
155    }
156
157    pub fn validate(&self) -> Result<(), String> {
158        for (index, rule) in self.custom_strings.iter().enumerate() {
159            if rule.pattern.is_empty() {
160                return Err(format!(
161                    "custom_strings[{index}]: pattern must not be empty"
162                ));
163            }
164            if matches!(rule.match_type, CustomStringMatch::Regex)
165                && regex::Regex::new(&rule.pattern).is_err()
166            {
167                return Err(format!(
168                    "custom_strings[{index}]: invalid regex pattern: {}",
169                    rule.pattern
170                ));
171            }
172        }
173        for (index, rule) in self.custom_files.iter().enumerate() {
174            if rule.path.is_empty() {
175                return Err(format!("custom_files[{index}]: path must not be empty"));
176            }
177        }
178        Ok(())
179    }
180}
181
182impl From<RedactionRules> for RedactionPolicy {
183    fn from(rules: RedactionRules) -> Self {
184        Self {
185            rules,
186            custom_strings: Vec::new(),
187            custom_files: Vec::new(),
188        }
189    }
190}
191
192#[derive(Debug, Clone, Copy, PartialEq, Eq)]
193struct FindingKindMeta {
194    label: &'static str,
195    token_label: &'static str,
196    priority: u8,
197    containment_priority: u8,
198}
199
200impl FindingKind {
201    const fn meta(self) -> FindingKindMeta {
202        match self {
203            Self::Secret => FindingKindMeta {
204                label: "secret",
205                token_label: "SECRET",
206                priority: 100,
207                containment_priority: 75,
208            },
209            Self::Domain => FindingKindMeta {
210                label: "domain",
211                token_label: "DOMAIN",
212                priority: 70,
213                containment_priority: 80,
214            },
215            Self::Url => FindingKindMeta {
216                label: "url",
217                token_label: "URL",
218                priority: 90,
219                containment_priority: 100,
220            },
221            Self::Email => FindingKindMeta {
222                label: "email",
223                token_label: "EMAIL",
224                priority: 85,
225                containment_priority: 95,
226            },
227            Self::Ip => FindingKindMeta {
228                label: "ip",
229                token_label: "IP",
230                priority: 75,
231                containment_priority: 85,
232            },
233            Self::Cidr => FindingKindMeta {
234                label: "cidr",
235                token_label: "CIDR",
236                priority: 80,
237                containment_priority: 90,
238            },
239            Self::Phone => FindingKindMeta {
240                label: "phone",
241                token_label: "PHONE",
242                priority: 60,
243                containment_priority: 70,
244            },
245            Self::Person => FindingKindMeta {
246                label: "person",
247                token_label: "PERSON",
248                priority: 50,
249                containment_priority: 50,
250            },
251            Self::Organization => FindingKindMeta {
252                label: "organization",
253                token_label: "ORG",
254                priority: 45,
255                containment_priority: 45,
256            },
257            Self::CustomString => FindingKindMeta {
258                label: "custom_string",
259                token_label: "CSTR",
260                priority: 95,
261                containment_priority: 40,
262            },
263            Self::CustomFile => FindingKindMeta {
264                label: "custom_file",
265                token_label: "FILE",
266                priority: 99,
267                containment_priority: 99,
268            },
269        }
270    }
271
272    pub fn label(self) -> &'static str {
273        self.meta().label
274    }
275
276    pub fn token_label(self) -> &'static str {
277        self.meta().token_label
278    }
279
280    pub(crate) fn from_token_label(label: &str) -> Option<Self> {
281        match label {
282            "SECRET" => Some(Self::Secret),
283            "DOMAIN" => Some(Self::Domain),
284            "URL" => Some(Self::Url),
285            "EMAIL" => Some(Self::Email),
286            "IP" => Some(Self::Ip),
287            "CIDR" => Some(Self::Cidr),
288            "PHONE" => Some(Self::Phone),
289            "PERSON" => Some(Self::Person),
290            "ORG" => Some(Self::Organization),
291            "CSTR" => Some(Self::CustomString),
292            "FILE" => Some(Self::CustomFile),
293            _ => None,
294        }
295    }
296
297    pub fn priority(self) -> u8 {
298        self.meta().priority
299    }
300
301    pub fn containment_priority(self) -> u8 {
302        self.meta().containment_priority
303    }
304}
305
306#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
307#[serde(rename_all = "snake_case")]
308pub enum FindingSource {
309    Rule,
310    Llm,
311}
312
313impl FindingSource {
314    pub fn bonus(self) -> u8 {
315        match self {
316            Self::Rule => 10,
317            Self::Llm => 0,
318        }
319    }
320}
321
322#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
323pub struct Finding {
324    pub kind: FindingKind,
325    pub source: FindingSource,
326    pub match_text: String,
327    pub normalized_key: String,
328    pub confidence: u8,
329    pub start: usize,
330    pub end: usize,
331}
332
333impl Finding {
334    pub fn range(&self) -> Range<usize> {
335        self.start..self.end
336    }
337
338    pub fn score(&self) -> u16 {
339        u16::from(self.kind.priority())
340            + u16::from(self.source.bonus())
341            + u16::from(self.confidence)
342    }
343}
344
345#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
346#[serde(rename_all = "snake_case")]
347pub enum ReplacementStrategy {
348    StructuredToken,
349}
350
351#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
352pub struct AppliedReplacement {
353    pub kind: FindingKind,
354    #[serde(skip_serializing)]
355    pub original: String,
356    pub replacement: String,
357    pub strategy: ReplacementStrategy,
358    pub display_value: Option<String>,
359}
360
361#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
362pub struct RedactionStats {
363    pub total_findings: usize,
364    pub applied_replacements: usize,
365    pub dropped_findings: usize,
366    pub llm_configured: bool,
367    pub llm_request_failed: bool,
368    pub llm_candidates_accepted: usize,
369    pub llm_candidates_rejected: usize,
370    pub llm_error: Option<String>,
371}
372
373#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
374pub struct RedactionResult {
375    pub redacted_text: String,
376    pub findings: Vec<Finding>,
377    pub applied_replacements: Vec<AppliedReplacement>,
378    pub stats: RedactionStats,
379}
380
381#[derive(Debug, Clone, PartialEq, Eq)]
382pub struct RedactionArtifact {
383    pub result: RedactionResult,
384    pub session: RedactionSession,
385}
386
387#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
388pub struct RestorationEntry {
389    pub token: String,
390    pub kind: FindingKind,
391    pub original: String,
392    pub replacement_hint: Option<String>,
393    pub occurrences: usize,
394}
395
396#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
397pub struct RedactionSession {
398    pub version: u32,
399    pub session_id: String,
400    pub scope_id: String,
401    pub external_id: Option<String>,
402    pub fingerprint: String,
403    pub redacted_fingerprint: String,
404    pub redacted_text: String,
405    #[serde(default)]
406    pub policy: RedactionPolicy,
407    pub entries: Vec<RestorationEntry>,
408}
409
410#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
411pub struct RestoreResult {
412    pub restored_text: String,
413    pub restored_count: usize,
414    pub unresolved_tokens: Vec<String>,
415    pub validation_errors: Vec<String>,
416}
417
418impl RestoreResult {
419    pub fn is_valid(&self) -> bool {
420        self.validation_errors.is_empty() && self.unresolved_tokens.is_empty()
421    }
422}
423
424#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
425pub struct SessionEntrySummary {
426    pub token: String,
427    pub kind: FindingKind,
428    pub replacement_hint: Option<String>,
429    pub occurrences: usize,
430}
431
432#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
433pub struct SessionSummary {
434    pub version: u32,
435    pub session_id: String,
436    pub scope_id: String,
437    pub external_id: Option<String>,
438    pub fingerprint: String,
439    pub redacted_fingerprint: String,
440    pub entry_count: usize,
441    pub entries: Vec<SessionEntrySummary>,
442}