use deps_core::diagnostic::{Diagnostic, Severity};
use deps_core::lsp_helpers::{
DEPRECATED_DIAGNOSTIC_CODE, DependencyOutcomes, LICENSE_POLICY_VIOLATION_DIAGNOSTIC_CODE,
UNSATISFIABLE_DIAGNOSTIC_CODE,
};
use deps_core::osv::{OsvClient, ScanOutcome, VulnSeverity, VulnerabilityMap};
use deps_core::policy_config::PolicyConfig;
use deps_core::position::Range;
use deps_core::{Dependency, Ecosystem, EcosystemId, HttpCache, PackageName, VersionData};
use deps_engine::classify::diff::{
merge_deprecations_after_fetch, merge_no_comparable_versions_after_fetch,
};
use deps_engine::classify::fetch::{
apply_fetch_outcomes, composer_minimum_stability, dedup_dependencies_by_source,
fetch_latest_versions_parallel,
};
use deps_engine::classify::osv::build_scan_targets;
use deps_engine::classify::resolved::{collect_in_use_versions, load_resolved_versions};
use std::collections::{BTreeMap, HashMap};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use std::time::Duration;
const GITHUB_ACTIONS_MUTABLE_REF_PIN_CODE: &str = "mutable-ref-pin";
const GITLAB_CI_MUTABLE_REF_PIN_CODE: &str = "gitlab-ci-mutable-ref-pin";
const GITLAB_CI_UNRESOLVED_HOST_CODE: &str = "unresolved-gitlab-host";
const OSV_SCAN_TIMEOUT_CEILING_SECS: u64 = 30;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, clap::ValueEnum)]
pub enum Category {
Outdated,
Yanked,
Vulnerable,
Unsatisfiable,
#[value(name = "mutable-ref")]
MutableRefPin,
License,
Deprecated,
#[value(skip)]
Other,
}
impl Category {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Outdated => "outdated",
Self::Yanked => "yanked",
Self::Vulnerable => "vulnerable",
Self::Unsatisfiable => "unsatisfiable",
Self::MutableRefPin => "mutable-ref",
Self::License => "license",
Self::Deprecated => "deprecated",
Self::Other => "other",
}
}
#[must_use]
pub const fn description(self) -> &'static str {
match self {
Self::Outdated => {
"A newer version is published for the dependency's declared requirement."
}
Self::Yanked => "The in-use version has been yanked or retracted from the registry.",
Self::Vulnerable => "A known security advisory affects the in-use version.",
Self::Unsatisfiable => "No published version satisfies the declared requirement.",
Self::MutableRefPin => {
"Pinned to a mutable ref (tag or branch) instead of a commit SHA."
}
Self::License => "The resolved license violates the configured allow/deny policy.",
Self::Deprecated => {
"The registry reports the package itself as deprecated or abandoned."
}
Self::Other => "A finding that does not map to any of the other categories.",
}
}
}
impl std::fmt::Display for Category {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone)]
pub struct CheckFinding {
pub ecosystem: EcosystemId,
pub manifest_path: PathBuf,
pub dependency_name: Option<String>,
pub requirement: Option<String>,
pub category: Category,
pub code: Option<String>,
pub advisory_url: Option<String>,
pub advisory_severity: Option<VulnSeverity>,
pub severity: Severity,
pub range: Range,
pub message: String,
}
#[derive(Debug, Clone, Default)]
pub struct CheckReport {
pub findings: Vec<CheckFinding>,
}
impl CheckReport {
#[must_use]
pub fn summary(&self) -> BTreeMap<Category, usize> {
let mut counts = BTreeMap::new();
for finding in &self.findings {
*counts.entry(finding.category).or_insert(0_usize) += 1;
}
counts
}
}
#[derive(Debug, Clone)]
pub struct FailOnPolicy {
categories: Vec<Category>,
}
impl FailOnPolicy {
#[must_use]
pub const fn new(categories: Vec<Category>) -> Self {
Self { categories }
}
#[must_use]
pub fn default_categories() -> Self {
Self::new(vec![
Category::Vulnerable,
Category::Yanked,
Category::Unsatisfiable,
])
}
#[must_use]
pub fn categories(&self) -> &[Category] {
&self.categories
}
#[must_use]
pub fn matches(&self, findings: &[CheckFinding]) -> bool {
findings
.iter()
.any(|finding| self.categories.contains(&finding.category))
}
}
impl Default for FailOnPolicy {
fn default() -> Self {
Self::default_categories()
}
}
#[derive(Clone)]
pub struct CheckContext {
pub cache: Arc<HttpCache>,
pub osv: Arc<OsvClient>,
pub lockfile_cache: Arc<deps_core::lockfile::LockFileCache>,
pub policy: PolicyConfig,
}
#[derive(Debug, thiserror::Error)]
pub enum CheckError {
#[error("failed to parse {path}: {source}")]
Parse {
path: PathBuf,
#[source]
source: deps_core::DepsError,
},
#[error("could not build a file URI for {path}")]
InvalidPath {
path: PathBuf,
},
}
#[derive(Debug, Clone)]
pub struct ManifestCheckResult {
pub findings: Vec<CheckFinding>,
pub registry_unreachable: bool,
}
pub async fn check_manifest(
ecosystem: &Arc<dyn Ecosystem>,
manifest_path: &Path,
display_path: &Path,
content: &str,
ctx: &CheckContext,
) -> Result<ManifestCheckResult, CheckError> {
let uri = path_to_uri(manifest_path).ok_or_else(|| CheckError::InvalidPath {
path: manifest_path.to_path_buf(),
})?;
let parse_result = deps_core::parse_manifest_blocking(ecosystem, content, &uri)
.await
.map_err(|source| CheckError::Parse {
path: manifest_path.to_path_buf(),
source,
})?;
let formatter = ecosystem.formatter();
let ecosystem_id = ecosystem.ecosystem_id();
let (resolved_versions, resolved_version_candidates) =
load_resolved_versions(&uri, &ctx.lockfile_cache, ecosystem.as_ref()).await;
let (dep_sources, collided_names) =
dedup_dependencies_by_source(parse_result.as_ref(), formatter);
let in_use = collect_in_use_versions(
parse_result.as_ref(),
&resolved_versions,
&resolved_version_candidates,
formatter,
ecosystem_id,
);
let minimum_stability = composer_minimum_stability(parse_result.as_ref());
let attempted_names: Vec<PackageName> = dep_sources.keys().cloned().collect();
let fetch_result = fetch_latest_versions_parallel(
ecosystem.registry(),
dep_sources.into_iter().collect(),
&in_use,
None,
ctx.policy.freshness.to_settings(),
ctx.policy.cache.fetch_timeout_secs,
ctx.policy.cache.max_concurrent_fetches,
minimum_stability.as_deref(),
)
.await;
let registry_unreachable = !ctx.policy.network.offline && !fetch_result.fetch_failed.is_empty();
let mut outcomes = DependencyOutcomes::new();
let fetched_names: Vec<PackageName> = fetch_result.versions.keys().cloned().collect();
apply_fetch_outcomes(
&mut outcomes,
fetch_result.yanked_versions,
fetch_result.fetch_failed,
collided_names,
formatter,
);
merge_deprecations_after_fetch(
&mut outcomes,
&fetched_names,
fetch_result.deprecations,
formatter,
);
merge_no_comparable_versions_after_fetch(
&mut outcomes,
&attempted_names,
fetch_result.no_comparable_versions,
formatter,
);
let cached_versions = fetch_result.versions;
let licenses = fetch_result.licenses;
let vulnerabilities: Option<VulnerabilityMap> =
if ctx.policy.diagnostics.vulnerabilities_enabled && !ctx.policy.network.offline {
let (targets, skipped) = build_scan_targets(
parse_result.as_ref(),
&resolved_versions,
&resolved_version_candidates,
formatter,
ecosystem_id,
);
let mut vulns = skipped;
if !targets.is_empty() {
let timeout = Duration::from_secs(
ctx.policy
.cache
.fetch_timeout_secs
.min(OSV_SCAN_TIMEOUT_CEILING_SECS),
);
let scanned = ctx.osv.scan(ecosystem_id, &targets, timeout).await;
vulns.extend(scanned);
}
Some(vulns)
} else {
None
};
let license_policy = ctx.policy.license_policy.to_policy();
let mut version_data = VersionData::new(&cached_versions, &resolved_versions)
.with_resolved_version_candidates(&resolved_version_candidates)
.with_outcomes(&outcomes)
.with_ecosystem(ecosystem_id)
.with_offline(ctx.policy.network.offline)
.with_license_source(ecosystem.license_source())
.with_license_policy(&license_policy)
.with_license_prefetch(&licenses);
if let Some(vulnerabilities) = vulnerabilities.as_ref() {
version_data = version_data.with_vulnerabilities(vulnerabilities);
}
let severities = ctx.policy.diagnostics.to_severities();
let diagnostics = ecosystem
.generate_diagnostics(
parse_result.as_ref(),
version_data,
&uri,
ctx.policy.freshness.to_settings(),
severities,
)
.await;
let dep_index = DependencyIndex::build(parse_result.as_ref());
let advisory_severities = advisory_severity_index(vulnerabilities.as_ref());
let vuln_keys = deps_core::osv::vulnerability_keys(
parse_result.as_ref(),
&resolved_versions,
Some(&resolved_version_candidates),
formatter,
ecosystem_id,
);
let findings = diagnostics
.into_iter()
.map(|diagnostic| {
to_finding(
ecosystem_id,
display_path,
&dep_index,
formatter,
diagnostic,
&advisory_severities,
&vuln_keys,
)
})
.collect();
Ok(ManifestCheckResult {
findings,
registry_unreachable,
})
}
struct DependencyIndex<'a> {
by_name_range: HashMap<Range, &'a dyn Dependency>,
by_version_range: HashMap<Range, &'a dyn Dependency>,
}
impl<'a> DependencyIndex<'a> {
fn build(parse_result: &'a dyn deps_core::ParseResult) -> Self {
let mut by_name_range = HashMap::new();
let mut by_version_range = HashMap::new();
for dep in parse_result.dependencies() {
if !dep.name_range_is_synthetic() {
by_name_range.insert(dep.name_range(), dep);
}
if let Some(version_range) = dep.version_range() {
by_version_range.insert(version_range, dep);
}
}
Self {
by_name_range,
by_version_range,
}
}
fn lookup(&self, range: Range) -> Option<&'a dyn Dependency> {
self.by_name_range
.get(&range)
.or_else(|| self.by_version_range.get(&range))
.copied()
}
}
fn advisory_severity_index(
vulnerabilities: Option<&VulnerabilityMap>,
) -> HashMap<(String, String), VulnSeverity> {
let mut index = HashMap::new();
let Some(vulnerabilities) = vulnerabilities else {
return index;
};
for (dependency_key, outcome) in vulnerabilities {
if let ScanOutcome::Vulnerable(dv) = outcome {
for advisory in dv.advisories.items() {
index.insert(
(dependency_key.clone(), advisory.id.clone()),
advisory.severity,
);
}
}
}
index
}
fn to_finding(
ecosystem: EcosystemId,
display_path: &Path,
dep_index: &DependencyIndex<'_>,
formatter: &dyn deps_core::lsp_helpers::EcosystemFormatter,
diagnostic: Diagnostic,
advisory_severities: &HashMap<(String, String), VulnSeverity>,
vuln_keys: &HashMap<Range, String>,
) -> CheckFinding {
let category = classify(&diagnostic, formatter);
let dep = dep_index.lookup(diagnostic.range);
let code = diagnostic.code.clone();
let advisory_url = diagnostic
.code_description
.as_ref()
.map(|code_description| code_description.href.as_str().to_string());
let advisory_severity = code.as_deref().zip(dep).and_then(|(code, dep)| {
let dependency_key = vuln_keys.get(&dep.name_range())?;
advisory_severities
.get(&(dependency_key.clone(), code.to_string()))
.copied()
});
CheckFinding {
ecosystem,
manifest_path: display_path.to_path_buf(),
dependency_name: dep.map(|d| d.name().to_string()),
requirement: dep
.and_then(Dependency::version_requirement)
.map(ToString::to_string),
category,
code,
advisory_url,
advisory_severity,
severity: diagnostic.severity.unwrap_or(Severity::Warning),
range: diagnostic.range,
message: diagnostic.message,
}
}
fn classify(
diagnostic: &Diagnostic,
formatter: &dyn deps_core::lsp_helpers::EcosystemFormatter,
) -> Category {
if let Some(code) = &diagnostic.code {
return match code.as_str() {
UNSATISFIABLE_DIAGNOSTIC_CODE => Category::Unsatisfiable,
LICENSE_POLICY_VIOLATION_DIAGNOSTIC_CODE => Category::License,
DEPRECATED_DIAGNOSTIC_CODE => Category::Deprecated,
GITHUB_ACTIONS_MUTABLE_REF_PIN_CODE | GITLAB_CI_MUTABLE_REF_PIN_CODE => {
Category::MutableRefPin
}
GITLAB_CI_UNRESOLVED_HOST_CODE => Category::Other,
_ => Category::Vulnerable,
};
}
if diagnostic.message.starts_with("Newer version available") {
return Category::Outdated;
}
if diagnostic.message.contains(formatter.yanked_message()) {
return Category::Yanked;
}
if diagnostic.message.ends_with("more advisories") {
return Category::Vulnerable;
}
Category::Other
}
fn path_to_uri(path: &Path) -> Option<url::Url> {
let absolute = if path.is_absolute() {
path.to_path_buf()
} else {
std::env::current_dir()
.map(|cwd| cwd.join(path))
.unwrap_or_else(|_| path.to_path_buf())
};
url::Url::from_file_path(&absolute).ok()
}
#[cfg(test)]
mod tests {
use super::*;
fn finding(category: Category) -> 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: Severity::Warning,
range: Range::default(),
message: "test".to_string(),
}
}
#[test]
fn test_category_as_str_matches_fr009_tokens() {
assert_eq!(Category::Outdated.as_str(), "outdated");
assert_eq!(Category::Yanked.as_str(), "yanked");
assert_eq!(Category::Vulnerable.as_str(), "vulnerable");
assert_eq!(Category::Unsatisfiable.as_str(), "unsatisfiable");
assert_eq!(Category::MutableRefPin.as_str(), "mutable-ref");
assert_eq!(Category::License.as_str(), "license");
assert_eq!(Category::Deprecated.as_str(), "deprecated");
assert_eq!(Category::Other.as_str(), "other");
}
#[test]
fn test_fail_on_policy_default_categories() {
let policy = FailOnPolicy::default_categories();
assert!(policy.matches(&[finding(Category::Vulnerable)]));
assert!(policy.matches(&[finding(Category::Yanked)]));
assert!(policy.matches(&[finding(Category::Unsatisfiable)]));
assert!(!policy.matches(&[finding(Category::Outdated)]));
assert!(!policy.matches(&[finding(Category::License)]));
assert!(!policy.matches(&[finding(Category::Deprecated)]));
assert!(!policy.matches(&[finding(Category::MutableRefPin)]));
assert!(!policy.matches(&[finding(Category::Other)]));
}
#[test]
fn test_fail_on_policy_custom_categories() {
let policy = FailOnPolicy::new(vec![Category::License]);
assert!(policy.matches(&[finding(Category::License)]));
assert!(!policy.matches(&[finding(Category::Vulnerable)]));
}
#[test]
fn test_fail_on_policy_empty_findings_never_matches() {
assert!(!FailOnPolicy::default_categories().matches(&[]));
}
#[test]
fn test_check_report_summary_counts_per_category() {
let report = CheckReport {
findings: vec![
finding(Category::Outdated),
finding(Category::Outdated),
finding(Category::Vulnerable),
],
};
let summary = report.summary();
assert_eq!(summary.get(&Category::Outdated), Some(&2));
assert_eq!(summary.get(&Category::Vulnerable), Some(&1));
assert_eq!(summary.get(&Category::License), None);
}
#[test]
fn test_check_report_summary_empty_for_no_findings() {
let report = CheckReport::default();
assert!(report.summary().is_empty());
}
struct StubFormatter;
impl deps_core::lsp_helpers::PackageNaming for StubFormatter {}
impl deps_core::lsp_helpers::PackageRendering for StubFormatter {
fn format_version_for_text_edit(&self, version: &deps_core::ConcreteVersion) -> String {
version.to_string()
}
fn package_url(&self, name: &PackageName) -> String {
name.to_string()
}
}
impl deps_core::lsp_helpers::RequirementResolution for StubFormatter {}
impl deps_core::lsp_helpers::DiagnosticMessages for StubFormatter {}
impl deps_core::lsp_helpers::DiagnosticPolicy for StubFormatter {}
impl deps_core::lsp_helpers::SourcePolicy for StubFormatter {}
impl deps_core::lsp_helpers::OsvNaming for StubFormatter {}
fn diagnostic_with(code: Option<&str>, message: &str) -> Diagnostic {
let diagnostic =
Diagnostic::new(Range::default(), message).with_severity(Severity::Warning);
match code {
Some(code) => diagnostic.with_code(code),
None => diagnostic,
}
}
#[test]
fn test_classify_unsatisfiable_by_code() {
let d = diagnostic_with(Some(UNSATISFIABLE_DIAGNOSTIC_CODE), "no matching version");
assert_eq!(classify(&d, &StubFormatter), Category::Unsatisfiable);
}
#[test]
fn test_classify_license_by_code() {
let d = diagnostic_with(
Some(LICENSE_POLICY_VIOLATION_DIAGNOSTIC_CODE),
"GPL-3.0 denied",
);
assert_eq!(classify(&d, &StubFormatter), Category::License);
}
#[test]
fn test_classify_deprecated_by_code() {
let d = diagnostic_with(Some(DEPRECATED_DIAGNOSTIC_CODE), "package deprecated");
assert_eq!(classify(&d, &StubFormatter), Category::Deprecated);
}
#[test]
fn test_classify_mutable_ref_pin_by_code() {
let d = diagnostic_with(Some(GITHUB_ACTIONS_MUTABLE_REF_PIN_CODE), "pinned to a tag");
assert_eq!(classify(&d, &StubFormatter), Category::MutableRefPin);
let d = diagnostic_with(Some(GITLAB_CI_MUTABLE_REF_PIN_CODE), "pinned to a tag");
assert_eq!(classify(&d, &StubFormatter), Category::MutableRefPin);
}
#[test]
fn test_classify_advisory_code_is_vulnerable() {
let d = diagnostic_with(Some("RUSTSEC-2024-0001"), "advisory summary");
assert_eq!(classify(&d, &StubFormatter), Category::Vulnerable);
}
#[test]
fn test_classify_outdated_by_message_prefix() {
let d = diagnostic_with(None, "Newer version available: 2.0.0");
assert_eq!(classify(&d, &StubFormatter), Category::Outdated);
}
#[test]
fn test_classify_yanked_by_formatter_message() {
let d = diagnostic_with(None, "This version has been yanked (1.0.0)");
assert_eq!(classify(&d, &StubFormatter), Category::Yanked);
}
#[test]
fn test_classify_unknown_package_is_other() {
let d = diagnostic_with(None, "Unknown package 'left-pad'");
assert_eq!(classify(&d, &StubFormatter), Category::Other);
}
#[test]
fn test_classify_advisory_overflow_summary_is_vulnerable() {
let d = diagnostic_with(None, "+5 more advisories");
assert_eq!(classify(&d, &StubFormatter), Category::Vulnerable);
}
#[test]
fn test_classify_gitlab_unresolved_host_is_other_not_vulnerable() {
let d = diagnostic_with(
Some(GITLAB_CI_UNRESOLVED_HOST_CODE),
"registries.gitlab_instance_host is unset; skipping component/project host resolution",
);
assert_eq!(classify(&d, &StubFormatter), Category::Other);
}
fn dep_index_with_one_dependency() -> Box<dyn deps_core::ParseResult> {
deps_core::test_util::stub_parse_result_with_dependencies(1)
}
fn empty_dep_index() -> Box<dyn deps_core::ParseResult> {
deps_core::test_util::stub_parse_result_with_dependencies(0)
}
#[test]
fn test_to_finding_extracts_string_code_from_diagnostic() {
let parse_result = empty_dep_index();
let dep_index = DependencyIndex::build(parse_result.as_ref());
let diagnostic = diagnostic_with(Some("RUSTSEC-2024-0001"), "advisory summary");
let finding = to_finding(
EcosystemId::Cargo,
Path::new("Cargo.toml"),
&dep_index,
&StubFormatter,
diagnostic,
&HashMap::new(),
&HashMap::new(),
);
assert_eq!(finding.code.as_deref(), Some("RUSTSEC-2024-0001"));
}
#[test]
fn test_to_finding_code_is_none_without_a_diagnostic_code() {
let parse_result = empty_dep_index();
let dep_index = DependencyIndex::build(parse_result.as_ref());
let diagnostic = diagnostic_with(None, "Newer version available: 2.0.0");
let finding = to_finding(
EcosystemId::Cargo,
Path::new("Cargo.toml"),
&dep_index,
&StubFormatter,
diagnostic,
&HashMap::new(),
&HashMap::new(),
);
assert!(finding.code.is_none());
}
#[test]
fn test_to_finding_extracts_advisory_url_from_code_description() {
let parse_result = empty_dep_index();
let dep_index = DependencyIndex::build(parse_result.as_ref());
let href: url::Url = "https://osv.dev/vulnerability/RUSTSEC-2024-0001"
.parse()
.expect("valid URL");
let diagnostic = diagnostic_with(Some("RUSTSEC-2024-0001"), "advisory summary")
.with_code_description(deps_core::diagnostic::CodeDescription::new(href));
let finding = to_finding(
EcosystemId::Cargo,
Path::new("Cargo.toml"),
&dep_index,
&StubFormatter,
diagnostic,
&HashMap::new(),
&HashMap::new(),
);
assert_eq!(
finding.advisory_url.as_deref(),
Some("https://osv.dev/vulnerability/RUSTSEC-2024-0001")
);
}
#[test]
fn test_to_finding_advisory_url_is_none_without_code_description() {
let parse_result = empty_dep_index();
let dep_index = DependencyIndex::build(parse_result.as_ref());
let diagnostic = diagnostic_with(Some("RUSTSEC-2024-0001"), "advisory summary");
let finding = to_finding(
EcosystemId::Cargo,
Path::new("Cargo.toml"),
&dep_index,
&StubFormatter,
diagnostic,
&HashMap::new(),
&HashMap::new(),
);
assert!(finding.advisory_url.is_none());
}
#[test]
fn test_to_finding_resolves_advisory_severity_from_index() {
let parse_result = dep_index_with_one_dependency();
let dep_index = DependencyIndex::build(parse_result.as_ref());
let diagnostic = diagnostic_with(Some("RUSTSEC-2024-0001"), "advisory summary");
let mut vuln_keys = HashMap::new();
vuln_keys.insert(Range::default(), "dep-0".to_string());
let mut severities = HashMap::new();
severities.insert(
("dep-0".to_string(), "RUSTSEC-2024-0001".to_string()),
VulnSeverity::Critical,
);
let finding = to_finding(
EcosystemId::Cargo,
Path::new("Cargo.toml"),
&dep_index,
&StubFormatter,
diagnostic,
&severities,
&vuln_keys,
);
assert_eq!(finding.advisory_severity, Some(VulnSeverity::Critical));
}
#[test]
fn test_to_finding_advisory_severity_is_none_for_an_unindexed_code() {
let parse_result = dep_index_with_one_dependency();
let dep_index = DependencyIndex::build(parse_result.as_ref());
let diagnostic = diagnostic_with(Some("RUSTSEC-2024-0001"), "advisory summary");
let mut vuln_keys = HashMap::new();
vuln_keys.insert(Range::default(), "dep-0".to_string());
let finding = to_finding(
EcosystemId::Cargo,
Path::new("Cargo.toml"),
&dep_index,
&StubFormatter,
diagnostic,
&HashMap::new(),
&vuln_keys,
);
assert!(finding.advisory_severity.is_none());
}
#[test]
fn test_to_finding_advisory_severity_is_none_without_a_matched_dependency() {
let parse_result = empty_dep_index();
let dep_index = DependencyIndex::build(parse_result.as_ref());
let diagnostic = diagnostic_with(Some("RUSTSEC-2024-0001"), "advisory summary");
let mut severities = HashMap::new();
severities.insert(
("dep-0".to_string(), "RUSTSEC-2024-0001".to_string()),
VulnSeverity::Critical,
);
let finding = to_finding(
EcosystemId::Cargo,
Path::new("Cargo.toml"),
&dep_index,
&StubFormatter,
diagnostic,
&severities,
&HashMap::new(),
);
assert!(finding.advisory_severity.is_none());
}
#[test]
fn test_advisory_severity_index_collects_from_vulnerable_outcomes() {
use deps_core::osv::{Advisory, Capped, DependencyVulnerabilities};
use std::sync::Arc;
let advisory = Advisory::new(
"RUSTSEC-2024-0001".to_string(),
"2024-01-01T00:00:00Z".to_string(),
VulnSeverity::High,
"https://osv.dev/vulnerability/RUSTSEC-2024-0001".to_string(),
);
let dv = DependencyVulnerabilities::new(Capped::new(vec![Arc::new(advisory)], 1));
let mut map: VulnerabilityMap = HashMap::new();
map.insert("serde".to_string(), ScanOutcome::Vulnerable(dv));
let index = advisory_severity_index(Some(&map));
assert_eq!(
index.get(&("serde".to_string(), "RUSTSEC-2024-0001".to_string())),
Some(&VulnSeverity::High)
);
}
#[test]
fn test_advisory_severity_index_does_not_collide_across_dependencies_sharing_an_advisory_id() {
use deps_core::osv::{Advisory, Capped, DependencyVulnerabilities};
use std::sync::Arc;
let advisory_for = |severity: VulnSeverity| {
Arc::new(Advisory::new(
"GHSA-shared-id".to_string(),
"2024-01-01T00:00:00Z".to_string(),
severity,
"https://osv.dev/vulnerability/GHSA-shared-id".to_string(),
))
};
let mut map: VulnerabilityMap = HashMap::new();
map.insert(
"package-a".to_string(),
ScanOutcome::Vulnerable(DependencyVulnerabilities::new(Capped::new(
vec![advisory_for(VulnSeverity::Critical)],
1,
))),
);
map.insert(
"package-b".to_string(),
ScanOutcome::Vulnerable(DependencyVulnerabilities::new(Capped::new(
vec![advisory_for(VulnSeverity::Low)],
1,
))),
);
let index = advisory_severity_index(Some(&map));
assert_eq!(
index.get(&("package-a".to_string(), "GHSA-shared-id".to_string())),
Some(&VulnSeverity::Critical)
);
assert_eq!(
index.get(&("package-b".to_string(), "GHSA-shared-id".to_string())),
Some(&VulnSeverity::Low)
);
}
#[test]
fn test_advisory_severity_index_empty_without_vulnerabilities() {
assert!(advisory_severity_index(None).is_empty());
}
}