Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,205 changes: 0 additions & 1,205 deletions src/render/sarif.rs

This file was deleted.

32 changes: 32 additions & 0 deletions src/render/sarif/document.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
use serde_json::json;

use crate::diff::ChangeSet;
use crate::enrich::Enrichment;

use super::results::results;
use super::rules::rules;
use super::{SARIF_SCHEMA, SARIF_VERSION};

pub fn render(cs: &ChangeSet, e: &Enrichment) -> String {
let doc = json!({
"$schema": SARIF_SCHEMA,
"version": SARIF_VERSION,
"runs": [{
"tool": {
"driver": {
"name": "bomdrift",
"semanticVersion": env!("CARGO_PKG_VERSION"),
"informationUri": "https://metbcy.github.io/bomdrift/",
"rules": rules(),
}
},
"results": results(cs, e),
}]
});
#[allow(
clippy::expect_used,
reason = "invariant: serde_json::to_string_pretty cannot fail on a Value built from owned data with string keys"
)]
serde_json::to_string_pretty(&doc)
.expect("invariant: serde_json::to_string_pretty cannot fail on a Value built from owned data with string keys")
}
58 changes: 58 additions & 0 deletions src/render/sarif/helpers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
use serde_json::{Value, json};
use sha2::{Digest, Sha256};

use super::SARIF_ARTIFACT_URI;

/// Stable per-rule identity hash for SARIF `partialFingerprints`. GitHub
/// Code Scanning uses these to thread alert state across runs (resolved /
/// dismissed / open) so the value MUST stay byte-equal for the same logical
/// finding. We hex-encode SHA-256 of a `|`-joined identity string so the
/// inputs are inspectable from a debugger and the output is filename-safe.
///
/// The `/v1` suffix on the fingerprint key (see emit sites) lets us evolve
/// the identity scheme later without GitHub re-opening every alert.
pub(crate) fn fingerprint(parts: &[&str]) -> String {
let mut h = Sha256::new();
for (i, p) in parts.iter().enumerate() {
if i > 0 {
h.update(b"|");
}
h.update(p.as_bytes());
}
let digest = h.finalize();
let mut out = String::with_capacity(64);
for byte in digest {
use std::fmt::Write;
let _ = write!(out, "{byte:02x}");
}
out
}

pub(super) fn plugin_sarif_level(severity: crate::plugin::PluginSeverity) -> &'static str {
use crate::plugin::PluginSeverity;
match severity {
PluginSeverity::Info => "note",
PluginSeverity::Warning => "warning",
PluginSeverity::Error => "error",
}
}

pub(super) fn synthetic_location() -> Value {
json!({
"physicalLocation": {
"artifactLocation": { "uri": SARIF_ARTIFACT_URI }
}
})
}

/// Map our internal [`Severity`] enum to the SARIF `level` enum. Critical and
/// High are the actionable buckets that block-on-merge tooling cares about;
/// everything below collapses to `warning` so reviewers still see the finding
/// without a hard fail in code-scanning views.
pub(super) fn sarif_level(severity: crate::enrich::Severity) -> &'static str {
use crate::enrich::Severity;
match severity {
Severity::Critical | Severity::High => "error",
Severity::Medium | Severity::Low | Severity::None => "warning",
}
}
72 changes: 72 additions & 0 deletions src/render/sarif/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//! SARIF v2.1.0 renderer (`--output sarif`).
//!
//! Produces a single-run SARIF document suitable for ingestion by GitHub Code
//! Scanning, GitLab Vulnerability Reports, and any other consumer that speaks
//! SARIF. The schema is hand-built via `serde_json::json!` — pulling a `sarif`
//! crate would add ~30 transitive dependencies for what amounts to ~50 lines
//! of object construction.
//!
//! ## Stable rule IDs (NEVER rename)
//!
//! These IDs surface in GitHub Code Scanning's UI and are the join key for
//! suppressions, so they're load-bearing public API once a finding has been
//! seen by any consumer:
//!
//! - `bomdrift.cve` — one result per `(component, advisory_id)` from
//! `enrichment.vulns`.
//! - `bomdrift.typosquat` — one per `enrichment.typosquats` finding.
//! - `bomdrift.version-jump` — one per `enrichment.version_jumps` finding.
//! - `bomdrift.young-maintainer` — one per `enrichment.maintainer_age` finding.
//! - `bomdrift.license-change` — one per `cs.license_changed` pair (license
//! changed without a version bump — the suspicious case).
//!
//! All five rules are always emitted in `tool.driver.rules`, even when the
//! current diff has zero findings of that kind. Code Scanning consumers
//! enumerate the rules independently of the results, so omitting unused
//! rules confuses suppression UIs.
//!
//! ## Severity
//!
//! `bomdrift.cve` results map their per-advisory severity to SARIF `level`:
//! `Critical` and `High` → `"error"`, everything else (including `None`,
//! used when /v1/vulns/{id} couldn't resolve a label) → `"warning"`. The
//! heuristic-enricher rules (typosquat, version-jump, maintainer-age,
//! license-change) stay at `"warning"` — they're intentionally informational.
//!
//! ## Locations
//!
//! SARIF requires `locations` on every `result`. We emit a synthetic
//! `physicalLocation.artifactLocation.uri = "sbom"`, matching the convention
//! used by `trivy` for SBOM-derived findings (no source line numbers exist —
//! the SBOM itself is the artifact).
//!
//! ## Determinism
//!
//! Renderer determinism is the upsert contract for PR comment workflows.
//! `Enrichment::vulns` is a `HashMap` (iteration order non-deterministic), so
//! its entries are sorted by purl key before emission. The other finding
//! collections are already `Vec`s ordered by their enrichers (which iterate
//! the `ChangeSet`'s BTreeMap-derived order), so they need no extra sorting.
//! The render-twice-byte-equal regression test below guards this.

mod document;
mod helpers;
mod results;
mod rules;

#[cfg(test)]
mod tests;

pub use document::render;
#[cfg(test)]
use helpers::fingerprint;

/// SARIF schema URL pinned to v2.1.0. GitHub Code Scanning accepts both
/// `master` and `2.1.0` paths; pin to the version-tagged one for stability.
const SARIF_SCHEMA: &str = "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json";
const SARIF_VERSION: &str = "2.1.0";

/// Synthetic artifact URI for all results. SARIF requires a `physicalLocation`
/// on every `result`; an SBOM-derived finding has no source line, so we
/// project all results onto a single virtual `sbom` artifact.
const SARIF_ARTIFACT_URI: &str = "sbom";
Loading
Loading