use padlock_core::findings::{Finding, Report};
use serde::Serialize;
#[derive(Serialize)]
struct SarifRoot {
#[serde(rename = "$schema")]
schema: &'static str,
version: &'static str,
runs: Vec<SarifRun>,
}
#[derive(Serialize)]
struct SarifRun {
tool: SarifTool,
results: Vec<SarifResult>,
}
#[derive(Serialize)]
struct SarifTool {
driver: SarifDriver,
}
#[derive(Serialize)]
struct SarifDriver {
name: &'static str,
version: &'static str,
rules: Vec<SarifRule>,
}
#[derive(Serialize)]
struct SarifRule {
id: String,
#[serde(rename = "shortDescription")]
short_description: SarifMessage,
}
#[derive(Serialize)]
struct SarifResult {
#[serde(rename = "ruleId")]
rule_id: String,
level: &'static str,
message: SarifMessage,
locations: Vec<SarifLocation>,
}
#[derive(Serialize)]
struct SarifMessage {
text: String,
}
#[derive(Serialize)]
struct SarifLocation {
#[serde(rename = "physicalLocation")]
physical_location: SarifPhysicalLocation,
}
#[derive(Serialize)]
struct SarifPhysicalLocation {
#[serde(rename = "artifactLocation")]
artifact_location: SarifArtifactLocation,
region: SarifRegion,
}
#[derive(Serialize)]
struct SarifArtifactLocation {
uri: String,
}
#[derive(Serialize)]
struct SarifRegion {
#[serde(rename = "startLine")]
start_line: u32,
}
fn rules() -> Vec<SarifRule> {
vec![
SarifRule {
id: "PAD001".into(),
short_description: SarifMessage {
text: "Struct padding waste".into(),
},
},
SarifRule {
id: "PAD002".into(),
short_description: SarifMessage {
text: "False sharing risk".into(),
},
},
SarifRule {
id: "PAD003".into(),
short_description: SarifMessage {
text: "Field reorder suggestion".into(),
},
},
SarifRule {
id: "PAD004".into(),
short_description: SarifMessage {
text: "Cache locality issue".into(),
},
},
]
}
fn level_for(finding: &Finding) -> &'static str {
use padlock_core::findings::Severity;
match finding.severity() {
Severity::High => "error",
Severity::Medium => "warning",
Severity::Low => "note",
}
}
fn rule_id_for(finding: &Finding) -> &'static str {
match finding {
Finding::PaddingWaste { .. } => "PAD001",
Finding::FalseSharing { .. } => "PAD002",
Finding::ReorderSuggestion { .. } => "PAD003",
Finding::LocalityIssue { .. } => "PAD004",
}
}
fn message_for(finding: &Finding) -> String {
match finding {
Finding::PaddingWaste {
wasted_bytes,
waste_pct,
struct_name,
..
} => format!("{struct_name}: {wasted_bytes}B wasted ({waste_pct:.0}% of struct)"),
Finding::FalseSharing {
struct_name,
conflicts,
..
} => format!("{struct_name}: {} cache-line conflict(s)", conflicts.len()),
Finding::ReorderSuggestion {
struct_name,
savings,
..
} => format!("{struct_name}: reordering fields saves {savings}B"),
Finding::LocalityIssue { struct_name, .. } => {
format!("{struct_name}: hot and cold fields are interleaved")
}
}
}
pub fn to_sarif(report: &Report) -> anyhow::Result<String> {
let mut results = Vec::new();
for sr in &report.structs {
for finding in &sr.findings {
let uri = sr.source_file.clone().unwrap_or_else(|| "unknown".into());
let line = sr.source_line.unwrap_or(1);
results.push(SarifResult {
rule_id: rule_id_for(finding).into(),
level: level_for(finding),
message: SarifMessage {
text: message_for(finding),
},
locations: vec![SarifLocation {
physical_location: SarifPhysicalLocation {
artifact_location: SarifArtifactLocation { uri },
region: SarifRegion { start_line: line },
},
}],
});
}
}
let root = SarifRoot {
schema: "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0-rtm.5.json",
version: "2.1.0",
runs: vec![SarifRun {
tool: SarifTool {
driver: SarifDriver {
name: "padlock",
version: env!("CARGO_PKG_VERSION"),
rules: rules(),
},
},
results,
}],
};
Ok(serde_json::to_string_pretty(&root)?)
}
#[cfg(test)]
mod tests {
use super::*;
use padlock_core::findings::Report;
use padlock_core::ir::test_fixtures::connection_layout;
#[test]
fn sarif_is_valid_json() {
let report = Report::from_layouts(&[connection_layout()]);
let sarif = to_sarif(&report).unwrap();
let val: serde_json::Value = serde_json::from_str(&sarif).expect("invalid JSON");
assert!(val.is_object());
}
#[test]
fn sarif_version_is_2_1_0() {
let report = Report::from_layouts(&[connection_layout()]);
let sarif = to_sarif(&report).unwrap();
let val: serde_json::Value = serde_json::from_str(&sarif).unwrap();
assert_eq!(val["version"], "2.1.0");
}
#[test]
fn sarif_has_runs_array() {
let report = Report::from_layouts(&[connection_layout()]);
let sarif = to_sarif(&report).unwrap();
let val: serde_json::Value = serde_json::from_str(&sarif).unwrap();
assert!(val["runs"].is_array());
assert!(!val["runs"].as_array().unwrap().is_empty());
}
#[test]
fn sarif_results_contain_pad001() {
let report = Report::from_layouts(&[connection_layout()]);
let sarif = to_sarif(&report).unwrap();
assert!(sarif.contains("PAD001"));
}
}