use std::collections::{BTreeMap, HashSet};
use zizmor_sarif::{
ArtifactContent, ArtifactLocation, CodeFlow, Invocation, Location as SarifLocation,
LogicalLocation, Message, MultiformatMessageString, PhysicalLocation, PropertyBag, Region,
ReportingDescriptor, Result as SarifResult, ResultKind, ResultLevel, Run, Sarif, ThreadFlow,
ThreadFlowLocation, ThreadFlowLocationImportance, Tool, ToolComponent,
};
use crate::finding::{Finding, Severity, location::Location};
impl From<Severity> for ResultKind {
fn from(value: Severity) -> Self {
match value {
Severity::Informational => ResultKind::Review,
Severity::Low => ResultKind::Fail,
Severity::Medium => ResultKind::Fail,
Severity::High => ResultKind::Fail,
}
}
}
impl From<Severity> for ResultLevel {
fn from(value: Severity) -> Self {
match value {
Severity::Informational => ResultLevel::Note,
Severity::Low => ResultLevel::Note,
Severity::Medium => ResultLevel::Warning,
Severity::High => ResultLevel::Error,
}
}
}
pub(crate) fn build(findings: &[Finding]) -> Sarif {
Sarif {
schema: Some(
"https://docs.oasis-open.org/sarif/sarif/v2.1.0/os/schemas/sarif-schema-2.1.0.json"
.into(),
),
runs: vec![build_run(findings)],
version: "2.1.0".into(),
}
}
fn build_run(findings: &[Finding]) -> Run {
Run {
invocations: vec![Invocation {
execution_successful: true,
}],
results: build_results(findings),
tool: Tool {
driver: ToolComponent {
download_uri: Some(env!("CARGO_PKG_REPOSITORY").into()),
information_uri: Some(env!("CARGO_PKG_HOMEPAGE").into()),
name: env!("CARGO_CRATE_NAME").into(),
rules: build_rules(findings),
semantic_version: Some(env!("CARGO_PKG_VERSION").into()),
version: Some(env!("CARGO_PKG_VERSION").into()),
},
},
}
}
fn build_rules(findings: &[Finding]) -> Vec<ReportingDescriptor> {
let mut unique_rules = HashSet::new();
findings
.iter()
.filter(|finding| unique_rules.insert(finding.ident))
.map(|finding| build_rule(finding))
.collect()
}
fn build_rule(finding: &Finding) -> ReportingDescriptor {
ReportingDescriptor {
help: Some(MultiformatMessageString {
markdown: Some(finding.to_markdown()),
text: finding.desc.into(),
}),
help_uri: Some(finding.url.into()),
id: format!("zizmor/{id}", id = finding.ident),
name: Some(finding.ident.into()),
properties: Some(PropertyBag {
tags: vec!["security".into()],
additional_properties: BTreeMap::new(),
}),
}
}
fn build_results(findings: &[Finding]) -> Vec<SarifResult> {
findings.iter().map(build_result).collect()
}
fn build_result(finding: &Finding<'_>) -> SarifResult {
let primary = finding.primary_location();
let thread_flow_locations: Vec<ThreadFlowLocation> = finding
.visible_locations()
.map(|loc| {
let importance = if loc.symbolic.is_primary() {
ThreadFlowLocationImportance::Essential
} else {
ThreadFlowLocationImportance::Important
};
ThreadFlowLocation {
importance: Some(importance),
location: build_location(loc, None),
}
})
.collect();
let code_flows = vec![CodeFlow {
thread_flows: vec![ThreadFlow {
locations: thread_flow_locations,
}],
}];
let mut additional_properties = BTreeMap::new();
additional_properties.insert(
"zizmor/confidence".into(),
serde_json::value::to_value(finding.determinations.confidence)
.expect("failed to serialize confidence"),
);
additional_properties.insert(
"zizmor/severity".into(),
serde_json::value::to_value(finding.determinations.severity)
.expect("failed to serialize severity"),
);
additional_properties.insert(
"zizmor/persona".into(),
serde_json::value::to_value(finding.determinations.persona)
.expect("failed to serialize persona"),
);
SarifResult {
code_flows,
kind: Some(ResultKind::from(finding.determinations.severity)),
level: Some(ResultLevel::from(finding.determinations.severity)),
locations: vec![build_location(primary, None)],
message: Message {
text: format!(
"{desc}: {annotation}",
desc = finding.desc,
annotation = primary.symbolic.annotation
),
},
properties: Some(PropertyBag {
tags: vec![],
additional_properties,
}),
rule_id: Some(format!("zizmor/{id}", id = finding.ident)),
}
}
fn build_physical_location(location: &Location<'_>) -> PhysicalLocation {
PhysicalLocation {
artifact_location: ArtifactLocation {
uri: location.symbolic.key.best_relative_path().into(),
},
region: Region {
end_column: (location.concrete.location.end_point.column as i64) + 1,
end_line: (location.concrete.location.end_point.row as i64) + 1,
snippet: ArtifactContent {
text: location.concrete.feature.into(),
},
source_language: "yaml".into(),
start_column: (location.concrete.location.start_point.column as i64) + 1,
start_line: (location.concrete.location.start_point.row as i64) + 1,
},
}
}
fn build_logical_locations(location: &Location<'_>) -> Vec<LogicalLocation> {
let mut additional_properties = BTreeMap::new();
additional_properties.insert(
"symbolic".into(),
serde_json::value::to_value(location.symbolic.clone())
.expect("failed to serialize symbolic location"),
);
vec![LogicalLocation {
properties: Some(PropertyBag {
tags: vec![],
additional_properties,
}),
}]
}
fn build_location(location: &Location<'_>, id: Option<i64>) -> SarifLocation {
SarifLocation {
id,
logical_locations: build_logical_locations(location),
message: Some(Message {
text: location.symbolic.annotation.as_ref().into(),
}),
physical_location: Some(build_physical_location(location)),
}
}
#[cfg(test)]
mod tests {
use zizmor_sarif::ResultKind;
use crate::finding::Severity;
#[test]
fn test_resultkind_from_severity() {
assert_eq!(
serde_json::to_string(&ResultKind::from(Severity::High)).unwrap(),
"\"fail\""
);
}
}