use std::collections::{BTreeMap, HashSet};
use rayon::prelude::*;
use serde_sarif::sarif;
use super::*;
use crate::defaults::get_builtin_rules;
impl DetailsReporter {
fn sarif_level_for_confidence(confidence: &str) -> sarif::ResultLevel {
match confidence.to_ascii_lowercase().as_str() {
"low" => sarif::ResultLevel::Note,
"medium" => sarif::ResultLevel::Warning,
"high" => sarif::ResultLevel::Error,
_ => sarif::ResultLevel::Warning,
}
}
fn record_to_sarif_result(&self, record: &FindingReporterRecord) -> Result<sarif::Result> {
let finding = &record.finding;
let artifact_location =
sarif::ArtifactLocation::builder().uri(finding.path.clone()).build();
let region = sarif::Region::builder()
.start_line(finding.line as i64)
.start_column(finding.column_start as i64)
.end_line(finding.line as i64)
.end_column(finding.column_end as i64)
.snippet(sarif::ArtifactContent::builder().text(finding.snippet.clone()).build())
.build();
let mut props = BTreeMap::new();
props.insert("validation_status".to_string(), serde_json::json!(finding.validation.status));
props.insert(
"validation_outcome".to_string(),
serde_json::json!(finding.validation.outcome),
);
props.insert("entropy".to_string(), serde_json::json!(finding.entropy));
props.insert("description".to_string(), serde_json::json!(record.rule.description));
if let Some(git) = &finding.git_metadata {
props.insert("git_metadata".to_string(), git.clone());
}
if let Some(validate_cmd) = &finding.validate_command {
props.insert("validate_command".to_string(), serde_json::json!(validate_cmd));
}
if let Some(revoke_cmd) = &finding.revoke_command {
props.insert("revoke_command".to_string(), serde_json::json!(revoke_cmd));
}
if let Some(blast_radius_cmd) = &finding.blast_radius_command {
props.insert("blast_radius_command".to_string(), serde_json::json!(blast_radius_cmd));
}
let properties = sarif::PropertyBag::builder().additional_properties(props).build();
let location = sarif::Location::builder()
.physical_location(
sarif::PhysicalLocation::builder()
.artifact_location(artifact_location)
.region(region)
.build(),
)
.properties(properties)
.build();
let message = sarif::Message::builder()
.text(format!("{} matched {}", record.rule.title, finding.path))
.build();
let result = sarif::Result::builder()
.rule_id(&record.rule.id)
.message(message)
.kind(sarif::ResultKind::Review)
.locations(vec![location])
.level(Self::sarif_level_for_confidence(&finding.confidence))
.partial_fingerprints([("fingerprint".to_string(), finding.fingerprint.clone())])
.build();
Ok(result)
}
pub fn sarif_format<W: std::io::Write>(
&self,
mut writer: W,
_no_dedup: bool,
args: &cli::commands::scan::ScanArgs,
) -> Result<()> {
let envelope = self.build_report_envelope(args)?;
let finding_rule_ids: HashSet<_> =
envelope.findings.iter().map(|r| r.rule.id.clone()).collect();
let rules: Vec<sarif::ReportingDescriptor> = get_builtin_rules(None)?
.iter_rules()
.par_bridge()
.filter_map(|rule| {
if finding_rule_ids.contains(&rule.id) {
let help = sarif::MultiformatMessageString::builder()
.text(rule.references.join("\n"))
.build();
let title = sarif::MultiformatMessageString::builder()
.text(finding_title(&rule.id))
.build();
let description =
sarif::MultiformatMessageString::builder().text(&rule.name).build();
Some(
sarif::ReportingDescriptor::builder()
.id(&rule.id)
.short_description(title)
.full_description(description)
.help(help)
.build(),
)
} else {
None
}
})
.collect();
let tool = sarif::Tool::builder()
.driver(
sarif::ToolComponent::builder()
.name("kingfisher".to_string())
.semantic_version(env!("CARGO_PKG_VERSION").to_string())
.full_name(format!("Kingfisher {}", env!("CARGO_PKG_VERSION")))
.information_uri(env!("CARGO_PKG_HOMEPAGE").to_string())
.download_uri(env!("CARGO_PKG_REPOSITORY").to_string())
.short_description(
sarif::MultiformatMessageString::builder()
.text(env!("CARGO_PKG_DESCRIPTION"))
.build(),
)
.rules(rules)
.build(),
)
.build();
let sarif_results: Vec<sarif::Result> =
envelope.findings.iter().filter_map(|r| self.record_to_sarif_result(r).ok()).collect();
let run_builder = sarif::Run::builder().tool(tool).results(sarif_results);
let mut props = BTreeMap::new();
if let Some(access_map) = envelope.access_map {
props.insert("access_map".to_string(), serde_json::to_value(access_map)?);
}
if let Some(audit) = envelope.audit {
props.insert("repository_audit".to_string(), serde_json::to_value(audit)?);
}
let run = if props.is_empty() {
run_builder.build()
} else {
let property_bag = sarif::PropertyBag::builder().additional_properties(props).build();
run_builder.properties(property_bag).build()
};
let sarif = sarif::Sarif::builder()
.version(sarif::Version::V2_1_0.to_string())
.schema(sarif::SCHEMA_URL)
.runs(vec![run])
.build();
serde_json::to_writer_pretty(&mut writer, &sarif)?;
writeln!(writer)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{findings_store::FindingsStore, reporter::styles::Styles};
use std::sync::{Arc, Mutex};
use tempfile::tempdir;
fn test_reporter() -> DetailsReporter {
let tmp = tempdir().expect("tempdir");
let store = FindingsStore::new(tmp.path().to_path_buf());
DetailsReporter {
datastore: Arc::new(Mutex::new(store)),
styles: Styles::new(false),
validation_filter: cli::commands::scan::ValidationFilter::All,
audit_context: None,
}
}
fn sample_record(confidence: &str) -> FindingReporterRecord {
FindingReporterRecord {
rule: RuleMetadata {
title: "TEST-RULE => [RULE-1]".to_string(),
name: "test-rule".to_string(),
id: "rule-1".to_string(),
description: "Test rule description".to_string(),
},
finding: FindingRecordData {
dependent_captures: Default::default(),
ambiguous_dependencies: Default::default(),
snippet: "secret".to_string(),
fingerprint: "fingerprint".to_string(),
confidence: confidence.to_string(),
entropy: "0.0".to_string(),
validation: ValidationInfo {
outcome: kingfisher_core::ValidationOutcome::NotAttempted,
status: "unknown".to_string(),
response: "n/a".to_string(),
},
language: "Rust".to_string(),
line: 1,
column_start: 1,
column_end: 5,
path: "src/lib.rs".to_string(),
encoding: None,
git_metadata: None,
validate_command: None,
revoke_command: None,
blast_radius_command: None,
},
}
}
#[test]
fn sarif_level_maps_from_confidence() {
let reporter = test_reporter();
let low = reporter.record_to_sarif_result(&sample_record("low")).unwrap();
let medium = reporter.record_to_sarif_result(&sample_record("medium")).unwrap();
let high = reporter.record_to_sarif_result(&sample_record("high")).unwrap();
let expected_low = sarif::ResultLevel::Note.to_string();
let expected_medium = sarif::ResultLevel::Warning.to_string();
let expected_high = sarif::ResultLevel::Error.to_string();
assert_eq!(low.level.map(|level| level.to_string()), Some(expected_low));
assert_eq!(medium.level.map(|level| level.to_string()), Some(expected_medium));
assert_eq!(high.level.map(|level| level.to_string()), Some(expected_high));
}
}