use crate::findings::severity::Severity;
use crate::findings::types::{Confidence, Evidence, Finding};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct DecisionRecord {
pub severity: Severity,
pub confidence: Confidence,
pub evidence: Vec<Evidence>,
pub recommendation: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub verification_plan: Option<VerificationPlan>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct VerificationPlan {
pub steps: Vec<String>,
}
pub fn build_decision_record(finding: &Finding) -> DecisionRecord {
DecisionRecord {
severity: finding.severity,
confidence: finding.confidence,
evidence: finding.evidence.clone(),
recommendation: finding.recommendation_or_default().to_string(),
verification_plan: crate::findings::verification::build_verification_plan(finding),
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::findings::provenance::FindingProvenance;
use crate::findings::types::FindingCategory;
use std::path::PathBuf;
#[test]
fn build_decision_record_copies_severity_confidence_evidence_and_recommendation() {
let finding = Finding {
id: "rule.example:src/lib.rs:deadbeef".to_string(),
rule_id: "rule.example".to_string(),
title: "Example".to_string(),
description: "desc".to_string(),
recommendation: "fix it".to_string(),
category: FindingCategory::Security,
severity: Severity::High,
confidence: Confidence::High,
evidence: vec![Evidence {
path: PathBuf::from("src/lib.rs"),
line_start: 5,
line_end: None,
snippet: "let x = 1;".to_string(),
}],
workspace_package: None,
docs_url: None,
provenance: FindingProvenance::default(),
risk: Default::default(),
};
let decision = build_decision_record(&finding);
assert_eq!(decision.severity, Severity::High);
assert_eq!(decision.confidence, Confidence::High);
assert_eq!(decision.recommendation, "fix it");
assert_eq!(decision.evidence, finding.evidence);
assert!(
decision.verification_plan.is_some(),
"high-confidence findings should get a verification plan"
);
}
#[test]
fn build_decision_record_falls_back_to_generic_recommendation_when_empty() {
let finding = Finding {
id: "rule.example:src/lib.rs:deadbeef".to_string(),
rule_id: "rule.example".to_string(),
title: "Example".to_string(),
description: "desc".to_string(),
recommendation: String::new(),
category: FindingCategory::Security,
severity: Severity::Low,
confidence: Confidence::Medium,
evidence: vec![],
workspace_package: None,
docs_url: None,
provenance: FindingProvenance::default(),
risk: Default::default(),
};
let decision = build_decision_record(&finding);
assert_eq!(decision.recommendation, Finding::GENERIC_RECOMMENDATION);
assert!(decision.verification_plan.is_none());
}
}