// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2026 clbbbb
///|
pub struct AuditFinding {
path : String
kind : String
detail : String
} derive(Debug, Eq)
///|
pub fn audit_finding(
path : String,
kind : String,
detail : String,
) -> AuditFinding {
{ path, kind, detail }
}
///|
pub fn finding_key(finding : AuditFinding) -> String {
finding.path + "|" + finding.kind + "|" + finding.detail
}
///|
pub fn findings_from_sources(
paths : Array[String],
sources : Array[String],
policy : Policy,
) -> Array[AuditFinding] {
let rows : Array[AuditFinding] = []
for item in scan_sources(paths, sources) {
if !item.found {
rows.push(
audit_finding(item.path, "missing-spdx", "SPDX header is missing"),
)
} else if !item.valid {
rows.push(audit_finding(item.path, "invalid-spdx", item.message))
} else {
for error in policy_errors(item.expression, policy) {
rows.push(audit_finding(item.path, "policy", error))
}
}
}
rows
}
///|
pub fn findings_from_project(audit : ProjectAudit) -> Array[AuditFinding] {
let rows : Array[AuditFinding] = []
for error in audit.policy_errors {
rows.push(audit_finding("moon.mod", "policy", error))
}
for warning in audit.warnings {
rows.push(audit_finding("project", "warning", warning))
}
rows
}
///|
pub fn serialize_baseline(findings : Array[AuditFinding]) -> String {
let rows : Array[String] = [
"# moonbit-license-audit baseline", "# path|kind|detail",
]
for finding in findings {
rows.push(finding_key(finding))
}
join_lines(rows)
}
///|
pub fn parse_baseline(text : String) -> Array[AuditFinding] {
let rows : Array[AuditFinding] = []
for raw in split_char(text, '\n') {
let line = trim_ascii(raw)
if line != "" && !line.has_prefix("#") {
match parse_baseline_line(line) {
Some(finding) => rows.push(finding)
None => ()
}
}
}
rows
}
///|
pub fn baseline_parse_errors(text : String) -> Array[String] {
let errors : Array[String] = []
let seen : Array[String] = []
let lines = split_char(text, '\n')
for index = 0; index < lines.length(); index = index + 1 {
let line = trim_ascii(lines[index])
if line != "" && !line.has_prefix("#") {
match parse_baseline_line(line) {
None =>
errors.push(
"line " + (index + 1).to_string() + ": expected path|kind|detail",
)
Some(finding) => {
let key = finding_key(finding)
if contains_string(seen, key) {
errors.push(
"line " + (index + 1).to_string() + ": duplicate finding",
)
} else {
seen.push(key)
}
}
}
}
}
errors
}
///|
pub fn baseline_is_valid(text : String) -> Bool {
baseline_parse_errors(text).is_empty()
}
///|
pub fn parse_baseline_line(line : String) -> AuditFinding? {
let parts = split_char(line, '|')
if parts.length() < 3 {
None
} else {
let path = trim_ascii(parts[0])
let kind = trim_ascii(parts[1])
let detail = join_tail(parts, 2, "|")
if path == "" || kind == "" || detail == "" {
None
} else {
Some(audit_finding(path, kind, detail))
}
}
}
///|
pub fn join_tail(
parts : Array[String],
start : Int,
separator : String,
) -> String {
let rows : Array[String] = []
for index = start; index < parts.length(); index = index + 1 {
rows.push(parts[index])
}
join_with(rows, separator)
}
///|
pub fn finding_in(
findings : Array[AuditFinding],
target : AuditFinding,
) -> Bool {
for finding in findings {
if finding_key(finding) == finding_key(target) {
return true
}
}
false
}
///|
pub fn new_findings(
current : Array[AuditFinding],
baseline : Array[AuditFinding],
) -> Array[AuditFinding] {
let rows : Array[AuditFinding] = []
for finding in current {
if !finding_in(baseline, finding) {
rows.push(finding)
}
}
rows
}
///|
pub fn resolved_findings(
current : Array[AuditFinding],
baseline : Array[AuditFinding],
) -> Array[AuditFinding] {
let rows : Array[AuditFinding] = []
for finding in baseline {
if !finding_in(current, finding) {
rows.push(finding)
}
}
rows
}
///|
pub fn unchanged_findings(
current : Array[AuditFinding],
baseline : Array[AuditFinding],
) -> Array[AuditFinding] {
let rows : Array[AuditFinding] = []
for finding in current {
if finding_in(baseline, finding) {
rows.push(finding)
}
}
rows
}
///|
pub fn baseline_passes(
current : Array[AuditFinding],
baseline : Array[AuditFinding],
) -> Bool {
new_findings(current, baseline).is_empty()
}
///|
pub fn findings_lines(findings : Array[AuditFinding]) -> Array[String] {
let rows : Array[String] = []
for finding in findings {
rows.push(finding.path + ": [" + finding.kind + "] " + finding.detail)
}
rows
}
///|
pub fn baseline_report(
current : Array[AuditFinding],
baseline : Array[AuditFinding],
) -> String {
let added = new_findings(current, baseline)
let fixed = resolved_findings(current, baseline)
let unchanged = unchanged_findings(current, baseline)
let rows : Array[String] = [
"new findings: " + added.length().to_string(),
"resolved findings: " + fixed.length().to_string(),
"unchanged findings: " + unchanged.length().to_string(),
]
push_section(rows, "new findings", findings_lines(added))
push_section(rows, "resolved findings", findings_lines(fixed))
join_lines(rows)
}
///|
pub fn baseline_markdown(
current : Array[AuditFinding],
baseline : Array[AuditFinding],
) -> String {
let rows : Array[String] = [
"# Audit baseline comparison",
"",
"- new findings: " + new_findings(current, baseline).length().to_string(),
"- resolved findings: " +
resolved_findings(current, baseline).length().to_string(),
"- unchanged findings: " +
unchanged_findings(current, baseline).length().to_string(),
"",
]
push_markdown_section(
rows,
"New findings",
findings_lines(new_findings(current, baseline)),
)
push_markdown_section(
rows,
"Resolved findings",
findings_lines(resolved_findings(current, baseline)),
)
join_lines(rows)
}
///|
pub fn audit_baseline_report(
paths : Array[String],
sources : Array[String],
policy : Policy,
baseline_text : String,
) -> String {
baseline_report(
findings_from_sources(paths, sources, policy),
parse_baseline(baseline_text),
)
}