use std::process::Command;
use tempfile::TempDir;
fn binary() -> std::path::PathBuf {
std::path::PathBuf::from(env!("CARGO_BIN_EXE_keyhog"))
}
#[test]
fn sarif_is_github_code_scanning_compliant() {
let dir = TempDir::new().expect("tempdir");
std::fs::create_dir_all(dir.path().join("src")).expect("mkdir src");
std::fs::write(
dir.path().join("src/leak.env"),
concat!("AWS_ACCESS_KEY_ID = \"AKIA", "QYLPMN5HFIQR7XYA\"\n"),
)
.expect("write fixture");
let out = Command::new(binary())
.current_dir(dir.path())
.args(["scan", ".", "--no-daemon", "--format", "sarif"])
.output()
.expect("spawn keyhog scan");
let stdout = String::from_utf8_lossy(&out.stdout);
let v: serde_json::Value =
serde_json::from_str(stdout.trim()).expect("SARIF stdout must be valid JSON");
assert_eq!(v["version"], "2.1.0", "SARIF version must be 2.1.0");
let run = &v["runs"][0];
let rule_ids: std::collections::HashSet<String> = run["tool"]["driver"]["rules"]
.as_array()
.expect("tool.driver.rules must be an array")
.iter()
.filter_map(|r| r["id"].as_str().map(str::to_string))
.collect();
for rule in run["tool"]["driver"]["rules"].as_array().unwrap() {
let props = &rule["properties"];
let sev = props["security-severity"]
.as_str()
.unwrap_or_else(|| panic!("rule {} missing security-severity", rule["id"]));
sev.parse::<f64>()
.unwrap_or_else(|_| panic!("security-severity must be numeric; got {sev:?}"));
let tags: Vec<&str> = props["tags"]
.as_array()
.map(|a| a.iter().filter_map(|t| t.as_str()).collect())
.unwrap_or_default();
assert!(
tags.contains(&"security"),
"rule {} must be tagged `security` for code-scanning categorization; tags={tags:?}",
rule["id"]
);
}
let results = run["results"].as_array().expect("runs[0].results array");
assert!(
!results.is_empty(),
"the planted AWS key must produce at least one SARIF result"
);
for r in results {
let uri = r["locations"][0]["physicalLocation"]["artifactLocation"]["uri"]
.as_str()
.expect("each result needs artifactLocation.uri");
assert!(
!uri.starts_with("file:") && !uri.starts_with('/'),
"artifactLocation.uri must be repo-relative for code-scanning (no file://, no leading /); got {uri:?}"
);
assert!(
uri.starts_with("src/"),
"uri must be relative to the scan root; got {uri:?}"
);
let rule_id = r["ruleId"].as_str().expect("each result needs a ruleId");
assert!(
rule_ids.contains(rule_id),
"ruleId {rule_id:?} is not present in tool.driver.rules[] - GitHub would drop it"
);
let fps = r["partialFingerprints"].as_object();
assert!(
fps.map(|m| !m.is_empty()).unwrap_or(false),
"every result needs non-empty partialFingerprints for alert dedup; got {}",
r["partialFingerprints"]
);
assert!(
matches!(
r["level"].as_str(),
Some("error" | "warning" | "note" | "none")
),
"result.level must be a valid SARIF level; got {}",
r["level"]
);
}
}