use sarif_rust::{
Level, LocationBuilder, ResultBuilder, RunBuilder, SarifLogBuilder, SarifValidator,
};
use serde_json::Value;
use crate::check::GenericMap;
use crate::{BinError, BinResult};
pub fn build(
binary_uri: &str,
sections: &[(&str, &GenericMap)],
tool_version: &str,
) -> BinResult<String> {
let mut run = RunBuilder::with_tool("binsec", Some(tool_version));
for (category, map) in sections {
for (name, value) in map.iter() {
let location = LocationBuilder::new().with_file_location(binary_uri).build();
let result = ResultBuilder::with_text_message(format!("{name}: {}", render(value)))
.with_rule_id(rule_id(category, name))
.with_level(Level::Note)
.add_location(location)
.build();
run = run.add_result(result);
}
}
let log = SarifLogBuilder::with_standard_schema()
.add_run(run.build())
.build()
.map_err(|err| BinError::Internal(format!("SARIF build failed: {err}")))?;
sarif_rust::to_string_pretty(&log)
.map_err(|err| BinError::Internal(format!("SARIF serialization failed: {err}")))
}
pub fn validate_sarif(json: &str) -> BinResult<()> {
let log = sarif_rust::from_str(json)
.map_err(|err| BinError::Internal(format!("SARIF parse error: {err}")))?;
SarifValidator::strict()
.validate_sarif_log(&log)
.map_err(|err| BinError::Internal(format!("SARIF validation error: {err}")))
}
pub fn to_markdown(json: &str) -> BinResult<String> {
validate_sarif(json)?;
let processor = sarif_to_md_core::ReportProcessorBuilder::new()
.generator(sarif_to_md_core::generators::SarifMarkdownGenerator::new(
sarif_to_md_core::markdown::MarkdownFormat::GitHubFlavored,
true,
))
.content(json.to_owned())
.build()
.map_err(|err| BinError::Internal(format!("Markdown processor build failed: {err}")))?;
let markdown = processor
.generate()
.map_err(|err| BinError::Internal(format!("Markdown generation failed: {err}")))?;
validate_markdown(&markdown)?;
Ok(markdown)
}
pub fn validate_markdown(markdown: &str) -> BinResult<()> {
if markdown.trim().is_empty() {
return Err(BinError::Internal("Markdown output is empty".to_string()));
}
if !markdown.contains('#') && !markdown.contains('|') && !markdown.contains("---") {
return Err(BinError::Internal(
"Markdown output missing expected structure (no headings, tables, or rules)"
.to_string(),
));
}
Ok(())
}
fn rule_id(category: &str, name: &str) -> String {
let mut slug = String::with_capacity(name.len());
let mut prev_dash = false;
for ch in name.chars() {
if ch.is_ascii_alphanumeric() {
slug.push(ch.to_ascii_lowercase());
prev_dash = false;
} else if !prev_dash {
slug.push('-');
prev_dash = true;
}
}
format!("binsec.{category}.{}", slug.trim_matches('-'))
}
fn render(value: &Value) -> String {
match value {
Value::String(text) => text.clone(),
other => other.to_string(),
}
}
#[cfg(test)]
mod tests {
use super::{build, rule_id, validate_markdown, validate_sarif};
use crate::check::GenericMap;
use serde_json::{json, Value};
fn sample_sarif() -> String {
let mut mitigations = GenericMap::new();
mitigations.insert("Stack Canary".to_string(), json!(true));
mitigations.insert("Read-Only Relocatable (RELRO)".to_string(), json!("Full"));
let sections = [("mitigations", &mitigations)];
build("/bin/true", §ions, "3.2.0").expect("build SARIF")
}
#[test]
fn rule_id_slugs_names() {
assert_eq!(
rule_id("mitigations", "Stack Canary"),
"binsec.mitigations.stack-canary"
);
assert_eq!(
rule_id("mitigations", "Position Independent Executable (PIE)"),
"binsec.mitigations.position-independent-executable-pie"
);
}
#[test]
fn built_sarif_passes_strict_validation() {
let sarif = sample_sarif();
validate_sarif(&sarif).expect("a built report must pass SarifValidator::strict()");
let doc: Value = serde_json::from_str(&sarif).expect("valid JSON");
assert_eq!(doc["version"], "2.1.0");
assert_eq!(doc["runs"][0]["tool"]["driver"]["name"], "binsec");
assert_eq!(doc["runs"][0]["tool"]["driver"]["version"], "3.2.0");
assert_eq!(doc["runs"][0]["results"].as_array().unwrap().len(), 2);
}
#[test]
fn validate_sarif_rejects_non_sarif() {
assert!(validate_sarif(r#"{"not":"a sarif document"}"#).is_err());
}
#[test]
fn validate_markdown_checks_structure() {
assert!(validate_markdown("").is_err());
assert!(validate_markdown("plain text, no markers").is_err());
assert!(validate_markdown("# Heading\n").is_ok());
}
#[test]
fn markdown_renders_from_built_report() {
let sarif = sample_sarif();
let markdown = super::to_markdown(&sarif).expect("markdown render");
assert!(markdown.contains('#'));
}
}