use deps_core::ConcreteVersion;
use deps_core::EcosystemId;
use deps_core::PackageName;
use deps_core::lsp_helpers::resolve_in_use_version;
use std::collections::HashMap;
pub fn build_scan_targets(
parse_result: &dyn deps_core::ParseResult,
resolved_versions: &HashMap<PackageName, ConcreteVersion>,
resolved_version_candidates: &HashMap<PackageName, Vec<ConcreteVersion>>,
formatter: &dyn deps_core::lsp_helpers::EcosystemFormatter,
ecosystem: EcosystemId,
) -> (
Vec<deps_core::osv::ScanTarget>,
deps_core::osv::VulnerabilityMap,
) {
use deps_core::osv::{ScanOutcome, SkipReason};
let mut targets = Vec::new();
let mut skipped = deps_core::osv::VulnerabilityMap::new();
let keys = deps_core::osv::vulnerability_keys(
parse_result,
resolved_versions,
Some(resolved_version_candidates),
formatter,
ecosystem,
);
for dep in parse_result.dependencies() {
let normalized_name = formatter.normalize_package_name(dep.name());
let key = keys
.get(&dep.name_range())
.cloned()
.unwrap_or_else(|| normalized_name.clone());
if !formatter.source_is_public_registry_content(&dep.source()) {
skipped.insert(key, ScanOutcome::Skipped(SkipReason::NonRegistrySource));
continue;
}
let version = resolve_in_use_version(
dep,
&normalized_name,
resolved_versions,
Some(resolved_version_candidates),
formatter,
ecosystem,
);
let Some(version) = version else {
skipped.insert(key, ScanOutcome::Skipped(SkipReason::NoConcreteVersion));
continue;
};
let Some(osv_name) = formatter.osv_package_name(dep) else {
skipped.insert(key, ScanOutcome::Skipped(SkipReason::UnmappableName));
continue;
};
targets.push(deps_core::osv::ScanTarget::new(
key,
osv_name,
formatter.osv_version(&version),
version,
));
}
(targets, skipped)
}
const FIX_TARGET_KEY_SUFFIX: &str = "\u{0}fix";
#[derive(Debug, PartialEq, Eq)]
enum FixTargetResolution {
Skip,
Resolved(deps_core::osv::UpgradeStatus),
NeedsLiveCheck(deps_core::osv::ScanTarget),
}
fn resolve_fix_target(
dv: &deps_core::osv::DependencyVulnerabilities,
key: &str,
latest_native_by_key: &HashMap<String, String>,
osv_name_by_key: &HashMap<String, String>,
formatter: &dyn deps_core::lsp_helpers::EcosystemFormatter,
) -> FixTargetResolution {
use deps_core::lsp_helpers::is_safe_version_string;
use deps_core::osv::ScanTarget;
let Some(fix) = dv.recommended_fix() else {
return FixTargetResolution::Skip;
};
let version_native = formatter.osv_version_to_native(&fix.version);
if !is_safe_version_string(&version_native) {
tracing::debug!(
key,
version = %fix.version,
"OSV #462: fix-target version failed validation, skipping verification"
);
return FixTargetResolution::Skip;
}
if latest_native_by_key.get(key) == Some(&version_native) {
return FixTargetResolution::Resolved(dv.upgrade_status.clone());
}
let Some(osv_name) = osv_name_by_key.get(key).cloned() else {
tracing::debug!(
key,
"OSV #462: no osv_name on record for fix-target verification, skipping"
);
return FixTargetResolution::Skip;
};
FixTargetResolution::NeedsLiveCheck(ScanTarget::new(
format!("{key}{FIX_TARGET_KEY_SUFFIX}"),
osv_name,
fix.version,
version_native,
))
}
pub fn collect_fix_target_resolutions(
vulnerabilities: &deps_core::osv::VulnerabilityMap,
vulnerable_keys: &[String],
osv_name_by_key: &HashMap<String, String>,
latest_native_by_key: &HashMap<String, String>,
formatter: &dyn deps_core::lsp_helpers::EcosystemFormatter,
) -> (
Vec<(String, deps_core::osv::UpgradeStatus)>,
Vec<deps_core::osv::ScanTarget>,
) {
use deps_core::osv::ScanOutcome;
let mut resolved = Vec::new();
let mut live_check_candidates = Vec::new();
for key in vulnerable_keys {
let Some(ScanOutcome::Vulnerable(dv)) = vulnerabilities.get(key.as_str()) else {
continue;
};
match resolve_fix_target(dv, key, latest_native_by_key, osv_name_by_key, formatter) {
FixTargetResolution::Skip => {}
FixTargetResolution::Resolved(status) => resolved.push((key.clone(), status)),
FixTargetResolution::NeedsLiveCheck(target) => live_check_candidates.push(target),
}
}
(resolved, live_check_candidates)
}
pub fn apply_live_fix_target_statuses(
vulnerabilities: &mut deps_core::osv::VulnerabilityMap,
statuses: HashMap<String, deps_core::osv::UpgradeStatus>,
) {
use deps_core::osv::ScanOutcome;
for (synthetic_key, status) in statuses {
let Some(key) = synthetic_key.strip_suffix(FIX_TARGET_KEY_SUFFIX) else {
continue;
};
if let Some(ScanOutcome::Vulnerable(dv)) = vulnerabilities.get_mut(key) {
dv.fix_target_status = status;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::classify::resolved::collect_in_use_versions;
use deps_core::VersionReq;
use std::assert_matches;
mod osv_scan_target_tests {
use super::*;
use deps_core::Dependency;
use deps_core::lsp_helpers::{
DiagnosticMessages, DiagnosticPolicy, OsvNaming, PackageNaming, PackageRendering,
RequirementResolution, SourcePolicy,
};
use deps_core::parser::DependencySource;
use deps_core::position::{Position, Range};
use std::any::Any;
struct MockFormatter;
impl PackageNaming for MockFormatter {}
impl PackageRendering for MockFormatter {
fn format_version_for_text_edit(&self, version: &ConcreteVersion) -> String {
version.to_string()
}
fn package_url(&self, name: &PackageName) -> String {
format!("https://example.com/{name}")
}
}
impl RequirementResolution for MockFormatter {}
impl DiagnosticMessages for MockFormatter {}
impl DiagnosticPolicy for MockFormatter {}
impl SourcePolicy for MockFormatter {}
impl OsvNaming for MockFormatter {}
struct MockDep {
name: PackageName,
version_req: Option<VersionReq>,
source: DependencySource,
}
impl Dependency for MockDep {
fn name(&self) -> &PackageName {
&self.name
}
fn name_range(&self) -> Range {
let addr = std::ptr::from_ref(self) as u32;
Range::new(Position::new(0, addr), Position::new(0, addr + 1))
}
fn version_requirement(&self) -> Option<&VersionReq> {
self.version_req.as_ref()
}
fn version_range(&self) -> Option<Range> {
None
}
fn source(&self) -> DependencySource {
self.source.clone()
}
fn as_any(&self) -> &dyn Any {
self
}
}
struct MockParseResult {
deps: Vec<MockDep>,
}
impl deps_core::ParseResult for MockParseResult {
fn dependencies(&self) -> Vec<&dyn Dependency> {
self.deps.iter().map(|d| d as &dyn Dependency).collect()
}
fn workspace_root(&self) -> Option<&std::path::Path> {
None
}
fn uri(&self) -> &url::Url {
static URI: std::sync::OnceLock<url::Url> = std::sync::OnceLock::new();
URI.get_or_init(|| deps_core::test_util::test_uri("/test/Cargo.toml"))
}
fn as_any(&self) -> &dyn Any {
self
}
}
use deps_core::osv::{ScanOutcome, SkipReason};
#[test]
fn build_scan_targets_step0_skips_non_registry_source_even_with_lockfile_version() {
let parse_result = MockParseResult {
deps: vec![MockDep {
name: PackageName::new("time"),
version_req: Some(VersionReq::new("0.1.43")),
source: DependencySource::Git {
url: "https://github.com/example/time".to_string(),
rev: None,
},
}],
};
let mut resolved = HashMap::new();
resolved.insert(PackageName::new("time"), "0.1.43".into());
let (targets, skipped) = build_scan_targets(
&parse_result,
&resolved,
&HashMap::new(),
&MockFormatter,
EcosystemId::Cargo,
);
assert!(targets.is_empty());
assert_matches!(
skipped.get("time"),
Some(ScanOutcome::Skipped(SkipReason::NonRegistrySource))
);
}
#[test]
fn build_scan_targets_step1_prefers_lockfile_resolved_version() {
let parse_result = MockParseResult {
deps: vec![MockDep {
name: PackageName::new("serde"),
version_req: Some(VersionReq::new("^1.0")),
source: DependencySource::Registry,
}],
};
let mut resolved = HashMap::new();
resolved.insert(PackageName::new("serde"), "1.0.195".into());
let (targets, skipped) = build_scan_targets(
&parse_result,
&resolved,
&HashMap::new(),
&MockFormatter,
EcosystemId::Cargo,
);
assert_eq!(targets.len(), 1);
assert_eq!(targets[0].version, "1.0.195");
assert!(skipped.is_empty());
}
struct MockGoFormatter;
impl PackageNaming for MockGoFormatter {}
impl PackageRendering for MockGoFormatter {
fn format_version_for_text_edit(&self, version: &ConcreteVersion) -> String {
version.to_string()
}
fn package_url(&self, name: &PackageName) -> String {
format!("https://pkg.go.dev/{name}")
}
}
impl RequirementResolution for MockGoFormatter {
fn manifest_requirement_is_resolved_version(&self, _dep: &dyn Dependency) -> bool {
true
}
}
impl DiagnosticMessages for MockGoFormatter {}
impl DiagnosticPolicy for MockGoFormatter {}
impl SourcePolicy for MockGoFormatter {}
impl OsvNaming for MockGoFormatter {}
struct MockVPrefixFormatter;
impl PackageNaming for MockVPrefixFormatter {}
impl PackageRendering for MockVPrefixFormatter {
fn format_version_for_text_edit(&self, version: &ConcreteVersion) -> String {
version.to_string()
}
fn package_url(&self, name: &PackageName) -> String {
format!("https://example.com/{name}")
}
}
impl RequirementResolution for MockVPrefixFormatter {}
impl DiagnosticMessages for MockVPrefixFormatter {}
impl DiagnosticPolicy for MockVPrefixFormatter {}
impl SourcePolicy for MockVPrefixFormatter {}
impl OsvNaming for MockVPrefixFormatter {
fn osv_version(&self, version: &str) -> String {
version.strip_prefix('v').unwrap_or(version).to_string()
}
}
#[test]
fn build_scan_targets_normalizes_version_via_formatter_osv_version_hook() {
let parse_result = MockParseResult {
deps: vec![MockDep {
name: PackageName::new("github.com/gin-gonic/gin"),
version_req: Some(VersionReq::new("v1.9.0")),
source: DependencySource::Registry,
}],
};
let mut resolved = HashMap::new();
resolved.insert(
PackageName::new("github.com/gin-gonic/gin"),
"v1.9.0".into(),
);
let (targets, skipped) = build_scan_targets(
&parse_result,
&resolved,
&HashMap::new(),
&MockVPrefixFormatter,
EcosystemId::Go,
);
assert_eq!(targets.len(), 1);
assert_eq!(targets[0].version, "1.9.0");
assert_eq!(targets[0].display_version, "v1.9.0");
assert!(skipped.is_empty());
}
#[test]
fn build_scan_targets_leaves_version_unaffected_for_default_identity_formatter() {
let parse_result = MockParseResult {
deps: vec![MockDep {
name: PackageName::new("serde"),
version_req: Some(VersionReq::new("^1.0")),
source: DependencySource::Registry,
}],
};
let mut resolved = HashMap::new();
resolved.insert(PackageName::new("serde"), "1.0.195".into());
let (targets, skipped) = build_scan_targets(
&parse_result,
&resolved,
&HashMap::new(),
&MockFormatter,
EcosystemId::Cargo,
);
assert_eq!(targets.len(), 1);
assert_eq!(targets[0].version, "1.0.195");
assert_eq!(targets[0].display_version, "1.0.195");
assert!(skipped.is_empty());
}
#[test]
fn build_scan_targets_go_ignores_stale_lockfile_version_uses_go_mod_requirement() {
let parse_result = MockParseResult {
deps: vec![MockDep {
name: PackageName::new("github.com/pkg/errors"),
version_req: Some(VersionReq::new("v0.8.1")),
source: DependencySource::Registry,
}],
};
let mut resolved = HashMap::new();
resolved.insert(PackageName::new("github.com/pkg/errors"), "v0.9.1".into());
let (targets, skipped) = build_scan_targets(
&parse_result,
&resolved,
&HashMap::new(),
&MockGoFormatter,
EcosystemId::Go,
);
assert_eq!(targets.len(), 1);
assert_eq!(targets[0].version, "0.8.1");
assert_eq!(targets[0].display_version, "v0.8.1");
assert!(skipped.is_empty());
}
#[cfg(feature = "deno")]
#[test]
fn build_scan_targets_deno_bare_pinned_jsr_dep_is_unmappable_name_skip() {
let parse_result = MockParseResult {
deps: vec![MockDep {
name: PackageName::new("jsr:@std/fs"),
version_req: Some(VersionReq::new("1.0.0")),
source: DependencySource::Registry,
}],
};
let (targets, skipped) = build_scan_targets(
&parse_result,
&HashMap::new(),
&HashMap::new(),
&deps_deno::DenoFormatter,
EcosystemId::Deno,
);
assert!(targets.is_empty(), "jsr: dep must never reach an OSV query");
assert_eq!(skipped.len(), 1);
assert_matches!(
skipped.get("jsr:@std/fs"),
Some(ScanOutcome::Skipped(SkipReason::UnmappableName))
);
}
#[test]
fn build_scan_targets_step2_uses_concrete_requirement_verbatim() {
let parse_result = MockParseResult {
deps: vec![MockDep {
name: PackageName::new("log4j-core"),
version_req: Some(VersionReq::new("2.14.1")),
source: DependencySource::Registry,
}],
};
let resolved = HashMap::new();
let (targets, skipped) = build_scan_targets(
&parse_result,
&resolved,
&HashMap::new(),
&MockFormatter,
EcosystemId::Maven,
);
assert_eq!(targets.len(), 1);
assert_eq!(targets[0].version, "2.14.1");
assert!(skipped.is_empty());
}
#[test]
fn build_scan_targets_step2_strips_pin_marker_for_operator_prefixed_requirements() {
let cargo_result = MockParseResult {
deps: vec![MockDep {
name: PackageName::new("time"),
version_req: Some(VersionReq::new("=1.2.3")),
source: DependencySource::Registry,
}],
};
let (targets, skipped) = build_scan_targets(
&cargo_result,
&HashMap::new(),
&HashMap::new(),
&MockFormatter,
EcosystemId::Cargo,
);
assert_eq!(targets.len(), 1);
assert_eq!(targets[0].version, "1.2.3");
assert!(skipped.is_empty());
let nuget_result = MockParseResult {
deps: vec![MockDep {
name: PackageName::new("Newtonsoft.Json"),
version_req: Some(VersionReq::new("[1.0.0]")),
source: DependencySource::Registry,
}],
};
let (targets, skipped) = build_scan_targets(
&nuget_result,
&HashMap::new(),
&HashMap::new(),
&MockFormatter,
EcosystemId::NuGet,
);
assert_eq!(targets.len(), 1);
assert_eq!(targets[0].version, "1.0.0");
assert!(skipped.is_empty());
}
#[test]
fn build_scan_targets_step3_skips_caret_range_with_no_lockfile_entry() {
let parse_result = MockParseResult {
deps: vec![MockDep {
name: PackageName::new("serde"),
version_req: Some(VersionReq::new("^1.0")),
source: DependencySource::Registry,
}],
};
let resolved = HashMap::new();
let (targets, skipped) = build_scan_targets(
&parse_result,
&resolved,
&HashMap::new(),
&MockFormatter,
EcosystemId::Cargo,
);
assert!(targets.is_empty());
assert_matches!(
skipped.get("serde"),
Some(ScanOutcome::Skipped(SkipReason::NoConcreteVersion))
);
}
#[test]
fn build_scan_targets_step3_skips_wildcard_with_no_lockfile_entry() {
let parse_result = MockParseResult {
deps: vec![MockDep {
name: PackageName::new("serde"),
version_req: Some(VersionReq::new("*")),
source: DependencySource::Registry,
}],
};
let resolved = HashMap::new();
let (targets, skipped) = build_scan_targets(
&parse_result,
&resolved,
&HashMap::new(),
&MockFormatter,
EcosystemId::Cargo,
);
assert!(targets.is_empty());
assert_matches!(
skipped.get("serde"),
Some(ScanOutcome::Skipped(SkipReason::NoConcreteVersion))
);
}
#[test]
fn build_scan_targets_all_non_registry_sources_are_skipped() {
let sources = vec![
DependencySource::Path {
path: "../local".to_string(),
},
DependencySource::Url {
url: "https://example.com/pkg.tgz".to_string(),
},
DependencySource::Sdk {
sdk: "flutter".to_string(),
},
DependencySource::Workspace,
DependencySource::CustomRegistry {
url: "https://private.example.com".to_string(),
},
];
for source in sources {
let parse_result = MockParseResult {
deps: vec![MockDep {
name: PackageName::new("pkg"),
version_req: Some(VersionReq::new("1.0.0")),
source: source.clone(),
}],
};
let mut resolved = HashMap::new();
resolved.insert(PackageName::new("pkg"), "1.0.0".into());
let (targets, skipped) = build_scan_targets(
&parse_result,
&resolved,
&HashMap::new(),
&MockFormatter,
EcosystemId::Cargo,
);
assert!(targets.is_empty(), "{source:?} must be skipped (step 0)");
assert_matches!(
skipped.get("pkg"),
Some(ScanOutcome::Skipped(SkipReason::NonRegistrySource))
);
}
}
#[test]
fn build_scan_targets_never_drops_a_dependency_silently() {
let parse_result = MockParseResult {
deps: vec![
MockDep {
name: PackageName::new("concrete"),
version_req: Some(VersionReq::new("2.14.1")),
source: DependencySource::Registry,
},
MockDep {
name: PackageName::new("range-only"),
version_req: Some(VersionReq::new("^1.0")),
source: DependencySource::Registry,
},
MockDep {
name: PackageName::new("git-dep"),
version_req: Some(VersionReq::new("1.0.0")),
source: DependencySource::Git {
url: "https://example.com/git-dep".to_string(),
rev: None,
},
},
],
};
let resolved = HashMap::new();
let (targets, skipped) = build_scan_targets(
&parse_result,
&resolved,
&HashMap::new(),
&MockFormatter,
EcosystemId::Maven,
);
assert_eq!(targets.len(), 1);
assert_eq!(targets[0].key, "concrete");
assert_eq!(skipped.len(), 2);
assert_matches!(
skipped.get("range-only"),
Some(ScanOutcome::Skipped(SkipReason::NoConcreteVersion))
);
assert_matches!(
skipped.get("git-dep"),
Some(ScanOutcome::Skipped(SkipReason::NonRegistrySource))
);
}
#[test]
fn collect_in_use_versions_prefers_lockfile_resolved_version() {
let parse_result = MockParseResult {
deps: vec![MockDep {
name: PackageName::new("serde"),
version_req: Some(VersionReq::new("^1.0")),
source: DependencySource::Registry,
}],
};
let mut resolved = HashMap::new();
resolved.insert(PackageName::new("serde"), "1.0.195".into());
let in_use = collect_in_use_versions(
&parse_result,
&resolved,
&HashMap::new(),
&MockFormatter,
EcosystemId::Cargo,
);
assert_eq!(
in_use.get(&PackageName::new("serde")),
Some(&vec!["1.0.195".to_string()])
);
}
#[test]
fn collect_in_use_versions_concrete_pin_without_lockfile() {
let parse_result = MockParseResult {
deps: vec![MockDep {
name: PackageName::new("log4j-core"),
version_req: Some(VersionReq::new("2.14.1")),
source: DependencySource::Registry,
}],
};
let resolved = HashMap::new();
let in_use = collect_in_use_versions(
&parse_result,
&resolved,
&HashMap::new(),
&MockFormatter,
EcosystemId::Maven,
);
assert_eq!(
in_use.get(&PackageName::new("log4j-core")),
Some(&vec!["2.14.1".to_string()])
);
}
#[test]
fn collect_in_use_versions_strips_pep440_double_equals_pin_for_pypi() {
let parse_result = MockParseResult {
deps: vec![MockDep {
name: PackageName::new("typing_extensions"),
version_req: Some(VersionReq::new("==4.9.0")),
source: DependencySource::Registry,
}],
};
let resolved = HashMap::new();
let in_use = collect_in_use_versions(
&parse_result,
&resolved,
&HashMap::new(),
&MockFormatter,
EcosystemId::Pypi,
);
assert_eq!(
in_use.get(&PackageName::new("typing_extensions")),
Some(&vec!["4.9.0".to_string()]),
"pep440 '==' comparator must be stripped, not carried into the in-use version"
);
}
#[test]
fn collect_in_use_versions_skips_non_concrete_requirement_with_no_lockfile() {
let parse_result = MockParseResult {
deps: vec![MockDep {
name: PackageName::new("serde"),
version_req: Some(VersionReq::new("^1.0")),
source: DependencySource::Registry,
}],
};
let resolved = HashMap::new();
let in_use = collect_in_use_versions(
&parse_result,
&resolved,
&HashMap::new(),
&MockFormatter,
EcosystemId::Cargo,
);
assert!(in_use.is_empty());
}
#[test]
fn collect_in_use_versions_excludes_non_registry_source_even_with_lockfile_version() {
let parse_result = MockParseResult {
deps: vec![MockDep {
name: PackageName::new("time"),
version_req: Some(VersionReq::new("0.1.43")),
source: DependencySource::Git {
url: "https://github.com/example/time".to_string(),
rev: None,
},
}],
};
let mut resolved = HashMap::new();
resolved.insert(PackageName::new("time"), "0.1.43".into());
let in_use = collect_in_use_versions(
&parse_result,
&resolved,
&HashMap::new(),
&MockFormatter,
EcosystemId::Cargo,
);
assert!(in_use.is_empty());
}
#[test]
fn collect_in_use_versions_tracks_all_occurrences_of_duplicate_name() {
let parse_result = MockParseResult {
deps: vec![
MockDep {
name: PackageName::new("time"),
version_req: Some(VersionReq::new("=0.1.43")),
source: DependencySource::Registry,
},
MockDep {
name: PackageName::new("time"),
version_req: Some(VersionReq::new("=0.1.44")),
source: DependencySource::Registry,
},
],
};
let resolved = HashMap::new();
let in_use = collect_in_use_versions(
&parse_result,
&resolved,
&HashMap::new(),
&MockFormatter,
EcosystemId::Cargo,
);
assert_eq!(
in_use.get(&PackageName::new("time")),
Some(&vec!["0.1.43".to_string(), "0.1.44".to_string()]),
"both occurrences' in-use versions must be tracked, not just the last one"
);
}
}
mod fix_target_verification_tests {
use super::*;
use deps_core::lsp_helpers::{
DiagnosticMessages, DiagnosticPolicy, OsvNaming, PackageNaming, PackageRendering,
RequirementResolution, SourcePolicy,
};
use deps_core::osv::{
Advisory, Capped, DependencyVulnerabilities, ScanOutcome, UpgradeStatus, VulnSeverity,
VulnerabilityMap,
};
use std::sync::Arc;
struct IdentityFormatter;
impl PackageNaming for IdentityFormatter {}
impl PackageRendering for IdentityFormatter {
fn format_version_for_text_edit(&self, version: &ConcreteVersion) -> String {
version.to_string()
}
fn package_url(&self, name: &PackageName) -> String {
format!("https://example.com/{name}")
}
}
impl RequirementResolution for IdentityFormatter {}
impl DiagnosticMessages for IdentityFormatter {}
impl DiagnosticPolicy for IdentityFormatter {}
impl SourcePolicy for IdentityFormatter {}
impl OsvNaming for IdentityFormatter {}
fn advisory(id: &str, fixed_versions: &[&str]) -> Arc<Advisory> {
Arc::new(
Advisory::new(
id.to_string(),
"2023-01-01T00:00:00Z".to_string(),
VulnSeverity::High,
String::new(),
)
.with_fixed_versions(fixed_versions.iter().map(ToString::to_string).collect()),
)
}
fn dv(
advisories: Vec<Arc<Advisory>>,
upgrade_status: UpgradeStatus,
) -> DependencyVulnerabilities {
let total = advisories.len();
DependencyVulnerabilities::new(Capped::new(advisories, total))
.with_upgrade_status(upgrade_status)
}
#[test]
fn resolve_fix_target_skips_when_no_fix_is_recommended() {
let dv = dv(vec![advisory("A1", &[])], UpgradeStatus::NotChecked);
let resolution = resolve_fix_target(
&dv,
"pkg",
&HashMap::new(),
&HashMap::new(),
&IdentityFormatter,
);
assert_eq!(resolution, FixTargetResolution::Skip);
}
#[test]
fn resolve_fix_target_reuses_latest_when_f_equals_latest() {
let latest_status = UpgradeStatus::CandidateClean {
version: "1.2.0".to_string(),
};
let dv = dv(vec![advisory("A1", &["1.2.0"])], latest_status.clone());
let mut latest_native_by_key = HashMap::new();
latest_native_by_key.insert("pkg".to_string(), "1.2.0".to_string());
let resolution = resolve_fix_target(
&dv,
"pkg",
&latest_native_by_key,
&HashMap::new(),
&IdentityFormatter,
);
assert_eq!(resolution, FixTargetResolution::Resolved(latest_status));
}
#[test]
fn resolve_fix_target_always_needs_live_check_when_f_differs_from_latest() {
let dv = dv(
vec![advisory("A1", &["1.2.0"])],
UpgradeStatus::CandidateClean {
version: "3.0.0".to_string(),
},
);
let mut latest_native_by_key = HashMap::new();
latest_native_by_key.insert("pkg".to_string(), "3.0.0".to_string());
let mut osv_name_by_key = HashMap::new();
osv_name_by_key.insert("pkg".to_string(), "pkg".to_string());
let resolution = resolve_fix_target(
&dv,
"pkg",
&latest_native_by_key,
&osv_name_by_key,
&IdentityFormatter,
);
assert_eq!(
resolution,
FixTargetResolution::NeedsLiveCheck(deps_core::osv::ScanTarget::new(
format!("pkg{FIX_TARGET_KEY_SUFFIX}"),
"pkg".to_string(),
"1.2.0".to_string(),
"1.2.0".to_string(),
))
);
}
#[test]
fn resolve_fix_target_skips_when_osv_name_is_unavailable() {
let dv = dv(vec![advisory("A1", &["1.0.0"])], UpgradeStatus::NotChecked);
let resolution = resolve_fix_target(
&dv,
"pkg",
&HashMap::new(),
&HashMap::new(),
&IdentityFormatter,
);
assert_eq!(resolution, FixTargetResolution::Skip);
}
#[test]
fn resolve_fix_target_skips_when_f_is_not_a_safe_version_string() {
let dv = dv(
vec![advisory("A1", &["1.2.0\", \"evil\": \"true"])],
UpgradeStatus::NotChecked,
);
let resolution = resolve_fix_target(
&dv,
"pkg",
&HashMap::new(),
&HashMap::new(),
&IdentityFormatter,
);
assert_eq!(resolution, FixTargetResolution::Skip);
}
#[test]
fn collect_fix_target_resolutions_batches_multiple_dependencies_needing_live_check_into_one_vec()
{
let mut vulnerabilities = VulnerabilityMap::new();
vulnerabilities.insert(
"reused".to_string(),
ScanOutcome::Vulnerable(dv(
vec![advisory("A1", &["1.0.0"])],
UpgradeStatus::CandidateClean {
version: "1.0.0".to_string(),
},
)),
);
vulnerabilities.insert(
"live-a".to_string(),
ScanOutcome::Vulnerable(dv(
vec![advisory("A2", &["1.2.0"])],
UpgradeStatus::NotChecked,
)),
);
vulnerabilities.insert(
"live-b".to_string(),
ScanOutcome::Vulnerable(dv(
vec![advisory("A3", &["2.2.0"])],
UpgradeStatus::NotChecked,
)),
);
let vulnerable_keys = vec![
"reused".to_string(),
"live-a".to_string(),
"live-b".to_string(),
];
let mut latest_native_by_key = HashMap::new();
latest_native_by_key.insert("reused".to_string(), "1.0.0".to_string());
latest_native_by_key.insert("live-a".to_string(), "9.0.0".to_string());
latest_native_by_key.insert("live-b".to_string(), "9.0.0".to_string());
let mut osv_name_by_key = HashMap::new();
osv_name_by_key.insert("reused".to_string(), "reused".to_string());
osv_name_by_key.insert("live-a".to_string(), "live-a".to_string());
osv_name_by_key.insert("live-b".to_string(), "live-b".to_string());
let (resolved, live_check_candidates) = collect_fix_target_resolutions(
&vulnerabilities,
&vulnerable_keys,
&osv_name_by_key,
&latest_native_by_key,
&IdentityFormatter,
);
assert_eq!(resolved.len(), 1, "{resolved:?}");
assert_eq!(resolved[0].0, "reused");
assert_eq!(live_check_candidates.len(), 2, "{live_check_candidates:?}");
let keys: std::collections::HashSet<&str> = live_check_candidates
.iter()
.map(|t| t.key.as_str())
.collect();
assert!(keys.contains(format!("live-a{FIX_TARGET_KEY_SUFFIX}").as_str()));
assert!(keys.contains(format!("live-b{FIX_TARGET_KEY_SUFFIX}").as_str()));
}
#[test]
fn apply_live_fix_target_statuses_sets_only_matching_keys_leaving_others_untouched() {
let mut vulnerabilities = VulnerabilityMap::new();
vulnerabilities.insert(
"checked".to_string(),
ScanOutcome::Vulnerable(dv(
vec![advisory("A1", &["1.0.0"])],
UpgradeStatus::NotChecked,
)),
);
vulnerabilities.insert(
"timed-out".to_string(),
ScanOutcome::Vulnerable(dv(
vec![advisory("A2", &["1.0.0"])],
UpgradeStatus::NotChecked,
)),
);
let mut statuses = HashMap::new();
statuses.insert(
format!("checked{FIX_TARGET_KEY_SUFFIX}"),
UpgradeStatus::CandidateClean {
version: "1.0.0".to_string(),
},
);
apply_live_fix_target_statuses(&mut vulnerabilities, statuses);
let ScanOutcome::Vulnerable(checked) = vulnerabilities.get("checked").unwrap() else {
panic!("expected Vulnerable");
};
assert_eq!(
checked.fix_target_status,
UpgradeStatus::CandidateClean {
version: "1.0.0".to_string()
}
);
let ScanOutcome::Vulnerable(timed_out) = vulnerabilities.get("timed-out").unwrap()
else {
panic!("expected Vulnerable");
};
assert_eq!(timed_out.fix_target_status, UpgradeStatus::NotChecked);
}
}
}