use std::collections::HashMap;
use sha2::{Digest, Sha256};
use crate::error::CorpFinanceError;
use crate::security::scan_for_pii;
use crate::security::types::{Finding, FindingCategory, PiiCategory};
use crate::CorpFinanceResult;
use super::types::{PIIRedactionPolicy, RedactionAction, TrustTier};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RedactionResult {
pub redacted_text: String,
pub findings_count: usize,
pub action_taken: RedactionAction,
pub by_category: HashMap<PiiCategory, usize>,
}
pub fn default_policy_for_tier(tier: TrustTier) -> PIIRedactionPolicy {
let all = PiiCategory::ALL.to_vec();
match tier {
TrustTier::Open => PIIRedactionPolicy {
tier,
action: RedactionAction::Block,
categories: all,
},
TrustTier::Verified => PIIRedactionPolicy {
tier,
action: RedactionAction::Redact,
categories: all,
},
TrustTier::Trusted => PIIRedactionPolicy {
tier,
action: RedactionAction::Hash,
categories: vec![
PiiCategory::Ssn,
PiiCategory::Ein,
PiiCategory::CreditCard,
PiiCategory::Iban,
],
},
}
}
pub fn apply_policy(text: &str, policy: &PIIRedactionPolicy) -> CorpFinanceResult<RedactionResult> {
let findings = scan_for_pii(text);
let mut by_category: HashMap<PiiCategory, usize> = HashMap::new();
let mut covered_findings: Vec<Finding> = Vec::with_capacity(findings.len());
for f in findings {
if let FindingCategory::Pii(cat) = f.category {
if policy.categories.contains(&cat) {
*by_category.entry(cat).or_insert(0) += 1;
covered_findings.push(f);
}
}
}
covered_findings.sort_by(|a, b| b.span_start.cmp(&a.span_start));
let mut buf = text.to_string();
for f in &covered_findings {
if f.span_start > buf.len() || f.span_end > buf.len() || f.span_start > f.span_end {
return Err(CorpFinanceError::InvalidInput {
field: "finding span".to_string(),
reason: format!(
"span [{}, {}] exceeds buffer length {}",
f.span_start,
f.span_end,
buf.len()
),
});
}
let original = &buf[f.span_start..f.span_end].to_string();
let replacement = redact_text(original, f, policy.action);
buf.replace_range(f.span_start..f.span_end, &replacement);
}
Ok(RedactionResult {
redacted_text: buf,
findings_count: covered_findings.len(),
action_taken: policy.action,
by_category,
})
}
pub fn redact_text(original: &str, finding: &Finding, action: RedactionAction) -> String {
let category_label = match finding.category {
FindingCategory::Pii(c) => c.as_str(),
FindingCategory::Injection(k) => k.as_str(),
};
match action {
RedactionAction::Block => format!("[BLOCKED:{}]", category_label),
RedactionAction::Redact => finding
.redaction_proposal
.clone()
.unwrap_or_else(|| "[REDACTED]".to_string()),
RedactionAction::Hash => {
let mut hasher = Sha256::new();
hasher.update(original.as_bytes());
let hex = format!("{:x}", hasher.finalize());
format!("sha256:{}", &hex[..12])
}
RedactionAction::Pass => original.to_string(),
}
}