From 24d1238a59a35f33b97e525629d7ff65540e742d Mon Sep 17 00:00:00 2001 From: Test Date: Thu, 23 Jul 2026 12:21:06 +0200 Subject: [PATCH 1/5] Add canonical dependency finding catalog --- skills/corgea/SKILL.md | 16 +++ src/deps/catalog.rs | 202 +++++++++++++++++++++++++++++++ src/deps/ecosystems/evaluate.rs | 35 ++---- src/deps/mod.rs | 1 + src/deps/skill.rs | 15 +++ src/deps/tests/findings_tests.rs | 44 +++++++ src/deps/tests/report_tests.rs | 54 ++++++++- tests/cli_deps.rs | 29 ++++- tests/cli_deps_skill.rs | 29 +++++ 9 files changed, 397 insertions(+), 28 deletions(-) create mode 100644 src/deps/catalog.rs diff --git a/skills/corgea/SKILL.md b/skills/corgea/SKILL.md index 07a7977..5e18d36 100644 --- a/skills/corgea/SKILL.md +++ b/skills/corgea/SKILL.md @@ -129,6 +129,22 @@ Agent environments default to compact TSV; force output with `--format human|age Examples: `corgea deps policy init`; `corgea deps policy init --exist-ok --format quiet` Notes: `deps scan --out-format table|json|sarif` is the report/export selector; do not combine it with `deps scan --format`. + +### Dependency finding catalog + +| ID | Status | Severity | Title | Description | Remediation | +| --- | --- | --- | --- | --- | --- | +| `DEP001` | emitted | High | Missing lockfile | The dependency manifest has no expected lockfile, so resolution is not reproducible. | Generate and commit the ecosystem lockfile. | +| `DEP002` | emitted | High | Stale lockfile | A manifest dependency is missing from its lockfile. | Regenerate and commit the lockfile. | +| `DEP003` | emitted | Medium | Direct dependency uses broad range | A direct dependency uses a bounded range even though a lockfile resolves a version. | Pin the resolved version or explicitly allow the range by policy. | +| `DEP004` | emitted | High | Wildcard or latest dependency | A direct dependency uses wildcard, latest, or another unbounded range. | Pin an exact version. | +| `DEP005` | emitted | High | Mutable Git branch dependency | A direct dependency is sourced from a mutable Git branch reference. | Pin a commit SHA or immutable release tag. | +| `DEP006` | emitted | High | URL/tarball dependency without checksum | A direct URL or tarball dependency has no integrity checksum. | Add an integrity checksum or pin a registry package. | +| `DEP008` | emitted | Medium | Lockfile integrity hash missing | A lockfile entry lacks its integrity hash. | Add the integrity hash to the lockfile entry. | +| `DEP010` | reserved | Medium | Vulnerable package advisory | Reserved for vulnerable-package/advisory findings; `corgea deps` does not emit it. | Handle this code in an advisory or install-wrapper flow, never in `corgea deps`. | +| `DEP014` | emitted | Low | Duplicate versions of same package | More than one resolved version of a package is present. | Align or deduplicate the resolved dependency versions. | +| `DEP019` | emitted | Medium | Unsupported lockfile | A detected lockfile format is not supported by the parser. | Use a supported lockfile or wait for parser support. | +| `DEP021` | emitted | High | Mutable artifact version | A direct artifact version is mutable, such as a Maven SNAPSHOT. | Pin an immutable release version. | ### Advisories — `corgea advisories check` diff --git a/src/deps/catalog.rs b/src/deps/catalog.rs new file mode 100644 index 0000000..31b21e0 --- /dev/null +++ b/src/deps/catalog.rs @@ -0,0 +1,202 @@ +use crate::deps::model::Severity; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum FindingStatus { + Emitted, + Reserved, + Deprecated, +} + +impl FindingStatus { + pub const fn as_str(self) -> &'static str { + match self { + Self::Emitted => "emitted", + Self::Reserved => "reserved", + Self::Deprecated => "deprecated", + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct FindingDefinition { + pub id: &'static str, + pub title: &'static str, + pub severity: Severity, + pub description: &'static str, + pub remediation: &'static str, + pub status: FindingStatus, +} + +pub const FINDING_DEFINITIONS: &[FindingDefinition] = &[ + FindingDefinition { + id: "DEP001", + title: "Missing lockfile", + severity: Severity::High, + description: + "The dependency manifest has no expected lockfile, so resolution is not reproducible.", + remediation: "Generate and commit the ecosystem lockfile.", + status: FindingStatus::Emitted, + }, + FindingDefinition { + id: "DEP002", + title: "Stale lockfile", + severity: Severity::High, + description: "A manifest dependency is missing from its lockfile.", + remediation: "Regenerate and commit the lockfile.", + status: FindingStatus::Emitted, + }, + FindingDefinition { + id: "DEP003", + title: "Direct dependency uses broad range", + severity: Severity::Medium, + description: + "A direct dependency uses a bounded range even though a lockfile resolves a version.", + remediation: "Pin the resolved version or explicitly allow the range by policy.", + status: FindingStatus::Emitted, + }, + FindingDefinition { + id: "DEP004", + title: "Wildcard or latest dependency", + severity: Severity::High, + description: "A direct dependency uses wildcard, latest, or another unbounded range.", + remediation: "Pin an exact version.", + status: FindingStatus::Emitted, + }, + FindingDefinition { + id: "DEP005", + title: "Mutable Git branch dependency", + severity: Severity::High, + description: "A direct dependency is sourced from a mutable Git branch reference.", + remediation: "Pin a commit SHA or immutable release tag.", + status: FindingStatus::Emitted, + }, + FindingDefinition { + id: "DEP006", + title: "URL/tarball dependency without checksum", + severity: Severity::High, + description: "A direct URL or tarball dependency has no integrity checksum.", + remediation: "Add an integrity checksum or pin a registry package.", + status: FindingStatus::Emitted, + }, + FindingDefinition { + id: "DEP008", + title: "Lockfile integrity hash missing", + severity: Severity::Medium, + description: "A lockfile entry lacks its integrity hash.", + remediation: "Add the integrity hash to the lockfile entry.", + status: FindingStatus::Emitted, + }, + FindingDefinition { + id: "DEP010", + title: "Vulnerable package advisory", + severity: Severity::Medium, + description: + "Reserved for vulnerable-package/advisory findings; `corgea deps` does not emit it.", + remediation: + "Handle this code in an advisory or install-wrapper flow, never in `corgea deps`.", + status: FindingStatus::Reserved, + }, + FindingDefinition { + id: "DEP014", + title: "Duplicate versions of same package", + severity: Severity::Low, + description: "More than one resolved version of a package is present.", + remediation: "Align or deduplicate the resolved dependency versions.", + status: FindingStatus::Emitted, + }, + FindingDefinition { + id: "DEP019", + title: "Unsupported lockfile", + severity: Severity::Medium, + description: "A detected lockfile format is not supported by the parser.", + remediation: "Use a supported lockfile or wait for parser support.", + status: FindingStatus::Emitted, + }, + FindingDefinition { + id: "DEP021", + title: "Mutable artifact version", + severity: Severity::High, + description: "A direct artifact version is mutable, such as a Maven SNAPSHOT.", + remediation: "Pin an immutable release version.", + status: FindingStatus::Emitted, + }, +]; + +pub fn definition(id: &str) -> Option<&'static FindingDefinition> { + FINDING_DEFINITIONS + .iter() + .find(|definition| definition.id == id) +} + +pub fn emitted_definition(id: &str) -> Option<&'static FindingDefinition> { + definition(id).filter(|definition| definition.status == FindingStatus::Emitted) +} + +#[cfg(test)] +mod tests { + use super::{definition, emitted_definition, FindingStatus, FINDING_DEFINITIONS}; + + const EXPECTED_IDS: &[&str] = &[ + "DEP001", "DEP002", "DEP003", "DEP004", "DEP005", "DEP006", "DEP008", "DEP010", "DEP014", + "DEP019", "DEP021", + ]; + const EXPECTED_EMITTED_IDS: &[&str] = &[ + "DEP001", "DEP002", "DEP003", "DEP004", "DEP005", "DEP006", "DEP008", "DEP014", "DEP019", + "DEP021", + ]; + + #[test] + fn definitions_have_exact_ordered_unique_ids() { + let ids: Vec<_> = FINDING_DEFINITIONS + .iter() + .map(|definition| definition.id) + .collect(); + assert_eq!(ids, EXPECTED_IDS); + assert!(ids.windows(2).all(|pair| pair[0] < pair[1])); + } + + #[test] + fn definitions_have_nonempty_canonical_metadata() { + for definition in FINDING_DEFINITIONS { + assert!(!definition.id.is_empty()); + assert!(!definition.title.is_empty()); + assert!(!definition.description.is_empty()); + assert!(!definition.remediation.is_empty()); + } + } + + #[test] + fn general_lookup_round_trips_registered_definitions() { + for expected in FINDING_DEFINITIONS { + assert_eq!(definition(expected.id), Some(expected)); + } + assert!(definition("DEP999").is_none()); + } + + #[test] + fn definitions_have_exact_emitted_ids() { + let emitted_ids: Vec<_> = FINDING_DEFINITIONS + .iter() + .filter(|definition| definition.status == FindingStatus::Emitted) + .map(|definition| definition.id) + .collect(); + assert_eq!(emitted_ids, EXPECTED_EMITTED_IDS); + } + + #[test] + fn emitted_lookup_excludes_reserved_dep010() { + assert_eq!( + definition("DEP010").unwrap().status, + FindingStatus::Reserved + ); + assert!(emitted_definition("DEP010").is_none()); + assert!(emitted_definition("DEP999").is_none()); + } + + #[test] + fn statuses_have_lowercase_strings() { + assert_eq!(FindingStatus::Emitted.as_str(), "emitted"); + assert_eq!(FindingStatus::Reserved.as_str(), "reserved"); + assert_eq!(FindingStatus::Deprecated.as_str(), "deprecated"); + } +} diff --git a/src/deps/ecosystems/evaluate.rs b/src/deps/ecosystems/evaluate.rs index 6c76a5f..a39c4bd 100644 --- a/src/deps/ecosystems/evaluate.rs +++ b/src/deps/ecosystems/evaluate.rs @@ -3,11 +3,12 @@ use std::path::{Path, PathBuf}; use serde_json::Value; +use crate::deps::catalog::emitted_definition; use crate::deps::detect::{DepFileKind, DetectedFile}; use crate::deps::ecosystems::classify_constraint; use crate::deps::findings::Finding; use crate::deps::model::{ - ConstraintKind, DependencyGraph, DependencyNode, Ecosystem, PackageId, Severity, SourceType, + ConstraintKind, DependencyGraph, DependencyNode, Ecosystem, PackageId, SourceType, }; use crate::deps::policy::Policy; use crate::deps::DepsError; @@ -33,8 +34,6 @@ pub fn scan_all(ctx: &mut ScanContext<'_>) -> Result<(), DepsError> { pub fn add_pinning_finding( findings: &mut Vec, code: &str, - severity: Severity, - title: &str, package: Option, source_file: &str, declared: Option<&str>, @@ -42,10 +41,14 @@ pub fn add_pinning_finding( reproducible: bool, recommendation: &str, ) { + let Some(definition) = emitted_definition(code) else { + panic!("dependency finding code must be registered and emitted: {code}"); + }; + findings.push(Finding { - id: code.into(), - severity, - title: title.into(), + id: definition.id.into(), + severity: definition.severity, + title: definition.title.into(), package, source_file: source_file.into(), declared_constraint: declared.map(str::to_string), @@ -79,8 +82,6 @@ pub fn constraint_to_findings( add_pinning_finding( &mut out, "DEP003", - Severity::Medium, - "Direct dependency uses broad range", package_id, source_file, Some(declared), @@ -96,8 +97,6 @@ pub fn constraint_to_findings( add_pinning_finding( &mut out, "DEP004", - Severity::High, - "Wildcard or latest dependency", package_id, source_file, Some(declared), @@ -110,8 +109,6 @@ pub fn constraint_to_findings( add_pinning_finding( &mut out, "DEP021", - Severity::High, - "Mutable artifact version", package_id, source_file, Some(declared), @@ -124,8 +121,6 @@ pub fn constraint_to_findings( add_pinning_finding( &mut out, "DEP005", - Severity::High, - "Mutable Git branch dependency", package_id, source_file, Some(declared), @@ -139,8 +134,6 @@ pub fn constraint_to_findings( add_pinning_finding( &mut out, "DEP006", - Severity::High, - "URL/tarball dependency without checksum", package_id, source_file, Some(declared), @@ -165,8 +158,6 @@ pub fn dep001( add_pinning_finding( findings, "DEP001", - Severity::High, - "Missing lockfile", None, source_file, None, @@ -184,8 +175,6 @@ pub fn dep002(findings: &mut Vec, policy: &Policy, manifest_file: &str, add_pinning_finding( findings, "DEP002", - Severity::High, - "Stale lockfile", None, manifest_file, Some(missing), @@ -206,8 +195,6 @@ pub fn dep019_unsupported_lockfile( add_pinning_finding( findings, "DEP019", - Severity::Medium, - "Unsupported lockfile", None, source_file, None, @@ -227,8 +214,6 @@ pub fn dep008(findings: &mut Vec, policy: &Policy, node: &DependencyNod add_pinning_finding( findings, "DEP008", - Severity::Medium, - "Lockfile integrity hash missing", Some(node.id.clone()), node.lockfile.as_deref().unwrap_or("lockfile"), node.declared_constraint.as_deref(), @@ -287,8 +272,6 @@ pub fn dep014(findings: &mut Vec, graph: &DependencyGraph) { add_pinning_finding( findings, "DEP014", - Severity::Low, - "Duplicate versions of same package", Some(PackageId::npm(&name, vers.iter().next().unwrap())), "lockfile", None, diff --git a/src/deps/mod.rs b/src/deps/mod.rs index 91a6790..2c5b57e 100644 --- a/src/deps/mod.rs +++ b/src/deps/mod.rs @@ -2,6 +2,7 @@ #![allow(dead_code)] // library surface exceeds current bin wiring (Slice 8 vuln-api deferred) +pub mod catalog; pub mod detect; pub mod diff; pub mod ecosystems; diff --git a/src/deps/skill.rs b/src/deps/skill.rs index cf326b3..0d88827 100644 --- a/src/deps/skill.rs +++ b/src/deps/skill.rs @@ -2,6 +2,7 @@ use std::path::Path; use clap::{Command, CommandFactory, Parser}; +use crate::deps::catalog::FINDING_DEFINITIONS; use crate::deps::run::DepsSubcommand; pub const BEGIN_MARKER: &str = ""; @@ -120,6 +121,20 @@ pub fn generated_deps_skill_section() -> String { out.push_str( "\nNotes: `deps scan --out-format table|json|sarif` is the report/export selector; do not combine it with `deps scan --format`.\n", ); + out.push_str("\n### Dependency finding catalog\n\n"); + out.push_str("| ID | Status | Severity | Title | Description | Remediation |\n"); + out.push_str("| --- | --- | --- | --- | --- | --- |\n"); + for definition in FINDING_DEFINITIONS { + out.push_str(&format!( + "| `{}` | {} | {:?} | {} | {} | {} |\n", + definition.id, + definition.status.as_str(), + definition.severity, + definition.title, + definition.description, + definition.remediation, + )); + } out } diff --git a/src/deps/tests/findings_tests.rs b/src/deps/tests/findings_tests.rs index 5663d37..3c14dee 100644 --- a/src/deps/tests/findings_tests.rs +++ b/src/deps/tests/findings_tests.rs @@ -1,4 +1,6 @@ use super::common::scan_fixture; +use crate::deps::catalog::emitted_definition; +use crate::deps::ecosystems::evaluate::add_pinning_finding; use crate::deps::model::Severity; #[test] @@ -23,3 +25,45 @@ fn maven_no_lockfile_is_dep001() { fn gradle_lock_present_no_dep001() { assert!(scan_fixture("java-gradle").with_code("DEP001").is_empty()); } + +#[test] +fn fixture_findings_match_emitted_catalog_definitions() { + for fixture in [ + "node-app", + "node-stale", + "node-yarn", + "python-pip-nolock", + "java-maven", + ] { + let inventory = scan_fixture(fixture); + for finding in &inventory.findings { + let definition = emitted_definition(&finding.id) + .expect("every dependency finding must use an emitted definition"); + assert_eq!(finding.title, definition.title); + assert_eq!(finding.severity, definition.severity); + } + assert!(inventory.with_code("DEP010").is_empty()); + } +} + +#[test] +#[should_panic(expected = "dependency finding code must be registered and emitted: DEP010")] +fn add_pinning_finding_rejects_reserved_codes_before_appending() { + let mut findings = Vec::new(); + let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + add_pinning_finding( + &mut findings, + "DEP010", + None, + "manifest", + None, + None, + false, + "reserved findings cannot be emitted", + ); + })) + .expect_err("reserved finding must panic"); + + assert!(findings.is_empty()); + std::panic::resume_unwind(panic); +} diff --git a/src/deps/tests/report_tests.rs b/src/deps/tests/report_tests.rs index 038abdb..9668036 100644 --- a/src/deps/tests/report_tests.rs +++ b/src/deps/tests/report_tests.rs @@ -1,5 +1,6 @@ use super::common::scan_fixture; -use crate::deps::report::{to_cyclonedx, to_json, to_sarif}; +use crate::deps::catalog::emitted_definition; +use crate::deps::report::{table_output, to_cyclonedx, to_json, to_sarif}; #[test] fn report_json_has_findings_and_graph() { @@ -16,6 +17,57 @@ fn report_sarif_has_rules_and_results() { assert!(results.iter().any(|r| r["ruleId"] == "DEP004")); } +#[test] +fn dep004_report_values_remain_catalog_hydrated_and_dynamic() { + let inv = scan_fixture("node-app"); + let definition = emitted_definition("DEP004").unwrap(); + let finding = inv + .with_code("DEP004") + .into_iter() + .next() + .expect("node-app emits DEP004"); + let expected_recommendation = + "Pin to an exact version instead of using wildcard, latest, or unbounded ranges."; + + assert_eq!(finding.title, definition.title); + assert_eq!(finding.recommendation, expected_recommendation); + + let json = to_json(&inv); + let json_finding = json["findings"] + .as_array() + .unwrap() + .iter() + .find(|finding| finding["id"] == "DEP004") + .expect("JSON contains DEP004"); + assert_eq!(json_finding["severity"], "High"); + assert_eq!(json_finding["title"], definition.title); + assert_eq!(json_finding["recommendation"], finding.recommendation); + assert!(json_finding.get("description").is_none()); + assert!(json_finding.get("remediation").is_none()); + + let table = table_output(&inv); + assert!(table.contains("DEP004 High Wildcard or latest dependency")); + assert!(table.contains("package: lodash")); + assert!(table.contains(expected_recommendation)); + + let sarif = to_sarif(&inv); + let sarif_rule = sarif["runs"][0]["tool"]["driver"]["rules"] + .as_array() + .unwrap() + .iter() + .find(|rule| rule["id"] == "DEP004") + .expect("SARIF contains DEP004 rule"); + let sarif_result = sarif["runs"][0]["results"] + .as_array() + .unwrap() + .iter() + .find(|result| result["ruleId"] == "DEP004") + .expect("SARIF contains DEP004 result"); + assert_eq!(sarif_rule["shortDescription"]["text"], definition.title); + assert_eq!(sarif_result["level"], "error"); + assert_eq!(sarif_result["message"]["text"], finding.recommendation); +} + #[test] fn report_cyclonedx_has_components_and_deps() { let inv = scan_fixture("node-app"); diff --git a/tests/cli_deps.rs b/tests/cli_deps.rs index 774323e..5c19c97 100644 --- a/tests/cli_deps.rs +++ b/tests/cli_deps.rs @@ -97,7 +97,9 @@ fn cli_scan_agent_env_defaults_to_agent_format() { let stdout = String::from_utf8_lossy(&out.stdout); assert!(stdout.starts_with("record\troot\t"), "stdout: {stdout}"); assert!( - stdout.contains("\nfinding\tDEP004\tHigh\tpkg:npm/lodash@4.17.21\t"), + stdout.contains( + "\nfinding\tDEP004\tHigh\tpkg:npm/lodash@4.17.21\tWildcard or latest dependency\tPin to an exact version instead of using wildcard, latest, or unbounded ranges." + ), "stdout: {stdout}" ); } @@ -120,6 +122,17 @@ fn cli_scan_format_human_overrides_agent_env() { stdout.contains("Corgea dependency inventory"), "stdout: {stdout}" ); + assert!( + stdout.contains("DEP004 High Wildcard or latest dependency"), + "stdout: {stdout}" + ); + assert!(stdout.contains("package: lodash"), "stdout: {stdout}"); + assert!( + stdout.contains( + "Pin to an exact version instead of using wildcard, latest, or unbounded ranges." + ), + "stdout: {stdout}" + ); } #[test] @@ -138,6 +151,20 @@ fn cli_scan_format_json_outputs_parseable_inventory() { serde_json::from_slice(&out.stdout).expect("stdout must be valid JSON"); assert!(parsed.get("nodes").is_some()); assert!(parsed.get("findings").is_some()); + let findings = parsed["findings"] + .as_array() + .expect("findings must be an array"); + let dep004 = findings + .iter() + .find(|finding| finding["id"] == "DEP004" && finding["package"] == "pkg:npm/lodash@4.17.21") + .expect("node-app emits DEP004 for lodash"); + assert_eq!(dep004["severity"], "High"); + assert_eq!(dep004["title"], "Wildcard or latest dependency"); + assert_eq!( + dep004["recommendation"], + "Pin to an exact version instead of using wildcard, latest, or unbounded ranges." + ); + assert!(findings.iter().all(|finding| finding["id"] != "DEP010")); assert!( !String::from_utf8_lossy(&out.stdout).contains("Hint:"), "stdout: {}", diff --git a/tests/cli_deps_skill.rs b/tests/cli_deps_skill.rs index 3bbf897..575bcdd 100644 --- a/tests/cli_deps_skill.rs +++ b/tests/cli_deps_skill.rs @@ -1,5 +1,7 @@ use std::path::Path; +use corgea::deps::catalog::FINDING_DEFINITIONS; + #[test] fn generated_deps_skill_block_is_current() { let path = Path::new(env!("CARGO_MANIFEST_DIR")) @@ -8,3 +10,30 @@ fn generated_deps_skill_block_is_current() { .join("SKILL.md"); corgea::deps::skill::check_skill_file(&path).expect("deps skill block should be current"); } + +#[test] +fn generated_deps_skill_documents_the_catalog_in_declaration_order() { + let section = corgea::deps::skill::generated_deps_skill_section(); + let positions: Vec<_> = FINDING_DEFINITIONS + .iter() + .map(|definition| { + let row = format!("| `{}` |", definition.id); + assert_eq!( + section.matches(&row).count(), + 1, + "catalog definition must appear exactly once: {}", + definition.id + ); + section + .find(&row) + .expect("catalog definition must be documented") + }) + .collect(); + + assert!(positions.windows(2).all(|window| window[0] < window[1])); + assert!(section.contains("| `DEP004` | emitted | High | Wildcard or latest dependency |")); + assert!(section.contains("| `DEP010` | reserved | Medium | Vulnerable package advisory |")); + assert!(section.contains( + "Reserved for vulnerable-package/advisory findings; `corgea deps` does not emit it." + )); +} From fa79efd25f585f12893fb77bd8b66674b68bbfaa Mon Sep 17 00:00:00 2001 From: Test Date: Thu, 23 Jul 2026 17:48:03 +0200 Subject: [PATCH 2/5] Clarify unresolved DEP003 guidance --- skills/corgea/SKILL.md | 2 +- src/deps/catalog.rs | 5 ++--- src/deps/tests/findings_tests.rs | 22 ++++++++++++++++++++++ 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/skills/corgea/SKILL.md b/skills/corgea/SKILL.md index 5e18d36..37621df 100644 --- a/skills/corgea/SKILL.md +++ b/skills/corgea/SKILL.md @@ -136,7 +136,7 @@ Notes: `deps scan --out-format table|json|sarif` is the report/export selector; | --- | --- | --- | --- | --- | --- | | `DEP001` | emitted | High | Missing lockfile | The dependency manifest has no expected lockfile, so resolution is not reproducible. | Generate and commit the ecosystem lockfile. | | `DEP002` | emitted | High | Stale lockfile | A manifest dependency is missing from its lockfile. | Regenerate and commit the lockfile. | -| `DEP003` | emitted | Medium | Direct dependency uses broad range | A direct dependency uses a bounded range even though a lockfile resolves a version. | Pin the resolved version or explicitly allow the range by policy. | +| `DEP003` | emitted | Medium | Direct dependency uses broad range | A direct dependency uses a bounded version range. | Pin an exact version or explicitly allow the range by policy. | | `DEP004` | emitted | High | Wildcard or latest dependency | A direct dependency uses wildcard, latest, or another unbounded range. | Pin an exact version. | | `DEP005` | emitted | High | Mutable Git branch dependency | A direct dependency is sourced from a mutable Git branch reference. | Pin a commit SHA or immutable release tag. | | `DEP006` | emitted | High | URL/tarball dependency without checksum | A direct URL or tarball dependency has no integrity checksum. | Add an integrity checksum or pin a registry package. | diff --git a/src/deps/catalog.rs b/src/deps/catalog.rs index 31b21e0..725144b 100644 --- a/src/deps/catalog.rs +++ b/src/deps/catalog.rs @@ -49,9 +49,8 @@ pub const FINDING_DEFINITIONS: &[FindingDefinition] = &[ id: "DEP003", title: "Direct dependency uses broad range", severity: Severity::Medium, - description: - "A direct dependency uses a bounded range even though a lockfile resolves a version.", - remediation: "Pin the resolved version or explicitly allow the range by policy.", + description: "A direct dependency uses a bounded version range.", + remediation: "Pin an exact version or explicitly allow the range by policy.", status: FindingStatus::Emitted, }, FindingDefinition { diff --git a/src/deps/tests/findings_tests.rs b/src/deps/tests/findings_tests.rs index 3c14dee..5ba1bd7 100644 --- a/src/deps/tests/findings_tests.rs +++ b/src/deps/tests/findings_tests.rs @@ -46,6 +46,28 @@ fn fixture_findings_match_emitted_catalog_definitions() { } } +#[test] +fn unresolved_bounded_range_uses_resolution_neutral_dep003_metadata() { + let inventory = scan_fixture("node-stale"); + let finding = inventory + .with_code("DEP003") + .into_iter() + .find(|finding| finding.resolved_version.is_none()) + .expect("node-stale should emit DEP003 for unresolved chalk"); + let definition = emitted_definition("DEP003").expect("DEP003 must be emitted"); + + assert!(finding.package.is_none()); + assert!(!finding.reproducible); + assert_eq!( + definition.description, + "A direct dependency uses a bounded version range." + ); + assert_eq!( + definition.remediation, + "Pin an exact version or explicitly allow the range by policy." + ); +} + #[test] #[should_panic(expected = "dependency finding code must be registered and emitted: DEP010")] fn add_pinning_finding_rejects_reserved_codes_before_appending() { From a709d950404d7bedbe02cb70e584ffe06632b133 Mon Sep 17 00:00:00 2001 From: Test Date: Thu, 23 Jul 2026 22:08:56 +0200 Subject: [PATCH 3/5] Make DEP003 guidance resolution-neutral --- src/deps/ecosystems/evaluate.rs | 2 +- src/deps/tests/findings_tests.rs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/deps/ecosystems/evaluate.rs b/src/deps/ecosystems/evaluate.rs index a39c4bd..cb0d76a 100644 --- a/src/deps/ecosystems/evaluate.rs +++ b/src/deps/ecosystems/evaluate.rs @@ -87,7 +87,7 @@ pub fn constraint_to_findings( Some(declared), resolved, reproducible, - "Pin to the resolved version or allow by policy because the lockfile resolves it.", + "Pin an exact version or explicitly allow the range by policy.", ); } ConstraintKind::BoundedRange => {} diff --git a/src/deps/tests/findings_tests.rs b/src/deps/tests/findings_tests.rs index 5ba1bd7..97a838d 100644 --- a/src/deps/tests/findings_tests.rs +++ b/src/deps/tests/findings_tests.rs @@ -58,6 +58,7 @@ fn unresolved_bounded_range_uses_resolution_neutral_dep003_metadata() { assert!(finding.package.is_none()); assert!(!finding.reproducible); + assert_eq!(finding.recommendation, definition.remediation); assert_eq!( definition.description, "A direct dependency uses a bounded version range." From c8192b060df1adf42e6661a0b6d00b1ffb9064eb Mon Sep 17 00:00:00 2001 From: Test Date: Thu, 23 Jul 2026 22:41:03 +0200 Subject: [PATCH 4/5] Preserve DEP003 behavior and API --- src/deps/ecosystems/evaluate.rs | 54 ++++++++++++++++++++++------- src/deps/tests/correctness_tests.rs | 5 +++ src/deps/tests/findings_tests.rs | 25 +++++++++++++ 3 files changed, 72 insertions(+), 12 deletions(-) diff --git a/src/deps/ecosystems/evaluate.rs b/src/deps/ecosystems/evaluate.rs index cb0d76a..7dfa95c 100644 --- a/src/deps/ecosystems/evaluate.rs +++ b/src/deps/ecosystems/evaluate.rs @@ -8,7 +8,7 @@ use crate::deps::detect::{DepFileKind, DetectedFile}; use crate::deps::ecosystems::classify_constraint; use crate::deps::findings::Finding; use crate::deps::model::{ - ConstraintKind, DependencyGraph, DependencyNode, Ecosystem, PackageId, SourceType, + ConstraintKind, DependencyGraph, DependencyNode, Ecosystem, PackageId, Severity, SourceType, }; use crate::deps::policy::Policy; use crate::deps::DepsError; @@ -30,8 +30,34 @@ pub fn scan_all(ctx: &mut ScanContext<'_>) -> Result<(), DepsError> { Ok(()) } +#[deprecated(note = "finding metadata is catalog-backed; use the dependency scan APIs")] #[allow(clippy::too_many_arguments)] pub fn add_pinning_finding( + findings: &mut Vec, + code: &str, + _severity: Severity, + _title: &str, + package: Option, + source_file: &str, + declared: Option<&str>, + resolved: Option<&str>, + reproducible: bool, + recommendation: &str, +) { + add_catalog_pinning_finding( + findings, + code, + package, + source_file, + declared, + resolved, + reproducible, + recommendation, + ); +} + +#[allow(clippy::too_many_arguments)] +fn add_catalog_pinning_finding( findings: &mut Vec, code: &str, package: Option, @@ -79,7 +105,7 @@ pub fn constraint_to_findings( match kind { ConstraintKind::Exact => {} ConstraintKind::BoundedRange if is_direct && policy.warn_on_semver_range => { - add_pinning_finding( + add_catalog_pinning_finding( &mut out, "DEP003", package_id, @@ -87,14 +113,18 @@ pub fn constraint_to_findings( Some(declared), resolved, reproducible, - "Pin an exact version or explicitly allow the range by policy.", + if reproducible && resolved.is_some() { + "Pin to the resolved version or allow by policy because the lockfile resolves it." + } else { + "Pin an exact version or explicitly allow the range by policy." + }, ); } ConstraintKind::BoundedRange => {} ConstraintKind::Unbounded if is_direct && (policy.fail_on_wildcard || policy.fail_on_latest) => { - add_pinning_finding( + add_catalog_pinning_finding( &mut out, "DEP004", package_id, @@ -106,7 +136,7 @@ pub fn constraint_to_findings( ); } ConstraintKind::Mutable if is_direct && policy.fail_on_mutable_sources => { - add_pinning_finding( + add_catalog_pinning_finding( &mut out, "DEP021", package_id, @@ -118,7 +148,7 @@ pub fn constraint_to_findings( ); } ConstraintKind::GitRef { mutable: true } if is_direct && policy.fail_on_mutable_sources => { - add_pinning_finding( + add_catalog_pinning_finding( &mut out, "DEP005", package_id, @@ -131,7 +161,7 @@ pub fn constraint_to_findings( } ConstraintKind::GitRef { .. } => {} ConstraintKind::Url { checksum: false } if is_direct => { - add_pinning_finding( + add_catalog_pinning_finding( &mut out, "DEP006", package_id, @@ -155,7 +185,7 @@ pub fn dep001( ecosystem_label: &str, ) { if policy.fail_on_missing_lockfile { - add_pinning_finding( + add_catalog_pinning_finding( findings, "DEP001", None, @@ -172,7 +202,7 @@ pub fn dep001( pub fn dep002(findings: &mut Vec, policy: &Policy, manifest_file: &str, missing: &str) { if policy.fail_on_stale_lockfile { - add_pinning_finding( + add_catalog_pinning_finding( findings, "DEP002", None, @@ -192,7 +222,7 @@ pub fn dep019_unsupported_lockfile( source_file: &str, ecosystem_label: &str, ) { - add_pinning_finding( + add_catalog_pinning_finding( findings, "DEP019", None, @@ -211,7 +241,7 @@ pub fn dep008(findings: &mut Vec, policy: &Policy, node: &DependencyNod return; } if node.lock_integrity == Some(false) { - add_pinning_finding( + add_catalog_pinning_finding( findings, "DEP008", Some(node.id.clone()), @@ -269,7 +299,7 @@ pub fn dep014(findings: &mut Vec, graph: &DependencyGraph) { } for (name, vers) in versions { if vers.len() > 1 { - add_pinning_finding( + add_catalog_pinning_finding( findings, "DEP014", Some(PackageId::npm(&name, vers.iter().next().unwrap())), diff --git a/src/deps/tests/correctness_tests.rs b/src/deps/tests/correctness_tests.rs index 4db3947..656a118 100644 --- a/src/deps/tests/correctness_tests.rs +++ b/src/deps/tests/correctness_tests.rs @@ -22,6 +22,11 @@ fn node_direct_locked_range_is_medium_not_high() { .expect("expected DEP003 for express"); assert_eq!(dep003.severity, Severity::Medium); assert!(dep003.reproducible); + assert_eq!(dep003.resolved_version.as_deref(), Some("4.18.2")); + assert_eq!( + dep003.recommendation, + "Pin to the resolved version or allow by policy because the lockfile resolves it." + ); } #[test] diff --git a/src/deps/tests/findings_tests.rs b/src/deps/tests/findings_tests.rs index 97a838d..f26c282 100644 --- a/src/deps/tests/findings_tests.rs +++ b/src/deps/tests/findings_tests.rs @@ -1,5 +1,6 @@ use super::common::scan_fixture; use crate::deps::catalog::emitted_definition; +#[allow(deprecated)] use crate::deps::ecosystems::evaluate::add_pinning_finding; use crate::deps::model::Severity; @@ -70,6 +71,28 @@ fn unresolved_bounded_range_uses_resolution_neutral_dep003_metadata() { } #[test] +#[allow(deprecated)] +fn legacy_add_pinning_finding_signature_uses_catalog_metadata() { + let mut findings = Vec::new(); + add_pinning_finding( + &mut findings, + "DEP003", + Severity::Critical, + "Legacy caller title", + None, + "package.json", + Some("^5.3.0"), + Some("5.3.0"), + true, + "Legacy caller recommendation", + ); + + assert_eq!(findings[0].severity, Severity::Medium); + assert_eq!(findings[0].title, "Direct dependency uses broad range"); +} + +#[test] +#[allow(deprecated)] #[should_panic(expected = "dependency finding code must be registered and emitted: DEP010")] fn add_pinning_finding_rejects_reserved_codes_before_appending() { let mut findings = Vec::new(); @@ -77,6 +100,8 @@ fn add_pinning_finding_rejects_reserved_codes_before_appending() { add_pinning_finding( &mut findings, "DEP010", + Severity::High, + "Reserved advisory dependency", None, "manifest", None, From cf48b7845ce46108a8a7782551380da610afef83 Mon Sep 17 00:00:00 2001 From: Test Date: Thu, 23 Jul 2026 22:49:01 +0200 Subject: [PATCH 5/5] Preserve legacy finding helper behavior --- src/deps/ecosystems/evaluate.rs | 24 +++++++++++++----------- src/deps/tests/findings_tests.rs | 18 ++++++++---------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/deps/ecosystems/evaluate.rs b/src/deps/ecosystems/evaluate.rs index 7dfa95c..6ee3c81 100644 --- a/src/deps/ecosystems/evaluate.rs +++ b/src/deps/ecosystems/evaluate.rs @@ -35,8 +35,8 @@ pub fn scan_all(ctx: &mut ScanContext<'_>) -> Result<(), DepsError> { pub fn add_pinning_finding( findings: &mut Vec, code: &str, - _severity: Severity, - _title: &str, + severity: Severity, + title: &str, package: Option, source_file: &str, declared: Option<&str>, @@ -44,20 +44,22 @@ pub fn add_pinning_finding( reproducible: bool, recommendation: &str, ) { - add_catalog_pinning_finding( - findings, - code, + findings.push(Finding { + id: code.into(), + severity, + title: title.into(), package, - source_file, - declared, - resolved, + source_file: source_file.into(), + declared_constraint: declared.map(str::to_string), + resolved_version: resolved.map(str::to_string), + recommendation: recommendation.into(), reproducible, - recommendation, - ); + paths: vec![vec![PackageId::root()]], + }); } #[allow(clippy::too_many_arguments)] -fn add_catalog_pinning_finding( +pub(crate) fn add_catalog_pinning_finding( findings: &mut Vec, code: &str, package: Option, diff --git a/src/deps/tests/findings_tests.rs b/src/deps/tests/findings_tests.rs index f26c282..26fc420 100644 --- a/src/deps/tests/findings_tests.rs +++ b/src/deps/tests/findings_tests.rs @@ -1,7 +1,7 @@ use super::common::scan_fixture; use crate::deps::catalog::emitted_definition; #[allow(deprecated)] -use crate::deps::ecosystems::evaluate::add_pinning_finding; +use crate::deps::ecosystems::evaluate::{add_catalog_pinning_finding, add_pinning_finding}; use crate::deps::model::Severity; #[test] @@ -72,11 +72,11 @@ fn unresolved_bounded_range_uses_resolution_neutral_dep003_metadata() { #[test] #[allow(deprecated)] -fn legacy_add_pinning_finding_signature_uses_catalog_metadata() { +fn legacy_add_pinning_finding_preserves_supplied_metadata() { let mut findings = Vec::new(); add_pinning_finding( &mut findings, - "DEP003", + "CUSTOM001", Severity::Critical, "Legacy caller title", None, @@ -87,21 +87,19 @@ fn legacy_add_pinning_finding_signature_uses_catalog_metadata() { "Legacy caller recommendation", ); - assert_eq!(findings[0].severity, Severity::Medium); - assert_eq!(findings[0].title, "Direct dependency uses broad range"); + assert_eq!(findings[0].id, "CUSTOM001"); + assert_eq!(findings[0].severity, Severity::Critical); + assert_eq!(findings[0].title, "Legacy caller title"); } #[test] -#[allow(deprecated)] #[should_panic(expected = "dependency finding code must be registered and emitted: DEP010")] -fn add_pinning_finding_rejects_reserved_codes_before_appending() { +fn catalog_pinning_finding_rejects_reserved_codes_before_appending() { let mut findings = Vec::new(); let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - add_pinning_finding( + add_catalog_pinning_finding( &mut findings, "DEP010", - Severity::High, - "Reserved advisory dependency", None, "manifest", None,