corgea-cli 1.9.3__tar.gz → 1.10.0__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.3 → corgea_cli-1.10.0}/.github/workflows/test.yml +3 -1
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/Cargo.lock +1 -1
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/Cargo.toml +1 -1
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/PKG-INFO +1 -1
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/skills/corgea/SKILL.md +14 -2
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/config.rs +8 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/deps/ecosystems/maven.rs +180 -2
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/deps/ecosystems/mod.rs +10 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/deps/mod.rs +7 -0
- corgea_cli-1.10.0/src/deps/report.rs +261 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/deps/run.rs +3 -6
- corgea_cli-1.10.0/src/deps/tests/maven_tests.rs +338 -0
- corgea_cli-1.10.0/src/deps/tests/report_tests.rs +218 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/list.rs +126 -48
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/main.rs +200 -23
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/precheck/mod.rs +34 -12
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/precheck/test_support.rs +2 -2
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/precheck/verdict.rs +1 -1
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/scan.rs +24 -4
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/scanners/blast.rs +437 -9
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/utils/api.rs +937 -20
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/utils/generic.rs +145 -36
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/vuln_api_stub/mod.rs +135 -8
- corgea_cli-1.10.0/src/wait.rs +171 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/cli_deps.rs +99 -0
- corgea_cli-1.10.0/tests/cli_scan_sbom.rs +219 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/cli_verdict.rs +51 -0
- corgea_cli-1.10.0/tests/cloud_commands_e2e/common/mod.rs +842 -0
- corgea_cli-1.10.0/tests/cloud_commands_e2e/inspect.rs +140 -0
- corgea_cli-1.10.0/tests/cloud_commands_e2e/main.rs +7 -0
- corgea_cli-1.10.0/tests/cloud_commands_e2e/scan_list.rs +148 -0
- corgea_cli-1.10.0/tests/cloud_commands_e2e/upload_wait.rs +149 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/common/mod.rs +197 -1
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/README.md +1 -0
- corgea_cli-1.10.0/tests/fixtures/java-maven-alias/pom.xml +19 -0
- corgea_cli-1.10.0/tests/fixtures/java-maven-chained/pom.xml +20 -0
- corgea_cli-1.10.0/tests/fixtures/java-maven-deep-chain/pom.xml +25 -0
- corgea_cli-1.10.0/tests/fixtures/java-maven-managed/pom.xml +29 -0
- corgea_cli-1.10.0/tests/fixtures/java-maven-parent/pom.xml +29 -0
- corgea_cli-1.10.0/tests/fixtures/java-maven-plugin-deps/pom.xml +62 -0
- corgea_cli-1.10.0/tests/fixtures/java-maven-profile-mgmt/pom.xml +37 -0
- corgea_cli-1.10.0/tests/fixtures/java-maven-props/pom.xml +27 -0
- corgea_cli-1.10.0/tests/fixtures/java-maven-revision/pom.xml +19 -0
- corgea_cli-1.10.0/tests/fixtures/mixed-monorepo/services/api/requirements.txt +2 -0
- corgea_cli-1.10.0/tests/fixtures/mixed-monorepo/services/web/package-lock.json +29 -0
- corgea_cli-1.10.0/tests/fixtures/mixed-monorepo/services/web/package.json +10 -0
- corgea_cli-1.10.0/tests/list_code_quality.rs +209 -0
- corgea_cli-1.10.0/tests/list_resolution.rs +374 -0
- corgea_cli-1.10.0/tests/wait_resolution.rs +358 -0
- corgea_cli-1.9.3/.github/workflows/sync-skills.yml +0 -166
- corgea_cli-1.9.3/.github/workflows/update_docs..yml +0 -27
- corgea_cli-1.9.3/src/deps/report.rs +0 -172
- corgea_cli-1.9.3/src/deps/tests/maven_tests.rs +0 -129
- corgea_cli-1.9.3/src/deps/tests/report_tests.rs +0 -81
- corgea_cli-1.9.3/src/wait.rs +0 -102
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/.codex/hooks.json +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/.github/workflows/npm-publish.yml +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/.github/workflows/release-binaries.yml +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/.github/workflows/release.yml +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/.github/workflows/staging-contract.yml +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/.gitignore +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/AGENTS.md +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/LICENSE +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/README.md +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/RELEASING.md +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/bin/corgea.js +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/build_release.sh +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/examples/deps_skill.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/harness +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/package.json +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/pyproject.toml +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/scripts/check-linux-binary.sh +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/scripts/npm/bundle-binaries.js +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/advisories.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/authorize.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/cicd.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/deps/catalog.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/deps/detect.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/deps/diff.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/deps/ecosystems/evaluate.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/deps/ecosystems/npm.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/deps/ecosystems/pypi.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/deps/explain.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/deps/findings.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/deps/model.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/deps/parse/mod.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/deps/parse/npm_lock.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/deps/parse/python_lock.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/deps/policy.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/deps/skill.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/deps/tests/common.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/deps/tests/correctness_tests.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/deps/tests/detect_tests.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/deps/tests/diff_tests.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/deps/tests/explain_tests.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/deps/tests/findings_tests.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/deps/tests/mod.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/deps/tests/npm_tests.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/deps/tests/policy_tests.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/deps/tests/pypi_tests.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/deps/tests/robustness_tests.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/deps/tests/slice0_tests.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/inspect.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/lib.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/log.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/precheck/detect.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/precheck/exec.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/precheck/parse.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/precheck/render.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/precheck/tree.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/precheck/uv.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/scanners/fortify.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/scanners/parsers/checkmarx.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/scanners/parsers/coverity.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/scanners/parsers/mod.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/scanners/parsers/sarif.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/scanners/parsers/semgrep.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/setup_hooks.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/skill.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/targets.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/utils/terminal.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/verify_deps/mod.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/verify_deps/registry.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/src/vuln_api/mod.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/cli_advisories.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/cli_bare_install.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/cli_deps_skill.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/cli_exec_fallback.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/cli_install.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/cli_npm_ci.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/cli_provenance.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/cli_remediation.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/cli_tree.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/cli_uv_sync.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/go-mod-smoke/go.mod +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/go-mod-smoke/go.sum +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/java-gradle/build.gradle +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/java-gradle/gradle.lockfile +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/java-maven/pom.xml +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/malformed/not-xml-pom.xml +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/malformed/package-lock.json +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/malformed/package.json +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/malformed/poetry.lock +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/malformed/pyproject.toml +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/malformed/truncated-poetry.lock +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/node-app/.corgea/deps.yml +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/node-app/package-lock.json +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/node-app/package.json +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/node-monorepo/package-lock.json +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/node-monorepo/package.json +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/node-monorepo/packages/a/package.json +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/node-monorepo/packages/b/package.json +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/node-pnpm/package.json +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/node-pnpm/pnpm-lock.yaml +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/node-stale/package-lock.json +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/node-stale/package.json +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/node-transitive/package-lock.json +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/node-transitive/package.json +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/node-yarn/package.json +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/node-yarn/yarn.lock +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/python-pip-nolock/requirements.txt +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/python-poetry/poetry.lock +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/python-poetry/pyproject.toml +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/python-poetry-multi/poetry.lock +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/python-poetry-multi/pyproject.toml +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/python-uv-requirements/pyproject.toml +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/python-uv-requirements/requirements.txt +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/python-uv-requirements/uv.lock +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/vuln_api/README.md +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/vuln_api/check_clean.json +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/vuln_api/check_malware.json +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/vuln_api/check_unknown.json +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/vuln_api/check_vulnerable.json +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/vuln_api/package_profile.json +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/fixtures/vuln_api/package_profile_not_found.json +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/harness_smoke.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/logging_e2e.rs +0 -0
- {corgea_cli-1.9.3 → corgea_cli-1.10.0}/tests/vuln_api_contract.rs +0 -0
|
@@ -35,8 +35,12 @@ corgea scan --scan-type policy --policy 1 # Specific policy ID
|
|
|
35
35
|
corgea scan --fail-on CR # Exit 1 on critical issues (CR, HI, ME, LO)
|
|
36
36
|
corgea scan --fail-on malicious # Exit 1 if any dependency is classified malicious
|
|
37
37
|
corgea scan --fail-on HI,malicious # Comma-separated conditions combine
|
|
38
|
-
corgea scan --
|
|
38
|
+
corgea scan --block-on criticals # Exit 1 if the scan violates the named CI blocking rules
|
|
39
|
+
corgea scan --block-on criticals,malicious-deps # Comma-separated rule slugs
|
|
40
|
+
corgea scan --fail # Deprecated: exit 1 based on every active blocking rule
|
|
39
41
|
corgea scan --out-format json --out-file r.json # Export (json, html, sarif, markdown)
|
|
42
|
+
corgea scan --sbom # Also write a CycloneDX SBOM to bom.json
|
|
43
|
+
corgea scan --sbom sbom.cdx.json # SBOM to a custom file
|
|
40
44
|
corgea scan --project-name my-service # Override project name
|
|
41
45
|
```
|
|
42
46
|
|
|
@@ -44,7 +48,11 @@ Scan types: `blast` (base AI), `policy` (PolicyIQ), `malicious`, `secrets`, `pii
|
|
|
44
48
|
|
|
45
49
|
`--fail-on` takes comma-separated conditions: severity thresholds `CR`, `HI`, `ME`, `LO` (trip at or above the level) and/or `malicious` (trips when any dependency in the scan is classified malicious). Use `malicious` to block supply-chain findings in CI across every ecosystem the scan covers, including those the `corgea npm`/`corgea pip` install gate does not.
|
|
46
50
|
|
|
47
|
-
`--
|
|
51
|
+
`--block-on` takes comma-separated blocking-rule slugs. Blocking rules are configured in the web app and each one applies either to pull requests or to CI. Only CI rules can be named by `--block-on`; a slug that is unknown, inactive, or scoped to pull requests is a hard error (exit 1) rather than a silently skipped gate. The slug is shown next to each rule in the web app and is derived from the rule name, so renaming a rule changes its slug. Name rules for the condition that trips them — `criticals`, not `no-criticals` — so that `--block-on` reads as a direct assertion rather than a double negative.
|
|
52
|
+
|
|
53
|
+
`--fail` is deprecated. It evaluates every active blocking rule regardless of what it applies to; use `--block-on` to name the CI rules a pipeline should enforce.
|
|
54
|
+
|
|
55
|
+
`--only-uncommitted` and `--target` are mutually exclusive. `--fail-on`, `--fail`, and `--block-on` are mutually exclusive.
|
|
48
56
|
|
|
49
57
|
### Upload — `corgea upload [report]`
|
|
50
58
|
|
|
@@ -75,6 +83,7 @@ corgea wait --scan-id SCAN_ID # Wait for specific scan
|
|
|
75
83
|
corgea ls # List scans
|
|
76
84
|
corgea ls --issues --scan-id SCAN_ID # Issues for a scan
|
|
77
85
|
corgea ls --sca-issues # SCA (dependency) issues
|
|
86
|
+
corgea ls --code-quality # Code quality issues
|
|
78
87
|
corgea ls --issues --page 2 --page-size 10 # Pagination
|
|
79
88
|
corgea ls --issues --scan-id SCAN_ID --json # JSON output
|
|
80
89
|
```
|
|
@@ -83,6 +92,7 @@ corgea ls --issues --scan-id SCAN_ID --json # JSON output
|
|
|
83
92
|
|------|-------|-------------|
|
|
84
93
|
| `--issues` | `-i` | List code/SAST issues |
|
|
85
94
|
| `--sca-issues` | `-c` | List SCA issues |
|
|
95
|
+
| `--code-quality` | `-q` | List code quality issues (alias `--quality`) |
|
|
86
96
|
| `--scan-id` | `-s` | Filter to a scan |
|
|
87
97
|
| `--page` | `-p` | Page number |
|
|
88
98
|
| `--page-size` | | Items per page |
|
|
@@ -354,6 +364,7 @@ corgea inspect --issue --diff ISSUE_ID
|
|
|
354
364
|
```bash
|
|
355
365
|
corgea scan --fail-on CR --out-format sarif --out-file results.sarif
|
|
356
366
|
corgea scan --fail-on CR,malicious --out-format sarif --out-file results.sarif # also block malicious dependencies
|
|
367
|
+
corgea scan --block-on criticals --out-format sarif --out-file results.sarif # gate on a CI blocking rule from the web app
|
|
357
368
|
```
|
|
358
369
|
|
|
359
370
|
### Upload third-party reports
|
|
@@ -367,6 +378,7 @@ corgea upload report.json --project-name my-app
|
|
|
367
378
|
```bash
|
|
368
379
|
corgea scan --out-format html --out-file report.html
|
|
369
380
|
corgea scan --out-format sarif --out-file report.sarif
|
|
381
|
+
corgea scan --out-format sarif --out-file report.sarif --sbom bom.json # SARIF + CycloneDX SBOM
|
|
370
382
|
```
|
|
371
383
|
|
|
372
384
|
## Severity Levels
|
|
@@ -4,6 +4,14 @@ use std::{env, fs, io};
|
|
|
4
4
|
|
|
5
5
|
pub const DEFAULT_VULN_API_URL: &str = "https://cve-worker-staging.corgea.workers.dev";
|
|
6
6
|
|
|
7
|
+
/// Whether `DEFAULT_VULN_API_URL` is an endpoint Corgea tokens belong to.
|
|
8
|
+
/// The `-staging` hostname is historical: this worker serves as the production
|
|
9
|
+
/// vuln-api, so a token may be sent to it and authenticated (fail-closed)
|
|
10
|
+
/// verdicts apply by default. Set to `false` if the default is ever repointed
|
|
11
|
+
/// at an endpoint that customer tokens should not reach — token-send and
|
|
12
|
+
/// fail-closed then both require the explicit opt-in.
|
|
13
|
+
pub const DEFAULT_VULN_API_URL_IS_PRODUCTION: bool = true;
|
|
14
|
+
|
|
7
15
|
#[derive(Serialize, Deserialize, Clone)]
|
|
8
16
|
pub struct Config {
|
|
9
17
|
pub(crate) url: String,
|
|
@@ -5,7 +5,7 @@ use crate::deps::ecosystems::classify_constraint;
|
|
|
5
5
|
use crate::deps::ecosystems::evaluate::{
|
|
6
6
|
constraint_to_findings, dep001, file_in_dir, parent_dir, ScanContext,
|
|
7
7
|
};
|
|
8
|
-
use crate::deps::model::{DependencyNode, Ecosystem, PackageId, Scope, SourceType};
|
|
8
|
+
use crate::deps::model::{DependencyEdge, DependencyNode, Ecosystem, PackageId, Scope, SourceType};
|
|
9
9
|
use crate::deps::DepsError;
|
|
10
10
|
|
|
11
11
|
pub fn scan_maven_projects(ctx: &mut ScanContext<'_>) -> Result<(), DepsError> {
|
|
@@ -68,6 +68,14 @@ fn scan_maven_pom(ctx: &mut ScanContext<'_>, dir: &Path, pom_path: &Path) -> Res
|
|
|
68
68
|
Some(package_id.clone()),
|
|
69
69
|
false,
|
|
70
70
|
));
|
|
71
|
+
ctx.graph.edges.push(DependencyEdge {
|
|
72
|
+
from: PackageId::root(),
|
|
73
|
+
to: package_id.clone(),
|
|
74
|
+
declared_constraint: declared.clone(),
|
|
75
|
+
resolved_version: Some(dep.version.clone()),
|
|
76
|
+
scope: dep.scope,
|
|
77
|
+
source_file: rel.clone(),
|
|
78
|
+
});
|
|
71
79
|
ctx.graph.nodes.push(DependencyNode {
|
|
72
80
|
id: package_id,
|
|
73
81
|
name,
|
|
@@ -89,8 +97,178 @@ fn scan_maven_pom(ctx: &mut ScanContext<'_>, dir: &Path, pom_path: &Path) -> Res
|
|
|
89
97
|
Ok(())
|
|
90
98
|
}
|
|
91
99
|
|
|
100
|
+
/// Sections whose contents are not the project's own coordinates or
|
|
101
|
+
/// dependencies; stripped before any dependency or property extraction.
|
|
102
|
+
const NON_DEPENDENCY_SECTIONS: &[&str] = &["profiles", "build", "reporting"];
|
|
103
|
+
|
|
92
104
|
fn parse_pom_dependencies(content: &str) -> Result<Vec<MavenDep>, DepsError> {
|
|
93
|
-
|
|
105
|
+
let stripped = strip_sections(content, NON_DEPENDENCY_SECTIONS);
|
|
106
|
+
let props = parse_pom_properties(&stripped);
|
|
107
|
+
let (rest, management) = split_dependency_management(&stripped);
|
|
108
|
+
let managed: std::collections::HashMap<(String, String), String> = parse_pom_regex(management)
|
|
109
|
+
.into_iter()
|
|
110
|
+
.filter(|d| !d.version.is_empty())
|
|
111
|
+
.map(|d| ((d.group, d.artifact), d.version))
|
|
112
|
+
.collect();
|
|
113
|
+
let mut deps = parse_pom_regex(&rest);
|
|
114
|
+
for dep in &mut deps {
|
|
115
|
+
if dep.version.is_empty() {
|
|
116
|
+
if let Some(v) = managed.get(&(dep.group.clone(), dep.artifact.clone())) {
|
|
117
|
+
dep.version = v.clone();
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
dep.version = resolve_placeholders(&dep.version, &props);
|
|
121
|
+
}
|
|
122
|
+
Ok(deps)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// Remove every `<tag>...</tag>` section. Their dependency blocks are not
|
|
126
|
+
/// application dependencies, and a profile's `<properties>` or
|
|
127
|
+
/// `<dependencyManagement>` must not affect the base dependency graph, so
|
|
128
|
+
/// stripping runs before property and management extraction.
|
|
129
|
+
fn strip_sections(content: &str, tags: &[&str]) -> String {
|
|
130
|
+
let mut content = content.to_string();
|
|
131
|
+
for tag in tags {
|
|
132
|
+
while let Some((rest, _)) = split_section(&content, tag) {
|
|
133
|
+
content = rest;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
content
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/// Split out the `<dependencyManagement>` section: its entries pin versions
|
|
140
|
+
/// for the project's dependencies but are not dependencies themselves.
|
|
141
|
+
/// Returns (pom without the section, the section's inner content).
|
|
142
|
+
fn split_dependency_management(content: &str) -> (String, &str) {
|
|
143
|
+
split_section(content, "dependencyManagement").unwrap_or_else(|| (content.to_string(), ""))
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// Locate a `<tag>...</tag>` section and split content into (content with
|
|
147
|
+
/// the section removed, the section's inner content). `None` if the tag
|
|
148
|
+
/// isn't present or is malformed (close before open).
|
|
149
|
+
fn split_section<'a>(content: &'a str, tag: &str) -> Option<(String, &'a str)> {
|
|
150
|
+
let open = format!("<{tag}>");
|
|
151
|
+
let close = format!("</{tag}>");
|
|
152
|
+
let s = content.find(&open)?;
|
|
153
|
+
let e = content.find(&close)?;
|
|
154
|
+
if s >= e {
|
|
155
|
+
return None;
|
|
156
|
+
}
|
|
157
|
+
let inner = &content[s + open.len()..e];
|
|
158
|
+
let rest = format!("{}{}", &content[..s], &content[e + close.len()..]);
|
|
159
|
+
Some((rest, inner))
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/// Collect `<properties>` entries plus the built-in `project.version`.
|
|
163
|
+
fn parse_pom_properties(content: &str) -> std::collections::HashMap<String, String> {
|
|
164
|
+
let mut props = std::collections::HashMap::new();
|
|
165
|
+
if let Some(start) = content.find("<properties>") {
|
|
166
|
+
let rest = &content[start + "<properties>".len()..];
|
|
167
|
+
if let Some(end) = rest.find("</properties>") {
|
|
168
|
+
let mut block = &rest[..end];
|
|
169
|
+
while let Some(open_start) = block.find('<') {
|
|
170
|
+
let after = &block[open_start + 1..];
|
|
171
|
+
let Some(open_end) = after.find('>') else {
|
|
172
|
+
break;
|
|
173
|
+
};
|
|
174
|
+
let tag = after[..open_end].trim();
|
|
175
|
+
let body = &after[open_end + 1..];
|
|
176
|
+
if tag.starts_with('/') || tag.starts_with('!') || tag.ends_with('/') {
|
|
177
|
+
block = body;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
let close = format!("</{tag}>");
|
|
181
|
+
match body.find(&close) {
|
|
182
|
+
Some(close_pos) => {
|
|
183
|
+
props.insert(tag.to_string(), body[..close_pos].trim().to_string());
|
|
184
|
+
block = &body[close_pos + close.len()..];
|
|
185
|
+
}
|
|
186
|
+
None => block = body,
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// Property values may reference other properties; resolve the map to a
|
|
192
|
+
// fixed point, bounded to guard against definition cycles.
|
|
193
|
+
resolve_props_fixed_point(&mut props);
|
|
194
|
+
let project_version = resolve_placeholders(&pom_project_version(content), &props);
|
|
195
|
+
if !project_version.is_empty() && !project_version.contains("${") {
|
|
196
|
+
props.insert("project.version".to_string(), project_version);
|
|
197
|
+
}
|
|
198
|
+
// Properties aliasing ${project.version} (e.g. `<shared.version>`) only
|
|
199
|
+
// resolve now that project.version itself is in the map.
|
|
200
|
+
resolve_props_fixed_point(&mut props);
|
|
201
|
+
props
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/// Resolve `${...}` references among property values to a fixed point. A
|
|
205
|
+
/// resolution chain can't be longer than the number of properties without
|
|
206
|
+
/// a cycle, so bound the iteration count by the map size.
|
|
207
|
+
fn resolve_props_fixed_point(props: &mut std::collections::HashMap<String, String>) {
|
|
208
|
+
for _ in 0..=props.len() {
|
|
209
|
+
let resolved: std::collections::HashMap<String, String> = props
|
|
210
|
+
.iter()
|
|
211
|
+
.map(|(k, v)| (k.clone(), resolve_placeholders(v, props)))
|
|
212
|
+
.collect();
|
|
213
|
+
if resolved == *props {
|
|
214
|
+
break;
|
|
215
|
+
}
|
|
216
|
+
*props = resolved;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/// The pom's own `<version>`: first `<version>` before `<dependencies>`,
|
|
221
|
+
/// excluding the `<parent>` block. A child that inherits its version has
|
|
222
|
+
/// none of its own, so fall back to the parent's (Maven's inheritance rule).
|
|
223
|
+
fn pom_project_version(content: &str) -> String {
|
|
224
|
+
let head = content.split("<dependencies>").next().unwrap_or(content);
|
|
225
|
+
// The project's own <version> lives among its coordinates, before any
|
|
226
|
+
// nested section that may carry unrelated <version> tags of its own
|
|
227
|
+
// (plugin versions in <build>, managed versions in
|
|
228
|
+
// <dependencyManagement>, etc).
|
|
229
|
+
let nested_start = NON_DEPENDENCY_SECTIONS
|
|
230
|
+
.iter()
|
|
231
|
+
.copied()
|
|
232
|
+
.chain(["dependencyManagement"])
|
|
233
|
+
.filter_map(|tag| head.find(&format!("<{tag}>")))
|
|
234
|
+
.min();
|
|
235
|
+
let head = match nested_start {
|
|
236
|
+
Some(pos) => &head[..pos],
|
|
237
|
+
None => head,
|
|
238
|
+
};
|
|
239
|
+
if let Some((cleaned, parent)) = split_section(head, "parent") {
|
|
240
|
+
let own = extract_xml_tag(&cleaned, "version");
|
|
241
|
+
if !own.is_empty() {
|
|
242
|
+
return own;
|
|
243
|
+
}
|
|
244
|
+
return extract_xml_tag(parent, "version");
|
|
245
|
+
}
|
|
246
|
+
extract_xml_tag(head, "version")
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/// Substitute `${name}` placeholders; unresolved ones pass through unchanged.
|
|
250
|
+
fn resolve_placeholders(raw: &str, props: &std::collections::HashMap<String, String>) -> String {
|
|
251
|
+
if !raw.contains("${") {
|
|
252
|
+
return raw.to_string();
|
|
253
|
+
}
|
|
254
|
+
let mut out = String::new();
|
|
255
|
+
let mut rest = raw;
|
|
256
|
+
while let Some(start) = rest.find("${") {
|
|
257
|
+
out.push_str(&rest[..start]);
|
|
258
|
+
let after = &rest[start + 2..];
|
|
259
|
+
let Some(end) = after.find('}') else {
|
|
260
|
+
out.push_str(&rest[start..]);
|
|
261
|
+
return out;
|
|
262
|
+
};
|
|
263
|
+
let key = &after[..end];
|
|
264
|
+
match props.get(key) {
|
|
265
|
+
Some(v) => out.push_str(v),
|
|
266
|
+
None => out.push_str(&rest[start..start + 2 + end + 1]),
|
|
267
|
+
}
|
|
268
|
+
rest = &after[end + 1..];
|
|
269
|
+
}
|
|
270
|
+
out.push_str(rest);
|
|
271
|
+
out
|
|
94
272
|
}
|
|
95
273
|
|
|
96
274
|
fn parse_pom_regex(content: &str) -> Vec<MavenDep> {
|
|
@@ -10,6 +10,16 @@ pub fn scan_all(ctx: &mut ScanContext<'_>) -> Result<(), DepsError> {
|
|
|
10
10
|
evaluate::scan_all(ctx)
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
/// Whether `scan_all` builds graph nodes for this ecosystem. Keep in sync
|
|
14
|
+
/// with the scanners wired in `evaluate::scan_all` — detect-only ecosystems
|
|
15
|
+
/// (Go, Cargo) are found on disk but contribute nothing to the graph or SBOM.
|
|
16
|
+
pub fn is_graphed(ecosystem: Ecosystem) -> bool {
|
|
17
|
+
matches!(
|
|
18
|
+
ecosystem,
|
|
19
|
+
Ecosystem::Npm | Ecosystem::PyPI | Ecosystem::Maven
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
13
23
|
use crate::deps::model::{ConstraintKind, Ecosystem};
|
|
14
24
|
|
|
15
25
|
/// Classify a raw declared constraint string.
|
|
@@ -62,6 +62,13 @@ impl Inventory {
|
|
|
62
62
|
|
|
63
63
|
/// Scan a directory tree: detect files, build the graph, evaluate policy.
|
|
64
64
|
pub fn scan(root: &Path, policy: &Policy) -> Result<Inventory, DepsError> {
|
|
65
|
+
if !root.exists() {
|
|
66
|
+
return Err(DepsError(format!(
|
|
67
|
+
"path does not exist: {}",
|
|
68
|
+
root.display()
|
|
69
|
+
)));
|
|
70
|
+
}
|
|
71
|
+
|
|
65
72
|
let detected = detect::detect_dependency_files(root);
|
|
66
73
|
let mut graph = DependencyGraph::default();
|
|
67
74
|
let mut findings = Vec::new();
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
use serde_json::{json, Value};
|
|
2
|
+
use std::fmt::Write as _;
|
|
3
|
+
|
|
4
|
+
use crate::deps::detect::DetectedFile;
|
|
5
|
+
use crate::deps::model::{DependencyGraph, Ecosystem, PackageId};
|
|
6
|
+
use crate::deps::Inventory;
|
|
7
|
+
|
|
8
|
+
/// Load the policy at `root`, scan the tree, and emit a CycloneDX SBOM.
|
|
9
|
+
pub fn sbom(root: &std::path::Path) -> Result<Value, crate::deps::DepsError> {
|
|
10
|
+
let policy = crate::deps::run::load_policy(root)?;
|
|
11
|
+
let inv = crate::deps::scan(root, &policy)?;
|
|
12
|
+
warn_unsupported_ecosystems(&inv.detected_files);
|
|
13
|
+
Ok(to_cyclonedx(&inv))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/// Print one deduplicated stderr warning per detected ecosystem that
|
|
17
|
+
/// `to_cyclonedx` does not emit components for (currently Go and Cargo),
|
|
18
|
+
/// so `deps sbom` / `scan --sbom` don't silently produce an empty-looking
|
|
19
|
+
/// SBOM for those trees.
|
|
20
|
+
pub fn warn_unsupported_ecosystems(detected: &[DetectedFile]) {
|
|
21
|
+
let mut by_ecosystem: std::collections::BTreeMap<
|
|
22
|
+
&'static str,
|
|
23
|
+
std::collections::BTreeSet<String>,
|
|
24
|
+
> = std::collections::BTreeMap::new();
|
|
25
|
+
|
|
26
|
+
for f in detected {
|
|
27
|
+
let Some(label) = unsupported_ecosystem_label(f.ecosystem) else {
|
|
28
|
+
continue;
|
|
29
|
+
};
|
|
30
|
+
let file_name = f
|
|
31
|
+
.path
|
|
32
|
+
.file_name()
|
|
33
|
+
.map(|n| n.to_string_lossy().into_owned())
|
|
34
|
+
.unwrap_or_default();
|
|
35
|
+
by_ecosystem.entry(label).or_default().insert(file_name);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (label, files) in by_ecosystem {
|
|
39
|
+
let files: Vec<String> = files.into_iter().collect();
|
|
40
|
+
eprintln!(
|
|
41
|
+
"Warning: detected {} but {} is not yet included in SBOMs",
|
|
42
|
+
files.join(", "),
|
|
43
|
+
label
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
fn unsupported_ecosystem_label(ecosystem: Ecosystem) -> Option<&'static str> {
|
|
49
|
+
if crate::deps::ecosystems::is_graphed(ecosystem) {
|
|
50
|
+
return None;
|
|
51
|
+
}
|
|
52
|
+
Some(match ecosystem {
|
|
53
|
+
Ecosystem::Go => "Go",
|
|
54
|
+
Ecosystem::Cargo => "Cargo",
|
|
55
|
+
_ => "this ecosystem",
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
pub fn to_json(inv: &Inventory) -> Value {
|
|
60
|
+
inventory_to_json(inv)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
pub fn to_sarif(inv: &Inventory) -> Value {
|
|
64
|
+
let rules: Vec<Value> = inv
|
|
65
|
+
.findings
|
|
66
|
+
.iter()
|
|
67
|
+
.map(|f| {
|
|
68
|
+
json!({
|
|
69
|
+
"id": f.id,
|
|
70
|
+
"name": f.title,
|
|
71
|
+
"shortDescription": { "text": f.title },
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
.collect();
|
|
75
|
+
|
|
76
|
+
let results: Vec<Value> = inv
|
|
77
|
+
.findings
|
|
78
|
+
.iter()
|
|
79
|
+
.map(|f| {
|
|
80
|
+
json!({
|
|
81
|
+
"ruleId": f.id,
|
|
82
|
+
"level": severity_to_sarif(f.severity),
|
|
83
|
+
"message": { "text": f.recommendation },
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
.collect();
|
|
87
|
+
|
|
88
|
+
json!({
|
|
89
|
+
"version": "2.1.0",
|
|
90
|
+
"runs": [{
|
|
91
|
+
"tool": {
|
|
92
|
+
"driver": {
|
|
93
|
+
"name": "corgea-deps",
|
|
94
|
+
"rules": rules,
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
"results": results,
|
|
98
|
+
}]
|
|
99
|
+
})
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
fn severity_to_sarif(sev: crate::deps::model::Severity) -> &'static str {
|
|
103
|
+
use crate::deps::model::Severity;
|
|
104
|
+
match sev {
|
|
105
|
+
Severity::Critical | Severity::High => "error",
|
|
106
|
+
Severity::Medium => "warning",
|
|
107
|
+
Severity::Low | Severity::Info => "note",
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// A version is resolved when it names a concrete release — not empty, not
|
|
112
|
+
/// the `?` placeholder for lockfile-less manifests, not an unsubstituted
|
|
113
|
+
/// `${...}` Maven property. Unresolved versions (and the purls fabricated
|
|
114
|
+
/// from them) must be omitted, or the document fails CycloneDX 1.7 validation.
|
|
115
|
+
fn is_resolved_version(version: &str) -> bool {
|
|
116
|
+
!version.is_empty() && version != "?" && !version.contains("${")
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
pub fn to_cyclonedx(inv: &Inventory) -> Value {
|
|
120
|
+
let graph = &inv.graph;
|
|
121
|
+
// Deduplicate by bom-ref: multi-module trees list the same package once
|
|
122
|
+
// per manifest, but the schema sets uniqueItems on components.
|
|
123
|
+
let mut components: std::collections::BTreeMap<&str, Value> = std::collections::BTreeMap::new();
|
|
124
|
+
for n in graph.nodes.iter().filter(|n| *n.id() != PackageId::root()) {
|
|
125
|
+
let mut c = json!({
|
|
126
|
+
"type": "library",
|
|
127
|
+
"bom-ref": n.id().0,
|
|
128
|
+
"name": n.name(),
|
|
129
|
+
});
|
|
130
|
+
if let Some(v) = n.version().filter(|v| is_resolved_version(v)) {
|
|
131
|
+
c["version"] = json!(v);
|
|
132
|
+
c["purl"] = json!(n.id().0);
|
|
133
|
+
}
|
|
134
|
+
components.entry(&n.id().0).or_insert(c);
|
|
135
|
+
}
|
|
136
|
+
let components: Vec<Value> = components.into_values().collect();
|
|
137
|
+
|
|
138
|
+
let mut depends_on: std::collections::BTreeMap<&str, std::collections::BTreeSet<&str>> =
|
|
139
|
+
std::collections::BTreeMap::new();
|
|
140
|
+
for e in &graph.edges {
|
|
141
|
+
depends_on.entry(&e.from.0).or_default().insert(&e.to.0);
|
|
142
|
+
}
|
|
143
|
+
let deps: Vec<Value> = depends_on
|
|
144
|
+
.iter()
|
|
145
|
+
.map(|(from, tos)| {
|
|
146
|
+
json!({
|
|
147
|
+
"ref": from,
|
|
148
|
+
"dependsOn": tos,
|
|
149
|
+
})
|
|
150
|
+
})
|
|
151
|
+
.collect();
|
|
152
|
+
|
|
153
|
+
let root_name = std::fs::canonicalize(&inv.root)
|
|
154
|
+
.ok()
|
|
155
|
+
.and_then(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()))
|
|
156
|
+
.unwrap_or_else(|| "project".to_string());
|
|
157
|
+
|
|
158
|
+
json!({
|
|
159
|
+
"bomFormat": "CycloneDX",
|
|
160
|
+
"specVersion": "1.7",
|
|
161
|
+
"serialNumber": format!("urn:uuid:{}", uuid::Uuid::new_v4()),
|
|
162
|
+
"version": 1,
|
|
163
|
+
"metadata": {
|
|
164
|
+
"timestamp": chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
|
|
165
|
+
"tools": {
|
|
166
|
+
"components": [{
|
|
167
|
+
"type": "application",
|
|
168
|
+
"name": "corgea",
|
|
169
|
+
"version": env!("CARGO_PKG_VERSION"),
|
|
170
|
+
}]
|
|
171
|
+
},
|
|
172
|
+
"component": {
|
|
173
|
+
"type": "application",
|
|
174
|
+
"bom-ref": "root",
|
|
175
|
+
"name": root_name,
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
"components": components,
|
|
179
|
+
"dependencies": deps,
|
|
180
|
+
})
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
pub fn graph_nodes_json(graph: &DependencyGraph) -> Vec<Value> {
|
|
184
|
+
graph
|
|
185
|
+
.nodes
|
|
186
|
+
.iter()
|
|
187
|
+
.map(|n| {
|
|
188
|
+
json!({
|
|
189
|
+
"id": n.id().0,
|
|
190
|
+
"name": n.name(),
|
|
191
|
+
"version": n.version(),
|
|
192
|
+
"direct": n.is_direct(),
|
|
193
|
+
"scope": format!("{:?}", n.scope()),
|
|
194
|
+
"depth": n.depth(),
|
|
195
|
+
})
|
|
196
|
+
})
|
|
197
|
+
.collect()
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
pub fn inventory_to_json(inv: &Inventory) -> Value {
|
|
201
|
+
let nodes = graph_nodes_json(&inv.graph);
|
|
202
|
+
|
|
203
|
+
let findings: Vec<Value> = inv
|
|
204
|
+
.findings
|
|
205
|
+
.iter()
|
|
206
|
+
.map(|f| {
|
|
207
|
+
json!({
|
|
208
|
+
"id": f.id,
|
|
209
|
+
"severity": format!("{:?}", f.severity),
|
|
210
|
+
"title": f.title,
|
|
211
|
+
"package": f.package.as_ref().map(|p| p.0.clone()),
|
|
212
|
+
"reproducible": f.reproducible,
|
|
213
|
+
"recommendation": f.recommendation,
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
.collect();
|
|
217
|
+
|
|
218
|
+
json!({
|
|
219
|
+
"root": inv.root,
|
|
220
|
+
"nodes": nodes,
|
|
221
|
+
"findings": findings,
|
|
222
|
+
})
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
pub fn table_output(inv: &Inventory) -> String {
|
|
226
|
+
let mut out = String::new();
|
|
227
|
+
writeln!(&mut out, "Corgea dependency inventory\n").unwrap();
|
|
228
|
+
writeln!(
|
|
229
|
+
&mut out,
|
|
230
|
+
"Detected {} dependency file(s)",
|
|
231
|
+
inv.detected_files.len()
|
|
232
|
+
)
|
|
233
|
+
.unwrap();
|
|
234
|
+
writeln!(
|
|
235
|
+
&mut out,
|
|
236
|
+
"Inventory: {} packages, {} findings\n",
|
|
237
|
+
inv.graph.nodes.len(),
|
|
238
|
+
inv.findings.len()
|
|
239
|
+
)
|
|
240
|
+
.unwrap();
|
|
241
|
+
|
|
242
|
+
let mut by_sev: std::collections::BTreeMap<String, usize> = std::collections::BTreeMap::new();
|
|
243
|
+
for f in &inv.findings {
|
|
244
|
+
*by_sev.entry(format!("{:?}", f.severity)).or_default() += 1;
|
|
245
|
+
}
|
|
246
|
+
for (sev, count) in by_sev {
|
|
247
|
+
writeln!(&mut out, " {sev}: {count}").unwrap();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
for f in &inv.findings {
|
|
251
|
+
let pkg = f.package.as_ref().map(|p| p.name()).unwrap_or("project");
|
|
252
|
+
writeln!(&mut out, "\n {} {:?} {}", f.id, f.severity, f.title).unwrap();
|
|
253
|
+
writeln!(&mut out, " package: {pkg}").unwrap();
|
|
254
|
+
writeln!(&mut out, " {}", f.recommendation).unwrap();
|
|
255
|
+
}
|
|
256
|
+
out
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
pub fn print_table(inv: &Inventory) {
|
|
260
|
+
print!("{}", table_output(inv));
|
|
261
|
+
}
|
|
@@ -9,7 +9,7 @@ use serde_json::{json, Value};
|
|
|
9
9
|
use crate::deps::findings::Finding;
|
|
10
10
|
use crate::deps::model::{DependencyNode, Severity};
|
|
11
11
|
use crate::deps::policy::Policy;
|
|
12
|
-
use crate::deps::report::{graph_nodes_json, table_output,
|
|
12
|
+
use crate::deps::report::{graph_nodes_json, table_output, to_json, to_sarif};
|
|
13
13
|
use crate::deps::{scan, DepsError};
|
|
14
14
|
|
|
15
15
|
#[derive(Subcommand, Debug, Clone)]
|
|
@@ -236,13 +236,10 @@ fn run_inner(sub: DepsSubcommand) -> Result<u8, DepsError> {
|
|
|
236
236
|
Ok(0)
|
|
237
237
|
}
|
|
238
238
|
DepsSubcommand::Sbom { format, path, out } => {
|
|
239
|
-
let root = Path::new(&path);
|
|
240
|
-
let policy = load_policy(root)?;
|
|
241
|
-
let inv = scan(root, &policy)?;
|
|
242
239
|
if format != "cyclonedx" {
|
|
243
240
|
return Err(DepsError(format!("unsupported SBOM format: {format}")));
|
|
244
241
|
}
|
|
245
|
-
let sbom =
|
|
242
|
+
let sbom = crate::deps::report::sbom(Path::new(&path))?.to_string();
|
|
246
243
|
if let Some(out_path) = out {
|
|
247
244
|
std::fs::write(&out_path, sbom)
|
|
248
245
|
.map_err(|e| DepsError(format!("write sbom: {e}")))?;
|
|
@@ -728,7 +725,7 @@ fn shell_word(value: &str) -> String {
|
|
|
728
725
|
format!("'{}'", value.replace('\'', "'\\''"))
|
|
729
726
|
}
|
|
730
727
|
|
|
731
|
-
fn load_policy(root: &Path) -> Result<Policy, DepsError> {
|
|
728
|
+
pub(crate) fn load_policy(root: &Path) -> Result<Policy, DepsError> {
|
|
732
729
|
let policy_path = root.join(".corgea").join("deps.yml");
|
|
733
730
|
if !policy_path.exists() {
|
|
734
731
|
return Ok(Policy::default());
|