#![allow(clippy::expect_used)]
use deps_cli::format::sarif::to_sarif;
use deps_cli::report::{Category, CheckFinding, CheckReport};
use deps_core::EcosystemId;
use deps_core::diagnostic::Severity;
use deps_core::osv::VulnSeverity;
use deps_core::position::{Position, Range};
use std::path::PathBuf;
const SCHEMA_JSON: &str = include_str!("fixtures/sarif-2.1.0.schema.json");
const BROKEN_LANGUAGE_PATTERN: &str = "^[a-zA-Z]{2}|^[a-zA-Z]{2}-[a-zA-Z]{2}]?$";
fn strip_broken_language_pattern(value: &mut serde_json::Value) {
match value {
serde_json::Value::Object(map) => {
if map.get("pattern").and_then(serde_json::Value::as_str)
== Some(BROKEN_LANGUAGE_PATTERN)
{
map.remove("pattern");
}
for nested in map.values_mut() {
strip_broken_language_pattern(nested);
}
}
serde_json::Value::Array(items) => {
for item in items {
strip_broken_language_pattern(item);
}
}
_ => {}
}
}
fn schema_validator() -> jsonschema::Validator {
let mut schema: serde_json::Value =
serde_json::from_str(SCHEMA_JSON).expect("vendored SARIF schema must be valid JSON");
strip_broken_language_pattern(&mut schema);
jsonschema::validator_for(&schema).expect("vendored SARIF schema must itself be valid")
}
fn finding(category: Category, severity: Severity) -> CheckFinding {
CheckFinding {
ecosystem: EcosystemId::Cargo,
manifest_path: PathBuf::from("Cargo.toml"),
dependency_name: Some("serde".to_string()),
requirement: Some("1.0".to_string()),
category,
code: None,
advisory_url: None,
advisory_severity: None,
severity,
range: Range::new(Position::new(4, 0), Position::new(4, 10)),
message: "Newer version available: 1.1.0".to_string(),
}
}
fn assert_valid_sarif(report: &CheckReport) {
let validator = schema_validator();
let document = serde_json::to_value(to_sarif(report)).expect("Sarif must serialize to JSON");
if let Err(error) = validator.validate(&document) {
panic!(
"produced SARIF document does not conform to the SARIF 2.1.0 schema: {error}\n\
document: {document:#}"
);
}
}
#[test]
fn test_empty_report_produces_schema_valid_sarif() {
assert_valid_sarif(&CheckReport::default());
}
#[test]
fn test_single_finding_produces_schema_valid_sarif() {
assert_valid_sarif(&CheckReport {
findings: vec![finding(Category::Outdated, Severity::Hint)],
});
}
#[test]
fn test_multi_category_report_produces_schema_valid_sarif() {
assert_valid_sarif(&CheckReport {
findings: vec![
finding(Category::Outdated, Severity::Hint),
finding(Category::Vulnerable, Severity::Error),
finding(Category::License, Severity::Warning),
finding(Category::Other, Severity::Information),
],
});
}
#[test]
fn test_advisory_coded_finding_produces_schema_valid_sarif() {
let mut vulnerable = finding(Category::Vulnerable, Severity::Error);
vulnerable.code = Some("RUSTSEC-2020-0071".to_string());
vulnerable.message = "RUSTSEC-2020-0071: Potential segfault in the time crate".to_string();
vulnerable.advisory_url = Some("https://osv.dev/vulnerability/RUSTSEC-2020-0071".to_string());
vulnerable.advisory_severity = Some(VulnSeverity::High);
assert_valid_sarif(&CheckReport {
findings: vec![vulnerable],
});
}
#[test]
fn test_malformed_advisory_id_does_not_break_schema_validation() {
let mut vulnerable = finding(Category::Vulnerable, Severity::Error);
vulnerable.code = Some("evil id\nwith\"quotes and spaces".to_string());
assert_valid_sarif(&CheckReport {
findings: vec![vulnerable],
});
}