use std::collections::{HashMap, HashSet};
use rustsec::{Report, Vulnerability, Warning, WarningKind, advisory};
use serde::{Serialize, Serializer, ser::SerializeStruct};
#[derive(Debug)]
pub struct SarifLog {
runs: Vec<Run>,
}
impl SarifLog {
pub fn from_report(report: &Report, cargo_lock_path: &str) -> Self {
Self {
runs: vec![Run::from_report(report, cargo_lock_path)],
}
}
}
impl Serialize for SarifLog {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut state = Serializer::serialize_struct(serializer, "SarifLog", 3)?;
state.serialize_field("$schema", "https://json.schemastore.org/sarif-2.1.0.json")?;
state.serialize_field("version", "2.1.0")?;
state.serialize_field("runs", &self.runs)?;
state.end()
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Run {
tool: Tool,
results: Vec<SarifResult>,
}
impl Run {
fn from_report(report: &Report, cargo_lock_path: &str) -> Self {
let mut rules = Vec::new();
let mut seen_rules = HashSet::new();
let mut results = Vec::new();
for vuln in &report.vulnerabilities.list {
let rule_id = vuln.advisory.id.to_string();
if seen_rules.insert(rule_id.clone()) {
rules.push(ReportingDescriptor::from_advisory(&vuln.advisory, true));
}
results.push(SarifResult::from_vulnerability(vuln, cargo_lock_path));
}
for (warning_kind, warnings) in &report.warnings {
for warning in warnings {
let rule_id = if let Some(advisory) = &warning.advisory {
advisory.id.to_string()
} else {
format!("{warning_kind:?}").to_lowercase()
};
if seen_rules.insert(rule_id) {
rules.push(match &warning.advisory {
Some(advisory) => ReportingDescriptor::from_advisory(advisory, false),
None => ReportingDescriptor::from_warning_kind(*warning_kind),
});
}
results.push(SarifResult::from_warning(warning, cargo_lock_path));
}
}
Self {
tool: Tool {
driver: ToolComponent { rules },
},
results,
}
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct Tool {
driver: ToolComponent,
}
#[derive(Debug)]
struct ToolComponent {
rules: Vec<ReportingDescriptor>,
}
impl Serialize for ToolComponent {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut state = serializer.serialize_struct("ToolComponent", 4)?;
state.serialize_field("name", "cargo-audit")?;
state.serialize_field("version", env!("CARGO_PKG_VERSION"))?;
state.serialize_field("semanticVersion", env!("CARGO_PKG_VERSION"))?;
state.serialize_field("rules", &self.rules)?;
state.end()
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct ReportingDescriptor {
id: String,
name: String,
short_description: MultiformatMessageString,
#[serde(skip_serializing_if = "Option::is_none")]
full_description: Option<MultiformatMessageString>,
default_configuration: ReportingConfiguration,
#[serde(skip_serializing_if = "Option::is_none")]
help: Option<MultiformatMessageString>,
properties: RuleProperties,
}
impl ReportingDescriptor {
fn from_advisory(metadata: &advisory::Metadata, is_vulnerability: bool) -> Self {
let tags = if is_vulnerability {
&[Tag::Security, Tag::Vulnerability]
} else {
&[Tag::Security, Tag::Warning]
};
let security_severity = metadata
.cvss
.as_ref()
.map(|cvss| format!("{:.1}", cvss.score()));
ReportingDescriptor {
id: metadata.id.to_string(),
name: metadata.id.to_string(),
short_description: MultiformatMessageString {
text: metadata.title.clone(),
markdown: None,
},
full_description: if metadata.description.is_empty() {
None
} else {
Some(MultiformatMessageString {
text: metadata.description.clone(),
markdown: None,
})
},
default_configuration: ReportingConfiguration {
level: match is_vulnerability {
true => ReportingLevel::Error,
false => ReportingLevel::Warning,
},
},
help: metadata.url.as_ref().map(|url| MultiformatMessageString {
text: format!("For more information, see: {url}"),
markdown: Some(format!(
"For more information, see: [{}]({url})",
metadata.id
)),
}),
properties: RuleProperties {
tags,
precision: Precision::VeryHigh,
problem_severity: if !is_vulnerability {
Some(ProblemSeverity::Warning)
} else {
None
},
security_severity,
},
}
}
fn from_warning_kind(kind: WarningKind) -> Self {
let (name, description) = match kind {
WarningKind::Unmaintained => (
"unmaintained",
"Package is unmaintained and may have unaddressed security vulnerabilities",
),
WarningKind::Unsound => (
"unsound",
"Package has known soundness issues that may lead to memory safety problems",
),
WarningKind::Yanked => (
"yanked",
"Package version has been yanked from the registry",
),
_ => ("unknown", "Unknown warning type"),
};
ReportingDescriptor {
id: name.to_string(),
name: name.to_string(),
short_description: MultiformatMessageString {
text: description.to_string(),
markdown: None,
},
full_description: None,
default_configuration: ReportingConfiguration {
level: ReportingLevel::Warning,
},
help: None,
properties: RuleProperties {
tags: &[Tag::Security, Tag::Warning],
precision: Precision::High,
problem_severity: Some(ProblemSeverity::Warning),
security_severity: None,
},
}
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct RuleProperties {
#[serde(skip_serializing_if = "<[Tag]>::is_empty")]
tags: &'static [Tag],
precision: Precision,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "problem.severity")]
problem_severity: Option<ProblemSeverity>,
#[serde(skip_serializing_if = "Option::is_none")]
#[serde(rename = "security-severity")]
security_severity: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "lowercase")]
enum ProblemSeverity {
Warning,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct ReportingConfiguration {
level: ReportingLevel,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
enum ReportingLevel {
Error,
Warning,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MultiformatMessageString {
text: String,
#[serde(skip_serializing_if = "Option::is_none")]
markdown: Option<String>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SarifResult {
rule_id: String,
message: Message,
level: ResultLevel,
locations: Vec<Location>,
partial_fingerprints: HashMap<String, String>,
}
impl SarifResult {
fn from_vulnerability(vuln: &Vulnerability, cargo_lock_path: &str) -> Self {
let fingerprint = format!(
"{}:{}:{}",
vuln.advisory.id, vuln.package.name, vuln.package.version
);
SarifResult {
rule_id: vuln.advisory.id.to_string(),
message: Message {
text: format!(
"{} {} is vulnerable to {} ({})",
vuln.package.name, vuln.package.version, vuln.advisory.id, vuln.advisory.title
),
},
level: ResultLevel::Error,
locations: vec![Location::new(cargo_lock_path)],
partial_fingerprints: {
let mut fingerprints = HashMap::new();
fingerprints.insert("cargo-audit/advisory-fingerprint".to_string(), fingerprint);
fingerprints
},
}
}
fn from_warning(warning: &Warning, cargo_lock_path: &str) -> Self {
let rule_id = if let Some(advisory) = &warning.advisory {
advisory.id.to_string()
} else {
format!("{:?}", warning.kind).to_lowercase()
};
let message_text = if let Some(advisory) = &warning.advisory {
format!(
"{} {} has a {} warning: {}",
warning.package.name,
warning.package.version,
warning.kind.as_str(),
advisory.title
)
} else {
format!(
"{} {} has a {} warning",
warning.package.name,
warning.package.version,
warning.kind.as_str()
)
};
let fingerprint = format!(
"{rule_id}:{}:{}",
warning.package.name, warning.package.version
);
SarifResult {
rule_id,
message: Message { text: message_text },
level: ResultLevel::Warning,
locations: vec![Location::new(cargo_lock_path)],
partial_fingerprints: {
let mut fingerprints = HashMap::new();
fingerprints.insert("cargo-audit/advisory-fingerprint".to_string(), fingerprint);
fingerprints
},
}
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
enum ResultLevel {
Error,
Warning,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct Message {
text: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct Location {
physical_location: PhysicalLocation,
}
impl Location {
fn new(cargo_lock_path: &str) -> Self {
Self {
physical_location: PhysicalLocation {
artifact_location: ArtifactLocation {
uri: cargo_lock_path.to_string(),
},
region: Region { start_line: 1 },
},
}
}
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct PhysicalLocation {
artifact_location: ArtifactLocation,
region: Region,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct ArtifactLocation {
uri: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
struct Region {
start_line: u32,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
enum Tag {
Security,
Vulnerability,
Warning,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "kebab-case")]
enum Precision {
High,
VeryHigh,
}