///|
/// A transparent score contribution from one detector.
pub(all) struct ScoreFactor {
rule_id : String
points : Int
reason : String
}
///|
pub(all) struct RiskScore {
subject_id : String
total : Int
factors : Array[ScoreFactor]
band : AlertSeverity
}
///|
pub fn ScoreFactor::new(
rule_id : String,
points : Int,
reason : String,
) -> ScoreFactor {
{ rule_id, points, reason }
}
///|
pub fn RiskScore::empty(subject_id : String) -> RiskScore {
{ subject_id, total: 0, factors: [], band: Info }
}
///|
pub fn RiskScore::add(self : RiskScore, factor : ScoreFactor) -> RiskScore {
let total = self.total + factor.points
let factors : Array[ScoreFactor] = []
for old in self.factors {
factors.push(old)
}
factors.push(factor)
{ ..self, total, factors, band: AlertSeverity::from_score(total) }
}
///|
pub fn RiskScore::capped(self : RiskScore, cap : Int) -> RiskScore {
let total = if self.total > cap { cap } else { self.total }
{ ..self, total, band: AlertSeverity::from_score(total) }
}
///|
pub fn score_alert(alert : Alert) -> RiskScore {
let score = RiskScore::empty(alert.transaction_id)
let factor = ScoreFactor::new(
alert.rule_id,
10 + alert.evidence.length() * 5,
"detector matched",
)
score.add(factor).capped(100)
}
///|
pub fn aggregate_scores(alerts : Array[Alert]) -> Array[RiskScore] {
let result : Array[RiskScore] = []
for alert in alerts {
let scored = score_alert(alert)
let mut merged = false
let mut index = 0
for old in result {
if old.subject_id == scored.subject_id {
result[index] = merge_scores(old, scored)
merged = true
}
index += 1
}
if !merged {
result.push(scored)
}
}
result
}
///|
fn merge_scores(left : RiskScore, right : RiskScore) -> RiskScore {
let total = if left.total + right.total > 100 {
100
} else {
left.total + right.total
}
let factors : Array[ScoreFactor] = []
for factor in left.factors {
factors.push(factor)
}
for factor in right.factors {
factors.push(factor)
}
{
subject_id: left.subject_id,
total,
factors,
band: AlertSeverity::from_score(total),
}
}
///|
pub fn score_rules(
rules : Array[Rule],
transactions : Array[Transaction],
) -> Array[RiskScore] {
aggregate_scores(evaluate(rules, transactions))
}
///|
pub fn highest_score(scores : Array[RiskScore]) -> RiskScore? {
if scores.length() == 0 {
None
} else {
let mut best = scores[0]
for score in scores {
if score.total > best.total {
best = score
}
}
Some(best)
}
}