@cdxgen/cdxgen 0.0.1 → 13.0.0
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.
- package/README.md +921 -87
- package/bin/audit.js +239 -0
- package/bin/cdxgen.js +2324 -0
- package/bin/convert.js +213 -0
- package/bin/evinse.js +353 -0
- package/bin/hbom.js +495 -0
- package/bin/repl.js +2224 -0
- package/bin/sign.js +195 -0
- package/bin/tracebom.js +295 -0
- package/bin/validate.js +268 -0
- package/bin/verify.js +189 -0
- package/data/README.md +307 -0
- package/data/bom-1.4.schema.json +1605 -0
- package/data/bom-1.5.schema.json +3660 -0
- package/data/bom-1.6.schema.json +5511 -0
- package/data/bom-1.7.schema.json +6700 -0
- package/data/cbomosdb-queries.json +68 -0
- package/data/component-tags.json +339 -0
- package/data/container-knowledge-index.json +125 -0
- package/data/cosdb-queries.json +17 -0
- package/data/crypto-oid.json +1998 -0
- package/data/cryptography-defs.json +3793 -0
- package/data/cryptography-defs.schema.json +576 -0
- package/data/cyclonedx-2.0-bundled.schema.json +7182 -0
- package/data/frameworks-list.json +219 -0
- package/data/glibc-stdlib.json +117 -0
- package/data/gtfobins-index.json +6296 -0
- package/data/helpers/init.gradle +71 -0
- package/data/jsf-0.82.schema.json +211 -0
- package/data/known-licenses.json +82 -0
- package/data/lic-mapping.json +338 -0
- package/data/license-aliases.json +6624 -0
- package/data/license-db.json +39059 -0
- package/data/license-deprecations.json +302 -0
- package/data/lolbas-index.json +150 -0
- package/data/predictive-audit-allowlist.json +11 -0
- package/data/pypi-pkg-aliases.json +1199 -0
- package/data/python-stdlib.json +308 -0
- package/data/queries-darwin.json +193 -0
- package/data/queries-win.json +257 -0
- package/data/queries.json +268 -0
- package/data/ruby-known-modules.json +281 -0
- package/data/rules/ai-agent-governance.yaml +323 -0
- package/data/rules/ai-bom.yaml +268 -0
- package/data/rules/ai-oversight.yaml +186 -0
- package/data/rules/ai-provenance.yaml +92 -0
- package/data/rules/asar-archives.yaml +150 -0
- package/data/rules/cbom-security.yaml +165 -0
- package/data/rules/chrome-extensions.yaml +248 -0
- package/data/rules/ci-permissions.yaml +802 -0
- package/data/rules/container-risk.yaml +353 -0
- package/data/rules/dependency-sources.yaml +236 -0
- package/data/rules/golem-go.yaml +229 -0
- package/data/rules/hbom-compliance.yaml +325 -0
- package/data/rules/hbom-performance.yaml +307 -0
- package/data/rules/hbom-security.yaml +248 -0
- package/data/rules/host-topology.yaml +165 -0
- package/data/rules/mcp-servers.yaml +388 -0
- package/data/rules/obom-runtime.yaml +1776 -0
- package/data/rules/package-integrity.yaml +364 -0
- package/data/rules/rootfs-hardening.yaml +179 -0
- package/data/rules/vscode-extensions.yaml +237 -0
- package/data/spdx-export.schema.json +6794 -0
- package/data/spdx-license-list.json +7449 -0
- package/data/spdx-licenses.json +759 -0
- package/data/spdx.schema.json +819 -0
- package/data/templates/README.md +3 -0
- package/data/templates/asvs-4.0.3.cdx.json +2943 -0
- package/data/templates/asvs-5.0.cdx.json +3046 -0
- package/data/templates/bsimm-v13.cdx.json +1008 -0
- package/data/templates/masvs-2.0.0.cdx.json +225 -0
- package/data/templates/nist-ssdf-1.1.cdx.json +828 -0
- package/data/templates/pcissc-secure-slc-1.1.cdx.json +1027 -0
- package/data/templates/scvs-1.0.0.cdx.json +813 -0
- package/data/templates/ssaf-DRAFT-2023-11.cdx.json +127 -0
- package/data/vendor-alias.json +41967 -0
- package/data/wrapdb-releases.json +4227 -0
- package/index.cjs +15 -0
- package/lib/audit/index.js +2800 -0
- package/lib/audit/progress.js +114 -0
- package/lib/audit/reporters.js +937 -0
- package/lib/audit/scoring.js +560 -0
- package/lib/audit/targets.js +798 -0
- package/lib/cli/bomAssembly.js +1833 -0
- package/lib/cli/cliOptions.js +513 -0
- package/lib/cli/index.js +2759 -0
- package/lib/cli/jsBom.js +2087 -0
- package/lib/cli/jvmBom.js +1858 -0
- package/lib/cli/managedBom.js +2083 -0
- package/lib/cli/nativeBom.js +2125 -0
- package/lib/core/activity.js +1087 -0
- package/lib/core/env.js +719 -0
- package/lib/core/fs.js +1058 -0
- package/lib/core/httpClient.js +730 -0
- package/lib/core/logger.js +142 -0
- package/lib/core/parallel.js +323 -0
- package/lib/core/paths.js +70 -0
- package/lib/core/projectConfig.js +122 -0
- package/lib/core/propertySanitizer.js +174 -0
- package/lib/core/spdx.js +5 -0
- package/lib/core/state.js +59 -0
- package/lib/core/text.js +207 -0
- package/lib/core/ui.js +876 -0
- package/lib/core/urls.js +84 -0
- package/lib/ecosystems/asarutils.js +1636 -0
- package/lib/ecosystems/bunutils.js +402 -0
- package/lib/ecosystems/caxa.js +77 -0
- package/lib/ecosystems/chromextutils.js +1174 -0
- package/lib/ecosystems/cmakeResolver.js +587 -0
- package/lib/ecosystems/cppEvidence.js +305 -0
- package/lib/ecosystems/denoutils.js +1094 -0
- package/lib/ecosystems/dotnetutils.js +132 -0
- package/lib/ecosystems/ecosystems.js +2772 -0
- package/lib/ecosystems/golem.js +1807 -0
- package/lib/ecosystems/gradleutils.js +1048 -0
- package/lib/ecosystems/jsEvidence.js +317 -0
- package/lib/ecosystems/npmutils.js +586 -0
- package/lib/ecosystems/parsers-bazel.js +875 -0
- package/lib/ecosystems/parsers-dotnet.js +1430 -0
- package/lib/ecosystems/parsers-gleam.js +302 -0
- package/lib/ecosystems/parsers-go.js +818 -0
- package/lib/ecosystems/parsers-js.js +4222 -0
- package/lib/ecosystems/parsers-jvm.js +948 -0
- package/lib/ecosystems/parsers-misc.js +3191 -0
- package/lib/ecosystems/parsers-python.js +2070 -0
- package/lib/ecosystems/parsers-rust.js +1294 -0
- package/lib/ecosystems/parsers-zig.js +861 -0
- package/lib/ecosystems/pep770.js +341 -0
- package/lib/ecosystems/pylockutils.js +427 -0
- package/lib/ecosystems/pythonutils.js +1281 -0
- package/lib/ecosystems/registryProvenance.js +1003 -0
- package/lib/ecosystems/remote/huggingface.js +1343 -0
- package/lib/ecosystems/rubyutils.js +1629 -0
- package/lib/ecosystems/rusi.js +820 -0
- package/lib/ecosystems/sbtutils.js +972 -0
- package/lib/ecosystems/tea.js +607 -0
- package/lib/ecosystems/utils.js +575 -0
- package/lib/ecosystems/zigResolver.js +410 -0
- package/lib/evinser/db.js +145 -0
- package/lib/evinser/evinser.js +2362 -0
- package/lib/evinser/scalasem.js +65 -0
- package/lib/evinser/swiftsem.js +767 -0
- package/lib/helpers/annotationFormatter.js +84 -0
- package/lib/helpers/bomSigner.js +312 -0
- package/lib/helpers/exportUtils.js +160 -0
- package/lib/helpers/remote/dependency-track.js +117 -0
- package/lib/helpers/versutils.js +202 -0
- package/lib/helpers/vsixutils.js +1069 -0
- package/lib/inventory/agentFormulationParser.js +322 -0
- package/lib/inventory/aiCollector.js +2140 -0
- package/lib/inventory/aiInventory.js +414 -0
- package/lib/inventory/aiModelVariants.js +121 -0
- package/lib/inventory/aiOversightCollector.js +661 -0
- package/lib/inventory/aiProvenanceCollector.js +822 -0
- package/lib/inventory/analyzer.js +6089 -0
- package/lib/inventory/analyzerScope.js +807 -0
- package/lib/inventory/astWorker.js +20 -0
- package/lib/inventory/atomUtils.js +1092 -0
- package/lib/inventory/auditCategories.js +192 -0
- package/lib/inventory/bomUtils.js +369 -0
- package/lib/inventory/cacheCommands.js +182 -0
- package/lib/inventory/cacheDir.js +104 -0
- package/lib/inventory/cbomutils.js +590 -0
- package/lib/inventory/cdxrs.js +434 -0
- package/lib/inventory/ciParsers/azurePipelines.js +295 -0
- package/lib/inventory/ciParsers/circleCi.js +286 -0
- package/lib/inventory/ciParsers/common.js +24 -0
- package/lib/inventory/ciParsers/githubActions.js +2434 -0
- package/lib/inventory/ciParsers/gitlabCi.js +213 -0
- package/lib/inventory/ciParsers/jenkins.js +180 -0
- package/lib/inventory/citations.js +234 -0
- package/lib/inventory/communityAiConfigParser.js +756 -0
- package/lib/inventory/containerRisk.js +204 -0
- package/lib/inventory/cryptoAlgorithmFamily.js +258 -0
- package/lib/inventory/deps.js +807 -0
- package/lib/inventory/depsUtils.js +588 -0
- package/lib/inventory/display.js +2128 -0
- package/lib/inventory/dosai.js +601 -0
- package/lib/inventory/dosaiParsers.js +154 -0
- package/lib/inventory/dynamic.js +145 -0
- package/lib/inventory/envcontext.js +1789 -0
- package/lib/inventory/evidenceUtils.js +774 -0
- package/lib/inventory/fetchBatch.js +779 -0
- package/lib/inventory/fetchRate.js +255 -0
- package/lib/inventory/forgeEnricher.js +300 -0
- package/lib/inventory/formulationParsers.js +422 -0
- package/lib/inventory/gtfobins.js +339 -0
- package/lib/inventory/hbom.js +719 -0
- package/lib/inventory/hbomAnalysis.js +310 -0
- package/lib/inventory/hbomLoader.js +35 -0
- package/lib/inventory/hostTopology.js +835 -0
- package/lib/inventory/inventoryStats.js +100 -0
- package/lib/inventory/jsonLike.js +121 -0
- package/lib/inventory/licenseEnhancer.js +918 -0
- package/lib/inventory/lolbas.js +285 -0
- package/lib/inventory/mcp.js +253 -0
- package/lib/inventory/mcpConfigParser.js +683 -0
- package/lib/inventory/mcpDiscovery.js +112 -0
- package/lib/inventory/mcpPinning.js +209 -0
- package/lib/inventory/osPackageResolver.js +493 -0
- package/lib/inventory/osinfo.js +277 -0
- package/lib/inventory/osqueryTransform.js +274 -0
- package/lib/inventory/plugins.js +466 -0
- package/lib/inventory/protobom.js +231 -0
- package/lib/inventory/protobomLoader.js +43 -0
- package/lib/inventory/provenanceUtils.js +224 -0
- package/lib/inventory/purl.js +749 -0
- package/lib/inventory/rustFormulationParser.js +339 -0
- package/lib/inventory/sbomDocument.js +340 -0
- package/lib/inventory/source.js +1488 -0
- package/lib/inventory/spdx.js +361 -0
- package/lib/inventory/spdxExpression.js +512 -0
- package/lib/inventory/spdxUtils.js +107 -0
- package/lib/inventory/table.js +263 -0
- package/lib/inventory/traceRunner.js +439 -0
- package/lib/inventory/unicodeScan.js +147 -0
- package/lib/managers/binary.js +3307 -0
- package/lib/managers/containerutils.js +125 -0
- package/lib/managers/docker.js +2145 -0
- package/lib/managers/dockerConnection.js +162 -0
- package/lib/managers/oci.js +704 -0
- package/lib/managers/piptree.js +267 -0
- package/lib/parsers/cmakeCache.js +199 -0
- package/lib/parsers/gguf.js +492 -0
- package/lib/parsers/gitAiNotes.js +233 -0
- package/lib/parsers/gitmodules.js +239 -0
- package/lib/parsers/htmlExtract.js +610 -0
- package/lib/parsers/huggingfaceManifest.js +551 -0
- package/lib/parsers/huggingfaceUtils.js +506 -0
- package/lib/parsers/iri.js +618 -0
- package/lib/parsers/npmrc.js +99 -0
- package/lib/parsers/ollama.js +101 -0
- package/lib/parsers/xml.js +841 -0
- package/lib/server/openapi.yaml +433 -0
- package/lib/server/server.js +616 -0
- package/lib/stages/postgen/annotator.js +977 -0
- package/lib/stages/postgen/auditBom.js +468 -0
- package/lib/stages/postgen/postgen.js +1239 -0
- package/lib/stages/postgen/ruleEngine.js +731 -0
- package/lib/stages/postgen/sortBom.js +195 -0
- package/lib/stages/postgen/spdxConverter.js +796 -0
- package/lib/stages/postgen/specVersionCompat.js +703 -0
- package/lib/stages/pregen/envAudit.js +388 -0
- package/lib/stages/pregen/pregen.js +637 -0
- package/lib/third-party/README.md +9 -0
- package/lib/third-party/arborist/CHANGELOG.md +1109 -0
- package/lib/third-party/arborist/LICENSE.md +20 -0
- package/lib/third-party/arborist/README.md +315 -0
- package/lib/third-party/arborist/lib/arborist/index.js +258 -0
- package/lib/third-party/arborist/lib/arborist/load-actual.js +605 -0
- package/lib/third-party/arborist/lib/arborist/load-virtual.js +300 -0
- package/lib/third-party/arborist/lib/bin-links.js +120 -0
- package/lib/third-party/arborist/lib/calc-dep-flags.js +130 -0
- package/lib/third-party/arborist/lib/case-insensitive-map.js +51 -0
- package/lib/third-party/arborist/lib/consistent-resolve.js +42 -0
- package/lib/third-party/arborist/lib/debug.js +50 -0
- package/lib/third-party/arborist/lib/dep-valid.js +190 -0
- package/lib/third-party/arborist/lib/edge.js +417 -0
- package/lib/third-party/arborist/lib/from-path.js +36 -0
- package/lib/third-party/arborist/lib/gather-dep-set.js +43 -0
- package/lib/third-party/arborist/lib/index.js +22 -0
- package/lib/third-party/arborist/lib/inventory.js +138 -0
- package/lib/third-party/arborist/lib/link.js +169 -0
- package/lib/third-party/arborist/lib/node.js +1673 -0
- package/lib/third-party/arborist/lib/npm-extension.js +45 -0
- package/lib/third-party/arborist/lib/override-resolves.js +11 -0
- package/lib/third-party/arborist/lib/override-set.js +319 -0
- package/lib/third-party/arborist/lib/package-extensions.js +289 -0
- package/lib/third-party/arborist/lib/printable.js +210 -0
- package/lib/third-party/arborist/lib/proc-log.js +30 -0
- package/lib/third-party/arborist/lib/realpath.js +100 -0
- package/lib/third-party/arborist/lib/relpath.js +4 -0
- package/lib/third-party/arborist/lib/shrinkwrap.js +1291 -0
- package/lib/third-party/arborist/lib/spec-from-lock.js +34 -0
- package/lib/third-party/arborist/lib/tree-check.js +165 -0
- package/lib/third-party/arborist/lib/validate-npm-package-name.js +107 -0
- package/lib/third-party/arborist/lib/version-from-tgz.js +51 -0
- package/lib/third-party/arborist/lib/yarn-lock.js +397 -0
- package/lib/validator/bomValidator.js +985 -0
- package/lib/validator/complianceEngine.js +241 -0
- package/lib/validator/complianceRules.js +1680 -0
- package/lib/validator/generated/runtime.mjs +17 -0
- package/lib/validator/generated/validate-1.6.mjs +21258 -0
- package/lib/validator/generated/validate-1.7.mjs +24803 -0
- package/lib/validator/generated/validate-2.0.mjs +22931 -0
- package/lib/validator/index.js +246 -0
- package/lib/validator/reporters/annotations.js +142 -0
- package/lib/validator/reporters/console.js +160 -0
- package/lib/validator/reporters/index.js +41 -0
- package/lib/validator/reporters/json.js +37 -0
- package/lib/validator/reporters/sarif.js +184 -0
- package/package.json +209 -26
- package/types/bin/audit.d.ts +3 -0
- package/types/bin/audit.d.ts.map +1 -0
- package/types/bin/cdxgen.d.ts +3 -0
- package/types/bin/cdxgen.d.ts.map +1 -0
- package/types/bin/convert.d.ts +3 -0
- package/types/bin/convert.d.ts.map +1 -0
- package/types/bin/evinse.d.ts +3 -0
- package/types/bin/evinse.d.ts.map +1 -0
- package/types/bin/hbom.d.ts +3 -0
- package/types/bin/hbom.d.ts.map +1 -0
- package/types/bin/repl.d.ts +3 -0
- package/types/bin/repl.d.ts.map +1 -0
- package/types/bin/sign.d.ts +3 -0
- package/types/bin/sign.d.ts.map +1 -0
- package/types/bin/tracebom.d.ts +3 -0
- package/types/bin/tracebom.d.ts.map +1 -0
- package/types/bin/validate.d.ts +14 -0
- package/types/bin/validate.d.ts.map +1 -0
- package/types/bin/verify.d.ts +3 -0
- package/types/bin/verify.d.ts.map +1 -0
- package/types/lib/audit/index.d.ts +153 -0
- package/types/lib/audit/index.d.ts.map +1 -0
- package/types/lib/audit/progress.d.ts +31 -0
- package/types/lib/audit/progress.d.ts.map +1 -0
- package/types/lib/audit/reporters.d.ts +62 -0
- package/types/lib/audit/reporters.d.ts.map +1 -0
- package/types/lib/audit/scoring.d.ts +36 -0
- package/types/lib/audit/scoring.d.ts.map +1 -0
- package/types/lib/audit/targets.d.ts +77 -0
- package/types/lib/audit/targets.d.ts.map +1 -0
- package/types/lib/cli/bomAssembly.d.ts +315 -0
- package/types/lib/cli/bomAssembly.d.ts.map +1 -0
- package/types/lib/cli/cliOptions.d.ts +156 -0
- package/types/lib/cli/cliOptions.d.ts.map +1 -0
- package/types/lib/cli/index.d.ts +107 -0
- package/types/lib/cli/index.d.ts.map +1 -0
- package/types/lib/cli/jsBom.d.ts +94 -0
- package/types/lib/cli/jsBom.d.ts.map +1 -0
- package/types/lib/cli/jvmBom.d.ts +54 -0
- package/types/lib/cli/jvmBom.d.ts.map +1 -0
- package/types/lib/cli/managedBom.d.ts +105 -0
- package/types/lib/cli/managedBom.d.ts.map +1 -0
- package/types/lib/cli/nativeBom.d.ts +143 -0
- package/types/lib/cli/nativeBom.d.ts.map +1 -0
- package/types/lib/core/activity.d.ts +178 -0
- package/types/lib/core/activity.d.ts.map +1 -0
- package/types/lib/core/env.d.ts +192 -0
- package/types/lib/core/env.d.ts.map +1 -0
- package/types/lib/core/fs.d.ts +158 -0
- package/types/lib/core/fs.d.ts.map +1 -0
- package/types/lib/core/httpClient.d.ts +149 -0
- package/types/lib/core/httpClient.d.ts.map +1 -0
- package/types/lib/core/logger.d.ts +28 -0
- package/types/lib/core/logger.d.ts.map +1 -0
- package/types/lib/core/parallel.d.ts +95 -0
- package/types/lib/core/parallel.d.ts.map +1 -0
- package/types/lib/core/paths.d.ts +36 -0
- package/types/lib/core/paths.d.ts.map +1 -0
- package/types/lib/core/projectConfig.d.ts +23 -0
- package/types/lib/core/projectConfig.d.ts.map +1 -0
- package/types/lib/core/propertySanitizer.d.ts +23 -0
- package/types/lib/core/propertySanitizer.d.ts.map +1 -0
- package/types/lib/core/spdx.d.ts +5 -0
- package/types/lib/core/spdx.d.ts.map +1 -0
- package/types/lib/core/state.d.ts +20 -0
- package/types/lib/core/state.d.ts.map +1 -0
- package/types/lib/core/text.d.ts +78 -0
- package/types/lib/core/text.d.ts.map +1 -0
- package/types/lib/core/ui.d.ts +87 -0
- package/types/lib/core/ui.d.ts.map +1 -0
- package/types/lib/core/urls.d.ts +43 -0
- package/types/lib/core/urls.d.ts.map +1 -0
- package/types/lib/ecosystems/asarutils.d.ts +71 -0
- package/types/lib/ecosystems/asarutils.d.ts.map +1 -0
- package/types/lib/ecosystems/bunutils.d.ts +38 -0
- package/types/lib/ecosystems/bunutils.d.ts.map +1 -0
- package/types/lib/ecosystems/caxa.d.ts +8 -0
- package/types/lib/ecosystems/caxa.d.ts.map +1 -0
- package/types/lib/ecosystems/chromextutils.d.ts +97 -0
- package/types/lib/ecosystems/chromextutils.d.ts.map +1 -0
- package/types/lib/ecosystems/cmakeResolver.d.ts +176 -0
- package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -0
- package/types/lib/ecosystems/cppEvidence.d.ts +35 -0
- package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -0
- package/types/lib/ecosystems/denoutils.d.ts +76 -0
- package/types/lib/ecosystems/denoutils.d.ts.map +1 -0
- package/types/lib/ecosystems/dotnetutils.d.ts +45 -0
- package/types/lib/ecosystems/dotnetutils.d.ts.map +1 -0
- package/types/lib/ecosystems/ecosystems.d.ts +279 -0
- package/types/lib/ecosystems/ecosystems.d.ts.map +1 -0
- package/types/lib/ecosystems/golem.d.ts +60 -0
- package/types/lib/ecosystems/golem.d.ts.map +1 -0
- package/types/lib/ecosystems/gradleutils.d.ts +139 -0
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -0
- package/types/lib/ecosystems/jsEvidence.d.ts +17 -0
- package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -0
- package/types/lib/ecosystems/npmutils.d.ts +183 -0
- package/types/lib/ecosystems/npmutils.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-bazel.d.ts +36 -0
- package/types/lib/ecosystems/parsers-bazel.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-dotnet.d.ts +116 -0
- package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-gleam.d.ts +33 -0
- package/types/lib/ecosystems/parsers-gleam.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-go.d.ts +92 -0
- package/types/lib/ecosystems/parsers-go.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-js.d.ts +242 -0
- package/types/lib/ecosystems/parsers-js.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-jvm.d.ts +100 -0
- package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-misc.d.ts +450 -0
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-python.d.ts +145 -0
- package/types/lib/ecosystems/parsers-python.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-rust.d.ts +56 -0
- package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-zig.d.ts +83 -0
- package/types/lib/ecosystems/parsers-zig.d.ts.map +1 -0
- package/types/lib/ecosystems/pep770.d.ts +38 -0
- package/types/lib/ecosystems/pep770.d.ts.map +1 -0
- package/types/lib/ecosystems/pylockutils.d.ts +68 -0
- package/types/lib/ecosystems/pylockutils.d.ts.map +1 -0
- package/types/lib/ecosystems/pythonutils.d.ts +70 -0
- package/types/lib/ecosystems/pythonutils.d.ts.map +1 -0
- package/types/lib/ecosystems/registryProvenance.d.ts +26 -0
- package/types/lib/ecosystems/registryProvenance.d.ts.map +1 -0
- package/types/lib/ecosystems/remote/huggingface.d.ts +31 -0
- package/types/lib/ecosystems/remote/huggingface.d.ts.map +1 -0
- package/types/lib/ecosystems/rubyutils.d.ts +182 -0
- package/types/lib/ecosystems/rubyutils.d.ts.map +1 -0
- package/types/lib/ecosystems/rusi.d.ts +40 -0
- package/types/lib/ecosystems/rusi.d.ts.map +1 -0
- package/types/lib/ecosystems/sbtutils.d.ts +179 -0
- package/types/lib/ecosystems/sbtutils.d.ts.map +1 -0
- package/types/lib/ecosystems/tea.d.ts +180 -0
- package/types/lib/ecosystems/tea.d.ts.map +1 -0
- package/types/lib/ecosystems/utils.d.ts +41 -0
- package/types/lib/ecosystems/utils.d.ts.map +1 -0
- package/types/lib/ecosystems/zigResolver.d.ts +70 -0
- package/types/lib/ecosystems/zigResolver.d.ts.map +1 -0
- package/types/lib/evinser/db.d.ts +17 -0
- package/types/lib/evinser/db.d.ts.map +1 -0
- package/types/lib/evinser/evinser.d.ts +354 -0
- package/types/lib/evinser/evinser.d.ts.map +1 -0
- package/types/lib/evinser/scalasem.d.ts +15 -0
- package/types/lib/evinser/scalasem.d.ts.map +1 -0
- package/types/lib/evinser/swiftsem.d.ts +103 -0
- package/types/lib/evinser/swiftsem.d.ts.map +1 -0
- package/types/lib/helpers/annotationFormatter.d.ts +23 -0
- package/types/lib/helpers/annotationFormatter.d.ts.map +1 -0
- package/types/lib/helpers/bomSigner.d.ts +28 -0
- package/types/lib/helpers/bomSigner.d.ts.map +1 -0
- package/types/lib/helpers/exportUtils.d.ts +52 -0
- package/types/lib/helpers/exportUtils.d.ts.map +1 -0
- package/types/lib/helpers/remote/dependency-track.d.ts +23 -0
- package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -0
- package/types/lib/helpers/versutils.d.ts +8 -0
- package/types/lib/helpers/versutils.d.ts.map +1 -0
- package/types/lib/helpers/vsixutils.d.ts +130 -0
- package/types/lib/helpers/vsixutils.d.ts.map +1 -0
- package/types/lib/inventory/agentFormulationParser.d.ts +21 -0
- package/types/lib/inventory/agentFormulationParser.d.ts.map +1 -0
- package/types/lib/inventory/aiCollector.d.ts +61 -0
- package/types/lib/inventory/aiCollector.d.ts.map +1 -0
- package/types/lib/inventory/aiInventory.d.ts +103 -0
- package/types/lib/inventory/aiInventory.d.ts.map +1 -0
- package/types/lib/inventory/aiModelVariants.d.ts +31 -0
- package/types/lib/inventory/aiModelVariants.d.ts.map +1 -0
- package/types/lib/inventory/aiOversightCollector.d.ts +28 -0
- package/types/lib/inventory/aiOversightCollector.d.ts.map +1 -0
- package/types/lib/inventory/aiProvenanceCollector.d.ts +13 -0
- package/types/lib/inventory/aiProvenanceCollector.d.ts.map +1 -0
- package/types/lib/inventory/analyzer.d.ts +157 -0
- package/types/lib/inventory/analyzer.d.ts.map +1 -0
- package/types/lib/inventory/analyzerScope.d.ts +106 -0
- package/types/lib/inventory/analyzerScope.d.ts.map +1 -0
- package/types/lib/inventory/astWorker.d.ts +9 -0
- package/types/lib/inventory/astWorker.d.ts.map +1 -0
- package/types/lib/inventory/atomUtils.d.ts +159 -0
- package/types/lib/inventory/atomUtils.d.ts.map +1 -0
- package/types/lib/inventory/auditCategories.d.ts +80 -0
- package/types/lib/inventory/auditCategories.d.ts.map +1 -0
- package/types/lib/inventory/bomUtils.d.ts +145 -0
- package/types/lib/inventory/bomUtils.d.ts.map +1 -0
- package/types/lib/inventory/cacheCommands.d.ts +28 -0
- package/types/lib/inventory/cacheCommands.d.ts.map +1 -0
- package/types/lib/inventory/cacheDir.d.ts +55 -0
- package/types/lib/inventory/cacheDir.d.ts.map +1 -0
- package/types/lib/inventory/cbomutils.d.ts +37 -0
- package/types/lib/inventory/cbomutils.d.ts.map +1 -0
- package/types/lib/inventory/cdxrs.d.ts +90 -0
- package/types/lib/inventory/cdxrs.d.ts.map +1 -0
- package/types/lib/inventory/ciParsers/azurePipelines.d.ts +27 -0
- package/types/lib/inventory/ciParsers/azurePipelines.d.ts.map +1 -0
- package/types/lib/inventory/ciParsers/circleCi.d.ts +27 -0
- package/types/lib/inventory/ciParsers/circleCi.d.ts.map +1 -0
- package/types/lib/inventory/ciParsers/common.d.ts +11 -0
- package/types/lib/inventory/ciParsers/common.d.ts.map +1 -0
- package/types/lib/inventory/ciParsers/githubActions.d.ts +38 -0
- package/types/lib/inventory/ciParsers/githubActions.d.ts.map +1 -0
- package/types/lib/inventory/ciParsers/gitlabCi.d.ts +26 -0
- package/types/lib/inventory/ciParsers/gitlabCi.d.ts.map +1 -0
- package/types/lib/inventory/ciParsers/jenkins.d.ts +26 -0
- package/types/lib/inventory/ciParsers/jenkins.d.ts.map +1 -0
- package/types/lib/inventory/citations.d.ts +90 -0
- package/types/lib/inventory/citations.d.ts.map +1 -0
- package/types/lib/inventory/communityAiConfigParser.d.ts +14 -0
- package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -0
- package/types/lib/inventory/containerRisk.d.ts +20 -0
- package/types/lib/inventory/containerRisk.d.ts.map +1 -0
- package/types/lib/inventory/cryptoAlgorithmFamily.d.ts +37 -0
- package/types/lib/inventory/cryptoAlgorithmFamily.d.ts.map +1 -0
- package/types/lib/inventory/deps.d.ts +140 -0
- package/types/lib/inventory/deps.d.ts.map +1 -0
- package/types/lib/inventory/depsUtils.d.ts +74 -0
- package/types/lib/inventory/depsUtils.d.ts.map +1 -0
- package/types/lib/inventory/display.d.ts +227 -0
- package/types/lib/inventory/display.d.ts.map +1 -0
- package/types/lib/inventory/dosai.d.ts +130 -0
- package/types/lib/inventory/dosai.d.ts.map +1 -0
- package/types/lib/inventory/dosaiParsers.d.ts +59 -0
- package/types/lib/inventory/dosaiParsers.d.ts.map +1 -0
- package/types/lib/inventory/dynamic.d.ts +25 -0
- package/types/lib/inventory/dynamic.d.ts.map +1 -0
- package/types/lib/inventory/envcontext.d.ts +377 -0
- package/types/lib/inventory/envcontext.d.ts.map +1 -0
- package/types/lib/inventory/evidenceUtils.d.ts +74 -0
- package/types/lib/inventory/evidenceUtils.d.ts.map +1 -0
- package/types/lib/inventory/fetchBatch.d.ts +127 -0
- package/types/lib/inventory/fetchBatch.d.ts.map +1 -0
- package/types/lib/inventory/fetchRate.d.ts +118 -0
- package/types/lib/inventory/fetchRate.d.ts.map +1 -0
- package/types/lib/inventory/forgeEnricher.d.ts +31 -0
- package/types/lib/inventory/forgeEnricher.d.ts.map +1 -0
- package/types/lib/inventory/formulationParsers.d.ts +50 -0
- package/types/lib/inventory/formulationParsers.d.ts.map +1 -0
- package/types/lib/inventory/gtfobins.d.ts +27 -0
- package/types/lib/inventory/gtfobins.d.ts.map +1 -0
- package/types/lib/inventory/hbom.d.ts +58 -0
- package/types/lib/inventory/hbom.d.ts.map +1 -0
- package/types/lib/inventory/hbomAnalysis.d.ts +56 -0
- package/types/lib/inventory/hbomAnalysis.d.ts.map +1 -0
- package/types/lib/inventory/hbomLoader.d.ts +7 -0
- package/types/lib/inventory/hbomLoader.d.ts.map +1 -0
- package/types/lib/inventory/hostTopology.d.ts +47 -0
- package/types/lib/inventory/hostTopology.d.ts.map +1 -0
- package/types/lib/inventory/inventoryStats.d.ts +42 -0
- package/types/lib/inventory/inventoryStats.d.ts.map +1 -0
- package/types/lib/inventory/jsonLike.d.ts +23 -0
- package/types/lib/inventory/jsonLike.d.ts.map +1 -0
- package/types/lib/inventory/licenseEnhancer.d.ts +70 -0
- package/types/lib/inventory/licenseEnhancer.d.ts.map +1 -0
- package/types/lib/inventory/lolbas.d.ts +16 -0
- package/types/lib/inventory/lolbas.d.ts.map +1 -0
- package/types/lib/inventory/mcp.d.ts +29 -0
- package/types/lib/inventory/mcp.d.ts.map +1 -0
- package/types/lib/inventory/mcpConfigParser.d.ts +20 -0
- package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -0
- package/types/lib/inventory/mcpDiscovery.d.ts +33 -0
- package/types/lib/inventory/mcpDiscovery.d.ts.map +1 -0
- package/types/lib/inventory/mcpPinning.d.ts +11 -0
- package/types/lib/inventory/mcpPinning.d.ts.map +1 -0
- package/types/lib/inventory/osPackageResolver.d.ts +49 -0
- package/types/lib/inventory/osPackageResolver.d.ts.map +1 -0
- package/types/lib/inventory/osinfo.d.ts +120 -0
- package/types/lib/inventory/osinfo.d.ts.map +1 -0
- package/types/lib/inventory/osqueryTransform.d.ts +84 -0
- package/types/lib/inventory/osqueryTransform.d.ts.map +1 -0
- package/types/lib/inventory/plugins.d.ts +58 -0
- package/types/lib/inventory/plugins.d.ts.map +1 -0
- package/types/lib/inventory/protobom.d.ts +71 -0
- package/types/lib/inventory/protobom.d.ts.map +1 -0
- package/types/lib/inventory/protobomLoader.d.ts +17 -0
- package/types/lib/inventory/protobomLoader.d.ts.map +1 -0
- package/types/lib/inventory/provenanceUtils.d.ts +98 -0
- package/types/lib/inventory/provenanceUtils.d.ts.map +1 -0
- package/types/lib/inventory/purl.d.ts +313 -0
- package/types/lib/inventory/purl.d.ts.map +1 -0
- package/types/lib/inventory/rustFormulationParser.d.ts +17 -0
- package/types/lib/inventory/rustFormulationParser.d.ts.map +1 -0
- package/types/lib/inventory/sbomDocument.d.ts +59 -0
- package/types/lib/inventory/sbomDocument.d.ts.map +1 -0
- package/types/lib/inventory/source.d.ts +163 -0
- package/types/lib/inventory/source.d.ts.map +1 -0
- package/types/lib/inventory/spdx.d.ts +66 -0
- package/types/lib/inventory/spdx.d.ts.map +1 -0
- package/types/lib/inventory/spdxExpression.d.ts +58 -0
- package/types/lib/inventory/spdxExpression.d.ts.map +1 -0
- package/types/lib/inventory/spdxUtils.d.ts +12 -0
- package/types/lib/inventory/spdxUtils.d.ts.map +1 -0
- package/types/lib/inventory/table.d.ts +19 -0
- package/types/lib/inventory/table.d.ts.map +1 -0
- package/types/lib/inventory/traceRunner.d.ts +80 -0
- package/types/lib/inventory/traceRunner.d.ts.map +1 -0
- package/types/lib/inventory/unicodeScan.d.ts +46 -0
- package/types/lib/inventory/unicodeScan.d.ts.map +1 -0
- package/types/lib/managers/binary.d.ts +76 -0
- package/types/lib/managers/binary.d.ts.map +1 -0
- package/types/lib/managers/containerutils.d.ts +30 -0
- package/types/lib/managers/containerutils.d.ts.map +1 -0
- package/types/lib/managers/docker.d.ts +157 -0
- package/types/lib/managers/docker.d.ts.map +1 -0
- package/types/lib/managers/dockerConnection.d.ts +42 -0
- package/types/lib/managers/dockerConnection.d.ts.map +1 -0
- package/types/lib/managers/oci.d.ts +21 -0
- package/types/lib/managers/oci.d.ts.map +1 -0
- package/types/lib/managers/piptree.d.ts +13 -0
- package/types/lib/managers/piptree.d.ts.map +1 -0
- package/types/lib/parsers/cmakeCache.d.ts +70 -0
- package/types/lib/parsers/cmakeCache.d.ts.map +1 -0
- package/types/lib/parsers/gguf.d.ts +39 -0
- package/types/lib/parsers/gguf.d.ts.map +1 -0
- package/types/lib/parsers/gitAiNotes.d.ts +15 -0
- package/types/lib/parsers/gitAiNotes.d.ts.map +1 -0
- package/types/lib/parsers/gitmodules.d.ts +88 -0
- package/types/lib/parsers/gitmodules.d.ts.map +1 -0
- package/types/lib/parsers/htmlExtract.d.ts +34 -0
- package/types/lib/parsers/htmlExtract.d.ts.map +1 -0
- package/types/lib/parsers/huggingfaceManifest.d.ts +153 -0
- package/types/lib/parsers/huggingfaceManifest.d.ts.map +1 -0
- package/types/lib/parsers/huggingfaceUtils.d.ts +155 -0
- package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -0
- package/types/lib/parsers/iri.d.ts +68 -0
- package/types/lib/parsers/iri.d.ts.map +1 -0
- package/types/lib/parsers/npmrc.d.ts +33 -0
- package/types/lib/parsers/npmrc.d.ts.map +1 -0
- package/types/lib/parsers/ollama.d.ts +22 -0
- package/types/lib/parsers/ollama.d.ts.map +1 -0
- package/types/lib/parsers/xml.d.ts +47 -0
- package/types/lib/parsers/xml.d.ts.map +1 -0
- package/types/lib/server/server.d.ts +51 -0
- package/types/lib/server/server.d.ts.map +1 -0
- package/types/lib/stages/postgen/annotator.d.ts +36 -0
- package/types/lib/stages/postgen/annotator.d.ts.map +1 -0
- package/types/lib/stages/postgen/auditBom.d.ts +92 -0
- package/types/lib/stages/postgen/auditBom.d.ts.map +1 -0
- package/types/lib/stages/postgen/postgen.d.ts +59 -0
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -0
- package/types/lib/stages/postgen/ruleEngine.d.ts +18 -0
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -0
- package/types/lib/stages/postgen/sortBom.d.ts +34 -0
- package/types/lib/stages/postgen/sortBom.d.ts.map +1 -0
- package/types/lib/stages/postgen/spdxConverter.d.ts +11 -0
- package/types/lib/stages/postgen/spdxConverter.d.ts.map +1 -0
- package/types/lib/stages/postgen/specVersionCompat.d.ts +71 -0
- package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -0
- package/types/lib/stages/pregen/envAudit.d.ts +14 -0
- package/types/lib/stages/pregen/envAudit.d.ts.map +1 -0
- package/types/lib/stages/pregen/pregen.d.ts +59 -0
- package/types/lib/stages/pregen/pregen.d.ts.map +1 -0
- package/types/lib/third-party/arborist/lib/arborist/index.d.ts +47 -0
- package/types/lib/third-party/arborist/lib/arborist/index.d.ts.map +1 -0
- package/types/lib/third-party/arborist/lib/arborist/load-actual.d.ts +4 -0
- package/types/lib/third-party/arborist/lib/arborist/load-actual.d.ts.map +1 -0
- package/types/lib/third-party/arborist/lib/arborist/load-virtual.d.ts +4 -0
- package/types/lib/third-party/arborist/lib/arborist/load-virtual.d.ts.map +1 -0
- package/types/lib/third-party/arborist/lib/bin-links.d.ts +18 -0
- package/types/lib/third-party/arborist/lib/bin-links.d.ts.map +1 -0
- package/types/lib/third-party/arborist/lib/calc-dep-flags.d.ts +3 -0
- package/types/lib/third-party/arborist/lib/calc-dep-flags.d.ts.map +1 -0
- package/types/lib/third-party/arborist/lib/case-insensitive-map.d.ts +9 -0
- package/types/lib/third-party/arborist/lib/case-insensitive-map.d.ts.map +1 -0
- package/types/lib/third-party/arborist/lib/consistent-resolve.d.ts +3 -0
- package/types/lib/third-party/arborist/lib/consistent-resolve.d.ts.map +1 -0
- package/types/lib/third-party/arborist/lib/debug.d.ts +3 -0
- package/types/lib/third-party/arborist/lib/debug.d.ts.map +1 -0
- package/types/lib/third-party/arborist/lib/dep-valid.d.ts +3 -0
- package/types/lib/third-party/arborist/lib/dep-valid.d.ts.map +1 -0
- package/types/lib/third-party/arborist/lib/edge.d.ts +46 -0
- package/types/lib/third-party/arborist/lib/edge.d.ts.map +1 -0
- package/types/lib/third-party/arborist/lib/from-path.d.ts +3 -0
- package/types/lib/third-party/arborist/lib/from-path.d.ts.map +1 -0
- package/types/lib/third-party/arborist/lib/gather-dep-set.d.ts +3 -0
- package/types/lib/third-party/arborist/lib/gather-dep-set.d.ts.map +1 -0
- package/types/lib/third-party/arborist/lib/index.d.ts +3 -0
- package/types/lib/third-party/arborist/lib/index.d.ts.map +1 -0
- package/types/lib/third-party/arborist/lib/inventory.d.ts +14 -0
- package/types/lib/third-party/arborist/lib/inventory.d.ts.map +1 -0
- package/types/lib/third-party/arborist/lib/link.d.ts +18 -0
- package/types/lib/third-party/arborist/lib/link.d.ts.map +1 -0
- package/types/lib/third-party/arborist/lib/node.d.ts +112 -0
- package/types/lib/third-party/arborist/lib/node.d.ts.map +1 -0
- package/types/lib/third-party/arborist/lib/npm-extension.d.ts +18 -0
- package/types/lib/third-party/arborist/lib/npm-extension.d.ts.map +1 -0
- package/types/lib/third-party/arborist/lib/override-resolves.d.ts +3 -0
- package/types/lib/third-party/arborist/lib/override-resolves.d.ts.map +1 -0
- package/types/lib/third-party/arborist/lib/override-set.d.ts +27 -0
- package/types/lib/third-party/arborist/lib/override-set.d.ts.map +1 -0
- package/types/lib/third-party/arborist/lib/package-extensions.d.ts +36 -0
- package/types/lib/third-party/arborist/lib/package-extensions.d.ts.map +1 -0
- package/types/lib/third-party/arborist/lib/printable.d.ts +3 -0
- package/types/lib/third-party/arborist/lib/printable.d.ts.map +1 -0
- package/types/lib/third-party/arborist/lib/proc-log.d.ts +20 -0
- package/types/lib/third-party/arborist/lib/proc-log.d.ts.map +1 -0
- package/types/lib/third-party/arborist/lib/realpath.d.ts +3 -0
- package/types/lib/third-party/arborist/lib/realpath.d.ts.map +1 -0
- package/types/lib/third-party/arborist/lib/relpath.d.ts +3 -0
- package/types/lib/third-party/arborist/lib/relpath.d.ts.map +1 -0
- package/types/lib/third-party/arborist/lib/shrinkwrap.d.ts +54 -0
- package/types/lib/third-party/arborist/lib/shrinkwrap.d.ts.map +1 -0
- package/types/lib/third-party/arborist/lib/spec-from-lock.d.ts +3 -0
- package/types/lib/third-party/arborist/lib/spec-from-lock.d.ts.map +1 -0
- package/types/lib/third-party/arborist/lib/tree-check.d.ts +3 -0
- package/types/lib/third-party/arborist/lib/tree-check.d.ts.map +1 -0
- package/types/lib/third-party/arborist/lib/validate-npm-package-name.d.ts +13 -0
- package/types/lib/third-party/arborist/lib/validate-npm-package-name.d.ts.map +1 -0
- package/types/lib/third-party/arborist/lib/version-from-tgz.d.ts +6 -0
- package/types/lib/third-party/arborist/lib/version-from-tgz.d.ts.map +1 -0
- package/types/lib/third-party/arborist/lib/yarn-lock.d.ts +35 -0
- package/types/lib/third-party/arborist/lib/yarn-lock.d.ts.map +1 -0
- package/types/lib/validator/bomValidator.d.ts +40 -0
- package/types/lib/validator/bomValidator.d.ts.map +1 -0
- package/types/lib/validator/complianceEngine.d.ts +72 -0
- package/types/lib/validator/complianceEngine.d.ts.map +1 -0
- package/types/lib/validator/complianceRules.d.ts +61 -0
- package/types/lib/validator/complianceRules.d.ts.map +1 -0
- package/types/lib/validator/index.d.ts +101 -0
- package/types/lib/validator/index.d.ts.map +1 -0
- package/types/lib/validator/reporters/annotations.d.ts +37 -0
- package/types/lib/validator/reporters/annotations.d.ts.map +1 -0
- package/types/lib/validator/reporters/console.d.ts +34 -0
- package/types/lib/validator/reporters/console.d.ts.map +1 -0
- package/types/lib/validator/reporters/index.d.ts +30 -0
- package/types/lib/validator/reporters/index.d.ts.map +1 -0
- package/types/lib/validator/reporters/json.d.ts +11 -0
- package/types/lib/validator/reporters/json.d.ts.map +1 -0
- package/types/lib/validator/reporters/sarif.d.ts +22 -0
- package/types/lib/validator/reporters/sarif.d.ts.map +1 -0
- package/mod.d.ts +0 -7
- package/mod.js +0 -22
|
@@ -0,0 +1,2800 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { readdirSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
3
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
|
|
6
|
+
import { createBom } from "../cli/index.js";
|
|
7
|
+
import {
|
|
8
|
+
isDryRun,
|
|
9
|
+
readEnvironmentVariable,
|
|
10
|
+
recordActivity,
|
|
11
|
+
} from "../core/activity.js";
|
|
12
|
+
import {
|
|
13
|
+
getTmpDir,
|
|
14
|
+
safeExistsSync,
|
|
15
|
+
safeMkdirSync,
|
|
16
|
+
safeMkdtempSync,
|
|
17
|
+
safeRmSync,
|
|
18
|
+
safeWriteSync,
|
|
19
|
+
} from "../core/fs.js";
|
|
20
|
+
import { thoughtLog } from "../core/logger.js";
|
|
21
|
+
import { dirNameStr } from "../core/paths.js";
|
|
22
|
+
import { PROJECT_CONFIG_FILENAMES } from "../core/projectConfig.js";
|
|
23
|
+
import { fetchPomXmlAsJson } from "../ecosystems/ecosystems.js";
|
|
24
|
+
import { DEFAULT_HBOM_AUDIT_CATEGORIES } from "../inventory/auditCategories.js";
|
|
25
|
+
import {
|
|
26
|
+
DEFAULT_CDX_SPEC_VERSION,
|
|
27
|
+
getCycloneDxFormat,
|
|
28
|
+
getNonCycloneDxErrorMessage,
|
|
29
|
+
isCycloneDxBom,
|
|
30
|
+
} from "../inventory/bomUtils.js";
|
|
31
|
+
import {
|
|
32
|
+
collectPolicyViolations,
|
|
33
|
+
loadPolicy,
|
|
34
|
+
} from "../inventory/licenseEnhancer.js";
|
|
35
|
+
import {
|
|
36
|
+
hasRegistryProvenanceEvidenceProperties,
|
|
37
|
+
hasTrustedPublishingProperties,
|
|
38
|
+
} from "../inventory/provenanceUtils.js";
|
|
39
|
+
import {
|
|
40
|
+
cleanupSourceDir,
|
|
41
|
+
findGitRefForPurlVersion,
|
|
42
|
+
hardenedGitCommand,
|
|
43
|
+
isSafeGitRefName,
|
|
44
|
+
prefetchGitUrlSources,
|
|
45
|
+
resolveGitUrlFromPurl,
|
|
46
|
+
resolvePurlSourceDirectory,
|
|
47
|
+
sanitizeRemoteUrlForLogs,
|
|
48
|
+
} from "../inventory/source.js";
|
|
49
|
+
import { executeOsQuery } from "../managers/binary.js";
|
|
50
|
+
import { getBomWithOras } from "../managers/oci.js";
|
|
51
|
+
import {
|
|
52
|
+
auditBom,
|
|
53
|
+
isHbomLikeBom,
|
|
54
|
+
isObomLikeBom,
|
|
55
|
+
} from "../stages/postgen/auditBom.js";
|
|
56
|
+
import { postProcess } from "../stages/postgen/postgen.js";
|
|
57
|
+
import { formatTargetLabel } from "./progress.js";
|
|
58
|
+
import { renderAuditReport } from "./reporters.js";
|
|
59
|
+
import {
|
|
60
|
+
SEVERITY_ORDER,
|
|
61
|
+
scoreTargetRisk,
|
|
62
|
+
severityMeetsThreshold,
|
|
63
|
+
} from "./scoring.js";
|
|
64
|
+
import {
|
|
65
|
+
collectAuditTargets,
|
|
66
|
+
enrichInputBomsWithRegistryMetadata,
|
|
67
|
+
normalizePackageName,
|
|
68
|
+
} from "./targets.js";
|
|
69
|
+
|
|
70
|
+
/** Default predictive audit category list applied when none are explicitly requested. */
|
|
71
|
+
export const DEFAULT_AUDIT_CATEGORIES = [
|
|
72
|
+
"ai-agent",
|
|
73
|
+
"ci-permission",
|
|
74
|
+
"dependency-source",
|
|
75
|
+
"package-integrity",
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
const DIRECT_AUDIT_TOOL_NAME = "cdx-audit";
|
|
79
|
+
|
|
80
|
+
const AUDIT_CACHE_DIRNAME = ".cdx-audit";
|
|
81
|
+
const AUDIT_CACHE_BOM_FILE = "source-bom.json";
|
|
82
|
+
const AUDIT_CACHE_META_FILE = "source-bom.meta.json";
|
|
83
|
+
const CLONE_RETRY_DELAYS_MS = [750, 1500];
|
|
84
|
+
const LARGE_PREDICTIVE_AUDIT_THRESHOLD = 50;
|
|
85
|
+
const VERY_LARGE_PREDICTIVE_AUDIT_THRESHOLD = 100;
|
|
86
|
+
|
|
87
|
+
const PYTHON_METADATA_FILES = ["pyproject.toml", "setup.cfg", "setup.py"];
|
|
88
|
+
const PYTHON_HEURISTIC_FILENAMES = new Set(["setup.py", "__init__.py"]);
|
|
89
|
+
const PYTHON_HEURISTIC_FILE_LIMIT = 32;
|
|
90
|
+
const PYTHON_HEURISTIC_MAX_FILE_BYTES = 256 * 1024;
|
|
91
|
+
const PYTHON_SKIP_DIRS = new Set([
|
|
92
|
+
".git",
|
|
93
|
+
".hg",
|
|
94
|
+
".tox",
|
|
95
|
+
".venv",
|
|
96
|
+
"__pycache__",
|
|
97
|
+
"build",
|
|
98
|
+
"dist",
|
|
99
|
+
"node_modules",
|
|
100
|
+
"site-packages",
|
|
101
|
+
"venv",
|
|
102
|
+
]);
|
|
103
|
+
|
|
104
|
+
const PYTHON_EXECUTION_PATTERN =
|
|
105
|
+
/\b(?:exec|eval|compile)\s*\(|\b(?:subprocess\.(?:Popen|run|call|check_output)|os\.(?:system|popen))\b/i;
|
|
106
|
+
|
|
107
|
+
const PYTHON_NETWORK_PATTERN =
|
|
108
|
+
/\b(?:requests\.(?:get|post|put|patch)|urllib(?:\.request)?\.urlopen|http\.client|socket\.socket)\b/i;
|
|
109
|
+
|
|
110
|
+
const PYTHON_OBFUSCATION_PATTERN =
|
|
111
|
+
/\b(?:base64\.(?:b64decode|urlsafe_b64decode)|binascii\.a2b_base64|marshal\.loads|zlib\.decompress|codecs\.decode\s*\([^)]*base64|bytes\.fromhex)\b/i;
|
|
112
|
+
|
|
113
|
+
const PYTHON_SETUP_CMDCLASS_PATTERN = /\bcmdclass\s*=/i;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Read and validate a CycloneDX BOM file.
|
|
117
|
+
*
|
|
118
|
+
* @param {string} bomPath BOM file path
|
|
119
|
+
* @returns {object} parsed CycloneDX BOM
|
|
120
|
+
*/
|
|
121
|
+
export function loadBomFile(bomPath) {
|
|
122
|
+
const resolvedPath = resolve(bomPath);
|
|
123
|
+
let bomJson;
|
|
124
|
+
try {
|
|
125
|
+
bomJson = JSON.parse(readFileSync(resolvedPath, "utf8"));
|
|
126
|
+
} catch (error) {
|
|
127
|
+
throw new Error(`Failed to parse ${resolvedPath}: ${error.message}`);
|
|
128
|
+
}
|
|
129
|
+
if (!isCycloneDxBom(bomJson)) {
|
|
130
|
+
throw new Error(getNonCycloneDxErrorMessage(bomJson, "cdx-audit"));
|
|
131
|
+
}
|
|
132
|
+
return bomJson;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Recursively list JSON files under a BOM directory.
|
|
137
|
+
*
|
|
138
|
+
* @param {string} bomDir directory path
|
|
139
|
+
* @returns {string[]} discovered file paths
|
|
140
|
+
*/
|
|
141
|
+
export function listBomFiles(bomDir) {
|
|
142
|
+
const foundFiles = [];
|
|
143
|
+
const queue = [resolve(bomDir)];
|
|
144
|
+
while (queue.length) {
|
|
145
|
+
const currentDir = queue.shift();
|
|
146
|
+
const entries = readdirSync(currentDir, { withFileTypes: true });
|
|
147
|
+
for (const entry of entries) {
|
|
148
|
+
const entryPath = join(currentDir, entry.name);
|
|
149
|
+
if (entry.isDirectory()) {
|
|
150
|
+
queue.push(entryPath);
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
if (entry.isFile() && entry.name.endsWith(".json")) {
|
|
154
|
+
foundFiles.push(entryPath);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return foundFiles.sort();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Load input BOM files from either a single file or a directory.
|
|
163
|
+
*
|
|
164
|
+
* @param {object} options CLI options
|
|
165
|
+
* @returns {{ source: string, bomJson: object }[]} loaded input BOMs
|
|
166
|
+
*/
|
|
167
|
+
export function loadInputBoms(options) {
|
|
168
|
+
const inputBoms = [];
|
|
169
|
+
if (options.bom) {
|
|
170
|
+
inputBoms.push({
|
|
171
|
+
bomJson: loadBomFile(options.bom),
|
|
172
|
+
source: resolve(options.bom),
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
if (options.bomDir) {
|
|
176
|
+
const bomFiles = listBomFiles(options.bomDir);
|
|
177
|
+
for (const bomFile of bomFiles) {
|
|
178
|
+
try {
|
|
179
|
+
inputBoms.push({
|
|
180
|
+
bomJson: loadBomFile(bomFile),
|
|
181
|
+
source: bomFile,
|
|
182
|
+
});
|
|
183
|
+
} catch (error) {
|
|
184
|
+
console.warn(
|
|
185
|
+
`Skipping non-CycloneDX JSON file '${bomFile}': ${error.message}`,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return inputBoms;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function summarizeDirectAuditFindings(findings = []) {
|
|
194
|
+
const findingsBySeverity = {
|
|
195
|
+
critical: 0,
|
|
196
|
+
high: 0,
|
|
197
|
+
low: 0,
|
|
198
|
+
medium: 0,
|
|
199
|
+
};
|
|
200
|
+
let maxSeverity = "none";
|
|
201
|
+
for (const finding of findings) {
|
|
202
|
+
const severity = finding?.severity || "low";
|
|
203
|
+
if (findingsBySeverity[severity] !== undefined) {
|
|
204
|
+
findingsBySeverity[severity] += 1;
|
|
205
|
+
}
|
|
206
|
+
if (
|
|
207
|
+
(SEVERITY_ORDER[severity] ?? -1) > (SEVERITY_ORDER[maxSeverity] ?? -1)
|
|
208
|
+
) {
|
|
209
|
+
maxSeverity = severity;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
findingsBySeverity,
|
|
214
|
+
findingsCount: findings.length,
|
|
215
|
+
maxSeverity,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function buildDirectAuditOptions(bomJson, options = {}) {
|
|
220
|
+
const explicitCategories = options.categories?.length
|
|
221
|
+
? options.categories.join(",")
|
|
222
|
+
: undefined;
|
|
223
|
+
return {
|
|
224
|
+
aiProvenance: options.aiProvenance,
|
|
225
|
+
bomAuditCategories:
|
|
226
|
+
explicitCategories ||
|
|
227
|
+
(isHbomLikeBom(bomJson)
|
|
228
|
+
? DEFAULT_HBOM_AUDIT_CATEGORIES
|
|
229
|
+
: isObomLikeBom(bomJson)
|
|
230
|
+
? "obom-runtime"
|
|
231
|
+
: undefined),
|
|
232
|
+
bomAuditMinSeverity: options.minSeverity || "low",
|
|
233
|
+
bomAuditRulesDir: options.rulesDir,
|
|
234
|
+
workspaceDir: options.workspaceDir,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Run the direct (post-generation) BOM policy audit over loaded input BOMs.
|
|
240
|
+
*
|
|
241
|
+
* Audits each supplied BOM with {@link auditBom}, collects per-BOM findings and
|
|
242
|
+
* summaries, and aggregates them into a single report keyed by `auditMode: "direct"`.
|
|
243
|
+
*
|
|
244
|
+
* @param {{ source: string, bomJson: object }[]} inputBoms Loaded CycloneDX BOM objects to audit
|
|
245
|
+
* @param {object} [options={}] CLI options including optional category, severity, and rules configuration
|
|
246
|
+
* @returns {Promise<object>} Aggregate direct audit report with per-BOM results and a rollup summary
|
|
247
|
+
*/
|
|
248
|
+
export async function runDirectBomAuditFromBoms(inputBoms, options = {}) {
|
|
249
|
+
if (!inputBoms.length) {
|
|
250
|
+
throw new Error("No CycloneDX BOM inputs were found.");
|
|
251
|
+
}
|
|
252
|
+
const results = [];
|
|
253
|
+
for (const inputBom of inputBoms) {
|
|
254
|
+
const directAuditOptions = buildDirectAuditOptions(
|
|
255
|
+
inputBom.bomJson,
|
|
256
|
+
options,
|
|
257
|
+
);
|
|
258
|
+
const findings = await auditBom(inputBom.bomJson, {
|
|
259
|
+
...directAuditOptions,
|
|
260
|
+
getBomWithOras,
|
|
261
|
+
});
|
|
262
|
+
results.push({
|
|
263
|
+
auditOptions: directAuditOptions,
|
|
264
|
+
bomFormat: getCycloneDxFormat(inputBom.bomJson),
|
|
265
|
+
findings,
|
|
266
|
+
serialNumber: inputBom.bomJson?.serialNumber,
|
|
267
|
+
source: inputBom.source,
|
|
268
|
+
specVersion: inputBom.bomJson?.specVersion,
|
|
269
|
+
status: "audited",
|
|
270
|
+
summary: summarizeDirectAuditFindings(findings),
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
const summary = {
|
|
274
|
+
findingsBySeverity: {
|
|
275
|
+
critical: 0,
|
|
276
|
+
high: 0,
|
|
277
|
+
low: 0,
|
|
278
|
+
medium: 0,
|
|
279
|
+
},
|
|
280
|
+
inputBomCount: inputBoms.length,
|
|
281
|
+
maxSeverity: "none",
|
|
282
|
+
totalFindings: 0,
|
|
283
|
+
bomsWithFindings: 0,
|
|
284
|
+
};
|
|
285
|
+
for (const result of results) {
|
|
286
|
+
summary.totalFindings += result.summary.findingsCount;
|
|
287
|
+
if (result.summary.findingsCount > 0) {
|
|
288
|
+
summary.bomsWithFindings += 1;
|
|
289
|
+
}
|
|
290
|
+
for (const [severity, count] of Object.entries(
|
|
291
|
+
result.summary.findingsBySeverity,
|
|
292
|
+
)) {
|
|
293
|
+
summary.findingsBySeverity[severity] += count;
|
|
294
|
+
}
|
|
295
|
+
if (
|
|
296
|
+
(SEVERITY_ORDER[result.summary.maxSeverity] ?? -1) >
|
|
297
|
+
(SEVERITY_ORDER[summary.maxSeverity] ?? -1)
|
|
298
|
+
) {
|
|
299
|
+
summary.maxSeverity = result.summary.maxSeverity;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
auditMode: "direct",
|
|
304
|
+
generatedAt: new Date().toISOString(),
|
|
305
|
+
inputs: inputBoms.map((inputBom) => inputBom.source),
|
|
306
|
+
results,
|
|
307
|
+
summary,
|
|
308
|
+
tool: {
|
|
309
|
+
name: DIRECT_AUDIT_TOOL_NAME,
|
|
310
|
+
version: readPackageVersion(),
|
|
311
|
+
},
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Read the package version from the local package.json file.
|
|
317
|
+
*
|
|
318
|
+
* @returns {string} package version
|
|
319
|
+
*/
|
|
320
|
+
function readPackageVersion() {
|
|
321
|
+
const packageJson = JSON.parse(
|
|
322
|
+
readFileSync(join(dirNameStr, "package.json"), "utf8"),
|
|
323
|
+
);
|
|
324
|
+
return packageJson.version;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Build a deterministic directory-safe slug for report and workspace paths.
|
|
329
|
+
*
|
|
330
|
+
* @param {object} target audit target
|
|
331
|
+
* @returns {string} slug string
|
|
332
|
+
*/
|
|
333
|
+
function targetSlug(target) {
|
|
334
|
+
const packageName = target.namespace
|
|
335
|
+
? `${target.namespace}-${target.name}`
|
|
336
|
+
: target.name;
|
|
337
|
+
const normalized = normalizePackageName(packageName)
|
|
338
|
+
.replace(/[^a-z0-9-]/g, "-")
|
|
339
|
+
.replace(/-+/g, "-")
|
|
340
|
+
.replace(/^-|-$/g, "");
|
|
341
|
+
const version = normalizePackageName(target.version || "latest") || "latest";
|
|
342
|
+
const digest = createHash("sha256")
|
|
343
|
+
.update(target.purl)
|
|
344
|
+
.digest("hex")
|
|
345
|
+
.slice(0, 12);
|
|
346
|
+
return `${target.type}-${normalized || "package"}-${version}-${digest}`;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Ensure a parent directory exists before writing a file.
|
|
351
|
+
*
|
|
352
|
+
* @param {string} filePath file path to create
|
|
353
|
+
* @param {string} content file content
|
|
354
|
+
* @returns {void}
|
|
355
|
+
*/
|
|
356
|
+
function writeTextFile(filePath, content) {
|
|
357
|
+
const parentDir = dirname(filePath);
|
|
358
|
+
if (!safeExistsSync(parentDir)) {
|
|
359
|
+
safeMkdirSync(parentDir, { recursive: true });
|
|
360
|
+
}
|
|
361
|
+
safeWriteSync(filePath, content);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Ensure a parent directory exists before writing JSON.
|
|
366
|
+
*
|
|
367
|
+
* @param {string} filePath file path to create
|
|
368
|
+
* @param {object} payload JSON payload
|
|
369
|
+
* @returns {void}
|
|
370
|
+
*/
|
|
371
|
+
function writeJsonFile(filePath, payload) {
|
|
372
|
+
writeTextFile(filePath, `${JSON.stringify(payload, null, 2)}\n`);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function sleep(ms) {
|
|
376
|
+
return new Promise((resolvePromise) => {
|
|
377
|
+
setTimeout(resolvePromise, ms);
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function isPathWithin(parentDir, childPath) {
|
|
382
|
+
const normalizePath = (candidatePath) => {
|
|
383
|
+
try {
|
|
384
|
+
return realpathSync.native
|
|
385
|
+
? realpathSync.native(candidatePath)
|
|
386
|
+
: realpathSync(candidatePath);
|
|
387
|
+
} catch {
|
|
388
|
+
return resolve(candidatePath);
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
const normalizedChild = normalizePath(childPath);
|
|
392
|
+
const candidateParents = [parentDir];
|
|
393
|
+
if (process.platform !== "win32") {
|
|
394
|
+
candidateParents.push("/tmp");
|
|
395
|
+
candidateParents.push("/private/tmp");
|
|
396
|
+
}
|
|
397
|
+
return candidateParents.some((candidateParent) => {
|
|
398
|
+
const normalizedParent = normalizePath(candidateParent);
|
|
399
|
+
return (
|
|
400
|
+
normalizedChild === normalizedParent ||
|
|
401
|
+
normalizedChild.startsWith(`${normalizedParent}/`)
|
|
402
|
+
);
|
|
403
|
+
});
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function isTemporaryWorkspaceDir(workspaceDir) {
|
|
407
|
+
return workspaceDir ? isPathWithin(getTmpDir(), workspaceDir) : false;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function prepareWorkspaceContext(options = {}) {
|
|
411
|
+
if (!options.workspaceDir) {
|
|
412
|
+
return {
|
|
413
|
+
cleanupOnFinish: false,
|
|
414
|
+
workspaceDir: undefined,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
const workspaceDir = resolve(options.workspaceDir);
|
|
418
|
+
const existed = safeExistsSync(workspaceDir);
|
|
419
|
+
if (!existed) {
|
|
420
|
+
safeMkdirSync(workspaceDir, { recursive: true });
|
|
421
|
+
}
|
|
422
|
+
return {
|
|
423
|
+
cleanupOnFinish: !existed && isTemporaryWorkspaceDir(workspaceDir),
|
|
424
|
+
workspaceDir,
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function getWorkspaceTargetDir(workspaceDir, target) {
|
|
429
|
+
return join(resolve(workspaceDir), targetSlug(target));
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function getWorkspaceCachePaths(workspaceDir, target) {
|
|
433
|
+
const targetDir = getWorkspaceTargetDir(workspaceDir, target);
|
|
434
|
+
const cacheDir = join(targetDir, AUDIT_CACHE_DIRNAME);
|
|
435
|
+
return {
|
|
436
|
+
cacheDir,
|
|
437
|
+
metadataFile: join(cacheDir, AUDIT_CACHE_META_FILE),
|
|
438
|
+
sourceBomFile: join(cacheDir, AUDIT_CACHE_BOM_FILE),
|
|
439
|
+
targetDir,
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
function loadCachedChildBom(workspaceDir, target) {
|
|
444
|
+
if (!workspaceDir) {
|
|
445
|
+
return undefined;
|
|
446
|
+
}
|
|
447
|
+
const cachePaths = getWorkspaceCachePaths(workspaceDir, target);
|
|
448
|
+
if (!safeExistsSync(cachePaths.sourceBomFile)) {
|
|
449
|
+
return undefined;
|
|
450
|
+
}
|
|
451
|
+
try {
|
|
452
|
+
const bomJson = loadBomFile(cachePaths.sourceBomFile);
|
|
453
|
+
let metadata = {};
|
|
454
|
+
if (safeExistsSync(cachePaths.metadataFile)) {
|
|
455
|
+
metadata = JSON.parse(readFileSync(cachePaths.metadataFile, "utf8"));
|
|
456
|
+
}
|
|
457
|
+
const scanDir = metadata.scanDirRelative
|
|
458
|
+
? resolve(cachePaths.targetDir, metadata.scanDirRelative)
|
|
459
|
+
: cachePaths.targetDir;
|
|
460
|
+
return {
|
|
461
|
+
bomJson,
|
|
462
|
+
cacheDir: cachePaths.cacheDir,
|
|
463
|
+
repoUrl: metadata.repoUrl,
|
|
464
|
+
resolution: metadata.resolution,
|
|
465
|
+
scanDir,
|
|
466
|
+
sourceDirectoryConfidence: metadata.sourceDirectoryConfidence || "medium",
|
|
467
|
+
versionMatched: metadata.versionMatched !== false,
|
|
468
|
+
};
|
|
469
|
+
} catch {
|
|
470
|
+
return undefined;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function writeCachedChildBom(workspaceDir, target, payload) {
|
|
475
|
+
if (!workspaceDir || !payload?.bomJson) {
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
const cachePaths = getWorkspaceCachePaths(workspaceDir, target);
|
|
479
|
+
safeMkdirSync(cachePaths.cacheDir, { recursive: true });
|
|
480
|
+
writeJsonFile(cachePaths.sourceBomFile, payload.bomJson);
|
|
481
|
+
writeJsonFile(cachePaths.metadataFile, {
|
|
482
|
+
generatedAt: new Date().toISOString(),
|
|
483
|
+
repoUrl: payload.repoUrl,
|
|
484
|
+
resolution: payload.resolution,
|
|
485
|
+
scanDirRelative: payload.scanDir
|
|
486
|
+
? relative(cachePaths.targetDir, resolve(payload.scanDir)) || "."
|
|
487
|
+
: ".",
|
|
488
|
+
sourceDirectoryConfidence: payload.sourceDirectoryConfidence,
|
|
489
|
+
versionMatched: payload.versionMatched,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function persistAuditArtifacts(result, options, sourceBomJson) {
|
|
494
|
+
if (!options.reportsDir) {
|
|
495
|
+
return result;
|
|
496
|
+
}
|
|
497
|
+
const resultDir = join(
|
|
498
|
+
resolve(options.reportsDir),
|
|
499
|
+
targetSlug(result.target),
|
|
500
|
+
);
|
|
501
|
+
safeMkdirSync(resultDir, { recursive: true });
|
|
502
|
+
result.reportDir = resultDir;
|
|
503
|
+
result.findingsFile = join(resultDir, "findings.json");
|
|
504
|
+
result.summaryFile = join(resultDir, "summary.json");
|
|
505
|
+
if (sourceBomJson) {
|
|
506
|
+
result.sourceBomFile = join(resultDir, "source-bom.json");
|
|
507
|
+
writeJsonFile(result.sourceBomFile, sourceBomJson);
|
|
508
|
+
}
|
|
509
|
+
writeJsonFile(result.findingsFile, result.findings || []);
|
|
510
|
+
writeJsonFile(result.summaryFile, {
|
|
511
|
+
assessment: result.assessment,
|
|
512
|
+
cacheHit: result.cacheHit || false,
|
|
513
|
+
error: result.error,
|
|
514
|
+
errorType: result.errorType,
|
|
515
|
+
findingsCount: result.findings?.length || 0,
|
|
516
|
+
repoUrl: result.repoUrl,
|
|
517
|
+
sourceDirectoryConfidence: result.sourceDirectoryConfidence,
|
|
518
|
+
status: result.status,
|
|
519
|
+
target: result.target,
|
|
520
|
+
});
|
|
521
|
+
return result;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Emit a progress event when a callback is configured.
|
|
526
|
+
*
|
|
527
|
+
* @param {object} options CLI options
|
|
528
|
+
* @param {object} event progress event payload
|
|
529
|
+
* @returns {void}
|
|
530
|
+
*/
|
|
531
|
+
function emitProgress(options, event) {
|
|
532
|
+
if (typeof options?.onProgress === "function") {
|
|
533
|
+
options.onProgress(event);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function buildPredictiveAuditEstimate(selectedTargets) {
|
|
538
|
+
if (selectedTargets >= VERY_LARGE_PREDICTIVE_AUDIT_THRESHOLD) {
|
|
539
|
+
return "This may take 10+ minutes depending on repository lookups and child SBOM generation.";
|
|
540
|
+
}
|
|
541
|
+
if (selectedTargets >= LARGE_PREDICTIVE_AUDIT_THRESHOLD) {
|
|
542
|
+
return "This may take several minutes depending on repository lookups and child SBOM generation.";
|
|
543
|
+
}
|
|
544
|
+
return undefined;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
function buildPredictiveAuditPreflightMessage(extractedTargets, options) {
|
|
548
|
+
const selectedTargets = extractedTargets?.targets?.length || 0;
|
|
549
|
+
const allowlistedTargetsExcluded =
|
|
550
|
+
extractedTargets?.stats?.allowlistedTargetsExcluded || 0;
|
|
551
|
+
const availableTargets = extractedTargets?.stats?.availableTargets || 0;
|
|
552
|
+
const requiredTargets = extractedTargets?.stats?.requiredTargets || 0;
|
|
553
|
+
const trustedTargetsExcluded =
|
|
554
|
+
extractedTargets?.stats?.trustedTargetsExcluded || 0;
|
|
555
|
+
const truncatedTargets = extractedTargets?.stats?.truncatedTargets || 0;
|
|
556
|
+
const estimate = buildPredictiveAuditEstimate(selectedTargets);
|
|
557
|
+
const trustedHint = options?.trustedSelectionHelp
|
|
558
|
+
? ` ${options.trustedSelectionHelp}`
|
|
559
|
+
: "";
|
|
560
|
+
const trustedExclusionMessage = trustedTargetsExcluded
|
|
561
|
+
? ` Skipping ${trustedTargetsExcluded} trusted-publishing-backed package(s) by default.${trustedHint}`
|
|
562
|
+
: "";
|
|
563
|
+
const customAllowlistSuffix = options?.allowlistFile
|
|
564
|
+
? " and your custom allowlist"
|
|
565
|
+
: "";
|
|
566
|
+
const allowlistExclusionMessage = allowlistedTargetsExcluded
|
|
567
|
+
? ` Skipping ${allowlistedTargetsExcluded} allowlisted package(s) using the built-in well-known purl prefix filter${customAllowlistSuffix}.`
|
|
568
|
+
: "";
|
|
569
|
+
if (!estimate && availableTargets < LARGE_PREDICTIVE_AUDIT_THRESHOLD) {
|
|
570
|
+
const passiveMessage =
|
|
571
|
+
`${trustedExclusionMessage}${allowlistExclusionMessage}`.trim();
|
|
572
|
+
return passiveMessage || undefined;
|
|
573
|
+
}
|
|
574
|
+
if (options?.scope === "required") {
|
|
575
|
+
return `Predictive audit will scan ${selectedTargets} required package(s). ${estimate || "Large required-only scans may still take a while depending on repository lookups and child SBOM generation."}${trustedExclusionMessage}${allowlistExclusionMessage}`;
|
|
576
|
+
}
|
|
577
|
+
if (truncatedTargets > 0) {
|
|
578
|
+
const additionalTargets = Math.max(0, selectedTargets - requiredTargets);
|
|
579
|
+
return `Predictive audit selected ${selectedTargets} of ${availableTargets} package(s) (${requiredTargets} required${additionalTargets ? ` + ${additionalTargets} additional` : ""}) using required-first prioritization. ${estimate || "This run was trimmed to keep audit time reasonable."}${trustedExclusionMessage}${allowlistExclusionMessage}`;
|
|
580
|
+
}
|
|
581
|
+
return `Predictive audit will scan ${selectedTargets} package(s). ${estimate || "Large predictive audits may still take a while depending on repository lookups and child SBOM generation."}${trustedExclusionMessage}${allowlistExclusionMessage}`;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* Read a custom property from a target descriptor.
|
|
586
|
+
*
|
|
587
|
+
* @param {object} target audit target
|
|
588
|
+
* @param {string} propertyName property name
|
|
589
|
+
* @returns {string | undefined} property value
|
|
590
|
+
*/
|
|
591
|
+
function getTargetProperty(target, propertyName) {
|
|
592
|
+
return target?.properties?.find((property) => property.name === propertyName)
|
|
593
|
+
?.value;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function getTargetNumberProperty(target, propertyName) {
|
|
597
|
+
const value = getTargetProperty(target, propertyName);
|
|
598
|
+
if (!value) {
|
|
599
|
+
return undefined;
|
|
600
|
+
}
|
|
601
|
+
const numericValue = Number(value);
|
|
602
|
+
return Number.isFinite(numericValue) ? numericValue : undefined;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function getTargetTimestampProperty(target, propertyName) {
|
|
606
|
+
const value = getTargetProperty(target, propertyName);
|
|
607
|
+
if (!value) {
|
|
608
|
+
return undefined;
|
|
609
|
+
}
|
|
610
|
+
const timestamp = Date.parse(value);
|
|
611
|
+
return Number.isNaN(timestamp) ? undefined : timestamp;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function getTargetListProperty(target, propertyName) {
|
|
615
|
+
const value = getTargetProperty(target, propertyName);
|
|
616
|
+
if (!value) {
|
|
617
|
+
return [];
|
|
618
|
+
}
|
|
619
|
+
return [
|
|
620
|
+
...new Set(
|
|
621
|
+
value
|
|
622
|
+
.split(",")
|
|
623
|
+
.map((entry) => entry.trim())
|
|
624
|
+
.filter(Boolean),
|
|
625
|
+
),
|
|
626
|
+
];
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function isEstablishedPackage(target, propertyPrefix) {
|
|
630
|
+
const packageCreatedTime = getTargetTimestampProperty(
|
|
631
|
+
target,
|
|
632
|
+
`${propertyPrefix}:packageCreatedTime`,
|
|
633
|
+
);
|
|
634
|
+
const versionCount = getTargetNumberProperty(
|
|
635
|
+
target,
|
|
636
|
+
`${propertyPrefix}:versionCount`,
|
|
637
|
+
);
|
|
638
|
+
if (!packageCreatedTime || !versionCount) {
|
|
639
|
+
return false;
|
|
640
|
+
}
|
|
641
|
+
const packageAgeMs = Date.now() - packageCreatedTime;
|
|
642
|
+
return packageAgeMs >= 1000 * 60 * 60 * 24 * 30 && versionCount >= 3;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function isRecentRelease(target, propertyPrefix) {
|
|
646
|
+
const publishTime = getTargetTimestampProperty(
|
|
647
|
+
target,
|
|
648
|
+
`${propertyPrefix}:publishTime`,
|
|
649
|
+
);
|
|
650
|
+
if (!publishTime) {
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
const releaseAgeMs = Date.now() - publishTime;
|
|
654
|
+
return releaseAgeMs >= 0 && releaseAgeMs <= 1000 * 60 * 60 * 72;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function hasPublisherDrift(target, propertyPrefix) {
|
|
658
|
+
return (
|
|
659
|
+
getTargetProperty(target, `${propertyPrefix}:publisherDrift`) === "true"
|
|
660
|
+
);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
function hasMaintainerSetDrift(target, propertyPrefix) {
|
|
664
|
+
return (
|
|
665
|
+
getTargetProperty(target, `${propertyPrefix}:maintainerSetDrift`) ===
|
|
666
|
+
"true" ||
|
|
667
|
+
getTargetProperty(target, `${propertyPrefix}:uploaderSetDrift`) === "true"
|
|
668
|
+
);
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function hasPartialIdentitySetDrift(target, propertyPrefix) {
|
|
672
|
+
const explicitPropertyName =
|
|
673
|
+
propertyPrefix === "cdx:npm"
|
|
674
|
+
? `${propertyPrefix}:maintainerSetPartialDrift`
|
|
675
|
+
: `${propertyPrefix}:uploaderSetPartialDrift`;
|
|
676
|
+
if (getTargetProperty(target, explicitPropertyName) === "true") {
|
|
677
|
+
return true;
|
|
678
|
+
}
|
|
679
|
+
const currentPropertyName =
|
|
680
|
+
propertyPrefix === "cdx:npm"
|
|
681
|
+
? `${propertyPrefix}:maintainerSet`
|
|
682
|
+
: `${propertyPrefix}:uploaderSet`;
|
|
683
|
+
const priorPropertyName =
|
|
684
|
+
propertyPrefix === "cdx:npm"
|
|
685
|
+
? `${propertyPrefix}:priorMaintainerSet`
|
|
686
|
+
: `${propertyPrefix}:priorUploaderSet`;
|
|
687
|
+
const currentSet = getTargetListProperty(target, currentPropertyName);
|
|
688
|
+
const priorSet = getTargetListProperty(target, priorPropertyName);
|
|
689
|
+
if (!currentSet.length || !priorSet.length) {
|
|
690
|
+
return false;
|
|
691
|
+
}
|
|
692
|
+
const priorValues = new Set(priorSet);
|
|
693
|
+
const overlapCount = currentSet.filter((value) =>
|
|
694
|
+
priorValues.has(value),
|
|
695
|
+
).length;
|
|
696
|
+
if (overlapCount === 0) {
|
|
697
|
+
return false;
|
|
698
|
+
}
|
|
699
|
+
const unionCount = new Set([...currentSet, ...priorSet]).size;
|
|
700
|
+
return (
|
|
701
|
+
overlapCount < unionCount &&
|
|
702
|
+
(overlapCount < currentSet.length || overlapCount < priorSet.length)
|
|
703
|
+
);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function hasDormantReleaseGapAnomaly(target, propertyPrefix) {
|
|
707
|
+
const currentGapDays = getTargetNumberProperty(
|
|
708
|
+
target,
|
|
709
|
+
`${propertyPrefix}:releaseGapDays`,
|
|
710
|
+
);
|
|
711
|
+
const baselineGapDays = getTargetNumberProperty(
|
|
712
|
+
target,
|
|
713
|
+
`${propertyPrefix}:releaseGapBaselineDays`,
|
|
714
|
+
);
|
|
715
|
+
const sampleSize = getTargetNumberProperty(
|
|
716
|
+
target,
|
|
717
|
+
`${propertyPrefix}:releaseGapSampleSize`,
|
|
718
|
+
);
|
|
719
|
+
if (!currentGapDays || !baselineGapDays || !sampleSize || sampleSize < 3) {
|
|
720
|
+
return false;
|
|
721
|
+
}
|
|
722
|
+
return currentGapDays >= Math.max(90, baselineGapDays * 8);
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
function hasCompressedCadence(target, propertyPrefix) {
|
|
726
|
+
if (
|
|
727
|
+
getTargetProperty(target, `${propertyPrefix}:compressedCadence`) === "true"
|
|
728
|
+
) {
|
|
729
|
+
return true;
|
|
730
|
+
}
|
|
731
|
+
const currentGapDays = getTargetNumberProperty(
|
|
732
|
+
target,
|
|
733
|
+
`${propertyPrefix}:releaseGapDays`,
|
|
734
|
+
);
|
|
735
|
+
const baselineGapDays = getTargetNumberProperty(
|
|
736
|
+
target,
|
|
737
|
+
`${propertyPrefix}:releaseGapBaselineDays`,
|
|
738
|
+
);
|
|
739
|
+
const sampleSize = getTargetNumberProperty(
|
|
740
|
+
target,
|
|
741
|
+
`${propertyPrefix}:releaseGapSampleSize`,
|
|
742
|
+
);
|
|
743
|
+
if (
|
|
744
|
+
currentGapDays === undefined ||
|
|
745
|
+
baselineGapDays === undefined ||
|
|
746
|
+
sampleSize === undefined ||
|
|
747
|
+
sampleSize < 3 ||
|
|
748
|
+
currentGapDays <= 0 ||
|
|
749
|
+
baselineGapDays <= 0 ||
|
|
750
|
+
baselineGapDays < 21
|
|
751
|
+
) {
|
|
752
|
+
return false;
|
|
753
|
+
}
|
|
754
|
+
return currentGapDays <= 14 && currentGapDays / baselineGapDays <= 0.33;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Build low-noise provenance-aware contextual findings from the root BOM target.
|
|
759
|
+
*
|
|
760
|
+
* These are intentionally conservative and only fire when there is explicit risk
|
|
761
|
+
* posture already present in the target metadata.
|
|
762
|
+
*
|
|
763
|
+
* @param {object} target audit target
|
|
764
|
+
* @returns {object[]} contextual findings
|
|
765
|
+
*/
|
|
766
|
+
export function buildTargetContextFindings(target) {
|
|
767
|
+
const findings = [];
|
|
768
|
+
const hasTrustedPublishing = hasTrustedPublishingProperties(
|
|
769
|
+
target?.properties,
|
|
770
|
+
);
|
|
771
|
+
const hasProvenanceEvidence = hasRegistryProvenanceEvidenceProperties(
|
|
772
|
+
target?.properties,
|
|
773
|
+
);
|
|
774
|
+
if (target.type === "npm") {
|
|
775
|
+
const hasInstallScript =
|
|
776
|
+
getTargetProperty(target, "cdx:npm:hasInstallScript") === "true";
|
|
777
|
+
const establishedPackage = isEstablishedPackage(target, "cdx:npm");
|
|
778
|
+
const recentRelease = isRecentRelease(target, "cdx:npm");
|
|
779
|
+
const publisherDrift = hasPublisherDrift(target, "cdx:npm");
|
|
780
|
+
const maintainerSetDrift = hasMaintainerSetDrift(target, "cdx:npm");
|
|
781
|
+
const partialMaintainerSetDrift = hasPartialIdentitySetDrift(
|
|
782
|
+
target,
|
|
783
|
+
"cdx:npm",
|
|
784
|
+
);
|
|
785
|
+
const dormantReleaseGapAnomaly = hasDormantReleaseGapAnomaly(
|
|
786
|
+
target,
|
|
787
|
+
"cdx:npm",
|
|
788
|
+
);
|
|
789
|
+
const compressedCadence = hasCompressedCadence(target, "cdx:npm");
|
|
790
|
+
if (
|
|
791
|
+
target.version &&
|
|
792
|
+
hasInstallScript &&
|
|
793
|
+
!hasTrustedPublishing &&
|
|
794
|
+
!hasProvenanceEvidence
|
|
795
|
+
) {
|
|
796
|
+
findings.push({
|
|
797
|
+
category: "package-integrity",
|
|
798
|
+
description:
|
|
799
|
+
"Install-time execution combined with missing registry-visible provenance raises future tampering risk.",
|
|
800
|
+
location: {
|
|
801
|
+
bomRef: target.bomRefs?.[0],
|
|
802
|
+
purl: target.purl,
|
|
803
|
+
},
|
|
804
|
+
message: `npm package '${target.name}@${target.version}' has install-time execution hooks but no registry-visible trusted publishing or provenance evidence.`,
|
|
805
|
+
mitigation:
|
|
806
|
+
"Prefer versions with registry-visible provenance evidence, review install scripts carefully, and pin/allowlist publishers for high-risk packages.",
|
|
807
|
+
ruleId: "PROV-001",
|
|
808
|
+
severity: "medium",
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
if (
|
|
812
|
+
target.version &&
|
|
813
|
+
establishedPackage &&
|
|
814
|
+
recentRelease &&
|
|
815
|
+
hasInstallScript &&
|
|
816
|
+
!hasTrustedPublishing &&
|
|
817
|
+
!hasProvenanceEvidence
|
|
818
|
+
) {
|
|
819
|
+
findings.push({
|
|
820
|
+
category: "package-integrity",
|
|
821
|
+
description:
|
|
822
|
+
"A very recent release on a mature package, combined with install-time execution and missing provenance, deserves extra scrutiny before adoption.",
|
|
823
|
+
location: {
|
|
824
|
+
bomRef: target.bomRefs?.[0],
|
|
825
|
+
purl: target.purl,
|
|
826
|
+
},
|
|
827
|
+
message: `npm package '${target.name}@${target.version}' is a very recent release on an established package and still lacks registry-visible provenance.`,
|
|
828
|
+
mitigation:
|
|
829
|
+
"Delay adoption briefly, verify publisher identity, and prefer registry-visible provenance for high-risk packages with install hooks.",
|
|
830
|
+
ruleId: "PROV-003",
|
|
831
|
+
severity: "low",
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
if (
|
|
835
|
+
target.version &&
|
|
836
|
+
establishedPackage &&
|
|
837
|
+
publisherDrift &&
|
|
838
|
+
hasInstallScript &&
|
|
839
|
+
!hasTrustedPublishing &&
|
|
840
|
+
!hasProvenanceEvidence
|
|
841
|
+
) {
|
|
842
|
+
findings.push({
|
|
843
|
+
category: "package-integrity",
|
|
844
|
+
description:
|
|
845
|
+
"Publisher drift on mature packages can be legitimate, but becomes more concerning when install-time execution is present and provenance is weak.",
|
|
846
|
+
location: {
|
|
847
|
+
bomRef: target.bomRefs?.[0],
|
|
848
|
+
purl: target.purl,
|
|
849
|
+
},
|
|
850
|
+
message: `npm package '${target.name}@${target.version}' was published by a different identity than the prior release and lacks registry-visible provenance.`,
|
|
851
|
+
mitigation:
|
|
852
|
+
"Review maintainer changes, compare the prior release publisher, and validate provenance before upgrading execution-capable packages.",
|
|
853
|
+
ruleId: "PROV-004",
|
|
854
|
+
severity: "medium",
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
if (
|
|
858
|
+
target.version &&
|
|
859
|
+
establishedPackage &&
|
|
860
|
+
maintainerSetDrift &&
|
|
861
|
+
hasInstallScript &&
|
|
862
|
+
!hasTrustedPublishing &&
|
|
863
|
+
!hasProvenanceEvidence
|
|
864
|
+
) {
|
|
865
|
+
findings.push({
|
|
866
|
+
category: "package-integrity",
|
|
867
|
+
description:
|
|
868
|
+
"Maintainer-set drift on execution-capable packages is a triage signal when the resolved release also lacks registry-visible provenance.",
|
|
869
|
+
location: {
|
|
870
|
+
bomRef: target.bomRefs?.[0],
|
|
871
|
+
purl: target.purl,
|
|
872
|
+
},
|
|
873
|
+
message: `npm package '${target.name}@${target.version}' has a fully different maintainer identity set than the prior release and lacks registry-visible provenance.`,
|
|
874
|
+
mitigation:
|
|
875
|
+
"Compare the prior and current maintainer sets, verify maintainer transitions, and prefer releases with provenance before upgrading packages with install hooks.",
|
|
876
|
+
ruleId: "PROV-007",
|
|
877
|
+
severity: "medium",
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
if (
|
|
881
|
+
target.version &&
|
|
882
|
+
establishedPackage &&
|
|
883
|
+
partialMaintainerSetDrift &&
|
|
884
|
+
!maintainerSetDrift &&
|
|
885
|
+
hasInstallScript &&
|
|
886
|
+
!hasTrustedPublishing &&
|
|
887
|
+
!hasProvenanceEvidence
|
|
888
|
+
) {
|
|
889
|
+
findings.push({
|
|
890
|
+
category: "package-integrity",
|
|
891
|
+
description:
|
|
892
|
+
"Partial maintainer-set drift is a low-severity triage signal when execution-capable releases retain some identities but also introduce maintainer churn without registry-visible provenance.",
|
|
893
|
+
location: {
|
|
894
|
+
bomRef: target.bomRefs?.[0],
|
|
895
|
+
purl: target.purl,
|
|
896
|
+
},
|
|
897
|
+
message: `npm package '${target.name}@${target.version}' retains only part of the prior maintainer identity set and lacks registry-visible provenance.`,
|
|
898
|
+
mitigation:
|
|
899
|
+
"Review which maintainer identities changed, compare against the prior release, and validate the transition before upgrading packages with install hooks.",
|
|
900
|
+
ruleId: "PROV-011",
|
|
901
|
+
severity: "low",
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
if (
|
|
905
|
+
target.version &&
|
|
906
|
+
establishedPackage &&
|
|
907
|
+
dormantReleaseGapAnomaly &&
|
|
908
|
+
hasInstallScript &&
|
|
909
|
+
!hasTrustedPublishing &&
|
|
910
|
+
!hasProvenanceEvidence
|
|
911
|
+
) {
|
|
912
|
+
findings.push({
|
|
913
|
+
category: "package-integrity",
|
|
914
|
+
description:
|
|
915
|
+
"A long dormant gap followed by a new execution-capable release can warrant a short review window when provenance is missing.",
|
|
916
|
+
location: {
|
|
917
|
+
bomRef: target.bomRefs?.[0],
|
|
918
|
+
purl: target.purl,
|
|
919
|
+
},
|
|
920
|
+
message: `npm package '${target.name}@${target.version}' arrived after an unusually long release gap and lacks registry-visible provenance.`,
|
|
921
|
+
mitigation:
|
|
922
|
+
"Review the release diff, compare against the prior version, and validate maintainer continuity before adopting after long dormancy.",
|
|
923
|
+
ruleId: "PROV-008",
|
|
924
|
+
severity: "low",
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
if (
|
|
928
|
+
target.version &&
|
|
929
|
+
establishedPackage &&
|
|
930
|
+
compressedCadence &&
|
|
931
|
+
hasInstallScript &&
|
|
932
|
+
!hasTrustedPublishing &&
|
|
933
|
+
!hasProvenanceEvidence
|
|
934
|
+
) {
|
|
935
|
+
findings.push({
|
|
936
|
+
category: "package-integrity",
|
|
937
|
+
description:
|
|
938
|
+
"A materially faster-than-usual release on a mature execution-capable package is a low-severity review signal when registry-visible provenance is absent.",
|
|
939
|
+
location: {
|
|
940
|
+
bomRef: target.bomRefs?.[0],
|
|
941
|
+
purl: target.purl,
|
|
942
|
+
},
|
|
943
|
+
message: `npm package '${target.name}@${target.version}' arrived materially faster than its prior release cadence and lacks registry-visible provenance.`,
|
|
944
|
+
mitigation:
|
|
945
|
+
"Review the release diff, compare the release timing against prior cadence, and validate the publisher transition before rapid upgrades of execution-capable packages.",
|
|
946
|
+
ruleId: "PROV-012",
|
|
947
|
+
severity: "low",
|
|
948
|
+
});
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
if (target.type === "pypi") {
|
|
952
|
+
const registry = getTargetProperty(target, "cdx:pypi:registry");
|
|
953
|
+
const isDefaultRegistry =
|
|
954
|
+
!registry ||
|
|
955
|
+
["https://pypi.org", "https://pypi.org/simple"].includes(registry);
|
|
956
|
+
const uploaderVerified =
|
|
957
|
+
getTargetProperty(target, "cdx:pypi:uploaderVerified") === "true";
|
|
958
|
+
const establishedPackage = isEstablishedPackage(target, "cdx:pypi");
|
|
959
|
+
const recentRelease = isRecentRelease(target, "cdx:pypi");
|
|
960
|
+
const publisherDrift = hasPublisherDrift(target, "cdx:pypi");
|
|
961
|
+
const maintainerSetDrift = hasMaintainerSetDrift(target, "cdx:pypi");
|
|
962
|
+
const partialMaintainerSetDrift = hasPartialIdentitySetDrift(
|
|
963
|
+
target,
|
|
964
|
+
"cdx:pypi",
|
|
965
|
+
);
|
|
966
|
+
const dormantReleaseGapAnomaly = hasDormantReleaseGapAnomaly(
|
|
967
|
+
target,
|
|
968
|
+
"cdx:pypi",
|
|
969
|
+
);
|
|
970
|
+
const compressedCadence = hasCompressedCadence(target, "cdx:pypi");
|
|
971
|
+
if (
|
|
972
|
+
target.version &&
|
|
973
|
+
isDefaultRegistry &&
|
|
974
|
+
!hasTrustedPublishing &&
|
|
975
|
+
!hasProvenanceEvidence &&
|
|
976
|
+
!uploaderVerified
|
|
977
|
+
) {
|
|
978
|
+
findings.push({
|
|
979
|
+
category: "package-integrity",
|
|
980
|
+
description:
|
|
981
|
+
"Default-registry PyPI packages without provenance or verified uploader context are weaker candidates for publisher-trust decisions.",
|
|
982
|
+
location: {
|
|
983
|
+
bomRef: target.bomRefs?.[0],
|
|
984
|
+
purl: target.purl,
|
|
985
|
+
},
|
|
986
|
+
message: `PyPI package '${target.name}@${target.version}' lacks registry-visible provenance and uploader verification signals.`,
|
|
987
|
+
mitigation:
|
|
988
|
+
"Prefer releases with provenance evidence or verified uploader metadata, especially for sensitive or newly introduced dependencies.",
|
|
989
|
+
ruleId: "PROV-002",
|
|
990
|
+
severity: "low",
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
if (
|
|
994
|
+
target.version &&
|
|
995
|
+
isDefaultRegistry &&
|
|
996
|
+
establishedPackage &&
|
|
997
|
+
recentRelease &&
|
|
998
|
+
!hasTrustedPublishing &&
|
|
999
|
+
!hasProvenanceEvidence &&
|
|
1000
|
+
!uploaderVerified
|
|
1001
|
+
) {
|
|
1002
|
+
findings.push({
|
|
1003
|
+
category: "package-integrity",
|
|
1004
|
+
description:
|
|
1005
|
+
"Very recent releases on mature packages can benefit from a short review window when provenance and uploader-verification signals are absent.",
|
|
1006
|
+
location: {
|
|
1007
|
+
bomRef: target.bomRefs?.[0],
|
|
1008
|
+
purl: target.purl,
|
|
1009
|
+
},
|
|
1010
|
+
message: `PyPI package '${target.name}@${target.version}' is a very recent release on an established package without provenance or uploader verification signals.`,
|
|
1011
|
+
mitigation:
|
|
1012
|
+
"Delay adoption briefly, compare the release to the previous known-good version, and prefer verified/provenance-backed uploads for sensitive dependencies.",
|
|
1013
|
+
ruleId: "PROV-005",
|
|
1014
|
+
severity: "low",
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
if (
|
|
1018
|
+
target.version &&
|
|
1019
|
+
isDefaultRegistry &&
|
|
1020
|
+
establishedPackage &&
|
|
1021
|
+
publisherDrift &&
|
|
1022
|
+
!hasTrustedPublishing &&
|
|
1023
|
+
!hasProvenanceEvidence &&
|
|
1024
|
+
!uploaderVerified
|
|
1025
|
+
) {
|
|
1026
|
+
findings.push({
|
|
1027
|
+
category: "package-integrity",
|
|
1028
|
+
description:
|
|
1029
|
+
"Uploader drift on established PyPI packages is usually a triage signal, but becomes more meaningful when provenance and verification are missing.",
|
|
1030
|
+
location: {
|
|
1031
|
+
bomRef: target.bomRefs?.[0],
|
|
1032
|
+
purl: target.purl,
|
|
1033
|
+
},
|
|
1034
|
+
message: `PyPI package '${target.name}@${target.version}' was uploaded by a different identity than the prior release and lacks provenance or uploader verification signals.`,
|
|
1035
|
+
mitigation:
|
|
1036
|
+
"Review the uploader change, compare the prior release uploader, and validate project ownership before upgrading critical dependencies.",
|
|
1037
|
+
ruleId: "PROV-006",
|
|
1038
|
+
severity: "low",
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
if (
|
|
1042
|
+
target.version &&
|
|
1043
|
+
isDefaultRegistry &&
|
|
1044
|
+
establishedPackage &&
|
|
1045
|
+
maintainerSetDrift &&
|
|
1046
|
+
!hasTrustedPublishing &&
|
|
1047
|
+
!hasProvenanceEvidence &&
|
|
1048
|
+
!uploaderVerified
|
|
1049
|
+
) {
|
|
1050
|
+
findings.push({
|
|
1051
|
+
category: "package-integrity",
|
|
1052
|
+
description:
|
|
1053
|
+
"Uploader-set drift on established PyPI packages is a triage signal when provenance and uploader verification are absent.",
|
|
1054
|
+
location: {
|
|
1055
|
+
bomRef: target.bomRefs?.[0],
|
|
1056
|
+
purl: target.purl,
|
|
1057
|
+
},
|
|
1058
|
+
message: `PyPI package '${target.name}@${target.version}' has a fully different uploader identity set than the prior release and lacks provenance or uploader verification signals.`,
|
|
1059
|
+
mitigation:
|
|
1060
|
+
"Review uploader transitions, compare the prior release uploader set, and validate project ownership before upgrading sensitive dependencies.",
|
|
1061
|
+
ruleId: "PROV-009",
|
|
1062
|
+
severity: "low",
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
if (
|
|
1066
|
+
target.version &&
|
|
1067
|
+
isDefaultRegistry &&
|
|
1068
|
+
establishedPackage &&
|
|
1069
|
+
partialMaintainerSetDrift &&
|
|
1070
|
+
!maintainerSetDrift &&
|
|
1071
|
+
!hasTrustedPublishing &&
|
|
1072
|
+
!hasProvenanceEvidence &&
|
|
1073
|
+
!uploaderVerified
|
|
1074
|
+
) {
|
|
1075
|
+
findings.push({
|
|
1076
|
+
category: "package-integrity",
|
|
1077
|
+
description:
|
|
1078
|
+
"Partial uploader-set drift is a low-severity review signal on established PyPI packages when provenance and uploader verification are absent.",
|
|
1079
|
+
location: {
|
|
1080
|
+
bomRef: target.bomRefs?.[0],
|
|
1081
|
+
purl: target.purl,
|
|
1082
|
+
},
|
|
1083
|
+
message: `PyPI package '${target.name}@${target.version}' retains only part of the prior uploader identity set and lacks provenance or uploader verification signals.`,
|
|
1084
|
+
mitigation:
|
|
1085
|
+
"Review which uploader identities changed, compare the release against the prior version, and validate project ownership before upgrading sensitive dependencies.",
|
|
1086
|
+
ruleId: "PROV-013",
|
|
1087
|
+
severity: "low",
|
|
1088
|
+
});
|
|
1089
|
+
}
|
|
1090
|
+
if (
|
|
1091
|
+
target.version &&
|
|
1092
|
+
isDefaultRegistry &&
|
|
1093
|
+
establishedPackage &&
|
|
1094
|
+
dormantReleaseGapAnomaly &&
|
|
1095
|
+
!hasTrustedPublishing &&
|
|
1096
|
+
!hasProvenanceEvidence &&
|
|
1097
|
+
!uploaderVerified
|
|
1098
|
+
) {
|
|
1099
|
+
findings.push({
|
|
1100
|
+
category: "package-integrity",
|
|
1101
|
+
description:
|
|
1102
|
+
"Established packages resurfacing after a long dormant gap benefit from extra review when provenance is weak.",
|
|
1103
|
+
location: {
|
|
1104
|
+
bomRef: target.bomRefs?.[0],
|
|
1105
|
+
purl: target.purl,
|
|
1106
|
+
},
|
|
1107
|
+
message: `PyPI package '${target.name}@${target.version}' followed an unusually long release gap and lacks provenance or uploader verification signals.`,
|
|
1108
|
+
mitigation:
|
|
1109
|
+
"Compare the release to the prior known-good version and review maintainership continuity before adopting after long dormancy.",
|
|
1110
|
+
ruleId: "PROV-010",
|
|
1111
|
+
severity: "low",
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
if (
|
|
1115
|
+
target.version &&
|
|
1116
|
+
isDefaultRegistry &&
|
|
1117
|
+
establishedPackage &&
|
|
1118
|
+
compressedCadence &&
|
|
1119
|
+
!hasTrustedPublishing &&
|
|
1120
|
+
!hasProvenanceEvidence &&
|
|
1121
|
+
!uploaderVerified
|
|
1122
|
+
) {
|
|
1123
|
+
findings.push({
|
|
1124
|
+
category: "package-integrity",
|
|
1125
|
+
description:
|
|
1126
|
+
"Materially faster-than-usual release timing is a low-severity triage signal on mature PyPI packages when provenance and uploader verification remain weak.",
|
|
1127
|
+
location: {
|
|
1128
|
+
bomRef: target.bomRefs?.[0],
|
|
1129
|
+
purl: target.purl,
|
|
1130
|
+
},
|
|
1131
|
+
message: `PyPI package '${target.name}@${target.version}' arrived materially faster than its prior release cadence and lacks provenance or uploader verification signals.`,
|
|
1132
|
+
mitigation:
|
|
1133
|
+
"Compare the release timing and contents against prior versions, then validate uploader continuity before rapid upgrades of sensitive dependencies.",
|
|
1134
|
+
ruleId: "PROV-014",
|
|
1135
|
+
severity: "low",
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
if (target.type === "cargo") {
|
|
1140
|
+
const yanked = getTargetProperty(target, "cdx:cargo:yanked") === "true";
|
|
1141
|
+
const establishedPackage = isEstablishedPackage(target, "cdx:cargo");
|
|
1142
|
+
const recentRelease = isRecentRelease(target, "cdx:cargo");
|
|
1143
|
+
const publisherDrift = hasPublisherDrift(target, "cdx:cargo");
|
|
1144
|
+
const dormantReleaseGapAnomaly = hasDormantReleaseGapAnomaly(
|
|
1145
|
+
target,
|
|
1146
|
+
"cdx:cargo",
|
|
1147
|
+
);
|
|
1148
|
+
const compressedCadence = hasCompressedCadence(target, "cdx:cargo");
|
|
1149
|
+
if (target.version && yanked) {
|
|
1150
|
+
findings.push({
|
|
1151
|
+
category: "package-integrity",
|
|
1152
|
+
description:
|
|
1153
|
+
"Yanked crates are removed from normal Cargo resolution and usually indicate a correctness, security, or publisher-action issue that deserves review before further adoption.",
|
|
1154
|
+
location: {
|
|
1155
|
+
bomRef: target.bomRefs?.[0],
|
|
1156
|
+
purl: target.purl,
|
|
1157
|
+
},
|
|
1158
|
+
message: `Cargo crate '${target.name}@${target.version}' has been yanked from crates.io.`,
|
|
1159
|
+
mitigation:
|
|
1160
|
+
"Prefer a non-yanked release and review the crate's publisher history and changelog before upgrading.",
|
|
1161
|
+
ruleId: "PROV-015",
|
|
1162
|
+
severity: "high",
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
if (
|
|
1166
|
+
target.version &&
|
|
1167
|
+
establishedPackage &&
|
|
1168
|
+
recentRelease &&
|
|
1169
|
+
!hasTrustedPublishing &&
|
|
1170
|
+
!hasProvenanceEvidence
|
|
1171
|
+
) {
|
|
1172
|
+
findings.push({
|
|
1173
|
+
category: "package-integrity",
|
|
1174
|
+
description:
|
|
1175
|
+
"Very recent releases on established crates benefit from a short review window when trusted publishing and provenance remain weak.",
|
|
1176
|
+
location: {
|
|
1177
|
+
bomRef: target.bomRefs?.[0],
|
|
1178
|
+
purl: target.purl,
|
|
1179
|
+
},
|
|
1180
|
+
message: `Cargo crate '${target.name}@${target.version}' is a very recent release on an established package without registry-visible provenance signals.`,
|
|
1181
|
+
mitigation:
|
|
1182
|
+
"Delay adoption briefly, compare the release to the prior version, and prefer trusted-publishing-backed releases for sensitive crates.",
|
|
1183
|
+
ruleId: "PROV-016",
|
|
1184
|
+
severity: "low",
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
if (
|
|
1188
|
+
target.version &&
|
|
1189
|
+
establishedPackage &&
|
|
1190
|
+
publisherDrift &&
|
|
1191
|
+
!hasTrustedPublishing &&
|
|
1192
|
+
!hasProvenanceEvidence
|
|
1193
|
+
) {
|
|
1194
|
+
findings.push({
|
|
1195
|
+
category: "package-integrity",
|
|
1196
|
+
description:
|
|
1197
|
+
"Publisher drift on established crates is often benign, but becomes more meaningful when provenance and trusted publishing are absent.",
|
|
1198
|
+
location: {
|
|
1199
|
+
bomRef: target.bomRefs?.[0],
|
|
1200
|
+
purl: target.purl,
|
|
1201
|
+
},
|
|
1202
|
+
message: `Cargo crate '${target.name}@${target.version}' was published by a different identity than the prior release and lacks registry-visible provenance signals.`,
|
|
1203
|
+
mitigation:
|
|
1204
|
+
"Review the publisher transition, compare the prior release metadata, and validate ownership before upgrading sensitive crates.",
|
|
1205
|
+
ruleId: "PROV-017",
|
|
1206
|
+
severity: "medium",
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
if (
|
|
1210
|
+
target.version &&
|
|
1211
|
+
establishedPackage &&
|
|
1212
|
+
(dormantReleaseGapAnomaly || compressedCadence) &&
|
|
1213
|
+
!hasTrustedPublishing &&
|
|
1214
|
+
!hasProvenanceEvidence
|
|
1215
|
+
) {
|
|
1216
|
+
findings.push({
|
|
1217
|
+
category: "package-integrity",
|
|
1218
|
+
description:
|
|
1219
|
+
"Release timing anomalies on established crates are low-noise triage signals when provenance remains weak.",
|
|
1220
|
+
location: {
|
|
1221
|
+
bomRef: target.bomRefs?.[0],
|
|
1222
|
+
purl: target.purl,
|
|
1223
|
+
},
|
|
1224
|
+
message: `Cargo crate '${target.name}@${target.version}' shows unusual release timing and lacks registry-visible provenance signals.`,
|
|
1225
|
+
mitigation:
|
|
1226
|
+
"Review the release diff and timing versus prior versions before rapidly adopting the new crate release.",
|
|
1227
|
+
ruleId: "PROV-018",
|
|
1228
|
+
severity: "low",
|
|
1229
|
+
});
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
return findings;
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
/**
|
|
1236
|
+
* Clone a repository into a deterministic workspace directory.
|
|
1237
|
+
*
|
|
1238
|
+
* @param {string} repoUrl repository URL
|
|
1239
|
+
* @param {string} cloneDir target clone directory
|
|
1240
|
+
* @param {string | undefined} gitRef git ref to checkout
|
|
1241
|
+
* @throws {Error} when gitRef is not a safe ref name
|
|
1242
|
+
* @returns {void}
|
|
1243
|
+
*/
|
|
1244
|
+
function cloneRepositoryToDir(repoUrl, cloneDir, gitRef) {
|
|
1245
|
+
const gitArgs = [
|
|
1246
|
+
"-c",
|
|
1247
|
+
"alias.clone=",
|
|
1248
|
+
"-c",
|
|
1249
|
+
"core.fsmonitor=false",
|
|
1250
|
+
"-c",
|
|
1251
|
+
"safe.bareRepository=explicit",
|
|
1252
|
+
"-c",
|
|
1253
|
+
"core.hooksPath=/dev/null",
|
|
1254
|
+
"clone",
|
|
1255
|
+
"--template=",
|
|
1256
|
+
"--depth",
|
|
1257
|
+
"1",
|
|
1258
|
+
"--",
|
|
1259
|
+
repoUrl,
|
|
1260
|
+
cloneDir,
|
|
1261
|
+
];
|
|
1262
|
+
if (gitRef) {
|
|
1263
|
+
if (!isSafeGitRefName(gitRef)) {
|
|
1264
|
+
const error = new Error("git ref contains unsupported characters");
|
|
1265
|
+
error.errorType = "clone";
|
|
1266
|
+
error.retryable = false;
|
|
1267
|
+
throw error;
|
|
1268
|
+
}
|
|
1269
|
+
const cloneIndex = gitArgs.indexOf("clone");
|
|
1270
|
+
gitArgs.splice(cloneIndex + 1, 0, "--branch", gitRef);
|
|
1271
|
+
}
|
|
1272
|
+
const result = hardenedGitCommand(gitArgs);
|
|
1273
|
+
if (result.status !== 0) {
|
|
1274
|
+
const stderr = result.stderr
|
|
1275
|
+
? result.stderr.toString()
|
|
1276
|
+
: "unknown git clone error";
|
|
1277
|
+
const error = new Error(stderr.trim());
|
|
1278
|
+
error.retryable =
|
|
1279
|
+
/(timed out|unable to connect|could not resolve host|network is unreachable|connection reset|connection refused|temporary failure|remote end hung up unexpectedly|http 5\d\d|tls|econnreset|econnrefused|etimedout)/i.test(
|
|
1280
|
+
stderr,
|
|
1281
|
+
);
|
|
1282
|
+
error.errorType = error.retryable ? "network" : "clone";
|
|
1283
|
+
throw error;
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
async function cloneRepositoryToDirWithRetry(repoUrl, cloneDir, gitRef) {
|
|
1288
|
+
let lastError;
|
|
1289
|
+
for (let attempt = 0; attempt <= CLONE_RETRY_DELAYS_MS.length; attempt += 1) {
|
|
1290
|
+
try {
|
|
1291
|
+
cloneRepositoryToDir(repoUrl, cloneDir, gitRef);
|
|
1292
|
+
return;
|
|
1293
|
+
} catch (error) {
|
|
1294
|
+
lastError = error;
|
|
1295
|
+
safeRmSync(cloneDir, { force: true, recursive: true });
|
|
1296
|
+
if (!error?.retryable || attempt >= CLONE_RETRY_DELAYS_MS.length) {
|
|
1297
|
+
break;
|
|
1298
|
+
}
|
|
1299
|
+
await sleep(CLONE_RETRY_DELAYS_MS[attempt]);
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
const sanitizedRepoUrl = sanitizeRemoteUrlForLogs(repoUrl);
|
|
1303
|
+
const message = lastError?.message || "unknown git clone error";
|
|
1304
|
+
const error = new Error(
|
|
1305
|
+
`Unable to clone '${sanitizedRepoUrl}' after ${CLONE_RETRY_DELAYS_MS.length + 1} attempt(s): ${message}`,
|
|
1306
|
+
);
|
|
1307
|
+
error.errorType = lastError?.errorType || "clone";
|
|
1308
|
+
error.retryable = Boolean(lastError?.retryable);
|
|
1309
|
+
throw error;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
/**
|
|
1313
|
+
* Reuse or create a checkout for a target repository.
|
|
1314
|
+
*
|
|
1315
|
+
* @param {object} target audit target
|
|
1316
|
+
* @param {object} resolution resolved repository metadata
|
|
1317
|
+
* @param {string | undefined} workspaceDir workspace directory
|
|
1318
|
+
* @param {string | undefined} gitRef git ref to checkout
|
|
1319
|
+
* @returns {{ cleanup: boolean, cloneDir: string, reused: boolean }} checkout info
|
|
1320
|
+
*/
|
|
1321
|
+
async function ensureCheckout(target, resolution, workspaceDir, gitRef) {
|
|
1322
|
+
if (!workspaceDir) {
|
|
1323
|
+
const cloneDir = safeMkdtempSync(
|
|
1324
|
+
join(getTmpDir(), `${targetSlug(target)}-`),
|
|
1325
|
+
);
|
|
1326
|
+
await cloneRepositoryToDirWithRetry(resolution.repoUrl, cloneDir, gitRef);
|
|
1327
|
+
return {
|
|
1328
|
+
cleanup: true,
|
|
1329
|
+
cloneDir,
|
|
1330
|
+
reused: false,
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
const resolvedWorkspaceDir = resolve(workspaceDir);
|
|
1334
|
+
if (!safeExistsSync(resolvedWorkspaceDir)) {
|
|
1335
|
+
safeMkdirSync(resolvedWorkspaceDir, { recursive: true });
|
|
1336
|
+
}
|
|
1337
|
+
const cloneDir = join(resolvedWorkspaceDir, targetSlug(target));
|
|
1338
|
+
if (safeExistsSync(join(cloneDir, ".git"))) {
|
|
1339
|
+
return {
|
|
1340
|
+
cleanup: false,
|
|
1341
|
+
cloneDir,
|
|
1342
|
+
reused: true,
|
|
1343
|
+
};
|
|
1344
|
+
}
|
|
1345
|
+
if (safeExistsSync(cloneDir)) {
|
|
1346
|
+
safeRmSync(cloneDir, { force: true, recursive: true });
|
|
1347
|
+
}
|
|
1348
|
+
await cloneRepositoryToDirWithRetry(resolution.repoUrl, cloneDir, gitRef);
|
|
1349
|
+
return {
|
|
1350
|
+
cleanup: false,
|
|
1351
|
+
cloneDir,
|
|
1352
|
+
reused: false,
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
/**
|
|
1357
|
+
* Extract an expected package name from Python packaging metadata.
|
|
1358
|
+
*
|
|
1359
|
+
* @param {string} filePath metadata file path
|
|
1360
|
+
* @returns {string | undefined} discovered package name
|
|
1361
|
+
*/
|
|
1362
|
+
function readPythonPackageName(filePath) {
|
|
1363
|
+
let fileContent;
|
|
1364
|
+
try {
|
|
1365
|
+
fileContent = readFileSync(filePath, "utf8");
|
|
1366
|
+
} catch {
|
|
1367
|
+
return undefined;
|
|
1368
|
+
}
|
|
1369
|
+
const patterns = [
|
|
1370
|
+
/(^|\n)\s*name\s*=\s*["']([^"'\n]+)["']/m,
|
|
1371
|
+
/(^|\n)\s*name\s*=\s*([^\n#]+)/m,
|
|
1372
|
+
/setup\s*\([^)]*name\s*=\s*["']([^"']+)["']/ms,
|
|
1373
|
+
];
|
|
1374
|
+
for (const pattern of patterns) {
|
|
1375
|
+
const match = fileContent.match(pattern);
|
|
1376
|
+
if (!match) {
|
|
1377
|
+
continue;
|
|
1378
|
+
}
|
|
1379
|
+
const packageName = (match[2] || match[1] || "").trim();
|
|
1380
|
+
if (packageName) {
|
|
1381
|
+
return packageName;
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
return undefined;
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
/**
|
|
1388
|
+
* Resolve the most specific Python package directory inside a cloned repo.
|
|
1389
|
+
*
|
|
1390
|
+
* @param {string} cloneDir cloned repository root
|
|
1391
|
+
* @param {object} target audit target
|
|
1392
|
+
* @returns {{ confidence: string, scanDir: string }} selected directory and confidence
|
|
1393
|
+
*/
|
|
1394
|
+
export function resolvePythonSourceDirectory(cloneDir, target) {
|
|
1395
|
+
const normalizedTargetName = normalizePackageName(target.name);
|
|
1396
|
+
const queue = [cloneDir];
|
|
1397
|
+
const matches = [];
|
|
1398
|
+
while (queue.length) {
|
|
1399
|
+
const currentDir = queue.shift();
|
|
1400
|
+
let entries = [];
|
|
1401
|
+
try {
|
|
1402
|
+
entries = readdirSync(currentDir, { withFileTypes: true });
|
|
1403
|
+
} catch {
|
|
1404
|
+
continue;
|
|
1405
|
+
}
|
|
1406
|
+
for (const entry of entries) {
|
|
1407
|
+
const entryPath = join(currentDir, entry.name);
|
|
1408
|
+
if (entry.isDirectory()) {
|
|
1409
|
+
if (!PYTHON_SKIP_DIRS.has(entry.name)) {
|
|
1410
|
+
queue.push(entryPath);
|
|
1411
|
+
}
|
|
1412
|
+
continue;
|
|
1413
|
+
}
|
|
1414
|
+
if (!entry.isFile() || !PYTHON_METADATA_FILES.includes(entry.name)) {
|
|
1415
|
+
continue;
|
|
1416
|
+
}
|
|
1417
|
+
const packageName = readPythonPackageName(entryPath);
|
|
1418
|
+
if (normalizePackageName(packageName) === normalizedTargetName) {
|
|
1419
|
+
matches.push(currentDir);
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
if (!matches.length) {
|
|
1424
|
+
return {
|
|
1425
|
+
confidence: "low",
|
|
1426
|
+
scanDir: cloneDir,
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
matches.sort((left, right) => left.length - right.length);
|
|
1430
|
+
return {
|
|
1431
|
+
confidence: matches[0] === cloneDir ? "medium" : "high",
|
|
1432
|
+
scanDir: matches[0],
|
|
1433
|
+
};
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
/**
|
|
1437
|
+
* Remove cdxgen config files from a directory the audit is about to scan.
|
|
1438
|
+
*
|
|
1439
|
+
* Audit mode clones repositories cdxgen picked itself, from registry metadata,
|
|
1440
|
+
* so nobody approved the contents. A config file in such a checkout would let
|
|
1441
|
+
* the audited repository choose its own scan options, which is the audited
|
|
1442
|
+
* repository deciding how it gets audited.
|
|
1443
|
+
*
|
|
1444
|
+
* @param {string} dir directory to clear
|
|
1445
|
+
* @returns {void}
|
|
1446
|
+
*/
|
|
1447
|
+
function rejectProjectConfigFiles(dir) {
|
|
1448
|
+
if (!dir) {
|
|
1449
|
+
return;
|
|
1450
|
+
}
|
|
1451
|
+
for (const configFileName of PROJECT_CONFIG_FILENAMES) {
|
|
1452
|
+
const configPath = join(dir, configFileName);
|
|
1453
|
+
if (!safeExistsSync(configPath)) {
|
|
1454
|
+
continue;
|
|
1455
|
+
}
|
|
1456
|
+
safeRmSync(configPath, { force: true });
|
|
1457
|
+
console.warn(
|
|
1458
|
+
`Ignoring the config file '${configFileName}' found in the cloned repository ${dir}.`,
|
|
1459
|
+
);
|
|
1460
|
+
recordActivity({
|
|
1461
|
+
kind: "inspect",
|
|
1462
|
+
reason:
|
|
1463
|
+
"Removed a cdxgen config file from an audit checkout. Cloned repositories do not configure their own audit.",
|
|
1464
|
+
status: "completed",
|
|
1465
|
+
target: configPath,
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
/**
|
|
1471
|
+
* Resolve the most appropriate scan directory for a cloned target repository.
|
|
1472
|
+
*
|
|
1473
|
+
* @param {string} cloneDir cloned repository root
|
|
1474
|
+
* @param {object} target audit target
|
|
1475
|
+
* @param {object} resolution repository resolution metadata
|
|
1476
|
+
* @returns {{ confidence: string, scanDir: string }} selected directory and confidence
|
|
1477
|
+
*/
|
|
1478
|
+
export function resolveTargetSourceDirectory(cloneDir, target, resolution) {
|
|
1479
|
+
const selection = selectTargetSourceDirectory(cloneDir, target, resolution);
|
|
1480
|
+
rejectProjectConfigFiles(cloneDir);
|
|
1481
|
+
if (selection.scanDir !== cloneDir) {
|
|
1482
|
+
rejectProjectConfigFiles(selection.scanDir);
|
|
1483
|
+
}
|
|
1484
|
+
return selection;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
/**
|
|
1488
|
+
* Pick the scan directory for a cloned target repository.
|
|
1489
|
+
*
|
|
1490
|
+
* @param {string} cloneDir cloned repository root
|
|
1491
|
+
* @param {object} target audit target
|
|
1492
|
+
* @param {object} resolution repository resolution metadata
|
|
1493
|
+
* @returns {{ confidence: string, scanDir: string }} selected directory and confidence
|
|
1494
|
+
*/
|
|
1495
|
+
function selectTargetSourceDirectory(cloneDir, target, resolution) {
|
|
1496
|
+
if (target.type === "npm") {
|
|
1497
|
+
const scanDir = resolvePurlSourceDirectory(cloneDir, resolution);
|
|
1498
|
+
if (!scanDir) {
|
|
1499
|
+
return {
|
|
1500
|
+
confidence: "medium",
|
|
1501
|
+
scanDir: cloneDir,
|
|
1502
|
+
};
|
|
1503
|
+
}
|
|
1504
|
+
return {
|
|
1505
|
+
confidence: scanDir === cloneDir ? "medium" : "high",
|
|
1506
|
+
scanDir,
|
|
1507
|
+
};
|
|
1508
|
+
}
|
|
1509
|
+
if (target.type === "pypi") {
|
|
1510
|
+
return resolvePythonSourceDirectory(cloneDir, target);
|
|
1511
|
+
}
|
|
1512
|
+
return {
|
|
1513
|
+
confidence: "low",
|
|
1514
|
+
scanDir: cloneDir,
|
|
1515
|
+
};
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
function collectPythonHeuristicFiles(scanDir) {
|
|
1519
|
+
const candidates = [];
|
|
1520
|
+
const queue = [scanDir];
|
|
1521
|
+
while (queue.length && candidates.length < PYTHON_HEURISTIC_FILE_LIMIT) {
|
|
1522
|
+
const currentDir = queue.shift();
|
|
1523
|
+
let entries = [];
|
|
1524
|
+
try {
|
|
1525
|
+
entries = readdirSync(currentDir, { withFileTypes: true });
|
|
1526
|
+
} catch {
|
|
1527
|
+
continue;
|
|
1528
|
+
}
|
|
1529
|
+
for (const entry of entries) {
|
|
1530
|
+
const entryPath = join(currentDir, entry.name);
|
|
1531
|
+
if (entry.isDirectory()) {
|
|
1532
|
+
if (!PYTHON_SKIP_DIRS.has(entry.name)) {
|
|
1533
|
+
queue.push(entryPath);
|
|
1534
|
+
}
|
|
1535
|
+
continue;
|
|
1536
|
+
}
|
|
1537
|
+
if (
|
|
1538
|
+
entry.isFile() &&
|
|
1539
|
+
PYTHON_HEURISTIC_FILENAMES.has(entry.name) &&
|
|
1540
|
+
candidates.length < PYTHON_HEURISTIC_FILE_LIMIT
|
|
1541
|
+
) {
|
|
1542
|
+
candidates.push(entryPath);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
}
|
|
1546
|
+
return candidates;
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
function inspectPythonHeuristicFile(filePath) {
|
|
1550
|
+
let fileSize;
|
|
1551
|
+
try {
|
|
1552
|
+
fileSize = statSync(filePath).size;
|
|
1553
|
+
} catch {
|
|
1554
|
+
return undefined;
|
|
1555
|
+
}
|
|
1556
|
+
if (fileSize > PYTHON_HEURISTIC_MAX_FILE_BYTES) {
|
|
1557
|
+
return undefined;
|
|
1558
|
+
}
|
|
1559
|
+
let fileContent;
|
|
1560
|
+
try {
|
|
1561
|
+
fileContent = readFileSync(filePath, "utf8");
|
|
1562
|
+
} catch {
|
|
1563
|
+
return undefined;
|
|
1564
|
+
}
|
|
1565
|
+
const indicators = [];
|
|
1566
|
+
if (PYTHON_EXECUTION_PATTERN.test(fileContent)) {
|
|
1567
|
+
indicators.push("process-or-dynamic-execution");
|
|
1568
|
+
}
|
|
1569
|
+
if (PYTHON_NETWORK_PATTERN.test(fileContent)) {
|
|
1570
|
+
indicators.push("network-access");
|
|
1571
|
+
}
|
|
1572
|
+
if (PYTHON_OBFUSCATION_PATTERN.test(fileContent)) {
|
|
1573
|
+
indicators.push("encoded-loader");
|
|
1574
|
+
}
|
|
1575
|
+
if (
|
|
1576
|
+
filePath.endsWith("setup.py") &&
|
|
1577
|
+
PYTHON_SETUP_CMDCLASS_PATTERN.test(fileContent)
|
|
1578
|
+
) {
|
|
1579
|
+
indicators.push("setup-cmdclass");
|
|
1580
|
+
}
|
|
1581
|
+
return indicators.length ? indicators : undefined;
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
/**
|
|
1585
|
+
* Build shallow predictive findings for suspicious Python packaging files.
|
|
1586
|
+
*
|
|
1587
|
+
* Phase 1 intentionally focuses on high-signal packaging surfaces (`setup.py`
|
|
1588
|
+
* and package `__init__.py`) until deeper Python static analysis is added.
|
|
1589
|
+
*
|
|
1590
|
+
* @param {string} scanDir cloned repository scan directory
|
|
1591
|
+
* @param {object} target audit target
|
|
1592
|
+
* @returns {object[]} predictive findings
|
|
1593
|
+
*/
|
|
1594
|
+
export function buildPythonSourceHeuristicFindings(scanDir, target) {
|
|
1595
|
+
if (!scanDir || target?.type !== "pypi") {
|
|
1596
|
+
return [];
|
|
1597
|
+
}
|
|
1598
|
+
const findings = [];
|
|
1599
|
+
collectPythonHeuristicFiles(scanDir).forEach((filePath) => {
|
|
1600
|
+
const indicators = inspectPythonHeuristicFile(filePath);
|
|
1601
|
+
if (!indicators?.length) {
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
const relativeFile = relative(scanDir, filePath) || filePath;
|
|
1605
|
+
if (
|
|
1606
|
+
relativeFile.endsWith("setup.py") &&
|
|
1607
|
+
indicators.includes("encoded-loader") &&
|
|
1608
|
+
(indicators.includes("process-or-dynamic-execution") ||
|
|
1609
|
+
indicators.includes("network-access") ||
|
|
1610
|
+
indicators.includes("setup-cmdclass"))
|
|
1611
|
+
) {
|
|
1612
|
+
findings.push({
|
|
1613
|
+
category: "package-integrity",
|
|
1614
|
+
description:
|
|
1615
|
+
"setup.py contains encoded or dynamically executed packaging logic, which is a strong signal of install-time code injection risk.",
|
|
1616
|
+
evidence: {
|
|
1617
|
+
indicators: indicators.join(","),
|
|
1618
|
+
},
|
|
1619
|
+
location: {
|
|
1620
|
+
bomRef: target.bomRefs?.[0],
|
|
1621
|
+
file: relativeFile,
|
|
1622
|
+
purl: target.purl,
|
|
1623
|
+
},
|
|
1624
|
+
message: `PyPI package '${target.name}@${target.version}' contains suspicious setup.py execution patterns in '${relativeFile}'.`,
|
|
1625
|
+
mitigation:
|
|
1626
|
+
"Inspect setup.py before installation, compare against prior releases, and avoid executing packaging hooks until the encoded or dynamic logic is explained and validated.",
|
|
1627
|
+
ruleId: "PYSRC-001",
|
|
1628
|
+
severity: "high",
|
|
1629
|
+
});
|
|
1630
|
+
return;
|
|
1631
|
+
}
|
|
1632
|
+
if (
|
|
1633
|
+
relativeFile.endsWith("__init__.py") &&
|
|
1634
|
+
(indicators.includes("process-or-dynamic-execution") ||
|
|
1635
|
+
indicators.includes("network-access"))
|
|
1636
|
+
) {
|
|
1637
|
+
findings.push({
|
|
1638
|
+
category: "package-integrity",
|
|
1639
|
+
description:
|
|
1640
|
+
"__init__.py appears to perform process spawning, dynamic execution, or network access during import, which is unusual for a package initializer.",
|
|
1641
|
+
evidence: {
|
|
1642
|
+
indicators: indicators.join(","),
|
|
1643
|
+
},
|
|
1644
|
+
location: {
|
|
1645
|
+
bomRef: target.bomRefs?.[0],
|
|
1646
|
+
file: relativeFile,
|
|
1647
|
+
purl: target.purl,
|
|
1648
|
+
},
|
|
1649
|
+
message: `PyPI package '${target.name}@${target.version}' contains suspicious import-time logic in '${relativeFile}'.`,
|
|
1650
|
+
mitigation:
|
|
1651
|
+
"Review the initializer for import-time side effects, compare it to prior versions, and quarantine the release until maintainers confirm the added behavior.",
|
|
1652
|
+
ruleId: "PYSRC-002",
|
|
1653
|
+
severity: indicators.includes("encoded-loader") ? "high" : "medium",
|
|
1654
|
+
});
|
|
1655
|
+
}
|
|
1656
|
+
});
|
|
1657
|
+
return findings;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
/**
|
|
1661
|
+
* Build cdxgen options for a child source scan.
|
|
1662
|
+
*
|
|
1663
|
+
* @param {object} options CLI options
|
|
1664
|
+
* @param {object} target audit target
|
|
1665
|
+
* @returns {object} createBom options
|
|
1666
|
+
*/
|
|
1667
|
+
function buildChildOptions(options, target) {
|
|
1668
|
+
const projectType =
|
|
1669
|
+
target.type === "npm"
|
|
1670
|
+
? ["js"]
|
|
1671
|
+
: target.type === "pypi"
|
|
1672
|
+
? ["py"]
|
|
1673
|
+
: target.type === "cargo"
|
|
1674
|
+
? ["cargo", "github"]
|
|
1675
|
+
: [target.type];
|
|
1676
|
+
return {
|
|
1677
|
+
deep: true,
|
|
1678
|
+
failOnError: false,
|
|
1679
|
+
filePath: options.workspaceDir || process.cwd(),
|
|
1680
|
+
includeFormulation: true,
|
|
1681
|
+
installDeps: false,
|
|
1682
|
+
multiProject: true,
|
|
1683
|
+
profile: "threat-modeling",
|
|
1684
|
+
projectType,
|
|
1685
|
+
specVersion: DEFAULT_CDX_SPEC_VERSION,
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
function normalizeFindingFile(filePath) {
|
|
1690
|
+
return String(filePath)
|
|
1691
|
+
.replace(/\\/g, "/")
|
|
1692
|
+
.replace(/\/+/g, "/")
|
|
1693
|
+
.replace(/^\.\//, "");
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
function sourceFindingKey(finding) {
|
|
1697
|
+
if (!finding?.ruleId || !finding?.location?.file) {
|
|
1698
|
+
return undefined;
|
|
1699
|
+
}
|
|
1700
|
+
return `${finding.ruleId}|${normalizeFindingFile(finding.location.file)}`;
|
|
1701
|
+
}
|
|
1702
|
+
|
|
1703
|
+
/**
|
|
1704
|
+
* Recheck high/critical findings against the default branch to identify issues
|
|
1705
|
+
* that have already been fixed upstream. This reduces false positives caused by
|
|
1706
|
+
* scanning a version-specific tag where an issue existed but has since been
|
|
1707
|
+
* resolved in the project's main development line.
|
|
1708
|
+
*
|
|
1709
|
+
* @param {object[]} findings findings from version-specific scan
|
|
1710
|
+
* @param {object} target audit target
|
|
1711
|
+
* @param {object} resolution repository resolution metadata
|
|
1712
|
+
* @param {string[]} categories active audit categories
|
|
1713
|
+
* @param {object} options CLI options
|
|
1714
|
+
* @returns {Promise<{findings: object[], fixedInDefaultBranch: number}|undefined>}
|
|
1715
|
+
*/
|
|
1716
|
+
async function recheckFindingsAgainstDefaultBranch(
|
|
1717
|
+
findings,
|
|
1718
|
+
target,
|
|
1719
|
+
resolution,
|
|
1720
|
+
categories,
|
|
1721
|
+
options,
|
|
1722
|
+
) {
|
|
1723
|
+
let defaultBranchDir;
|
|
1724
|
+
try {
|
|
1725
|
+
defaultBranchDir = safeMkdtempSync(
|
|
1726
|
+
join(getTmpDir(), `${targetSlug(target)}-default-`),
|
|
1727
|
+
);
|
|
1728
|
+
await cloneRepositoryToDirWithRetry(
|
|
1729
|
+
resolution.repoUrl,
|
|
1730
|
+
defaultBranchDir,
|
|
1731
|
+
undefined,
|
|
1732
|
+
);
|
|
1733
|
+
const defaultSourceSelection = resolveTargetSourceDirectory(
|
|
1734
|
+
defaultBranchDir,
|
|
1735
|
+
target,
|
|
1736
|
+
resolution,
|
|
1737
|
+
);
|
|
1738
|
+
const childOptions = {
|
|
1739
|
+
...buildChildOptions(options, target),
|
|
1740
|
+
executeOsQuery,
|
|
1741
|
+
};
|
|
1742
|
+
const bomNSData =
|
|
1743
|
+
(await createBom(defaultSourceSelection.scanDir, childOptions)) || {};
|
|
1744
|
+
if (!bomNSData?.bomJson) {
|
|
1745
|
+
return undefined;
|
|
1746
|
+
}
|
|
1747
|
+
const processedBomNSData = postProcess(
|
|
1748
|
+
bomNSData,
|
|
1749
|
+
childOptions,
|
|
1750
|
+
defaultSourceSelection.scanDir,
|
|
1751
|
+
);
|
|
1752
|
+
const defaultBranchFindings = await auditBom(processedBomNSData.bomJson, {
|
|
1753
|
+
bomAuditCategories: categories.join(","),
|
|
1754
|
+
bomAuditMinSeverity: options.minSeverity || "low",
|
|
1755
|
+
bomAuditRulesDir: options.rulesDir,
|
|
1756
|
+
getBomWithOras,
|
|
1757
|
+
});
|
|
1758
|
+
const defaultBranchSourceFindings = defaultBranchFindings.concat(
|
|
1759
|
+
buildPythonSourceHeuristicFindings(
|
|
1760
|
+
defaultSourceSelection.scanDir,
|
|
1761
|
+
target,
|
|
1762
|
+
),
|
|
1763
|
+
);
|
|
1764
|
+
// Build a set of finding signatures present in the default branch
|
|
1765
|
+
const defaultBranchFindingKeys = new Set();
|
|
1766
|
+
for (const finding of defaultBranchSourceFindings) {
|
|
1767
|
+
const key = sourceFindingKey(finding);
|
|
1768
|
+
if (key) {
|
|
1769
|
+
defaultBranchFindingKeys.add(key);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
// Retain findings that are still present in the default branch or are
|
|
1773
|
+
// contextual/heuristic findings (not from rule evaluation against source).
|
|
1774
|
+
// Source-derived findings have a location.file pointing to a repo file,
|
|
1775
|
+
// while contextual findings reference purls or bomRefs only.
|
|
1776
|
+
const retainedFindings = [];
|
|
1777
|
+
let fixedCount = 0;
|
|
1778
|
+
for (const finding of findings) {
|
|
1779
|
+
const isSourceDerivedFinding = Boolean(finding?.location?.file);
|
|
1780
|
+
if (!isSourceDerivedFinding) {
|
|
1781
|
+
// Contextual/heuristic findings are not source-dependent
|
|
1782
|
+
retainedFindings.push(finding);
|
|
1783
|
+
continue;
|
|
1784
|
+
}
|
|
1785
|
+
if (defaultBranchFindingKeys.has(sourceFindingKey(finding))) {
|
|
1786
|
+
retainedFindings.push(finding);
|
|
1787
|
+
} else {
|
|
1788
|
+
fixedCount += 1;
|
|
1789
|
+
thoughtLog("Finding fixed in default branch, removing.", {
|
|
1790
|
+
ruleId: finding.ruleId,
|
|
1791
|
+
file: finding?.location?.file,
|
|
1792
|
+
purl: target.purl,
|
|
1793
|
+
});
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
if (fixedCount === 0) {
|
|
1797
|
+
return undefined;
|
|
1798
|
+
}
|
|
1799
|
+
return {
|
|
1800
|
+
findings: retainedFindings,
|
|
1801
|
+
fixedInDefaultBranch: fixedCount,
|
|
1802
|
+
};
|
|
1803
|
+
} catch (error) {
|
|
1804
|
+
const errorMessage = error?.message ?? String(error);
|
|
1805
|
+
thoughtLog("Default branch recheck failed, keeping original findings.", {
|
|
1806
|
+
error: errorMessage,
|
|
1807
|
+
errorType: error?.errorType,
|
|
1808
|
+
purl: target.purl,
|
|
1809
|
+
});
|
|
1810
|
+
return undefined;
|
|
1811
|
+
} finally {
|
|
1812
|
+
if (defaultBranchDir) {
|
|
1813
|
+
cleanupSourceDir(defaultBranchDir);
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
/**
|
|
1819
|
+
* Analyze a single purl target by generating a child SBOM and auditing it.
|
|
1820
|
+
*
|
|
1821
|
+
* @param {object} target audit target
|
|
1822
|
+
* @param {object} options CLI options
|
|
1823
|
+
* @returns {Promise<object>} analyzed target result
|
|
1824
|
+
*/
|
|
1825
|
+
export async function auditTarget(target, options) {
|
|
1826
|
+
const categories = options.categories?.length
|
|
1827
|
+
? options.categories
|
|
1828
|
+
: DEFAULT_AUDIT_CATEGORIES;
|
|
1829
|
+
const targetIndex = options._targetIndex || 0;
|
|
1830
|
+
const targetTotal = options._targetTotal || 0;
|
|
1831
|
+
const targetLabel = formatTargetLabel(target);
|
|
1832
|
+
const originalFetchPackageMetadata = readEnvironmentVariable(
|
|
1833
|
+
"CDXGEN_FETCH_PKG_METADATA",
|
|
1834
|
+
);
|
|
1835
|
+
let checkout;
|
|
1836
|
+
let processedBomJson;
|
|
1837
|
+
let resolution;
|
|
1838
|
+
let sourceSelection;
|
|
1839
|
+
let cacheHit = false;
|
|
1840
|
+
let sanitizedRepoUrl;
|
|
1841
|
+
let versionMatched = false;
|
|
1842
|
+
try {
|
|
1843
|
+
const cachedChildBom = loadCachedChildBom(options.workspaceDir, target);
|
|
1844
|
+
if (cachedChildBom) {
|
|
1845
|
+
cacheHit = true;
|
|
1846
|
+
processedBomJson = cachedChildBom.bomJson;
|
|
1847
|
+
resolution = cachedChildBom.resolution;
|
|
1848
|
+
sanitizedRepoUrl = cachedChildBom.repoUrl;
|
|
1849
|
+
sourceSelection = {
|
|
1850
|
+
confidence: cachedChildBom.sourceDirectoryConfidence,
|
|
1851
|
+
scanDir: cachedChildBom.scanDir,
|
|
1852
|
+
};
|
|
1853
|
+
versionMatched = cachedChildBom.versionMatched;
|
|
1854
|
+
emitProgress(options, {
|
|
1855
|
+
index: targetIndex,
|
|
1856
|
+
label: targetLabel,
|
|
1857
|
+
target,
|
|
1858
|
+
total: targetTotal,
|
|
1859
|
+
type: "target:stage",
|
|
1860
|
+
stage: "reusing cached child SBOM",
|
|
1861
|
+
});
|
|
1862
|
+
} else {
|
|
1863
|
+
emitProgress(options, {
|
|
1864
|
+
index: targetIndex,
|
|
1865
|
+
label: targetLabel,
|
|
1866
|
+
target,
|
|
1867
|
+
total: targetTotal,
|
|
1868
|
+
type: "target:stage",
|
|
1869
|
+
stage: "resolving repository metadata",
|
|
1870
|
+
});
|
|
1871
|
+
resolution = await resolveGitUrlFromPurl(target.purl, {
|
|
1872
|
+
fetchPomXmlAsJson,
|
|
1873
|
+
});
|
|
1874
|
+
if (!resolution?.repoUrl) {
|
|
1875
|
+
return persistAuditArtifacts(
|
|
1876
|
+
{
|
|
1877
|
+
assessment: scoreTargetRisk([], target, {
|
|
1878
|
+
skipReason:
|
|
1879
|
+
"Unable to resolve repository URL from purl metadata.",
|
|
1880
|
+
}),
|
|
1881
|
+
error: "Unable to resolve repository URL from purl metadata.",
|
|
1882
|
+
findings: [],
|
|
1883
|
+
resolution,
|
|
1884
|
+
status: "skipped",
|
|
1885
|
+
target,
|
|
1886
|
+
},
|
|
1887
|
+
options,
|
|
1888
|
+
);
|
|
1889
|
+
}
|
|
1890
|
+
sanitizedRepoUrl = sanitizeRemoteUrlForLogs(resolution.repoUrl);
|
|
1891
|
+
thoughtLog("Preparing predictive audit target.", {
|
|
1892
|
+
purl: target.purl,
|
|
1893
|
+
repoUrl: sanitizedRepoUrl,
|
|
1894
|
+
});
|
|
1895
|
+
const gitRef = findGitRefForPurlVersion(resolution.repoUrl, resolution);
|
|
1896
|
+
versionMatched = Boolean(gitRef);
|
|
1897
|
+
emitProgress(options, {
|
|
1898
|
+
index: targetIndex,
|
|
1899
|
+
label: targetLabel,
|
|
1900
|
+
target,
|
|
1901
|
+
total: targetTotal,
|
|
1902
|
+
type: "target:stage",
|
|
1903
|
+
stage: gitRef ? `cloning source at ref ${gitRef}` : "cloning source",
|
|
1904
|
+
});
|
|
1905
|
+
checkout = await ensureCheckout(
|
|
1906
|
+
target,
|
|
1907
|
+
resolution,
|
|
1908
|
+
options.workspaceDir,
|
|
1909
|
+
gitRef,
|
|
1910
|
+
);
|
|
1911
|
+
sourceSelection = resolveTargetSourceDirectory(
|
|
1912
|
+
checkout.cloneDir,
|
|
1913
|
+
target,
|
|
1914
|
+
resolution,
|
|
1915
|
+
);
|
|
1916
|
+
const childOptions = {
|
|
1917
|
+
...buildChildOptions(options, target),
|
|
1918
|
+
executeOsQuery,
|
|
1919
|
+
};
|
|
1920
|
+
process.env.CDXGEN_FETCH_PKG_METADATA = "true";
|
|
1921
|
+
emitProgress(options, {
|
|
1922
|
+
index: targetIndex,
|
|
1923
|
+
label: targetLabel,
|
|
1924
|
+
target,
|
|
1925
|
+
total: targetTotal,
|
|
1926
|
+
type: "target:stage",
|
|
1927
|
+
stage: "generating child SBOM",
|
|
1928
|
+
});
|
|
1929
|
+
const bomNSData =
|
|
1930
|
+
(await createBom(sourceSelection.scanDir, childOptions)) || {};
|
|
1931
|
+
if (!bomNSData?.bomJson) {
|
|
1932
|
+
return persistAuditArtifacts(
|
|
1933
|
+
{
|
|
1934
|
+
assessment: scoreTargetRisk([], target, {
|
|
1935
|
+
errorMessage:
|
|
1936
|
+
"Unable to generate a child SBOM for the resolved source repository.",
|
|
1937
|
+
scanError: true,
|
|
1938
|
+
}),
|
|
1939
|
+
error:
|
|
1940
|
+
"Unable to generate a child SBOM for the resolved source repository.",
|
|
1941
|
+
errorType: "sbom-generation",
|
|
1942
|
+
findings: [],
|
|
1943
|
+
repoUrl: sanitizedRepoUrl,
|
|
1944
|
+
resolution,
|
|
1945
|
+
status: "error",
|
|
1946
|
+
target,
|
|
1947
|
+
},
|
|
1948
|
+
options,
|
|
1949
|
+
);
|
|
1950
|
+
}
|
|
1951
|
+
const processedBomNSData = postProcess(
|
|
1952
|
+
bomNSData,
|
|
1953
|
+
childOptions,
|
|
1954
|
+
sourceSelection.scanDir,
|
|
1955
|
+
);
|
|
1956
|
+
processedBomJson = processedBomNSData.bomJson;
|
|
1957
|
+
writeCachedChildBom(options.workspaceDir, target, {
|
|
1958
|
+
bomJson: processedBomJson,
|
|
1959
|
+
repoUrl: sanitizedRepoUrl,
|
|
1960
|
+
resolution,
|
|
1961
|
+
scanDir: sourceSelection.scanDir,
|
|
1962
|
+
sourceDirectoryConfidence: sourceSelection.confidence,
|
|
1963
|
+
versionMatched,
|
|
1964
|
+
});
|
|
1965
|
+
}
|
|
1966
|
+
emitProgress(options, {
|
|
1967
|
+
index: targetIndex,
|
|
1968
|
+
label: targetLabel,
|
|
1969
|
+
target,
|
|
1970
|
+
total: targetTotal,
|
|
1971
|
+
type: "target:stage",
|
|
1972
|
+
stage: "evaluating audit rules",
|
|
1973
|
+
});
|
|
1974
|
+
const findings = await auditBom(processedBomJson, {
|
|
1975
|
+
bomAuditCategories: categories.join(","),
|
|
1976
|
+
bomAuditMinSeverity: options.minSeverity || "low",
|
|
1977
|
+
bomAuditRulesDir: options.rulesDir,
|
|
1978
|
+
getBomWithOras,
|
|
1979
|
+
});
|
|
1980
|
+
// Evaluate the license policy against this dependency's child SBOM so
|
|
1981
|
+
// source-derived (and deeper transitive) licenses are covered, not just the
|
|
1982
|
+
// licenses declared in the parent BOM.
|
|
1983
|
+
const licenseViolations = options._licensePolicy
|
|
1984
|
+
? collectPolicyViolations(processedBomJson, options._licensePolicy, {
|
|
1985
|
+
includeWarnings: true,
|
|
1986
|
+
})
|
|
1987
|
+
: [];
|
|
1988
|
+
const contextualFindings = buildTargetContextFindings(target);
|
|
1989
|
+
const pythonSourceFindings = buildPythonSourceHeuristicFindings(
|
|
1990
|
+
sourceSelection.scanDir,
|
|
1991
|
+
target,
|
|
1992
|
+
);
|
|
1993
|
+
let predictiveFindings = findings.concat(
|
|
1994
|
+
contextualFindings,
|
|
1995
|
+
pythonSourceFindings,
|
|
1996
|
+
);
|
|
1997
|
+
let assessment = scoreTargetRisk(predictiveFindings, target, {
|
|
1998
|
+
bomJson: processedBomJson,
|
|
1999
|
+
repoReused: Boolean(checkout?.reused || cacheHit),
|
|
2000
|
+
resolution,
|
|
2001
|
+
sourceDirectoryConfidence: sourceSelection.confidence,
|
|
2002
|
+
versionMatched,
|
|
2003
|
+
});
|
|
2004
|
+
// When we scanned a version-specific ref and the severity is high or
|
|
2005
|
+
// critical, recheck against the default branch to reduce false positives
|
|
2006
|
+
// from issues already fixed upstream.
|
|
2007
|
+
if (
|
|
2008
|
+
versionMatched &&
|
|
2009
|
+
!options.skipDefaultBranchRecheck &&
|
|
2010
|
+
severityMeetsThreshold(assessment.severity, "high") &&
|
|
2011
|
+
resolution?.repoUrl
|
|
2012
|
+
) {
|
|
2013
|
+
emitProgress(options, {
|
|
2014
|
+
index: targetIndex,
|
|
2015
|
+
label: targetLabel,
|
|
2016
|
+
target,
|
|
2017
|
+
total: targetTotal,
|
|
2018
|
+
type: "target:stage",
|
|
2019
|
+
stage: "rechecking findings against default branch",
|
|
2020
|
+
});
|
|
2021
|
+
const recheckResult = await recheckFindingsAgainstDefaultBranch(
|
|
2022
|
+
predictiveFindings,
|
|
2023
|
+
target,
|
|
2024
|
+
resolution,
|
|
2025
|
+
categories,
|
|
2026
|
+
options,
|
|
2027
|
+
);
|
|
2028
|
+
if (recheckResult) {
|
|
2029
|
+
predictiveFindings = recheckResult.findings;
|
|
2030
|
+
assessment = scoreTargetRisk(predictiveFindings, target, {
|
|
2031
|
+
bomJson: processedBomJson,
|
|
2032
|
+
repoReused: Boolean(checkout?.reused || cacheHit),
|
|
2033
|
+
resolution,
|
|
2034
|
+
sourceDirectoryConfidence: sourceSelection.confidence,
|
|
2035
|
+
versionMatched,
|
|
2036
|
+
});
|
|
2037
|
+
assessment.defaultBranchRechecked = true;
|
|
2038
|
+
assessment.findingsFixedInDefaultBranch =
|
|
2039
|
+
recheckResult.fixedInDefaultBranch;
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
return persistAuditArtifacts(
|
|
2043
|
+
{
|
|
2044
|
+
assessment,
|
|
2045
|
+
cacheHit,
|
|
2046
|
+
findings: predictiveFindings,
|
|
2047
|
+
licenseViolations,
|
|
2048
|
+
repoUrl: sanitizedRepoUrl,
|
|
2049
|
+
resolution,
|
|
2050
|
+
scanDir: sourceSelection.scanDir,
|
|
2051
|
+
sourceDirectoryConfidence: sourceSelection.confidence,
|
|
2052
|
+
status: "audited",
|
|
2053
|
+
target,
|
|
2054
|
+
},
|
|
2055
|
+
options,
|
|
2056
|
+
processedBomJson,
|
|
2057
|
+
);
|
|
2058
|
+
} catch (error) {
|
|
2059
|
+
return persistAuditArtifacts(
|
|
2060
|
+
{
|
|
2061
|
+
assessment: scoreTargetRisk([], target, {
|
|
2062
|
+
errorMessage: error.message,
|
|
2063
|
+
scanError: true,
|
|
2064
|
+
}),
|
|
2065
|
+
error: error.message,
|
|
2066
|
+
errorType: error?.errorType || "runtime",
|
|
2067
|
+
findings: [],
|
|
2068
|
+
repoUrl: sanitizedRepoUrl,
|
|
2069
|
+
resolution,
|
|
2070
|
+
sourceDirectoryConfidence: sourceSelection?.confidence,
|
|
2071
|
+
status: "error",
|
|
2072
|
+
target,
|
|
2073
|
+
},
|
|
2074
|
+
options,
|
|
2075
|
+
processedBomJson,
|
|
2076
|
+
);
|
|
2077
|
+
} finally {
|
|
2078
|
+
if (originalFetchPackageMetadata === undefined) {
|
|
2079
|
+
delete process.env.CDXGEN_FETCH_PKG_METADATA;
|
|
2080
|
+
} else {
|
|
2081
|
+
process.env.CDXGEN_FETCH_PKG_METADATA = originalFetchPackageMetadata;
|
|
2082
|
+
}
|
|
2083
|
+
if (checkout?.cleanup) {
|
|
2084
|
+
cleanupSourceDir(checkout.cloneDir);
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
/**
|
|
2090
|
+
* Build an aggregate summary for all analyzed targets.
|
|
2091
|
+
*
|
|
2092
|
+
* @param {object[]} inputBoms loaded BOMs
|
|
2093
|
+
* @param {object[]} results target results
|
|
2094
|
+
* @param {object[]} skipped skipped component entries
|
|
2095
|
+
* @returns {object} summary object
|
|
2096
|
+
*/
|
|
2097
|
+
function summarizeAudit(inputBoms, results, skipped) {
|
|
2098
|
+
const analysisErrorCounts = {};
|
|
2099
|
+
const severityCounts = {
|
|
2100
|
+
critical: 0,
|
|
2101
|
+
high: 0,
|
|
2102
|
+
low: 0,
|
|
2103
|
+
medium: 0,
|
|
2104
|
+
none: 0,
|
|
2105
|
+
};
|
|
2106
|
+
let scannedTargets = 0;
|
|
2107
|
+
let erroredTargets = 0;
|
|
2108
|
+
for (const result of results) {
|
|
2109
|
+
const severity = result?.assessment?.severity || "none";
|
|
2110
|
+
severityCounts[severity] = (severityCounts[severity] || 0) + 1;
|
|
2111
|
+
if (result.status === "audited") {
|
|
2112
|
+
scannedTargets += 1;
|
|
2113
|
+
}
|
|
2114
|
+
if (result.status === "error") {
|
|
2115
|
+
erroredTargets += 1;
|
|
2116
|
+
const errorType = result?.errorType || "runtime";
|
|
2117
|
+
analysisErrorCounts[errorType] =
|
|
2118
|
+
(analysisErrorCounts[errorType] || 0) + 1;
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
return {
|
|
2122
|
+
analysisErrorCounts,
|
|
2123
|
+
erroredTargets,
|
|
2124
|
+
inputBomCount: inputBoms.length,
|
|
2125
|
+
scannedTargets,
|
|
2126
|
+
severityCounts,
|
|
2127
|
+
skippedTargets:
|
|
2128
|
+
skipped.length +
|
|
2129
|
+
results.filter((result) => result.status === "skipped").length,
|
|
2130
|
+
totalTargets: results.length,
|
|
2131
|
+
};
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
function normalizeRepoGroupingValue(repoUrl) {
|
|
2135
|
+
if (!repoUrl || typeof repoUrl !== "string") {
|
|
2136
|
+
return undefined;
|
|
2137
|
+
}
|
|
2138
|
+
return repoUrl
|
|
2139
|
+
.trim()
|
|
2140
|
+
.replace(/\.git$/i, "")
|
|
2141
|
+
.toLowerCase();
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
function preferredResult(left, right) {
|
|
2145
|
+
const leftSeverity = SEVERITY_ORDER[left?.assessment?.severity] ?? -1;
|
|
2146
|
+
const rightSeverity = SEVERITY_ORDER[right?.assessment?.severity] ?? -1;
|
|
2147
|
+
if (leftSeverity !== rightSeverity) {
|
|
2148
|
+
return leftSeverity > rightSeverity ? left : right;
|
|
2149
|
+
}
|
|
2150
|
+
const leftScore = left?.assessment?.score || 0;
|
|
2151
|
+
const rightScore = right?.assessment?.score || 0;
|
|
2152
|
+
return leftScore >= rightScore ? left : right;
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
function dedupeFindings(findings) {
|
|
2156
|
+
const seen = new Set();
|
|
2157
|
+
const deduped = [];
|
|
2158
|
+
for (const finding of findings || []) {
|
|
2159
|
+
const key = [
|
|
2160
|
+
finding?.ruleId,
|
|
2161
|
+
finding?.message,
|
|
2162
|
+
finding?.location?.file,
|
|
2163
|
+
finding?.location?.purl,
|
|
2164
|
+
finding?.location?.bomRef,
|
|
2165
|
+
].join("|");
|
|
2166
|
+
if (seen.has(key)) {
|
|
2167
|
+
continue;
|
|
2168
|
+
}
|
|
2169
|
+
seen.add(key);
|
|
2170
|
+
deduped.push(finding);
|
|
2171
|
+
}
|
|
2172
|
+
return deduped;
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
function getNamespaceGroupingKey(result) {
|
|
2176
|
+
if (
|
|
2177
|
+
result?.status !== "audited" ||
|
|
2178
|
+
result?.target?.type !== "npm" ||
|
|
2179
|
+
!result?.target?.namespace ||
|
|
2180
|
+
(result?.assessment?.severity || "none") === "none"
|
|
2181
|
+
) {
|
|
2182
|
+
return undefined;
|
|
2183
|
+
}
|
|
2184
|
+
const categories = Object.keys(
|
|
2185
|
+
result.assessment?.categoryCounts || {},
|
|
2186
|
+
).sort();
|
|
2187
|
+
const ruleIds = [
|
|
2188
|
+
...new Set((result.findings || []).map((f) => f.ruleId).filter(Boolean)),
|
|
2189
|
+
].sort();
|
|
2190
|
+
if (!categories.length || !ruleIds.length) {
|
|
2191
|
+
return undefined;
|
|
2192
|
+
}
|
|
2193
|
+
return `${result.target.namespace}|${categories.join(",")}|${ruleIds.join(",")}`;
|
|
2194
|
+
}
|
|
2195
|
+
|
|
2196
|
+
function getCargoRepositoryGroupingKey(result) {
|
|
2197
|
+
const normalizedRepoUrl = normalizeRepoGroupingValue(result?.repoUrl);
|
|
2198
|
+
if (
|
|
2199
|
+
result?.status !== "audited" ||
|
|
2200
|
+
result?.target?.type !== "cargo" ||
|
|
2201
|
+
!normalizedRepoUrl ||
|
|
2202
|
+
(result?.assessment?.severity || "none") === "none" ||
|
|
2203
|
+
!Array.isArray(result?.findings) ||
|
|
2204
|
+
!result.findings.length
|
|
2205
|
+
) {
|
|
2206
|
+
return undefined;
|
|
2207
|
+
}
|
|
2208
|
+
const categories = new Set();
|
|
2209
|
+
const ruleIds = new Set();
|
|
2210
|
+
for (const finding of result.findings) {
|
|
2211
|
+
if (!finding?.category || !finding?.ruleId) {
|
|
2212
|
+
return undefined;
|
|
2213
|
+
}
|
|
2214
|
+
if (finding.category === "ci-permission") {
|
|
2215
|
+
return undefined;
|
|
2216
|
+
}
|
|
2217
|
+
categories.add(finding.category);
|
|
2218
|
+
ruleIds.add(finding.ruleId);
|
|
2219
|
+
}
|
|
2220
|
+
if (!categories.size || !ruleIds.size) {
|
|
2221
|
+
return undefined;
|
|
2222
|
+
}
|
|
2223
|
+
return `${normalizedRepoUrl}|cargo|${[...categories].sort().join(",")}|${[...ruleIds].sort().join(",")}`;
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
function getSharedRepoCiGroupingKey(result) {
|
|
2227
|
+
const normalizedRepoUrl = normalizeRepoGroupingValue(result?.repoUrl);
|
|
2228
|
+
if (
|
|
2229
|
+
result?.status !== "audited" ||
|
|
2230
|
+
!normalizedRepoUrl ||
|
|
2231
|
+
!Array.isArray(result?.findings) ||
|
|
2232
|
+
!result.findings.length
|
|
2233
|
+
) {
|
|
2234
|
+
return undefined;
|
|
2235
|
+
}
|
|
2236
|
+
const categories = new Set();
|
|
2237
|
+
const ruleIds = new Set();
|
|
2238
|
+
for (const finding of result.findings) {
|
|
2239
|
+
const category = finding?.category;
|
|
2240
|
+
const ruleId = finding?.ruleId;
|
|
2241
|
+
const findingFile = finding?.location?.file || "";
|
|
2242
|
+
if (
|
|
2243
|
+
category !== "ci-permission" ||
|
|
2244
|
+
!ruleId?.startsWith("CI-") ||
|
|
2245
|
+
(findingFile && !findingFile.includes(".github/workflows"))
|
|
2246
|
+
) {
|
|
2247
|
+
return undefined;
|
|
2248
|
+
}
|
|
2249
|
+
categories.add(category);
|
|
2250
|
+
ruleIds.add(ruleId);
|
|
2251
|
+
}
|
|
2252
|
+
return `${normalizedRepoUrl}|${[...categories].sort().join(",")}|${[...ruleIds].sort().join(",")}`;
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
function getGroupingDescriptor(result, sharedRepoCiGroupCounts) {
|
|
2256
|
+
const sharedRepoCiKey = getSharedRepoCiGroupingKey(result);
|
|
2257
|
+
if (sharedRepoCiKey) {
|
|
2258
|
+
const groupSize = sharedRepoCiGroupCounts?.get(sharedRepoCiKey) || 0;
|
|
2259
|
+
if (groupSize > 1) {
|
|
2260
|
+
return {
|
|
2261
|
+
key: sharedRepoCiKey,
|
|
2262
|
+
kind: "shared-repo-ci",
|
|
2263
|
+
};
|
|
2264
|
+
}
|
|
2265
|
+
}
|
|
2266
|
+
const cargoRepositoryKey = getCargoRepositoryGroupingKey(result);
|
|
2267
|
+
if (cargoRepositoryKey) {
|
|
2268
|
+
return {
|
|
2269
|
+
key: cargoRepositoryKey,
|
|
2270
|
+
kind: "cargo-repository",
|
|
2271
|
+
};
|
|
2272
|
+
}
|
|
2273
|
+
const namespaceKey = getNamespaceGroupingKey(result);
|
|
2274
|
+
if (!namespaceKey) {
|
|
2275
|
+
return undefined;
|
|
2276
|
+
}
|
|
2277
|
+
return {
|
|
2278
|
+
key: namespaceKey,
|
|
2279
|
+
kind: "npm-namespace",
|
|
2280
|
+
};
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
function consolidateCargoRepositoryResult(group) {
|
|
2284
|
+
const representative = group.reduce((best, result) =>
|
|
2285
|
+
preferredResult(best, result),
|
|
2286
|
+
);
|
|
2287
|
+
const allBomRefs = [
|
|
2288
|
+
...new Set(group.flatMap((result) => result.target?.bomRefs || [])),
|
|
2289
|
+
];
|
|
2290
|
+
const groupedPurls = [
|
|
2291
|
+
...new Set(group.map((result) => result.target?.purl).filter(Boolean)),
|
|
2292
|
+
];
|
|
2293
|
+
const mergedFindings = dedupeFindings(
|
|
2294
|
+
group.flatMap((result) => result.findings || []),
|
|
2295
|
+
);
|
|
2296
|
+
const categoryCounts = {};
|
|
2297
|
+
for (const result of group) {
|
|
2298
|
+
for (const [category, count] of Object.entries(
|
|
2299
|
+
result.assessment?.categoryCounts || {},
|
|
2300
|
+
)) {
|
|
2301
|
+
categoryCounts[category] = (categoryCounts[category] || 0) + count;
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
const reasons = [
|
|
2305
|
+
`${group.length} Cargo packages resolved to the same repository and shared the same predictive pattern, so cdx-audit consolidated them into one alert.`,
|
|
2306
|
+
...(representative.assessment?.reasons || []),
|
|
2307
|
+
];
|
|
2308
|
+
return {
|
|
2309
|
+
...representative,
|
|
2310
|
+
assessment: {
|
|
2311
|
+
...representative.assessment,
|
|
2312
|
+
categoryCounts,
|
|
2313
|
+
findingsCount: mergedFindings.length,
|
|
2314
|
+
reasons: [...new Set(reasons)],
|
|
2315
|
+
},
|
|
2316
|
+
findings: mergedFindings,
|
|
2317
|
+
grouping: {
|
|
2318
|
+
kind: "cargo-repository",
|
|
2319
|
+
label: `cargo:${representative.repoUrl}`,
|
|
2320
|
+
memberCount: group.length,
|
|
2321
|
+
repoUrl: representative.repoUrl,
|
|
2322
|
+
groupedPurls,
|
|
2323
|
+
},
|
|
2324
|
+
target: {
|
|
2325
|
+
...representative.target,
|
|
2326
|
+
bomRefs: allBomRefs,
|
|
2327
|
+
name: "*",
|
|
2328
|
+
version: undefined,
|
|
2329
|
+
},
|
|
2330
|
+
};
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
function consolidateSharedRepoCiResult(group) {
|
|
2334
|
+
const representative = group.reduce((best, result) =>
|
|
2335
|
+
preferredResult(best, result),
|
|
2336
|
+
);
|
|
2337
|
+
const allBomRefs = [
|
|
2338
|
+
...new Set(group.flatMap((result) => result.target?.bomRefs || [])),
|
|
2339
|
+
];
|
|
2340
|
+
const groupedPurls = [
|
|
2341
|
+
...new Set(group.map((result) => result.target?.purl).filter(Boolean)),
|
|
2342
|
+
];
|
|
2343
|
+
const mergedFindings = dedupeFindings(
|
|
2344
|
+
group.flatMap((result) => result.findings || []),
|
|
2345
|
+
);
|
|
2346
|
+
const reasons = [
|
|
2347
|
+
`${group.length} packages resolved to the same repository and shared the same CI findings, so cdx-audit consolidated them into one alert.`,
|
|
2348
|
+
...(representative.assessment?.reasons || []),
|
|
2349
|
+
];
|
|
2350
|
+
return {
|
|
2351
|
+
...representative,
|
|
2352
|
+
assessment: {
|
|
2353
|
+
...representative.assessment,
|
|
2354
|
+
categoryCounts: {
|
|
2355
|
+
"ci-permission": mergedFindings.length,
|
|
2356
|
+
},
|
|
2357
|
+
findingsCount: mergedFindings.length,
|
|
2358
|
+
reasons: [...new Set(reasons)],
|
|
2359
|
+
},
|
|
2360
|
+
findings: mergedFindings,
|
|
2361
|
+
grouping: {
|
|
2362
|
+
kind: "shared-repo-ci",
|
|
2363
|
+
label: representative.repoUrl,
|
|
2364
|
+
memberCount: group.length,
|
|
2365
|
+
repoUrl: representative.repoUrl,
|
|
2366
|
+
groupedPurls,
|
|
2367
|
+
},
|
|
2368
|
+
target: {
|
|
2369
|
+
...representative.target,
|
|
2370
|
+
bomRefs: allBomRefs,
|
|
2371
|
+
name: "*",
|
|
2372
|
+
version: undefined,
|
|
2373
|
+
},
|
|
2374
|
+
};
|
|
2375
|
+
}
|
|
2376
|
+
|
|
2377
|
+
function consolidateNamespaceResult(group) {
|
|
2378
|
+
const representative = group.reduce((best, result) =>
|
|
2379
|
+
preferredResult(best, result),
|
|
2380
|
+
);
|
|
2381
|
+
const allBomRefs = [
|
|
2382
|
+
...new Set(group.flatMap((result) => result.target?.bomRefs || [])),
|
|
2383
|
+
];
|
|
2384
|
+
const groupedPurls = [
|
|
2385
|
+
...new Set(group.map((result) => result.target?.purl).filter(Boolean)),
|
|
2386
|
+
];
|
|
2387
|
+
const mergedFindings = dedupeFindings(
|
|
2388
|
+
group.flatMap((result) => result.findings || []),
|
|
2389
|
+
);
|
|
2390
|
+
const categoryCounts = {};
|
|
2391
|
+
for (const result of group) {
|
|
2392
|
+
for (const [category, count] of Object.entries(
|
|
2393
|
+
result.assessment?.categoryCounts || {},
|
|
2394
|
+
)) {
|
|
2395
|
+
categoryCounts[category] = (categoryCounts[category] || 0) + count;
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
const reasons = [
|
|
2399
|
+
`${group.length} npm packages under namespace '${representative.target.namespace}' shared the same predictive pattern and were consolidated into one alert.`,
|
|
2400
|
+
...(representative.assessment?.reasons || []),
|
|
2401
|
+
];
|
|
2402
|
+
return {
|
|
2403
|
+
...representative,
|
|
2404
|
+
assessment: {
|
|
2405
|
+
...representative.assessment,
|
|
2406
|
+
categoryCounts,
|
|
2407
|
+
findingsCount: mergedFindings.length,
|
|
2408
|
+
reasons: [...new Set(reasons)],
|
|
2409
|
+
},
|
|
2410
|
+
findings: mergedFindings,
|
|
2411
|
+
grouping: {
|
|
2412
|
+
kind: "npm-namespace",
|
|
2413
|
+
label: `npm:${representative.target.namespace}/*`,
|
|
2414
|
+
memberCount: group.length,
|
|
2415
|
+
namespace: representative.target.namespace,
|
|
2416
|
+
groupedPurls,
|
|
2417
|
+
},
|
|
2418
|
+
target: {
|
|
2419
|
+
...representative.target,
|
|
2420
|
+
bomRefs: allBomRefs,
|
|
2421
|
+
name: "*",
|
|
2422
|
+
version: undefined,
|
|
2423
|
+
},
|
|
2424
|
+
};
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
/**
|
|
2428
|
+
* Consolidate per-target audit results into shared repo/CI alert groups.
|
|
2429
|
+
*
|
|
2430
|
+
* Groups results that share a common repository/CI provenance (or cargo
|
|
2431
|
+
* repository/namespace) into consolidated alert entries while preserving
|
|
2432
|
+
* single-target results unchanged.
|
|
2433
|
+
*
|
|
2434
|
+
* @param {object[]} results Per-target audit result objects
|
|
2435
|
+
* @returns {object[]} Grouped result objects with consolidated entries where applicable
|
|
2436
|
+
*/
|
|
2437
|
+
export function groupAuditResults(results) {
|
|
2438
|
+
const groupedResults = [];
|
|
2439
|
+
const orderedEntries = [];
|
|
2440
|
+
const resultGroups = new Map();
|
|
2441
|
+
const sharedRepoCiGroupCounts = new Map();
|
|
2442
|
+
for (const result of results) {
|
|
2443
|
+
const sharedRepoCiKey = getSharedRepoCiGroupingKey(result);
|
|
2444
|
+
if (!sharedRepoCiKey) {
|
|
2445
|
+
continue;
|
|
2446
|
+
}
|
|
2447
|
+
sharedRepoCiGroupCounts.set(
|
|
2448
|
+
sharedRepoCiKey,
|
|
2449
|
+
(sharedRepoCiGroupCounts.get(sharedRepoCiKey) || 0) + 1,
|
|
2450
|
+
);
|
|
2451
|
+
}
|
|
2452
|
+
for (const result of results) {
|
|
2453
|
+
const descriptor = getGroupingDescriptor(result, sharedRepoCiGroupCounts);
|
|
2454
|
+
if (!descriptor) {
|
|
2455
|
+
orderedEntries.push({ result, type: "single" });
|
|
2456
|
+
continue;
|
|
2457
|
+
}
|
|
2458
|
+
if (!resultGroups.has(descriptor.key)) {
|
|
2459
|
+
resultGroups.set(descriptor.key, []);
|
|
2460
|
+
orderedEntries.push({ descriptor, type: "group" });
|
|
2461
|
+
}
|
|
2462
|
+
resultGroups.get(descriptor.key).push(result);
|
|
2463
|
+
}
|
|
2464
|
+
for (const entry of orderedEntries) {
|
|
2465
|
+
if (entry.type === "single") {
|
|
2466
|
+
groupedResults.push(entry.result);
|
|
2467
|
+
continue;
|
|
2468
|
+
}
|
|
2469
|
+
const group = resultGroups.get(entry.descriptor.key) || [];
|
|
2470
|
+
if (group.length <= 1) {
|
|
2471
|
+
groupedResults.push(group[0]);
|
|
2472
|
+
continue;
|
|
2473
|
+
}
|
|
2474
|
+
groupedResults.push(
|
|
2475
|
+
entry.descriptor.kind === "shared-repo-ci"
|
|
2476
|
+
? consolidateSharedRepoCiResult(group)
|
|
2477
|
+
: entry.descriptor.kind === "cargo-repository"
|
|
2478
|
+
? consolidateCargoRepositoryResult(group)
|
|
2479
|
+
: consolidateNamespaceResult(group),
|
|
2480
|
+
);
|
|
2481
|
+
}
|
|
2482
|
+
return groupedResults;
|
|
2483
|
+
}
|
|
2484
|
+
|
|
2485
|
+
function summarizeGroupedResults(results) {
|
|
2486
|
+
const severityCounts = {
|
|
2487
|
+
critical: 0,
|
|
2488
|
+
high: 0,
|
|
2489
|
+
low: 0,
|
|
2490
|
+
medium: 0,
|
|
2491
|
+
none: 0,
|
|
2492
|
+
};
|
|
2493
|
+
for (const result of results) {
|
|
2494
|
+
const severity = result?.assessment?.severity || "none";
|
|
2495
|
+
severityCounts[severity] = (severityCounts[severity] || 0) + 1;
|
|
2496
|
+
}
|
|
2497
|
+
return {
|
|
2498
|
+
groupedResultCount: results.length,
|
|
2499
|
+
groupedSeverityCounts: severityCounts,
|
|
2500
|
+
};
|
|
2501
|
+
}
|
|
2502
|
+
|
|
2503
|
+
/**
|
|
2504
|
+
* Run the predictive audit flow from one or more already-loaded CycloneDX BOM inputs.
|
|
2505
|
+
*
|
|
2506
|
+
* @param {{ source: string, bomJson: object }[]} inputBoms loaded CycloneDX BOM objects
|
|
2507
|
+
* @param {object} options CLI options
|
|
2508
|
+
* @returns {Promise<object>} aggregate audit report
|
|
2509
|
+
*/
|
|
2510
|
+
export async function runAuditFromBoms(inputBoms, options) {
|
|
2511
|
+
if (!inputBoms.length) {
|
|
2512
|
+
throw new Error("No CycloneDX BOM inputs were found.");
|
|
2513
|
+
}
|
|
2514
|
+
const shouldEnrichRegistryMetadata =
|
|
2515
|
+
!isDryRun && options.trusted !== "include";
|
|
2516
|
+
if (shouldEnrichRegistryMetadata) {
|
|
2517
|
+
await enrichInputBomsWithRegistryMetadata(inputBoms);
|
|
2518
|
+
}
|
|
2519
|
+
const targetSelectionOptions = {
|
|
2520
|
+
allowlistFile: options.allowlistFile,
|
|
2521
|
+
maxTargets: options.maxTargets,
|
|
2522
|
+
prioritizeDirectRuntime: options.prioritizeDirectRuntime,
|
|
2523
|
+
scope: options.scope,
|
|
2524
|
+
trusted: options.trusted,
|
|
2525
|
+
};
|
|
2526
|
+
const extractedTargets = collectAuditTargets(
|
|
2527
|
+
inputBoms,
|
|
2528
|
+
targetSelectionOptions,
|
|
2529
|
+
);
|
|
2530
|
+
if (isDryRun) {
|
|
2531
|
+
// Dry-run mode intentionally stops after target planning. Registry metadata
|
|
2532
|
+
// enrichment, repository cloning, and child SBOM generation are all
|
|
2533
|
+
// side-effecting or outbound behaviors that the predictive audit must avoid.
|
|
2534
|
+
const report = {
|
|
2535
|
+
dryRun: true,
|
|
2536
|
+
generatedAt: new Date().toISOString(),
|
|
2537
|
+
groupedResults: [],
|
|
2538
|
+
inputs: inputBoms.map((inputBom) => inputBom.source),
|
|
2539
|
+
results: extractedTargets.targets.map((target) => ({
|
|
2540
|
+
assessment: {
|
|
2541
|
+
categoryCounts: {},
|
|
2542
|
+
confidenceLabel: "none",
|
|
2543
|
+
findingsCount: 0,
|
|
2544
|
+
reasons: [
|
|
2545
|
+
"Dry run mode planned this predictive audit target from the input BOM but skipped registry metadata fetches, upstream repository cloning, and child SBOM generation.",
|
|
2546
|
+
],
|
|
2547
|
+
score: 0,
|
|
2548
|
+
severity: "none",
|
|
2549
|
+
},
|
|
2550
|
+
findings: [],
|
|
2551
|
+
status: "skipped",
|
|
2552
|
+
target,
|
|
2553
|
+
})),
|
|
2554
|
+
skipped: extractedTargets.skipped,
|
|
2555
|
+
summary: {
|
|
2556
|
+
predictiveDryRun: true,
|
|
2557
|
+
},
|
|
2558
|
+
tool: {
|
|
2559
|
+
name: "cdx-audit",
|
|
2560
|
+
version: readPackageVersion(),
|
|
2561
|
+
},
|
|
2562
|
+
};
|
|
2563
|
+
Object.assign(
|
|
2564
|
+
report.summary,
|
|
2565
|
+
summarizeAudit(inputBoms, report.results, extractedTargets.skipped),
|
|
2566
|
+
summarizeGroupedResults(report.groupedResults),
|
|
2567
|
+
);
|
|
2568
|
+
recordActivity({
|
|
2569
|
+
kind: "audit",
|
|
2570
|
+
reason:
|
|
2571
|
+
"Dry run mode planned predictive BOM audit targets from the input BOM but skipped registry metadata fetches, repository cloning, and child SBOM generation.",
|
|
2572
|
+
status: extractedTargets.targets.length ? "blocked" : "completed",
|
|
2573
|
+
target: "predictive-dependency-audit",
|
|
2574
|
+
});
|
|
2575
|
+
return report;
|
|
2576
|
+
}
|
|
2577
|
+
const results = [];
|
|
2578
|
+
const preflightMessage = buildPredictiveAuditPreflightMessage(
|
|
2579
|
+
extractedTargets,
|
|
2580
|
+
options,
|
|
2581
|
+
);
|
|
2582
|
+
if (preflightMessage) {
|
|
2583
|
+
emitProgress(options, {
|
|
2584
|
+
message: preflightMessage,
|
|
2585
|
+
total: extractedTargets.targets.length,
|
|
2586
|
+
type: "run:info",
|
|
2587
|
+
});
|
|
2588
|
+
}
|
|
2589
|
+
if (extractedTargets.targets.length) {
|
|
2590
|
+
emitProgress(options, {
|
|
2591
|
+
total: extractedTargets.targets.length,
|
|
2592
|
+
type: "run:start",
|
|
2593
|
+
});
|
|
2594
|
+
// Every target's repository lookup is known here, before the first clone,
|
|
2595
|
+
// so they are resolved in one round rather than one per iteration below.
|
|
2596
|
+
await prefetchGitUrlSources(
|
|
2597
|
+
extractedTargets.targets.map((target) => target.purl),
|
|
2598
|
+
);
|
|
2599
|
+
}
|
|
2600
|
+
for (const [index, target] of extractedTargets.targets.entries()) {
|
|
2601
|
+
const targetIndex = index + 1;
|
|
2602
|
+
emitProgress(options, {
|
|
2603
|
+
index: targetIndex,
|
|
2604
|
+
label: formatTargetLabel(target),
|
|
2605
|
+
target,
|
|
2606
|
+
total: extractedTargets.targets.length,
|
|
2607
|
+
type: "target:start",
|
|
2608
|
+
});
|
|
2609
|
+
const result = await auditTarget(target, {
|
|
2610
|
+
...options,
|
|
2611
|
+
_targetIndex: targetIndex,
|
|
2612
|
+
_targetTotal: extractedTargets.targets.length,
|
|
2613
|
+
});
|
|
2614
|
+
results.push(result);
|
|
2615
|
+
emitProgress(options, {
|
|
2616
|
+
index: targetIndex,
|
|
2617
|
+
label: formatTargetLabel(target),
|
|
2618
|
+
result,
|
|
2619
|
+
target,
|
|
2620
|
+
total: extractedTargets.targets.length,
|
|
2621
|
+
type: "target:finish",
|
|
2622
|
+
});
|
|
2623
|
+
}
|
|
2624
|
+
const groupedResults = groupAuditResults(results);
|
|
2625
|
+
const report = {
|
|
2626
|
+
generatedAt: new Date().toISOString(),
|
|
2627
|
+
inputs: inputBoms.map((inputBom) => inputBom.source),
|
|
2628
|
+
groupedResults,
|
|
2629
|
+
results,
|
|
2630
|
+
skipped: extractedTargets.skipped,
|
|
2631
|
+
summary: summarizeAudit(inputBoms, results, extractedTargets.skipped),
|
|
2632
|
+
tool: {
|
|
2633
|
+
name: "cdx-audit",
|
|
2634
|
+
version: readPackageVersion(),
|
|
2635
|
+
},
|
|
2636
|
+
};
|
|
2637
|
+
Object.assign(report.summary, summarizeGroupedResults(groupedResults));
|
|
2638
|
+
if (options.reportsDir) {
|
|
2639
|
+
const aggregateFile = join(
|
|
2640
|
+
resolve(options.reportsDir),
|
|
2641
|
+
"aggregate-report.json",
|
|
2642
|
+
);
|
|
2643
|
+
writeJsonFile(aggregateFile, report);
|
|
2644
|
+
report.aggregateReportFile = aggregateFile;
|
|
2645
|
+
}
|
|
2646
|
+
if (extractedTargets.targets.length) {
|
|
2647
|
+
emitProgress(options, {
|
|
2648
|
+
summary: report.summary,
|
|
2649
|
+
type: "run:finish",
|
|
2650
|
+
});
|
|
2651
|
+
}
|
|
2652
|
+
return report;
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
/**
|
|
2656
|
+
* Run the predictive audit flow from one or more CycloneDX BOM inputs.
|
|
2657
|
+
*
|
|
2658
|
+
* @param {object} options CLI options
|
|
2659
|
+
* @returns {Promise<object>} aggregate audit report
|
|
2660
|
+
*/
|
|
2661
|
+
export async function runAudit(options) {
|
|
2662
|
+
const inputBoms = loadInputBoms(options);
|
|
2663
|
+
const workspaceContext = prepareWorkspaceContext(options);
|
|
2664
|
+
const licensePolicy = options.licensePolicy
|
|
2665
|
+
? loadPolicy(options.licensePolicy)
|
|
2666
|
+
: null;
|
|
2667
|
+
try {
|
|
2668
|
+
const auditOptions = {
|
|
2669
|
+
...options,
|
|
2670
|
+
workspaceDir: workspaceContext.workspaceDir,
|
|
2671
|
+
// Preloaded so each target's child SBOM can be evaluated in auditTarget
|
|
2672
|
+
// without re-reading the policy file per dependency.
|
|
2673
|
+
_licensePolicy: licensePolicy,
|
|
2674
|
+
};
|
|
2675
|
+
const report = options.directBomAudit
|
|
2676
|
+
? await runDirectBomAuditFromBoms(inputBoms, auditOptions)
|
|
2677
|
+
: await runAuditFromBoms(inputBoms, auditOptions);
|
|
2678
|
+
attachLicenseViolations(report, inputBoms, licensePolicy);
|
|
2679
|
+
return report;
|
|
2680
|
+
} finally {
|
|
2681
|
+
if (workspaceContext.cleanupOnFinish) {
|
|
2682
|
+
cleanupSourceDir(workspaceContext.workspaceDir);
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
|
|
2687
|
+
/**
|
|
2688
|
+
* Aggregates license compliance violations onto the report. Violations come
|
|
2689
|
+
* from two sources: the input BOM(s) directly, and — in predictive mode — the
|
|
2690
|
+
* per-dependency child SBOMs generated from cloned upstream sources (carried on
|
|
2691
|
+
* each target result). Duplicates across both are collapsed so the same
|
|
2692
|
+
* component/license/alert is reported once. No-op without a policy.
|
|
2693
|
+
*
|
|
2694
|
+
* @param {object} report aggregate audit report (mutated)
|
|
2695
|
+
* @param {object[]} inputBoms loaded input BOMs
|
|
2696
|
+
* @param {object|null} policy preloaded compliance policy
|
|
2697
|
+
* @returns {void}
|
|
2698
|
+
*/
|
|
2699
|
+
export function attachLicenseViolations(report, inputBoms, policy) {
|
|
2700
|
+
if (!report || !policy) {
|
|
2701
|
+
return;
|
|
2702
|
+
}
|
|
2703
|
+
const seen = new Set();
|
|
2704
|
+
const violations = [];
|
|
2705
|
+
const add = (violation, source) => {
|
|
2706
|
+
const key = `${violation.ref}|${violation.license}|${violation.alert}`;
|
|
2707
|
+
if (seen.has(key)) {
|
|
2708
|
+
return;
|
|
2709
|
+
}
|
|
2710
|
+
seen.add(key);
|
|
2711
|
+
violations.push({ ...violation, source });
|
|
2712
|
+
};
|
|
2713
|
+
// Direct violations from the input BOM(s).
|
|
2714
|
+
for (const inputBom of inputBoms) {
|
|
2715
|
+
for (const violation of collectPolicyViolations(inputBom.bomJson, policy, {
|
|
2716
|
+
includeWarnings: true,
|
|
2717
|
+
})) {
|
|
2718
|
+
add(violation, inputBom.source);
|
|
2719
|
+
}
|
|
2720
|
+
}
|
|
2721
|
+
// Violations surfaced from each dependency's child SBOM (predictive mode).
|
|
2722
|
+
for (const result of report.results || []) {
|
|
2723
|
+
for (const violation of result.licenseViolations || []) {
|
|
2724
|
+
add(violation, formatTargetLabel(result.target) || result.repoUrl);
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
report.licenseViolations = violations;
|
|
2728
|
+
report.summary = report.summary || {};
|
|
2729
|
+
report.summary.licenseViolations = violations.length;
|
|
2730
|
+
report.summary.licenseViolationsByAlert = violations.reduce(
|
|
2731
|
+
(counts, violation) => {
|
|
2732
|
+
counts[violation.alert] = (counts[violation.alert] || 0) + 1;
|
|
2733
|
+
return counts;
|
|
2734
|
+
},
|
|
2735
|
+
{},
|
|
2736
|
+
);
|
|
2737
|
+
}
|
|
2738
|
+
|
|
2739
|
+
/**
|
|
2740
|
+
* Render a report and compute the proper process exit code.
|
|
2741
|
+
*
|
|
2742
|
+
* @param {object} report aggregate report
|
|
2743
|
+
* @param {object} options CLI options
|
|
2744
|
+
* @returns {{ exitCode: number, output: string }} rendered output and exit code
|
|
2745
|
+
*/
|
|
2746
|
+
export function finalizeAuditReport(report, options) {
|
|
2747
|
+
const output = renderAuditReport(options.report, report, {
|
|
2748
|
+
minSeverity: options.minSeverity,
|
|
2749
|
+
});
|
|
2750
|
+
// Prohibited (error-level) license violations are a hard fail regardless of
|
|
2751
|
+
// the supply-chain severity threshold.
|
|
2752
|
+
const licensePolicyFail = (report?.licenseViolations || []).some(
|
|
2753
|
+
(violation) => violation.alert === "error",
|
|
2754
|
+
);
|
|
2755
|
+
if (report?.auditMode === "direct") {
|
|
2756
|
+
const shouldFail = (report.results || []).some((result) =>
|
|
2757
|
+
(result.findings || []).some((finding) =>
|
|
2758
|
+
severityMeetsThreshold(
|
|
2759
|
+
finding?.severity || "none",
|
|
2760
|
+
options.failSeverity || "high",
|
|
2761
|
+
),
|
|
2762
|
+
),
|
|
2763
|
+
);
|
|
2764
|
+
return {
|
|
2765
|
+
exitCode: shouldFail || licensePolicyFail ? 3 : 0,
|
|
2766
|
+
output,
|
|
2767
|
+
};
|
|
2768
|
+
}
|
|
2769
|
+
const effectiveResults = report.groupedResults?.length
|
|
2770
|
+
? report.groupedResults
|
|
2771
|
+
: report.results;
|
|
2772
|
+
const shouldFail = effectiveResults.some(
|
|
2773
|
+
(result) =>
|
|
2774
|
+
!result?.error &&
|
|
2775
|
+
severityMeetsThreshold(
|
|
2776
|
+
result?.assessment?.severity || "none",
|
|
2777
|
+
options.failSeverity || "high",
|
|
2778
|
+
),
|
|
2779
|
+
);
|
|
2780
|
+
return {
|
|
2781
|
+
exitCode: shouldFail || licensePolicyFail ? 3 : 0,
|
|
2782
|
+
output,
|
|
2783
|
+
};
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2786
|
+
/**
|
|
2787
|
+
* Build a result file name for user-provided report output paths.
|
|
2788
|
+
*
|
|
2789
|
+
* @param {object} options CLI options
|
|
2790
|
+
* @returns {string | undefined} output file path
|
|
2791
|
+
*/
|
|
2792
|
+
export function defaultOutputFile(options) {
|
|
2793
|
+
if (!options.reportsDir) {
|
|
2794
|
+
return undefined;
|
|
2795
|
+
}
|
|
2796
|
+
return join(
|
|
2797
|
+
resolve(options.reportsDir),
|
|
2798
|
+
`cdx-audit-report.${options.report || "console"}.txt`,
|
|
2799
|
+
);
|
|
2800
|
+
}
|