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