///|
pub(all) struct BaselineFinding {
fingerprint : String
rule : String
severity : String
source : String
sink : String
path : String
} derive(Eq, Debug, ToJson)
///|
pub(all) struct BaselineComparison {
baseline_count : Int
current_count : Int
new_findings : Array[BaselineFinding]
resolved_findings : Array[BaselineFinding]
unchanged_findings : Array[BaselineFinding]
severity_regressions : Array[BaselineFinding]
passed : Bool
} derive(Eq, Debug, ToJson)
///|
pub fn build_baseline(findings : Array[Finding]) -> Array[BaselineFinding] {
let baseline : Array[BaselineFinding] = []
for finding in findings {
baseline.push(baseline_finding(finding))
}
baseline
}
///|
pub fn compare_baseline(
baseline : Array[BaselineFinding],
current : Array[Finding],
) -> BaselineComparison {
let current_baseline = build_baseline(current)
let new_findings : Array[BaselineFinding] = []
let resolved_findings : Array[BaselineFinding] = []
let unchanged_findings : Array[BaselineFinding] = []
let severity_regressions : Array[BaselineFinding] = []
for item in current_baseline {
match baseline_item(baseline, item.fingerprint) {
Some(previous) => {
unchanged_findings.push(item)
if severity_rank(item.severity) > severity_rank(previous.severity) {
severity_regressions.push(item)
}
}
None => new_findings.push(item)
}
}
for item in baseline {
if baseline_item(current_baseline, item.fingerprint) is None {
resolved_findings.push(item)
}
}
{
baseline_count: baseline.length(),
current_count: current_baseline.length(),
new_findings,
resolved_findings,
unchanged_findings,
severity_regressions,
passed: new_findings.length() == 0 && severity_regressions.length() == 0,
}
}
///|
pub fn baseline_comparison_json(comparison : BaselineComparison) -> String {
comparison.to_json().stringify(indent=2)
}
///|
pub fn format_baseline_comparison(comparison : BaselineComparison) -> String {
let out = StringBuilder()
out.write_string("baseline=\{comparison.baseline_count}")
out.write_string(" current=\{comparison.current_count}")
out.write_string(" new=\{comparison.new_findings.length()}")
out.write_string(" resolved=\{comparison.resolved_findings.length()}")
out.write_string(
" severity_regressions=\{comparison.severity_regressions.length()}",
)
out.write_string(" passed=\{comparison.passed}")
append_baseline_lines(out, "new", comparison.new_findings)
append_baseline_lines(out, "resolved", comparison.resolved_findings)
append_baseline_lines(out, "regression", comparison.severity_regressions)
out.to_string()
}
///|
pub fn baseline_fingerprint(finding : Finding) -> String {
finding.rule +
"|" +
finding.source +
"|" +
finding.sink +
"|" +
finding.path.join("/")
}
///|
pub fn baseline_is_empty(baseline : Array[BaselineFinding]) -> Bool {
baseline.length() == 0
}
///|
pub fn baseline_count_by_severity(
baseline : Array[BaselineFinding],
severity : String,
) -> Int {
let mut count = 0
for item in baseline {
if item.severity == severity {
count += 1
}
}
count
}
///|
fn baseline_finding(finding : Finding) -> BaselineFinding {
{
fingerprint: baseline_fingerprint(finding),
rule: finding.rule,
severity: finding.severity,
source: finding.source,
sink: finding.sink,
path: finding.path.join(" -> "),
}
}
///|
fn baseline_item(
baseline : Array[BaselineFinding],
fingerprint : String,
) -> BaselineFinding? {
for item in baseline {
if item.fingerprint == fingerprint {
return Some(item)
}
}
None
}
///|
fn severity_rank(severity : String) -> Int {
if severity == "high" {
3
} else if severity == "medium" {
2
} else if severity == "low" {
1
} else {
0
}
}
///|
fn append_baseline_lines(
out : StringBuilder,
label : String,
items : Array[BaselineFinding],
) -> Unit {
for item in items {
out.write_string("\n\{label}: [\{item.severity}] \{item.path}")
out.write_string(" (\{item.fingerprint})")
}
}