mod blast_radius;
mod capabilities;
mod compound;
mod hygiene;
mod permissions;
mod predicates;
mod root_causes;
pub(crate) const MAX_VERDICT_REASONS: usize = 8;
pub(crate) const MAX_TOP_RISK_DRIVERS: usize = 5;
pub(crate) const MAX_REPRESENTATIVE_RULES: usize = 5;
use crate::findings::{Finding, FindingSummary, PackageVerdictReport, VerdictReason};
use crate::verdict_calibration::{calibrate_verdict_inputs, VerdictCalibration};
#[must_use]
pub fn derive_package_verdict(
findings: &[Finding],
primary_summary: &FindingSummary,
supporting_summary: &FindingSummary,
package_summary: &FindingSummary,
) -> PackageVerdictReport {
let raw_root_cause_groups = root_causes::build_root_cause_groups(findings);
let calibration = calibrate_verdict_inputs(findings, &raw_root_cause_groups);
let VerdictCalibration {
root_cause_groups: calibrated_groups,
risk_adjustment: calibration_risk_adjustment,
notes: calibration_notes,
} = calibration;
let root_cause_groups = root_causes::merge_calibrated_groups(calibrated_groups);
debug_assert!(
root_cause_groups.len() <= raw_root_cause_groups.len(),
"Merging calibrated groups must not increase group count"
);
let compound_reasons =
compound::detect_compound_verdict_reasons(findings, &raw_root_cause_groups);
let mut verdict_reasons = compound_reasons.clone();
verdict_reasons.extend(root_cause_groups.iter().map(|group| VerdictReason {
scope: group.scope,
category: group.category,
signal_class: group.signal_class,
rationale: format!(
"{} finding(s) in {} with strongest action {}",
group.finding_count, group.scope, group.strongest_action
),
}));
verdict_reasons.truncate(MAX_VERDICT_REASONS);
let predicates = predicates::VerdictPredicates::compute(&predicates::VerdictInputs {
findings,
root_cause_groups: &root_cause_groups,
raw_root_cause_groups: &raw_root_cause_groups,
compound_reasons: &compound_reasons,
primary_summary,
supporting_summary,
});
let hygiene_summary = hygiene::build_hygiene_summary(findings);
let declared_permissions =
permissions::derive_declared_permissions(findings, supporting_summary);
let blast_radius_summary =
blast_radius::build_blast_radius_summary(findings, &declared_permissions);
let effective_capabilities = capabilities::derive_effective_capabilities(&root_cause_groups);
let calibrated_primary = primary_summary.clone();
let calibrated_package = package_summary.with_risk_adjustment(calibration_risk_adjustment);
let verdict = predicates.verdict(&calibration_notes, &calibrated_primary, &calibrated_package);
let package_health = predicates.package_health(&hygiene_summary, verdict);
let mut top_risk_drivers = package_summary.score_breakdown.clone();
top_risk_drivers.truncate(MAX_TOP_RISK_DRIVERS);
PackageVerdictReport {
verdict,
package_health,
hygiene_summary,
declared_permissions,
effective_capabilities,
blast_radius_summary,
verdict_reasons,
root_cause_groups,
top_risk_drivers,
calibration_notes,
calibration_risk_adjustment,
}
}
#[must_use]
pub fn is_conclusive_single_rule_id(rule_id: &str) -> bool {
predicates::CONCLUSIVE_SINGLE_RULE_IDS.contains(&rule_id)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::findings::{
Finding, FindingSummary, MatchTarget, PackageHealth, RecommendedAction, Severity,
ThreatCategory, Verdict,
};
#[test]
fn test_trivial_hygiene_produces_needs_review_not_elevated() {
let finding = Finding::builder("SCOPE_CREEP_GENERIC", ThreatCategory::ScopeCreep)
.severity(Severity::Low)
.action(RecommendedAction::Log)
.matched_on(MatchTarget::Document)
.match_value("broad scope")
.reason("Minor scope creep")
.build();
let findings = vec![finding];
let summary = FindingSummary::from_findings(&findings);
let report = derive_package_verdict(&findings, &summary, &summary, &summary);
assert_eq!(
report.package_health,
PackageHealth::NeedsReview,
"Trivial hygiene findings should produce NeedsReview, not Elevated"
);
assert_eq!(report.verdict, Verdict::Benign);
}
#[test]
fn is_conclusive_single_rule_id_matches_curated_set() {
for id in predicates::CONCLUSIVE_SINGLE_RULE_IDS {
assert!(
is_conclusive_single_rule_id(id),
"curated id {id} must be reported conclusive",
);
}
assert!(
!is_conclusive_single_rule_id("SKILL_CRED_HARDCODED_KEY"),
"a deliberately non-curated rule must not be reported conclusive",
);
}
}