mod common;
use common::{
assert_sarif_result, assert_sarif_results_match_rules, assert_sarif_rule,
assert_sarif_run, assert_sarif_schema, finding, finding_full, render_sarif,
};
use secreport::Format;
use secfinding::{Evidence, Finding, Reportable, Severity};
#[test]
fn sarif_empty_findings_structure() {
let value = render_sarif(&[]);
assert_sarif_schema(&value);
let run = &value["runs"][0];
assert_sarif_run(run, "test-tool");
let results = run["results"].as_array().unwrap();
assert!(results.is_empty());
let rules = run["tool"]["driver"]["rules"].as_array().unwrap();
assert!(rules.is_empty());
}
#[test]
fn sarif_single_finding_full_structure() {
let f = finding_full("RCE", Severity::Critical);
let value = render_sarif(&[f]);
assert_sarif_schema(&value);
let run = &value["runs"][0];
assert_sarif_run(run, "test-tool");
let rules = run["tool"]["driver"]["rules"].as_array().unwrap();
assert_eq!(rules.len(), 1);
assert_sarif_rule(&rules[0]);
let results = run["results"].as_array().unwrap();
assert_eq!(results.len(), 1);
assert_sarif_result(&results[0]);
assert_eq!(results[0]["level"], "error");
}
#[test]
fn sarif_multiple_findings_deduplicate_rules() {
let findings = vec![
finding("SameRule", Severity::High),
finding("SameRule", Severity::High),
];
let value = render_sarif(&findings);
let rules = value["runs"][0]["tool"]["driver"]["rules"]
.as_array()
.unwrap();
let results = value["runs"][0]["results"].as_array().unwrap();
assert_eq!(rules.len(), 1, "duplicate rules should be deduplicated");
assert_eq!(results.len(), 2);
}
#[test]
fn sarif_different_rules_preserved() {
let findings = vec![
finding("RuleA", Severity::High),
finding("RuleB", Severity::Medium),
finding("RuleC", Severity::Low),
];
let value = render_sarif(&findings);
let rules = value["runs"][0]["tool"]["driver"]["rules"]
.as_array()
.unwrap();
assert_eq!(rules.len(), 3);
}
#[test]
fn sarif_results_match_rules() {
let findings = vec![
finding("A", Severity::High),
finding("B", Severity::Medium),
finding("A", Severity::High),
];
let value = render_sarif(&findings);
assert_sarif_results_match_rules(&value);
}
#[test]
fn sarif_rule_properties_populated() {
let f = finding_full("Props", Severity::High);
let value = render_sarif(&[f]);
let rule = &value["runs"][0]["tool"]["driver"]["rules"][0];
let props = &rule["properties"];
assert_eq!(props["severity"], "high");
assert_eq!(props["category"], "Vulnerability");
assert_eq!(props["precision"], "high");
assert!(props["tags"].is_array());
assert!(props["cwe"].is_array());
assert!(props["cve"].is_array());
}
#[test]
fn sarif_result_properties_populated() {
let f = finding_full("ResultProps", Severity::Medium);
let value = render_sarif(&[f]);
let result = &value["runs"][0]["results"][0];
let props = &result["properties"];
assert_eq!(props["severity"], "medium");
assert_eq!(props["kind"], "Vulnerability");
assert_eq!(props["confidence"], 0.95);
assert_eq!(props["exploit_hint"], "curl -X GET https://example.com/poc");
assert!(props["evidence"].is_array());
assert!(props["cwe_ids"].is_array());
assert!(props["cve_ids"].is_array());
}
#[test]
fn sarif_locations_structure() {
let f = finding("Loc", Severity::High);
let value = render_sarif(&[f]);
let locs = value["runs"][0]["results"][0]["locations"].as_array().unwrap();
assert!(!locs.is_empty());
assert!(locs[0]["physicalLocation"]["artifactLocation"]["uri"].is_string());
}
#[test]
fn sarif_codeflows_structure() {
let f = finding("CF", Severity::High);
let value = render_sarif(&[f]);
let code_flows = value["runs"][0]["results"][0]["codeFlows"]
.as_array()
.unwrap();
assert_eq!(code_flows.len(), 1);
let thread_flows = code_flows[0]["threadFlows"].as_array().unwrap();
assert_eq!(thread_flows.len(), 1);
let locations = thread_flows[0]["locations"].as_array().unwrap();
assert_eq!(locations.len(), 1);
assert!(
locations[0]["location"]["physicalLocation"]["artifactLocation"]["uri"].is_string()
);
}
#[test]
fn sarif_fingerprint_is_stable() {
let f = finding("Stable", Severity::High);
let v1 = render_sarif(&[f.clone()]);
let v2 = render_sarif(&[f]);
let fp1 = v1["runs"][0]["results"][0]["partialFingerprints"]["primaryLocationLineHash"]
.as_str()
.unwrap();
let fp2 = v2["runs"][0]["results"][0]["partialFingerprints"]["primaryLocationLineHash"]
.as_str()
.unwrap();
assert_eq!(fp1, fp2);
}
#[test]
fn sarif_fingerprint_changes_with_content() {
let f1 = Finding::new("s", "t", Severity::High, "T", "D1").unwrap();
let f2 = Finding::new("s", "t", Severity::High, "T", "D2").unwrap();
let v1 = render_sarif(&[f1]);
let v2 = render_sarif(&[f2]);
let fp1 = v1["runs"][0]["results"][0]["partialFingerprints"]["primaryLocationLineHash"]
.as_str()
.unwrap();
let fp2 = v2["runs"][0]["results"][0]["partialFingerprints"]["primaryLocationLineHash"]
.as_str()
.unwrap();
assert_ne!(fp1, fp2);
}
#[test]
fn sarif_fingerprint_is_16_hex_chars() {
let f = finding("Hex", Severity::High);
let value = render_sarif(&[f]);
let fp = value["runs"][0]["results"][0]["partialFingerprints"]
["primaryLocationLineHash"]
.as_str()
.unwrap();
assert_eq!(fp.len(), 16);
assert!(fp.chars().all(|c| c.is_ascii_hexdigit()));
}
#[test]
fn sarif_levels_match_severity() {
let pairs = vec![
(Severity::Critical, "error"),
(Severity::High, "error"),
(Severity::Medium, "warning"),
(Severity::Low, "note"),
(Severity::Info, "note"),
];
for (sev, expected_level) in pairs {
let f = finding("L", sev);
let value = render_sarif(&[f]);
assert_eq!(
value["runs"][0]["results"][0]["level"], expected_level,
"severity {:?} should map to SARIF level {:?}",
sev, expected_level
);
}
}
#[test]
fn sarif_empty_rule_id_fallback() {
struct Custom {
scanner: String,
target: String,
title: String,
}
impl Reportable for Custom {
fn scanner(&self) -> &str {
&self.scanner
}
fn target(&self) -> &str {
&self.target
}
fn severity(&self) -> Severity {
Severity::High
}
fn title(&self) -> &str {
&self.title
}
fn rule_id(&self) -> String {
String::new()
}
}
let findings = vec![Custom {
scanner: "fallback-scanner".into(),
target: "https://target".into(),
title: "Fallback Title".into(),
}];
let out = secreport::render_any(&findings, Format::Sarif, "tool").unwrap();
let value: serde_json::Value = serde_json::from_str(&out).unwrap();
let rule = &value["runs"][0]["tool"]["driver"]["rules"][0];
assert_eq!(rule["id"], "fallback-scanner/fallback-title");
}
#[test]
fn sarif_unicode_in_fields() {
let f = Finding::new("スキャナ", "https://例え.jp", Severity::High, "日本語", "詳細").unwrap();
let out = secreport::render(&[f], Format::Sarif, "tool").unwrap();
let value: serde_json::Value = serde_json::from_str(&out).unwrap();
assert_eq!(
value["runs"][0]["results"][0]["message"]["text"], "日本語\n詳細"
);
}
#[test]
fn sarif_special_characters_in_rule_ids() {
struct Custom {
scanner: String,
target: String,
title: String,
}
impl Reportable for Custom {
fn scanner(&self) -> &str {
&self.scanner
}
fn target(&self) -> &str {
&self.target
}
fn severity(&self) -> Severity {
Severity::Critical
}
fn title(&self) -> &str {
&self.title
}
}
let findings = vec![
Custom {
scanner: "scan<ner>".into(),
target: "target".into(),
title: "SQL Injection <script>alert(1)</script>".into(),
},
Custom {
scanner: "scan\"ner\"".into(),
target: "target".into(),
title: "RCE $(whoami) `rm -rf /`".into(),
},
];
let out = secreport::render_any(&findings, Format::Sarif, "tool").unwrap();
let value: serde_json::Value = serde_json::from_str(&out).unwrap();
let results = value["runs"][0]["results"].as_array().unwrap();
assert_eq!(results.len(), 2);
for result in results {
assert_sarif_result(result);
}
}
#[test]
fn sarif_large_number_of_results() {
let findings: Vec<Finding> = (0..10_000)
.map(|i| finding(&format!("Finding-{}", i), Severity::Info))
.collect();
let value = render_sarif(&findings);
let results = value["runs"][0]["results"].as_array().unwrap();
assert_eq!(results.len(), 10_000);
}
#[test]
fn sarif_evidence_strings_in_properties() {
let f = Finding::builder("s", "t", Severity::High)
.title("T")
.evidence(Evidence::http_status(403).unwrap())
.build()
.unwrap();
let value = render_sarif(&[f]);
let ev = value["runs"][0]["results"][0]["properties"]["evidence"]
.as_array()
.unwrap();
assert!(!ev.is_empty());
assert!(ev[0].is_string());
}
#[test]
fn sarif_tool_name_reflected() {
let f = finding("T", Severity::High);
let value = render_sarif(&[f]);
assert_eq!(value["runs"][0]["tool"]["driver"]["name"], "test-tool");
}