use crate::core::engine::EngineResult;
use crate::rules::core::{ArtifactCacheStats, RuleCategory, RuleStatus, Severity, RULESET_VERSION};
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::time::{SystemTime, UNIX_EPOCH};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReportData {
pub ruleset_version: String,
pub generated_at_unix: u64,
pub total_duration_ms: u128,
pub cache_stats: ArtifactCacheStats,
pub slow_rules: Vec<SlowRule>,
pub results: Vec<ReportItem>,
#[serde(default)]
pub scanned_targets: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReportItem {
pub rule_id: String,
pub rule_name: String,
pub category: RuleCategory,
pub severity: Severity,
pub target: String,
pub status: RuleStatus,
pub message: Option<String>,
pub evidence: Option<String>,
pub recommendation: String,
pub duration_ms: u128,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SlowRule {
pub rule_id: String,
pub rule_name: String,
pub duration_ms: u128,
}
#[derive(Debug, Clone)]
pub struct BaselineSummary {
pub suppressed: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentPack {
pub generated_at_unix: u64,
pub total_findings: usize,
pub findings: Vec<AgentFinding>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentFinding {
pub rule_id: String,
pub rule_name: String,
pub severity: Severity,
pub category: RuleCategory,
pub priority: String,
pub message: String,
pub evidence: Option<String>,
pub recommendation: String,
pub suggested_fix_scope: String,
pub target_files: Vec<String>,
pub patch_hint: String,
pub why_it_fails_review: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AgentPackFormat {
Json,
Markdown,
Bundle,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FailOn {
Off,
Error,
Warning,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TimingMode {
Off,
Summary,
Full,
}
pub fn build_report(
results: Vec<EngineResult>,
total_duration_ms: u128,
cache_stats: ArtifactCacheStats,
) -> ReportData {
let generated_at_unix = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let mut items = Vec::new();
for res in results {
let (status, message, evidence) = match res.report {
Ok(report) => (report.status, report.message, report.evidence),
Err(err) => (
RuleStatus::Error,
Some(err.to_string()),
Some("Rule evaluation error".to_string()),
),
};
items.push(ReportItem {
rule_id: res.rule_id.to_string(),
rule_name: res.rule_name.to_string(),
category: res.category,
severity: res.severity,
target: res.target.clone(),
status,
message,
evidence,
recommendation: res.recommendation.to_string(),
duration_ms: res.duration_ms,
});
}
let mut scanned_targets: Vec<String> = items.iter().map(|i| i.target.clone()).collect();
scanned_targets.sort();
scanned_targets.dedup();
let report = ReportData {
ruleset_version: RULESET_VERSION.to_string(),
generated_at_unix,
total_duration_ms,
cache_stats,
slow_rules: Vec::new(),
results: items,
scanned_targets,
};
ReportData {
slow_rules: top_slow_rules(&report, 3),
..report
}
}
pub fn apply_baseline(report: &mut ReportData, baseline: &ReportData) -> BaselineSummary {
let mut suppressed = 0;
let baseline_keys: HashSet<String> = baseline
.results
.iter()
.filter(|r| matches!(r.status, RuleStatus::Fail | RuleStatus::Error))
.map(finding_key)
.collect();
report.results.retain(|r| {
if !matches!(r.status, RuleStatus::Fail | RuleStatus::Error) {
return true;
}
let key = finding_key(r);
let keep = !baseline_keys.contains(&key);
if !keep {
suppressed += 1;
}
keep
});
BaselineSummary { suppressed }
}
pub fn should_exit_with_failure(report: &ReportData, fail_on: FailOn) -> bool {
match fail_on {
FailOn::Off => false,
FailOn::Error => report.results.iter().any(|item| {
matches!(item.status, RuleStatus::Fail | RuleStatus::Error)
&& matches!(item.severity, Severity::Error)
}),
FailOn::Warning => report.results.iter().any(|item| {
matches!(item.status, RuleStatus::Fail | RuleStatus::Error)
&& matches!(item.severity, Severity::Error | Severity::Warning)
}),
}
}
pub fn top_slow_rules(report: &ReportData, limit: usize) -> Vec<SlowRule> {
let mut items: Vec<SlowRule> = report
.results
.iter()
.map(|item| SlowRule {
rule_id: item.rule_id.clone(),
rule_name: item.rule_name.clone(),
duration_ms: item.duration_ms,
})
.collect();
items.sort_by(|a, b| {
b.duration_ms
.cmp(&a.duration_ms)
.then_with(|| a.rule_id.cmp(&b.rule_id))
});
items.truncate(limit);
items
}
fn finding_key(item: &ReportItem) -> String {
format!(
"{}|{}|{}",
item.rule_id,
item.target,
item.evidence.clone().unwrap_or_default()
)
}