Skip to main content

cloudiful_redactor/
types.rs

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