use serde_json::{Value, json};
use crate::alerts::{AlertDetail, AlertSummary};
use crate::reporter::FindingReporterRecord;
const PER_FINDING_LIMIT: usize = 200;
const SCHEMA_VERSION: &str = "1";
pub fn build_payload(
summary: &AlertSummary,
findings: &[&FindingReporterRecord],
include_secret: bool,
) -> Value {
let take = if summary.detail == AlertDetail::Summary {
0
} else {
findings.len().min(PER_FINDING_LIMIT)
};
let included: Vec<Value> = findings
.iter()
.take(take)
.map(|f| {
let mut record = serde_json::to_value(f).unwrap_or(Value::Null);
if !include_secret
&& let Some(obj) = record.get_mut("finding").and_then(|v| v.as_object_mut())
{
obj.insert("snippet".into(), Value::String("<redacted>".to_string()));
}
record
})
.collect();
let mut summary_obj = json!({
"total": summary.total,
"active": summary.active,
"inactive": summary.inactive,
"unknown": summary.unknown,
"impacted_resources": summary.impacted_resources,
"unfiltered_total": summary.unfiltered_total,
"by_rule": summary.by_rule.iter().map(|(r, c)| json!({"rule_id": r, "count": c})).collect::<Vec<_>>(),
"target": summary.target,
});
if let Some(url) = &summary.report_url {
summary_obj["report_url"] = Value::String(url.clone());
}
json!({
"schema_version": SCHEMA_VERSION,
"kingfisher_version": summary.kingfisher_version,
"summary": summary_obj,
"findings": included,
"findings_omitted": findings.len().saturating_sub(take),
})
}
#[cfg(test)]
mod tests {
use super::*;
fn empty_summary() -> AlertSummary {
AlertSummary {
total: 0,
active: 0,
inactive: 0,
unknown: 0,
by_rule: vec![],
kingfisher_version: "test".to_string(),
target: None,
report_url: None,
detail: crate::alerts::AlertDetail::Detail,
impacted_resources: 0,
unfiltered_total: 0,
}
}
#[test]
fn schema_version_present() {
let p = build_payload(&empty_summary(), &[], false);
assert_eq!(p["schema_version"], "1");
}
#[test]
fn empty_findings_array() {
let p = build_payload(&empty_summary(), &[], false);
assert_eq!(p["findings"].as_array().unwrap().len(), 0);
assert_eq!(p["findings_omitted"], 0);
}
#[test]
fn report_url_appears_in_summary_block() {
let mut s = empty_summary();
s.report_url = Some("https://ci.example/run/7".to_string());
let p = build_payload(&s, &[], false);
assert_eq!(p["summary"]["report_url"], "https://ci.example/run/7");
}
#[test]
fn summary_mode_drops_findings_array() {
let mut s = empty_summary();
s.detail = crate::alerts::AlertDetail::Summary;
let rec = crate::alerts::make_test_record("betterleaks.aws-access-token", "fp-abc");
let p = build_payload(&s, &[&rec, &rec, &rec], false);
assert_eq!(p["findings"].as_array().unwrap().len(), 0);
assert_eq!(p["findings_omitted"], 3);
}
#[test]
fn fingerprint_round_trips_in_detail_mode() {
let s = empty_summary();
let rec = crate::alerts::make_test_record("betterleaks.aws-access-token", "fp-roundtrip");
let p = build_payload(&s, &[&rec], false);
assert_eq!(p["findings"][0]["finding"]["fingerprint"], "fp-roundtrip");
}
#[test]
fn impacted_resources_present_in_summary_block() {
let mut s = empty_summary();
s.impacted_resources = 7;
let p = build_payload(&s, &[], false);
assert_eq!(p["summary"]["impacted_resources"], 7);
}
#[test]
fn unfiltered_total_present_in_summary_block() {
let mut s = empty_summary();
s.unfiltered_total = 12;
let p = build_payload(&s, &[], false);
assert_eq!(p["summary"]["unfiltered_total"], 12);
}
}