///|
pub(all) struct Policy {
rules : Array[Rule]
fallback : RedactionAction
} derive(Eq, Debug)
///|
pub fn Policy::new(
rules : Array[Rule],
fallback? : RedactionAction = Replace("[REDACTED]"),
) -> Policy {
{ rules, fallback }
}
///|
pub fn Policy::secure_default() -> Policy {
Policy::new(
[
Rule::new("email", Mask(fill='*', keep_start=2, keep_end=3)),
Rule::new("cn-phone", Mask(fill='*', keep_start=3, keep_end=4)),
Rule::new("cn-identity", Mask(fill='*', keep_start=4, keep_end=4)),
Rule::new("bank-card", Mask(fill='*', keep_start=4, keep_end=4)),
Rule::new("ipv4", Replace("[IP]")),
Rule::new("high-entropy", Replace("[SECRET]"), minimum_severity=High),
],
fallback=Replace("[SECRET]"),
)
}
///|
fn action_for(policy : Policy, finding : Finding) -> RedactionAction {
for rule in policy.rules {
if rule.enabled &&
rule.detector_id == finding.detector_id &&
finding.severity.rank() >= rule.minimum_severity.rank() {
return rule.action
}
}
policy.fallback
}
///|
fn render_hash(original : String) -> String {
"[HASH:" + fnv1a(original).to_string(radix=16) + "]"
}
///|
fn apply_action(original : String, action : RedactionAction) -> String {
match action {
Replace(value) => value
Drop => ""
Hash => render_hash(original)
Mask(fill~, keep_start~, keep_end~) => {
let chars = original.to_array()
let left = if keep_start < 0 {
0
} else if keep_start > chars.length() {
chars.length()
} else {
keep_start
}
let available = chars.length() - left
let right = if keep_end < 0 {
0
} else if keep_end > available {
available
} else {
keep_end
}
slice_chars(chars, 0, left) +
repeat_char(fill, chars.length() - left - right) +
slice_chars(chars, chars.length() - right, chars.length())
}
}
}
///|
fn finding_priority(finding : Finding) -> Int {
match finding.kind {
Prefix => 50
Jwt => 45
Identity | BankCard => 40
Email | Phone => 35
Custom => 30
IPv4 => 20
HighEntropy => 10
}
}
///|
fn sort_findings(findings : Array[Finding]) -> Array[Finding] {
let sorted = findings.copy()
sorted.sort_by(fn(a, b) {
if a.span.start != b.span.start {
a.span.start.compare(b.span.start)
} else if a.span.end != b.span.end {
b.span.end.compare(a.span.end)
} else {
let kind_order = finding_priority(b).compare(finding_priority(a))
if kind_order != 0 {
kind_order
} else {
let severity_order = b.severity.rank().compare(a.severity.rank())
if severity_order != 0 {
severity_order
} else {
b.confidence.compare(a.confidence)
}
}
}
})
sorted
}
///|
fn remove_overlaps(findings : Array[Finding]) -> Array[Finding] {
let sorted = sort_findings(findings)
let out : Array[Finding] = []
let mut covered_until = -1
for finding in sorted {
if finding.span.start >= covered_until {
out.push(finding)
covered_until = finding.span.end
}
}
out
}
///|
pub fn redact(
text : String,
findings : Array[Finding],
policy? : Policy = Policy::secure_default(),
) -> String {
let chars = text.to_array()
let usable = remove_overlaps(findings)
let out = StringBuilder::new()
let mut cursor = 0
for finding in usable {
if finding.span.start < cursor || finding.span.end > chars.length() {
continue
}
out.write_string(slice_chars(chars, cursor, finding.span.start))
let original = slice_chars(chars, finding.span.start, finding.span.end)
out.write_string(apply_action(original, action_for(policy, finding)))
cursor = finding.span.end
}
out.write_string(slice_chars(chars, cursor, chars.length()))
out.to_string()
}