corgea-cli 1.9.1__tar.gz → 1.9.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/Cargo.lock +5 -5
  2. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/Cargo.toml +6 -2
  3. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/PKG-INFO +1 -1
  4. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/skills/corgea/SKILL.md +54 -3
  5. corgea_cli-1.9.2/src/advisories.rs +518 -0
  6. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/lib.rs +1 -0
  7. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/list.rs +32 -3
  8. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/main.rs +83 -14
  9. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/precheck/mod.rs +28 -2
  10. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/precheck/render.rs +74 -102
  11. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/precheck/test_support.rs +1 -0
  12. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/precheck/verdict.rs +167 -16
  13. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/scanners/blast.rs +309 -18
  14. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/scanners/fortify.rs +56 -2
  15. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/utils/api.rs +37 -0
  16. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/utils/generic.rs +84 -4
  17. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/verify_deps/registry.rs +2 -3
  18. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/vuln_api/mod.rs +504 -59
  19. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/vuln_api_stub/mod.rs +133 -5
  20. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/wait.rs +1 -7
  21. corgea_cli-1.9.2/tests/cli_advisories.rs +417 -0
  22. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/cli_bare_install.rs +153 -7
  23. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/cli_deps.rs +27 -0
  24. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/cli_install.rs +31 -0
  25. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/cli_npm_ci.rs +6 -6
  26. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/cli_provenance.rs +5 -5
  27. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/cli_remediation.rs +2 -2
  28. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/cli_tree.rs +7 -7
  29. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/cli_uv_sync.rs +3 -3
  30. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/cli_verdict.rs +225 -10
  31. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/common/mod.rs +1 -1
  32. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/vuln_api/README.md +7 -1
  33. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/vuln_api/check_malware.json +2 -1
  34. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/vuln_api/check_vulnerable.json +2 -1
  35. corgea_cli-1.9.2/tests/fixtures/vuln_api/package_profile.json +47 -0
  36. corgea_cli-1.9.2/tests/fixtures/vuln_api/package_profile_not_found.json +1 -0
  37. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/harness_smoke.rs +2 -2
  38. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/vuln_api_contract.rs +88 -2
  39. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/.codex/hooks.json +0 -0
  40. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/.github/workflows/npm-publish.yml +0 -0
  41. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/.github/workflows/release-binaries.yml +0 -0
  42. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/.github/workflows/release.yml +0 -0
  43. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/.github/workflows/staging-contract.yml +0 -0
  44. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/.github/workflows/test.yml +0 -0
  45. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/.github/workflows/update_docs..yml +0 -0
  46. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/.gitignore +0 -0
  47. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/AGENTS.md +0 -0
  48. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/LICENSE +0 -0
  49. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/README.md +0 -0
  50. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/RELEASING.md +0 -0
  51. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/bin/corgea.js +0 -0
  52. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/build_release.sh +0 -0
  53. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/examples/deps_skill.rs +0 -0
  54. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/harness +0 -0
  55. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/package.json +0 -0
  56. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/pyproject.toml +0 -0
  57. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/scripts/npm/bundle-binaries.js +0 -0
  58. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/authorize.rs +0 -0
  59. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/cicd.rs +0 -0
  60. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/config.rs +0 -0
  61. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/detect.rs +0 -0
  62. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/diff.rs +0 -0
  63. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/ecosystems/evaluate.rs +0 -0
  64. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/ecosystems/maven.rs +0 -0
  65. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/ecosystems/mod.rs +0 -0
  66. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/ecosystems/npm.rs +0 -0
  67. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/ecosystems/pypi.rs +0 -0
  68. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/explain.rs +0 -0
  69. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/findings.rs +0 -0
  70. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/mod.rs +0 -0
  71. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/model.rs +0 -0
  72. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/parse/mod.rs +0 -0
  73. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/parse/npm_lock.rs +0 -0
  74. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/parse/python_lock.rs +0 -0
  75. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/policy.rs +0 -0
  76. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/report.rs +0 -0
  77. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/run.rs +0 -0
  78. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/skill.rs +0 -0
  79. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/tests/common.rs +0 -0
  80. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/tests/correctness_tests.rs +0 -0
  81. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/tests/detect_tests.rs +0 -0
  82. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/tests/diff_tests.rs +0 -0
  83. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/tests/explain_tests.rs +0 -0
  84. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/tests/findings_tests.rs +0 -0
  85. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/tests/maven_tests.rs +0 -0
  86. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/tests/mod.rs +0 -0
  87. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/tests/npm_tests.rs +0 -0
  88. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/tests/policy_tests.rs +0 -0
  89. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/tests/pypi_tests.rs +0 -0
  90. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/tests/report_tests.rs +0 -0
  91. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/tests/robustness_tests.rs +0 -0
  92. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/tests/slice0_tests.rs +0 -0
  93. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/inspect.rs +0 -0
  94. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/log.rs +0 -0
  95. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/precheck/detect.rs +0 -0
  96. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/precheck/exec.rs +0 -0
  97. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/precheck/parse.rs +0 -0
  98. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/precheck/tree.rs +0 -0
  99. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/precheck/uv.rs +0 -0
  100. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/scan.rs +0 -0
  101. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/scanners/parsers/checkmarx.rs +0 -0
  102. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/scanners/parsers/coverity.rs +0 -0
  103. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/scanners/parsers/mod.rs +0 -0
  104. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/scanners/parsers/sarif.rs +0 -0
  105. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/scanners/parsers/semgrep.rs +0 -0
  106. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/setup_hooks.rs +0 -0
  107. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/skill.rs +0 -0
  108. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/targets.rs +0 -0
  109. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/utils/terminal.rs +0 -0
  110. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/verify_deps/mod.rs +0 -0
  111. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/cli_deps_skill.rs +0 -0
  112. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/cli_exec_fallback.rs +0 -0
  113. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/README.md +0 -0
  114. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/go-mod-smoke/go.mod +0 -0
  115. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/go-mod-smoke/go.sum +0 -0
  116. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/java-gradle/build.gradle +0 -0
  117. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/java-gradle/gradle.lockfile +0 -0
  118. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/java-maven/pom.xml +0 -0
  119. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/malformed/not-xml-pom.xml +0 -0
  120. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/malformed/package-lock.json +0 -0
  121. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/malformed/package.json +0 -0
  122. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/malformed/poetry.lock +0 -0
  123. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/malformed/pyproject.toml +0 -0
  124. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/malformed/truncated-poetry.lock +0 -0
  125. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/node-app/.corgea/deps.yml +0 -0
  126. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/node-app/package-lock.json +0 -0
  127. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/node-app/package.json +0 -0
  128. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/node-monorepo/package-lock.json +0 -0
  129. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/node-monorepo/package.json +0 -0
  130. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/node-monorepo/packages/a/package.json +0 -0
  131. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/node-monorepo/packages/b/package.json +0 -0
  132. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/node-pnpm/package.json +0 -0
  133. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/node-pnpm/pnpm-lock.yaml +0 -0
  134. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/node-stale/package-lock.json +0 -0
  135. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/node-stale/package.json +0 -0
  136. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/node-transitive/package-lock.json +0 -0
  137. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/node-transitive/package.json +0 -0
  138. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/node-yarn/package.json +0 -0
  139. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/node-yarn/yarn.lock +0 -0
  140. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/python-pip-nolock/requirements.txt +0 -0
  141. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/python-poetry/poetry.lock +0 -0
  142. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/python-poetry/pyproject.toml +0 -0
  143. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/python-poetry-multi/poetry.lock +0 -0
  144. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/python-poetry-multi/pyproject.toml +0 -0
  145. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/python-uv-requirements/pyproject.toml +0 -0
  146. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/python-uv-requirements/requirements.txt +0 -0
  147. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/python-uv-requirements/uv.lock +0 -0
  148. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/vuln_api/check_clean.json +0 -0
  149. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/vuln_api/check_unknown.json +0 -0
  150. {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/logging_e2e.rs +0 -0
@@ -369,7 +369,7 @@ checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
369
369
 
370
370
  [[package]]
371
371
  name = "corgea"
372
- version = "1.9.1"
372
+ version = "1.9.2"
373
373
  dependencies = [
374
374
  "chrono",
375
375
  "clap",
@@ -450,9 +450,9 @@ dependencies = [
450
450
 
451
451
  [[package]]
452
452
  name = "crossbeam-epoch"
453
- version = "0.9.18"
453
+ version = "0.9.20"
454
454
  source = "registry+https://github.com/rust-lang/crates.io-index"
455
- checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
455
+ checksum = "2d6914041f254d6e9176c01941b21115dcfb7089e55135a35411081bd106ef3f"
456
456
  dependencies = [
457
457
  "crossbeam-utils",
458
458
  ]
@@ -1579,9 +1579,9 @@ dependencies = [
1579
1579
 
1580
1580
  [[package]]
1581
1581
  name = "quick-xml"
1582
- version = "0.36.2"
1582
+ version = "0.41.0"
1583
1583
  source = "registry+https://github.com/rust-lang/crates.io-index"
1584
- checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe"
1584
+ checksum = "e660451e55124f798a69a5af3f49ccfbefbd41910eefd25caf2393e1f3473ec1"
1585
1585
  dependencies = [
1586
1586
  "memchr",
1587
1587
  ]
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "corgea"
3
- version = "1.9.1"
3
+ version = "1.9.2"
4
4
  edition = "2021"
5
5
  readme = "README.md"
6
6
 
@@ -42,7 +42,7 @@ uuid = { version = "1.7.0", features = ["v4"] }
42
42
  which = "6.0.0"
43
43
  zip = "2.3.0"
44
44
  tempfile = "3.12.0"
45
- quick-xml = "0.36.1"
45
+ quick-xml = "0.41"
46
46
  ignore = "0.4"
47
47
  globset = "0.4"
48
48
  termcolor = "1.1"
@@ -61,3 +61,7 @@ serde_yaml_ng = "0.10"
61
61
 
62
62
  [target.'cfg(not(target_os = "windows"))'.dependencies]
63
63
  openssl = { version = "0.10", features = ["vendored"] }
64
+
65
+ [profile.release]
66
+ strip = true
67
+ lto = "thin"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: corgea-cli
3
- Version: 1.9.1
3
+ Version: 1.9.2
4
4
  Classifier: Development Status :: 5 - Production/Stable
5
5
  Classifier: Environment :: Console
6
6
  Classifier: Intended Audience :: Developers
@@ -131,6 +131,52 @@ Agent environments default to compact TSV; force output with `--format human|age
131
131
  Notes: `deps scan --out-format table|json|sarif` is the report/export selector; do not combine it with `deps scan --format`.
132
132
  <!-- END GENERATED CORGEA DEPS SKILL -->
133
133
 
134
+ ### Advisories — `corgea advisories check`
135
+
136
+ Query Corgea's vulnerability database for a package **before** choosing or
137
+ installing it. Read-only: it reports, never blocks. Network access to the
138
+ vuln-api is required. The package-level form needs no token; the versioned
139
+ form uses the same auth story as the install gate (a Corgea token is
140
+ attached automatically when logged in on the default vuln-api, and the
141
+ production version-check route may require one — a tokenless 401 exits 2
142
+ with a clear message).
143
+
144
+ **Before adding or choosing a dependency version, run
145
+ `corgea advisories check <ecosystem> <package>` to see its advisory history,
146
+ then `corgea advisories check <ecosystem> <package>@<version>` once you have
147
+ a candidate version. Pick the safe version the output steers to. The install
148
+ gate (`corgea npm|pip|...`) remains the backstop.**
149
+
150
+ ```bash
151
+ corgea advisories check npm axios # axios's advisory history (up to 100 most recent)
152
+ corgea advisories check npm axios@1.0.0 # verdict for one exact version
153
+ corgea advisories check pypi requests@2.31.0 # pypi (pip is accepted as an alias)
154
+ corgea advisories check pypi requests==2.31.0 # pip-style separator also accepted; extras ([security]) are ignored
155
+ corgea advisories check npm axios@1.0.0 --json # stable machine-readable document
156
+ ```
157
+
158
+ | Exit code | Meaning |
159
+ |-----------|---------|
160
+ | 0 | Clean: no advisories for the version, no advisories for the package, or package unknown to the database |
161
+ | 1 | Advisories found (versioned: vulnerable; unversioned: at least one advisory linked) |
162
+ | 2 | Error: network, auth, parse, or bad arguments |
163
+
164
+ Versions must be exact (`1.2.3`, not `^1.2`). The versioned form gives the
165
+ same verdict the install gate enforces — advisory IDs, severities,
166
+ `fixed in <version>` notes, and a `→ safe version:` steer when every
167
+ advisory has a fix. The unversioned form lists the package's advisory
168
+ history (id, severity, cvss, tier, kev/malware markers) so you can pick a
169
+ version with a clean record; it has no fix data, so follow up with the
170
+ versioned form. `--json` emits one `schema_version: 1` document on stdout
171
+ (`verdict` object for the versioned form — same vocabulary as the install
172
+ gate's `--json`; `found` + `advisories[]` + `possibly_truncated` for the
173
+ unversioned form — the listing caps at 100 advisories and
174
+ `possibly_truncated: true` means more may exist). Errors after arguments
175
+ parse (network, auth, unknown package spec, bad version) emit
176
+ `{"schema_version":1,"error":...}` on stdout and exit 2; malformed
177
+ command-line usage (missing arguments, unknown flags) gets the CLI's
178
+ standard usage text on stderr instead, also exit 2.
179
+
134
180
  ### Install Wrappers — `corgea pip|npm|yarn|pnpm|uv <args...>`
135
181
 
136
182
  Run a package manager through Corgea's install gate. Install commands with
@@ -193,7 +239,7 @@ default**; turn it off with `recency_gate = false` in `~/.corgea/config.toml`
193
239
  (or `CORGEA_RECENCY_THRESHOLD_DAYS`), or pass `--force` for a single install.
194
240
  Packages whose publish date is unknown (pip backtracking to an unresolved
195
241
  version) never trip it, and a vulnerable/malicious verdict takes precedence —
196
- such a package blocks as vulnerable, not as fresh.
242
+ such a package blocks on that verdict, not as fresh.
197
243
 
198
244
  ```bash
199
245
  corgea pip install requests==2.31.0 # resolves, checks the vuln verdict, then runs pip
@@ -205,7 +251,7 @@ corgea pip list # non-install subcommands pass straight th
205
251
 
206
252
  | Flag | Short | Description |
207
253
  |------|-------|-------------|
208
- | `--force` | | Proceed despite all findings (vulnerable, unverifiable). Findings still print. Also bypasses the wrong-package-manager and PEP 668 refusals, and unparsable-lockfile refusals on `uv sync`/`npm ci`. |
254
+ | `--force` | | Proceed despite all findings (vulnerable, malicious, unverifiable). Findings still print. Also bypasses the wrong-package-manager and PEP 668 refusals, and unparsable-lockfile refusals on `uv sync`/`npm ci`. |
209
255
  | `--json` | | JSON report instead of text. Per-result `verdict` object + `verdict_mode` + `tree`. Stdout carries only the report; the package manager's output moves to stderr. |
210
256
 
211
257
  `--json` adds `verdict_mode` (`"public"` or `"authenticated"` from the CLI;
@@ -215,7 +261,12 @@ and a `tree` object: `null` when no tree pass ran; otherwise `mode` is
215
261
  `resolved_count` and a `transitive[]` array of `{name, version, origin,
216
262
  verdict}` for packages beyond the named targets. Vulnerable `verdict`
217
263
  objects carry a `remediation` field: the safe version covering every
218
- advisory, or `null` when any advisory has no known fix. A top-level
264
+ advisory, or `null` when any advisory has no known fix. Known-malicious
265
+ packages report a distinct verdict `status` of `malicious` (matches carry a
266
+ `malware` boolean; each summary object carries a separate `malicious` count)
267
+ and refuse with a distinct `known MALICIOUS package(s) detected` message;
268
+ their `remediation` field is always `null` because malware must be removed,
269
+ not upgraded. A top-level
219
270
  `recency_threshold_days` reports the active recency window (or `null` when
220
271
  the recency gate is off); pair it with each result's `age_seconds`.
221
272
 
@@ -0,0 +1,518 @@
1
+ //! `corgea advisories` — read-only pre-decision queries against the vuln-api.
2
+ //!
3
+ //! Reports, never refuses: the install-wrap gate (`corgea npm|pip|...`)
4
+ //! remains the enforcement backstop. Exit codes: 0 clean / no advisories,
5
+ //! 1 advisories found, 2 error (network, auth, parse, guard, bad args).
6
+
7
+ use clap::Subcommand;
8
+
9
+ /// Advisories JSON schema version — independent of precheck's
10
+ /// render::SCHEMA_VERSION so the two document families evolve separately.
11
+ const SCHEMA_VERSION: u32 = 1;
12
+
13
+ #[derive(Subcommand, Debug, Clone)]
14
+ pub enum AdvisoriesSubcommand {
15
+ /// Query known advisories for a package, optionally for one exact version
16
+ Check {
17
+ /// Registry ecosystem: npm or pypi (pip is accepted as an alias)
18
+ ecosystem: String,
19
+ /// Package spec: <package> or <package>@<version> (exact versions only)
20
+ spec: String,
21
+ /// Emit a stable machine-readable JSON document instead of text
22
+ #[arg(long)]
23
+ json: bool,
24
+ },
25
+ }
26
+
27
+ /// Connection options resolved by the binary crate (base URL + optional
28
+ /// token) — the same base-URL/token-isolation selection the install wrap
29
+ /// uses. `token: None` is public mode; the command must work tokenless.
30
+ pub struct AdvisoriesOptions {
31
+ pub base_url: String,
32
+ pub token: Option<String>,
33
+ }
34
+
35
+ pub fn run(cmd: AdvisoriesSubcommand, opts: AdvisoriesOptions) -> i32 {
36
+ let AdvisoriesSubcommand::Check {
37
+ ecosystem,
38
+ spec,
39
+ json,
40
+ } = cmd;
41
+ match run_check(&ecosystem, &spec, json, &opts) {
42
+ Ok(code) => code,
43
+ Err(e) => {
44
+ report_error(&e.to_string(), json);
45
+ 2
46
+ }
47
+ }
48
+ }
49
+
50
+ /// Report a failure. In json mode the error document goes to stdout
51
+ /// (mirroring precheck's pre-report error documents); otherwise human text
52
+ /// goes to stderr. Exit code stays 2 either way. Scope: only post-clap
53
+ /// failures reach here — clap-level usage errors abort before `run` sees
54
+ /// `--json` and keep clap's own stderr text.
55
+ fn report_error(message: &str, json: bool) {
56
+ if json {
57
+ let doc = serde_json::json!({ "schema_version": SCHEMA_VERSION, "error": message });
58
+ println!("{}", serde_json::to_string_pretty(&doc).unwrap());
59
+ } else {
60
+ eprintln!("error: {message}");
61
+ }
62
+ }
63
+
64
+ fn run_check(
65
+ ecosystem: &str,
66
+ spec: &str,
67
+ json: bool,
68
+ opts: &AdvisoriesOptions,
69
+ ) -> Result<i32, Box<dyn std::error::Error>> {
70
+ let ecosystem = parse_ecosystem(ecosystem)?;
71
+ let (name, version) = parse_package_spec(ecosystem, spec)?;
72
+ let client = crate::vuln_api::http_client()?;
73
+ match version {
74
+ Some(version) => {
75
+ let resp = crate::vuln_api::check_package_version(
76
+ &client,
77
+ &opts.base_url,
78
+ opts.token.as_deref(),
79
+ ecosystem,
80
+ &name,
81
+ &version,
82
+ )?;
83
+ Ok(render_version_verdict(
84
+ ecosystem, &name, &version, &resp, json,
85
+ ))
86
+ }
87
+ None => {
88
+ let profile = crate::vuln_api::fetch_package_profile(
89
+ &client,
90
+ &opts.base_url,
91
+ opts.token.as_deref(),
92
+ ecosystem,
93
+ &name,
94
+ )?;
95
+ Ok(render_profile(ecosystem, &name, profile.as_ref(), json))
96
+ }
97
+ }
98
+ }
99
+
100
+ /// npm|pypi, case-insensitive; `pip` accepted as an alias for pypi.
101
+ fn parse_ecosystem(raw: &str) -> Result<crate::vuln_api::Ecosystem, String> {
102
+ match raw.trim().to_ascii_lowercase().as_str() {
103
+ "npm" => Ok(crate::vuln_api::Ecosystem::Npm),
104
+ "pypi" | "pip" => Ok(crate::vuln_api::Ecosystem::Pypi),
105
+ other => Err(format!("unknown ecosystem '{other}' (expected: npm, pypi)")),
106
+ }
107
+ }
108
+
109
+ /// Split `<package>[@<version>]` on the LAST `@` at index > 0, so scoped
110
+ /// npm names survive: `@scope/pkg@1.2.3` → ("@scope/pkg", Some("1.2.3")),
111
+ /// `@scope/pkg` → ("@scope/pkg", None). For pypi the pip-native spelling is
112
+ /// also accepted: `requests==2.31.0` splits at the leftmost `==` (`===`,
113
+ /// PEP 440 arbitrary equality, reads as exact too) and extras are stripped
114
+ /// (`requests[security]` → `requests`) — they select install features, not
115
+ /// the registry name. Bare `@`, trailing separators, and empty specs are
116
+ /// usage errors. Versions must be exact — range syntax, tags, and (npm)
117
+ /// partial versions are rejected.
118
+ fn parse_package_spec(
119
+ ecosystem: crate::vuln_api::Ecosystem,
120
+ spec: &str,
121
+ ) -> Result<(String, Option<String>), String> {
122
+ let spec = spec.trim();
123
+ if spec.is_empty() {
124
+ return Err("package spec is empty".to_string());
125
+ }
126
+ let is_pypi = ecosystem == crate::vuln_api::Ecosystem::Pypi;
127
+ let (name, version, sep) = match spec.split_once("==") {
128
+ Some((n, v)) if is_pypi => (n, Some(v.strip_prefix('=').unwrap_or(v)), "=="),
129
+ _ => match spec.rfind('@') {
130
+ Some(0) | None => (spec, None, "@"),
131
+ Some(pos) => (&spec[..pos], Some(&spec[pos + 1..]), "@"),
132
+ },
133
+ };
134
+ let name = if is_pypi {
135
+ name.split_once('[').map_or(name, |(n, _)| n)
136
+ } else {
137
+ name
138
+ };
139
+ // PEP 508 allows spaces around the operator (`requests == 2.31.0`), so
140
+ // trim each half after splitting.
141
+ let name = name.trim();
142
+ if name.is_empty() || name == "@" {
143
+ return Err(format!("invalid package spec '{spec}'"));
144
+ }
145
+ match version.map(str::trim) {
146
+ None => Ok((name.to_string(), None)),
147
+ Some("") => Err(format!(
148
+ "invalid package spec '{spec}': trailing '{sep}' (write <package> or <package>{sep}<version>)"
149
+ )),
150
+ Some(v) => {
151
+ require_exact_version(ecosystem, v)?;
152
+ Ok((name.to_string(), Some(v.to_string())))
153
+ }
154
+ }
155
+ }
156
+
157
+ /// Reject anything that isn't a single exact version literal, per
158
+ /// ecosystem's own rules:
159
+ ///
160
+ /// * npm — strict semver parse. This is what "exact" means to npm, and it
161
+ /// rejects ranges, dist-tags (`latest`), and partial versions in one
162
+ /// check: npm itself treats `1.2` as the x-range `1.2.x`
163
+ /// (see `verify_deps::registry::parse_version_or_x_range`), so a partial
164
+ /// is NOT an exact version there.
165
+ /// * pypi — PEP 440 versions are too varied for a strict parse (epochs
166
+ /// `2!1.0`, `1.0rc1`, `.post1`, `.dev2`, and `1.2` IS exact), so gate on
167
+ /// range syntax instead: operator prefixes, whitespace joins, specifier
168
+ /// sets, and wildcards. Unrecognized literals pass through and the
169
+ /// server answers for that exact string.
170
+ fn require_exact_version(ecosystem: crate::vuln_api::Ecosystem, v: &str) -> Result<(), String> {
171
+ let exact_err = |detail: &str| {
172
+ Err(format!(
173
+ "version '{v}' is not an exact version ({detail}; pass one exact version)"
174
+ ))
175
+ };
176
+ match ecosystem {
177
+ crate::vuln_api::Ecosystem::Npm => {
178
+ if semver::Version::parse(v).is_ok() {
179
+ Ok(())
180
+ } else {
181
+ exact_err(
182
+ "npm versions must be full semver like 1.2.3 — no ranges, tags, or partials",
183
+ )
184
+ }
185
+ }
186
+ crate::vuln_api::Ecosystem::Pypi => {
187
+ // Range operators and joins: PEP 440 prefixes (== != >= <= > < ~=),
188
+ // whitespace, "," specifier sets, "||"; wildcard components.
189
+ if v.starts_with(['^', '~', '>', '<', '=', '!'])
190
+ || v.chars().any(char::is_whitespace)
191
+ || v.contains("||")
192
+ || v.contains(',')
193
+ || v == "*"
194
+ || v.split('.')
195
+ .any(|p| p == "*" || p.eq_ignore_ascii_case("x"))
196
+ {
197
+ exact_err("ranges are not supported")
198
+ } else {
199
+ Ok(())
200
+ }
201
+ }
202
+ }
203
+ }
204
+
205
+ /// Render the versioned-form verdict (text or `--json`) and return the exit
206
+ /// code. `ecosystem` is used only by the json document.
207
+ fn render_version_verdict(
208
+ ecosystem: crate::vuln_api::Ecosystem,
209
+ name: &str,
210
+ version: &str,
211
+ resp: &crate::vuln_api::VulnCheckResponse,
212
+ json: bool,
213
+ ) -> i32 {
214
+ if json {
215
+ let doc = version_verdict_json(ecosystem, name, version, resp);
216
+ println!("{}", serde_json::to_string_pretty(&doc).unwrap());
217
+ return if resp.is_vulnerable { 1 } else { 0 };
218
+ }
219
+ if resp.is_vulnerable {
220
+ println!(
221
+ "{name}@{version}: vulnerable ({} advisories)",
222
+ resp.matches.len()
223
+ );
224
+ for m in &resp.matches {
225
+ println!(" {} ({}){}", m.advisory_id, m.severity_level, fix_note(m));
226
+ }
227
+ if let Some(safe) = crate::vuln_api::safe_version(&resp.matches) {
228
+ println!(" → safe version: {name}@{safe}");
229
+ }
230
+ 1
231
+ } else {
232
+ println!("{name}@{version}: clean — no known advisories");
233
+ 0
234
+ }
235
+ }
236
+
237
+ /// CLI-owned, schema-versioned document for the versioned form. The
238
+ /// `verdict` object reuses the gate's `verdict_json` vocabulary
239
+ /// (`status`/`matches`/`remediation`) so both document families read alike.
240
+ fn version_verdict_json(
241
+ ecosystem: crate::vuln_api::Ecosystem,
242
+ name: &str,
243
+ version: &str,
244
+ resp: &crate::vuln_api::VulnCheckResponse,
245
+ ) -> serde_json::Value {
246
+ let verdict = if resp.is_vulnerable {
247
+ serde_json::json!({
248
+ "status": "vulnerable",
249
+ "matches": resp.matches,
250
+ "remediation": crate::vuln_api::safe_version(&resp.matches),
251
+ })
252
+ } else {
253
+ serde_json::json!({ "status": "clean" })
254
+ };
255
+ serde_json::json!({
256
+ "schema_version": SCHEMA_VERSION,
257
+ "ecosystem": ecosystem.path_segment(),
258
+ "package": name,
259
+ "version": version,
260
+ "verdict": verdict,
261
+ })
262
+ }
263
+
264
+ /// Same wording as the gate's fix suffix (precheck/render.rs::fix_note),
265
+ /// kept local: one format string, not worth a cross-module export.
266
+ fn fix_note(m: &crate::vuln_api::VulnMatch) -> String {
267
+ match &m.fixed_version {
268
+ Some(v) => format!(" — fixed in {v}"),
269
+ None => " — no fixed version known".to_string(),
270
+ }
271
+ }
272
+
273
+ /// The route's `LIMIT 100`: exactly 100 rows means "possibly more exist",
274
+ /// never provably truncated (the route serves no total).
275
+ const PROFILE_LISTING_CAP: usize = 100;
276
+
277
+ /// Truncate `s` to `max` chars (char-boundary safe, ellipsis when cut).
278
+ fn truncate_summary(s: &str, max: usize) -> String {
279
+ match s.char_indices().nth(max) {
280
+ Some((idx, _)) => format!("{}…", &s[..idx]),
281
+ None => s.to_string(),
282
+ }
283
+ }
284
+
285
+ /// Render the unversioned (package profile) form and return the exit code.
286
+ /// `None` = the route answered 404 (package unknown to the database).
287
+ fn render_profile(
288
+ ecosystem: crate::vuln_api::Ecosystem,
289
+ name: &str,
290
+ profile: Option<&crate::vuln_api::PackageProfile>,
291
+ json: bool,
292
+ ) -> i32 {
293
+ let advisories: &[crate::vuln_api::AdvisorySummary] =
294
+ profile.map(|p| p.advisories.as_slice()).unwrap_or(&[]);
295
+ let found = profile.is_some();
296
+
297
+ if json {
298
+ let doc = serde_json::json!({
299
+ "schema_version": SCHEMA_VERSION,
300
+ "ecosystem": ecosystem.path_segment(),
301
+ "package": name,
302
+ "found": found,
303
+ "advisories": advisories,
304
+ "possibly_truncated": advisories.len() == PROFILE_LISTING_CAP,
305
+ });
306
+ println!("{}", serde_json::to_string_pretty(&doc).unwrap());
307
+ return if advisories.is_empty() { 0 } else { 1 };
308
+ }
309
+
310
+ if advisories.is_empty() {
311
+ if found {
312
+ println!("{name}: no known advisories");
313
+ } else {
314
+ println!("{name}: not found in the advisory database (no known advisories)");
315
+ }
316
+ return 0;
317
+ }
318
+
319
+ let cap_note = if advisories.len() == PROFILE_LISTING_CAP {
320
+ format!(" (listing capped at {PROFILE_LISTING_CAP}; more may exist)")
321
+ } else {
322
+ String::new()
323
+ };
324
+ println!("{name}: {} known advisories{cap_note}", advisories.len());
325
+ for a in advisories {
326
+ println!(" {}{}", a.id, advisory_detail(a));
327
+ if let Some(summary) = &a.summary {
328
+ let summary = summary.trim();
329
+ if !summary.is_empty() {
330
+ println!(" {}", truncate_summary(summary, 100));
331
+ }
332
+ }
333
+ }
334
+ println!(
335
+ "note: package-level listing; run `corgea advisories check {} {name}@<version>` for a version verdict.",
336
+ ecosystem.path_segment()
337
+ );
338
+ 1
339
+ }
340
+
341
+ /// The parenthesized detail group plus flag markers for one advisory line:
342
+ /// `(high, cvss 7.5, tier 1) [kev] [malware] published 2023-01-12`. Each
343
+ /// element is included only when present.
344
+ fn advisory_detail(a: &crate::vuln_api::AdvisorySummary) -> String {
345
+ let mut group: Vec<String> = Vec::new();
346
+ if let Some(sev) = &a.severity {
347
+ group.push(sev.clone());
348
+ }
349
+ if let Some(cvss) = a.cvss_score {
350
+ group.push(format!("cvss {cvss}"));
351
+ }
352
+ if let Some(tier) = a.tier {
353
+ group.push(format!("tier {tier}"));
354
+ }
355
+ let mut out = String::new();
356
+ if !group.is_empty() {
357
+ out.push_str(&format!(" ({})", group.join(", ")));
358
+ }
359
+ if a.kev == Some(true) {
360
+ out.push_str(" [kev]");
361
+ }
362
+ if a.malware == Some(true) {
363
+ out.push_str(" [malware]");
364
+ }
365
+ if let Some(published) = &a.published_at {
366
+ out.push_str(&format!(" published {published}"));
367
+ }
368
+ out
369
+ }
370
+
371
+ #[cfg(test)]
372
+ mod tests {
373
+ use super::*;
374
+ use crate::vuln_api::Ecosystem;
375
+
376
+ #[test]
377
+ fn parse_ecosystem_accepts_known_values() {
378
+ assert_eq!(parse_ecosystem("npm"), Ok(Ecosystem::Npm));
379
+ assert_eq!(parse_ecosystem("NPM"), Ok(Ecosystem::Npm));
380
+ assert_eq!(parse_ecosystem("pypi"), Ok(Ecosystem::Pypi));
381
+ assert_eq!(parse_ecosystem("PyPI"), Ok(Ecosystem::Pypi));
382
+ assert_eq!(parse_ecosystem("pip"), Ok(Ecosystem::Pypi));
383
+ }
384
+
385
+ #[test]
386
+ fn parse_ecosystem_rejects_unknown() {
387
+ let err = parse_ecosystem("rubygems").expect_err("unknown ecosystem must error");
388
+ assert!(err.contains("npm"), "err lists valid values: {err}");
389
+ assert!(err.contains("pypi"), "err lists valid values: {err}");
390
+ }
391
+
392
+ #[test]
393
+ fn parse_spec_unscoped_with_and_without_version() {
394
+ assert_eq!(
395
+ parse_package_spec(Ecosystem::Npm, "leftpad"),
396
+ Ok(("leftpad".to_string(), None))
397
+ );
398
+ assert_eq!(
399
+ parse_package_spec(Ecosystem::Npm, "leftpad@1.0.0"),
400
+ Ok(("leftpad".to_string(), Some("1.0.0".to_string())))
401
+ );
402
+ }
403
+
404
+ #[test]
405
+ fn parse_spec_scoped_with_and_without_version() {
406
+ assert_eq!(
407
+ parse_package_spec(Ecosystem::Npm, "@scope/pkg"),
408
+ Ok(("@scope/pkg".to_string(), None))
409
+ );
410
+ assert_eq!(
411
+ parse_package_spec(Ecosystem::Npm, "@scope/pkg@1.2.3"),
412
+ Ok(("@scope/pkg".to_string(), Some("1.2.3".to_string())))
413
+ );
414
+ }
415
+
416
+ #[test]
417
+ fn parse_spec_pypi_accepts_pip_style_separators() {
418
+ for spec in [
419
+ "requests==2.31.0",
420
+ "requests===2.31.0",
421
+ "requests == 2.31.0",
422
+ ] {
423
+ assert_eq!(
424
+ parse_package_spec(Ecosystem::Pypi, spec),
425
+ Ok(("requests".to_string(), Some("2.31.0".to_string()))),
426
+ "spec: {spec:?}"
427
+ );
428
+ }
429
+ }
430
+
431
+ #[test]
432
+ fn parse_spec_pypi_strips_extras() {
433
+ assert_eq!(
434
+ parse_package_spec(Ecosystem::Pypi, "requests[security]"),
435
+ Ok(("requests".to_string(), None))
436
+ );
437
+ assert_eq!(
438
+ parse_package_spec(Ecosystem::Pypi, "requests[security]==2.31.0"),
439
+ Ok(("requests".to_string(), Some("2.31.0".to_string())))
440
+ );
441
+ assert_eq!(
442
+ parse_package_spec(Ecosystem::Pypi, "requests[security]@2.31.0"),
443
+ Ok(("requests".to_string(), Some("2.31.0".to_string())))
444
+ );
445
+ }
446
+
447
+ #[test]
448
+ fn parse_spec_pypi_rejects_bad_pip_forms() {
449
+ let err =
450
+ parse_package_spec(Ecosystem::Pypi, "requests==").expect_err("trailing == is an error");
451
+ assert!(err.contains("trailing"), "err: {err}");
452
+ assert!(parse_package_spec(Ecosystem::Pypi, "[security]==1.0").is_err());
453
+ assert!(parse_package_spec(Ecosystem::Pypi, "requests==2.31.*").is_err());
454
+ }
455
+
456
+ #[test]
457
+ fn parse_spec_rejects_trailing_at() {
458
+ let err = parse_package_spec(Ecosystem::Npm, "foo@").expect_err("trailing @ is an error");
459
+ assert!(err.contains("trailing"), "err: {err}");
460
+ }
461
+
462
+ #[test]
463
+ fn parse_spec_rejects_bare_and_empty() {
464
+ assert!(parse_package_spec(Ecosystem::Npm, "@").is_err());
465
+ assert!(parse_package_spec(Ecosystem::Npm, "").is_err());
466
+ assert!(parse_package_spec(Ecosystem::Npm, " ").is_err());
467
+ }
468
+
469
+ #[test]
470
+ fn npm_rejects_non_exact_versions() {
471
+ for bad in [
472
+ "^1.0",
473
+ "~1.0",
474
+ ">=1.0",
475
+ "1.0.0 - 2.0.0",
476
+ "1.0 || 2.0",
477
+ "*",
478
+ "1.x",
479
+ "1.2",
480
+ "latest",
481
+ ] {
482
+ assert!(
483
+ require_exact_version(Ecosystem::Npm, bad).is_err(),
484
+ "npm should reject {bad:?}"
485
+ );
486
+ }
487
+ }
488
+
489
+ #[test]
490
+ fn npm_accepts_exact_semver() {
491
+ for good in ["1.2.3", "1.2.3-beta.1", "1.2.3+build.5"] {
492
+ assert!(
493
+ require_exact_version(Ecosystem::Npm, good).is_ok(),
494
+ "npm should accept {good:?}"
495
+ );
496
+ }
497
+ }
498
+
499
+ #[test]
500
+ fn pypi_rejects_range_syntax() {
501
+ for bad in ["==1.0", ">=1.0,<2.0", "~=1.4", "!=1.0", "1.*", "1.0 || 2.0"] {
502
+ assert!(
503
+ require_exact_version(Ecosystem::Pypi, bad).is_err(),
504
+ "pypi should reject {bad:?}"
505
+ );
506
+ }
507
+ }
508
+
509
+ #[test]
510
+ fn pypi_accepts_exact_literals() {
511
+ for good in ["1.2", "2!1.0", "1.0rc1", "1.0.post1"] {
512
+ assert!(
513
+ require_exact_version(Ecosystem::Pypi, good).is_ok(),
514
+ "pypi should accept {good:?}"
515
+ );
516
+ }
517
+ }
518
+ }
@@ -1,3 +1,4 @@
1
+ pub mod advisories;
1
2
  pub mod deps;
2
3
  pub mod precheck;
3
4
  pub mod verify_deps;