///|
pub enum PrivacyClass {
DirectIdentifier
QuasiIdentifier
ContactIdentifier
AccountIdentifier
ContextualIdentifier
NonIdentifier
} derive(Debug, Eq, Compare)
///|
pub(all) struct PrivacySummary {
direct : Int
quasi : Int
contact : Int
account : Int
contextual : Int
non_identifier : Int
} derive(Debug, Eq)
///|
pub fn privacy_class(kind : PhiKind) -> PrivacyClass {
match kind {
IdNumber | MedicalRecord => DirectIdentifier
Date | Address | Organization => QuasiIdentifier
PersonName | Phone | Email => ContactIdentifier
Insurance => AccountIdentifier
Custom(name) =>
if name.contains("context") {
ContextualIdentifier
} else {
NonIdentifier
}
}
}
///|
pub fn privacy_class_name(class : PrivacyClass) -> String {
match class {
DirectIdentifier => "direct"
QuasiIdentifier => "quasi"
ContactIdentifier => "contact"
AccountIdentifier => "account"
ContextualIdentifier => "contextual"
NonIdentifier => "non-identifier"
}
}
///|
pub fn privacy_summary(findings : Array[Finding]) -> PrivacySummary {
let mut direct = 0
let mut quasi = 0
let mut contact = 0
let mut account = 0
let mut contextual = 0
let mut non_identifier = 0
for item in findings {
match privacy_class(item.kind) {
DirectIdentifier => direct += 1
QuasiIdentifier => quasi += 1
ContactIdentifier => contact += 1
AccountIdentifier => account += 1
ContextualIdentifier => contextual += 1
NonIdentifier => non_identifier += 1
}
}
{ direct, quasi, contact, account, contextual, non_identifier }
}
///|
pub fn privacy_summary_to_map(summary : PrivacySummary) -> Map[String, Int] {
{
"direct": summary.direct,
"quasi": summary.quasi,
"contact": summary.contact,
"account": summary.account,
"contextual": summary.contextual,
"non-identifier": summary.non_identifier,
}
}
///|
pub fn direct_findings(findings : Array[Finding]) -> Array[Finding] {
findings.filter(fn(item) { privacy_class(item.kind) == DirectIdentifier })
}
///|
pub fn quasi_findings(findings : Array[Finding]) -> Array[Finding] {
findings.filter(fn(item) { privacy_class(item.kind) == QuasiIdentifier })
}
///|
pub fn contact_findings(findings : Array[Finding]) -> Array[Finding] {
findings.filter(fn(item) { privacy_class(item.kind) == ContactIdentifier })
}
///|
pub fn account_findings(findings : Array[Finding]) -> Array[Finding] {
findings.filter(fn(item) { privacy_class(item.kind) == AccountIdentifier })
}
///|
pub fn privacy_class_counts(findings : Array[Finding]) -> Map[String, Int] {
let result : Map[String, Int] = Map([])
for item in findings {
let key = privacy_class_name(privacy_class(item.kind))
result[key] = result.get_or_default(key, 0) + 1
}
result
}
///|
pub fn high_risk_findings(findings : Array[Finding]) -> Array[Finding] {
findings.filter(fn(item) {
let risk = risk_level(item)
risk == High || risk == Critical
})
}
///|
pub fn contains_direct_identifier(findings : Array[Finding]) -> Bool {
!direct_findings(findings).is_empty()
}
///|
pub fn all_high_confidence(findings : Array[Finding], threshold : Int) -> Bool {
findings.all(fn(item) { item.confidence >= threshold })
}
///|
pub fn output_contains_original_identifier(result : DeidResult) -> Bool {
result.findings.any(fn(item) { result.text.contains(item.text) })
}
///|
pub fn output_is_redacted(result : DeidResult) -> Bool {
!output_contains_original_identifier(result)
}
///|
pub fn output_contract_issues(
input : String,
result : DeidResult,
) -> Array[String] {
let issues = []
if !validate_findings(input, result.findings).is_empty() {
issues.push("invalid findings")
}
if !offsets_are_monotonic(result.offsets) {
issues.push("non-monotonic offsets")
}
if !redaction_preserves_newlines(input, result) {
issues.push("newline count changed")
}
if result.audit.applied_count != result.offsets.length() {
issues.push("audit applied mismatch")
}
if !result.findings.is_empty() && !output_is_redacted(result) {
issues.push("original identifier remains")
}
issues
}
///|
pub fn output_contract_passes(input : String, result : DeidResult) -> Bool {
output_contract_issues(input, result).is_empty()
}
///|
pub fn audit_kind_count(report : AuditReport, kind : PhiKind) -> Int {
report.counts.get_or_default(phi_kind_name(kind), 0)
}
///|
pub fn audit_risk_count(report : AuditReport, level : RiskLevel) -> Int {
report.findings.filter(fn(item) { risk_level(item) == level }).length()
}
///|
pub fn audit_rule_ids(report : AuditReport) -> Array[String] {
let result = []
for item in report.findings {
if !result.contains(item.rule_id) {
result.push(item.rule_id)
}
}
result
}
///|
pub fn audit_finding_ids(report : AuditReport) -> Array[String] {
report.findings.map(fn(item) { item.id })
}
///|
pub fn audit_has_kind(report : AuditReport, kind : PhiKind) -> Bool {
report.findings.any(fn(item) { item.kind == kind })
}
///|
pub fn audit_has_rule(report : AuditReport, rule_id : String) -> Bool {
report.findings.any(fn(item) { item.rule_id == rule_id })
}
///|
pub fn audit_summary_json(report : AuditReport) -> String {
"{" +
"\"finding_count\":\{report.finding_count}," +
"\"applied_count\":\{report.applied_count}," +
"\"risk\":\{json_string("\{audit_risk_level(report)}")}," +
"\"coverage\":\{audit_offset_coverage(report)}" +
"}"
}