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.
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/Cargo.lock +5 -5
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/Cargo.toml +6 -2
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/PKG-INFO +1 -1
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/skills/corgea/SKILL.md +54 -3
- corgea_cli-1.9.2/src/advisories.rs +518 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/lib.rs +1 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/list.rs +32 -3
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/main.rs +83 -14
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/precheck/mod.rs +28 -2
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/precheck/render.rs +74 -102
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/precheck/test_support.rs +1 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/precheck/verdict.rs +167 -16
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/scanners/blast.rs +309 -18
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/scanners/fortify.rs +56 -2
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/utils/api.rs +37 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/utils/generic.rs +84 -4
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/verify_deps/registry.rs +2 -3
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/vuln_api/mod.rs +504 -59
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/vuln_api_stub/mod.rs +133 -5
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/wait.rs +1 -7
- corgea_cli-1.9.2/tests/cli_advisories.rs +417 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/cli_bare_install.rs +153 -7
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/cli_deps.rs +27 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/cli_install.rs +31 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/cli_npm_ci.rs +6 -6
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/cli_provenance.rs +5 -5
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/cli_remediation.rs +2 -2
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/cli_tree.rs +7 -7
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/cli_uv_sync.rs +3 -3
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/cli_verdict.rs +225 -10
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/common/mod.rs +1 -1
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/vuln_api/README.md +7 -1
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/vuln_api/check_malware.json +2 -1
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/vuln_api/check_vulnerable.json +2 -1
- corgea_cli-1.9.2/tests/fixtures/vuln_api/package_profile.json +47 -0
- corgea_cli-1.9.2/tests/fixtures/vuln_api/package_profile_not_found.json +1 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/harness_smoke.rs +2 -2
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/vuln_api_contract.rs +88 -2
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/.codex/hooks.json +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/.github/workflows/npm-publish.yml +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/.github/workflows/release-binaries.yml +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/.github/workflows/release.yml +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/.github/workflows/staging-contract.yml +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/.github/workflows/test.yml +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/.github/workflows/update_docs..yml +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/.gitignore +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/AGENTS.md +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/LICENSE +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/README.md +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/RELEASING.md +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/bin/corgea.js +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/build_release.sh +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/examples/deps_skill.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/harness +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/package.json +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/pyproject.toml +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/scripts/npm/bundle-binaries.js +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/authorize.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/cicd.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/config.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/detect.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/diff.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/ecosystems/evaluate.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/ecosystems/maven.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/ecosystems/mod.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/ecosystems/npm.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/ecosystems/pypi.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/explain.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/findings.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/mod.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/model.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/parse/mod.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/parse/npm_lock.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/parse/python_lock.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/policy.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/report.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/run.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/skill.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/tests/common.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/tests/correctness_tests.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/tests/detect_tests.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/tests/diff_tests.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/tests/explain_tests.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/tests/findings_tests.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/tests/maven_tests.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/tests/mod.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/tests/npm_tests.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/tests/policy_tests.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/tests/pypi_tests.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/tests/report_tests.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/tests/robustness_tests.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/deps/tests/slice0_tests.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/inspect.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/log.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/precheck/detect.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/precheck/exec.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/precheck/parse.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/precheck/tree.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/precheck/uv.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/scan.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/scanners/parsers/checkmarx.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/scanners/parsers/coverity.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/scanners/parsers/mod.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/scanners/parsers/sarif.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/scanners/parsers/semgrep.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/setup_hooks.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/skill.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/targets.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/utils/terminal.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/src/verify_deps/mod.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/cli_deps_skill.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/cli_exec_fallback.rs +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/README.md +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/go-mod-smoke/go.mod +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/go-mod-smoke/go.sum +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/java-gradle/build.gradle +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/java-gradle/gradle.lockfile +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/java-maven/pom.xml +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/malformed/not-xml-pom.xml +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/malformed/package-lock.json +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/malformed/package.json +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/malformed/poetry.lock +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/malformed/pyproject.toml +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/malformed/truncated-poetry.lock +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/node-app/.corgea/deps.yml +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/node-app/package-lock.json +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/node-app/package.json +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/node-monorepo/package-lock.json +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/node-monorepo/package.json +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/node-monorepo/packages/a/package.json +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/node-monorepo/packages/b/package.json +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/node-pnpm/package.json +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/node-pnpm/pnpm-lock.yaml +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/node-stale/package-lock.json +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/node-stale/package.json +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/node-transitive/package-lock.json +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/node-transitive/package.json +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/node-yarn/package.json +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/node-yarn/yarn.lock +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/python-pip-nolock/requirements.txt +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/python-poetry/poetry.lock +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/python-poetry/pyproject.toml +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/python-poetry-multi/poetry.lock +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/python-poetry-multi/pyproject.toml +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/python-uv-requirements/pyproject.toml +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/python-uv-requirements/requirements.txt +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/python-uv-requirements/uv.lock +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/vuln_api/check_clean.json +0 -0
- {corgea_cli-1.9.1 → corgea_cli-1.9.2}/tests/fixtures/vuln_api/check_unknown.json +0 -0
- {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.
|
|
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.
|
|
453
|
+
version = "0.9.20"
|
|
454
454
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
455
|
-
checksum = "
|
|
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.
|
|
1582
|
+
version = "0.41.0"
|
|
1583
1583
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
1584
|
-
checksum = "
|
|
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.
|
|
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.
|
|
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"
|
|
@@ -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
|
|
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.
|
|
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
|
+
}
|