@cdxgen/cdxgen 13.5.0 → 13.7.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 +17 -10
- package/bin/audit.js +7 -4
- package/bin/cdxgen.js +260 -37
- package/bin/convert.js +146 -22
- package/bin/evinse.js +28 -3
- package/bin/repl.js +47 -3
- package/bin/sign.js +211 -34
- package/bin/tracebom.js +18 -6
- package/bin/validate.js +24 -3
- package/bin/verify.js +113 -5
- package/data/behavior-taxonomy.schema.json +1491 -0
- package/data/cyclonedx-2.0-bundled.schema.json +18606 -5528
- package/data/cyclonedx-2.0-cryptography-defs.schema.json +857 -0
- package/data/license-aliases.json +24 -2
- package/data/pqc-migration.json +239 -0
- package/data/remediations.json +176 -0
- package/data/rules/ai-bom.yaml +1 -1
- package/data/rules/brotli-decoder.yaml +107 -0
- package/data/rules/cbom-security.yaml +4 -3
- package/data/rules/package-integrity.yaml +28 -0
- package/data/rules/vscode-extensions.yaml +75 -0
- package/data/spdx-license-list.json +121 -7
- package/data/spdx-licenses.json +69 -0
- package/data/spdx.schema.json +845 -2
- package/lib/audit/index.js +25 -3
- package/lib/cli/bomAssembly.js +172 -4
- package/lib/cli/cliOptions.js +121 -17
- package/lib/cli/index.js +720 -902
- package/lib/cli/jsBom.js +156 -64
- package/lib/cli/jvmBom.js +602 -86
- package/lib/cli/managedBom.js +161 -19
- package/lib/cli/nativeBom.js +52 -5
- package/lib/core/boundedFile.js +344 -0
- package/lib/core/brotliProbe.js +182 -0
- package/lib/core/env.js +34 -0
- package/lib/core/fs.js +138 -3
- package/lib/core/globPatterns.js +218 -0
- package/lib/core/httpClient.js +98 -1
- package/lib/core/lockPaths.js +38 -0
- package/lib/core/paths.js +24 -1
- package/lib/core/processTree.js +100 -8
- package/lib/core/projectConfig.js +332 -25
- package/lib/core/toolRegistry.js +184 -0
- package/lib/ecosystems/cppEvidence.js +263 -106
- package/lib/ecosystems/cppIncludeResolver.js +16 -5
- package/lib/ecosystems/denoutils.js +2 -2
- package/lib/ecosystems/ecosystems.js +25 -10
- package/lib/ecosystems/gemHostProbe.js +301 -0
- package/lib/ecosystems/golem.js +3 -8
- package/lib/ecosystems/gradleutils.js +1909 -229
- package/lib/ecosystems/jsEvidence.js +90 -38
- package/lib/ecosystems/kosi.js +3 -5
- package/lib/ecosystems/npmVersionResolution.js +376 -0
- package/lib/ecosystems/parsers-bazel.js +107 -6
- package/lib/ecosystems/parsers-js.js +505 -22
- package/lib/ecosystems/parsers-misc.js +209 -20
- package/lib/ecosystems/parsers-python.js +178 -30
- package/lib/ecosystems/parsers-rust.js +117 -15
- package/lib/ecosystems/pythonutils.js +128 -2
- package/lib/ecosystems/remote/huggingface.js +22 -0
- package/lib/ecosystems/rubyutils.js +538 -31
- package/lib/ecosystems/rusi.js +2 -4
- package/lib/ecosystems/scalacliutils.js +2 -0
- package/lib/ecosystems/scalasem.js +798 -152
- package/lib/ecosystems/vendoredCode.js +237 -3
- package/lib/ecosystems/venvProbe.js +360 -0
- package/lib/evinser/evinser.js +536 -71
- package/lib/evinser/scalasem.js +5 -1
- package/lib/helpers/agenticToolutils.js +525 -94
- package/lib/helpers/bomSigner.js +43 -9
- package/lib/helpers/jssSigner.js +1276 -0
- package/lib/helpers/vsixutils.js +1574 -315
- package/lib/inventory/agentBlueprint.js +193 -0
- package/lib/inventory/agentToolNames.js +145 -0
- package/lib/inventory/aiCollector.js +153 -7
- package/lib/inventory/aiInventory.js +44 -8
- package/lib/inventory/aiProviders.js +324 -0
- package/lib/inventory/atomUtils.js +77 -0
- package/lib/inventory/bomUtils.js +8 -1
- package/lib/inventory/brotliDecoder.js +142 -0
- package/lib/inventory/buildToolRateLimit.js +1 -1
- package/lib/inventory/cbomutils.js +56 -7
- package/lib/inventory/ciParsers/githubActionsLock.js +821 -0
- package/lib/inventory/citations.js +31 -2
- package/lib/inventory/communityAiConfigParser.js +978 -80
- package/lib/inventory/cyclonedx20.js +152 -0
- package/lib/inventory/cyclonedx20Common.js +286 -0
- package/lib/inventory/cyclonedx20Derived.js +159 -0
- package/lib/inventory/cyclonedx20Parties.js +552 -0
- package/lib/inventory/cyclonedx20Read.js +768 -0
- package/lib/inventory/cyclonedx20ReadCommon.js +374 -0
- package/lib/inventory/cyclonedx20ReadComponents.js +692 -0
- package/lib/inventory/cyclonedx20ReadContext.js +380 -0
- package/lib/inventory/cyclonedx20ReadEvidence.js +466 -0
- package/lib/inventory/cyclonedx20ReadSections.js +599 -0
- package/lib/inventory/cyclonedx20Render.js +766 -0
- package/lib/inventory/cyclonedx20RenderComponents.js +716 -0
- package/lib/inventory/cyclonedx20RenderDefinitions.js +405 -0
- package/lib/inventory/cyclonedx20RenderEvidence.js +726 -0
- package/lib/inventory/cyclonedx20RenderHardware.js +203 -0
- package/lib/inventory/cyclonedx20RenderPatents.js +174 -0
- package/lib/inventory/cyclonedx20RenderRefIds.js +314 -0
- package/lib/inventory/cyclonedx20RenderSections.js +499 -0
- package/lib/inventory/cyclonedx20Tables.js +594 -0
- package/lib/inventory/deps.js +141 -13
- package/lib/inventory/dosai.js +375 -6
- package/lib/inventory/dosaiParsers.js +3 -6
- package/lib/inventory/dotnetRuntime.js +367 -0
- package/lib/inventory/ecosystemFormulationParsers.js +979 -0
- package/lib/inventory/envcontext.js +436 -48
- package/lib/inventory/evidenceUtils.js +13 -1
- package/lib/inventory/fetchBatch.js +1 -1
- package/lib/inventory/formulationParsers.js +124 -14
- package/lib/inventory/gtfobins.js +13 -0
- package/lib/inventory/hbom.js +45 -6
- package/lib/inventory/hbomLoader.js +166 -0
- package/lib/inventory/hostRuntimeProbe.js +686 -0
- package/lib/inventory/jvmLocalRepos.js +9 -2
- package/lib/inventory/jvmToolEnv.js +29 -0
- package/lib/inventory/licenseEnhancer.js +12 -0
- package/lib/inventory/mavenCentralMirror.js +534 -0
- package/lib/inventory/mcpConfigParser.js +5 -0
- package/lib/inventory/nodeBuildInfo.js +218 -0
- package/lib/inventory/nodeHostProbe.js +202 -0
- package/lib/inventory/occurrenceKey.js +90 -0
- package/lib/inventory/osPackageResolver.js +182 -0
- package/lib/inventory/phpBuildInfo.js +10 -59
- package/lib/inventory/phpHostProbe.js +32 -488
- package/lib/inventory/pqcMigration.js +426 -0
- package/lib/inventory/rubyBuildInfo.js +972 -0
- package/lib/inventory/rubyHostProbe.js +218 -0
- package/lib/inventory/rustFormulationParser.js +21 -10
- package/lib/inventory/scalaCoords.js +4 -3
- package/lib/inventory/scalaDedupe.js +440 -0
- package/lib/inventory/source.js +114 -8
- package/lib/managers/binary.js +337 -3
- package/lib/managers/docker.js +79 -0
- package/lib/parsers/huggingfaceManifest.js +9 -1
- package/lib/parsers/huggingfaceUtils.js +18 -0
- package/lib/parsers/kiroPermissions.js +166 -0
- package/lib/parsers/largeJson.js +28 -11
- package/lib/parsers/vscodeExtensionId.js +183 -0
- package/lib/server/server.js +48 -35
- package/lib/stages/postgen/postgen.js +41 -1
- package/lib/stages/postgen/ruleEngine.js +13 -5
- package/lib/stages/postgen/specVersionCompat.js +76 -0
- package/lib/stages/pregen/envAudit.js +1 -0
- package/lib/stages/pregen/pregen.js +114 -57
- package/lib/validator/bomValidator.js +14 -2
- package/lib/validator/complianceEngine.js +30 -3
- package/lib/validator/complianceRules.js +1416 -4
- package/lib/validator/generated/validate-1.6.mjs +1 -1
- package/lib/validator/generated/validate-1.7.mjs +1 -1
- package/lib/validator/generated/validate-2.0.mjs +57554 -13851
- package/lib/validator/index.js +49 -8
- package/lib/validator/reporters/json.js +6 -0
- package/package.json +11 -10
- package/types/bin/repl.d.ts.map +1 -1
- package/types/lib/audit/index.d.ts +6 -2
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/cli/bomAssembly.d.ts +29 -1
- package/types/lib/cli/bomAssembly.d.ts.map +1 -1
- package/types/lib/cli/cliOptions.d.ts +6 -0
- package/types/lib/cli/cliOptions.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/cli/jsBom.d.ts +3 -18
- package/types/lib/cli/jsBom.d.ts.map +1 -1
- package/types/lib/cli/jvmBom.d.ts +15 -0
- package/types/lib/cli/jvmBom.d.ts.map +1 -1
- package/types/lib/cli/managedBom.d.ts.map +1 -1
- package/types/lib/cli/nativeBom.d.ts.map +1 -1
- package/types/lib/core/boundedFile.d.ts +100 -0
- package/types/lib/core/boundedFile.d.ts.map +1 -0
- package/types/lib/core/brotliProbe.d.ts +37 -0
- package/types/lib/core/brotliProbe.d.ts.map +1 -0
- package/types/lib/core/env.d.ts +14 -0
- package/types/lib/core/env.d.ts.map +1 -1
- package/types/lib/core/fs.d.ts +4 -0
- package/types/lib/core/fs.d.ts.map +1 -1
- package/types/lib/core/globPatterns.d.ts +70 -0
- package/types/lib/core/globPatterns.d.ts.map +1 -0
- package/types/lib/core/httpClient.d.ts +24 -0
- package/types/lib/core/httpClient.d.ts.map +1 -1
- package/types/lib/core/lockPaths.d.ts +19 -0
- package/types/lib/core/lockPaths.d.ts.map +1 -0
- package/types/lib/core/paths.d.ts +9 -0
- package/types/lib/core/paths.d.ts.map +1 -1
- package/types/lib/core/processTree.d.ts +17 -4
- package/types/lib/core/processTree.d.ts.map +1 -1
- package/types/lib/core/projectConfig.d.ts +47 -8
- package/types/lib/core/projectConfig.d.ts.map +1 -1
- package/types/lib/core/toolRegistry.d.ts +36 -0
- package/types/lib/core/toolRegistry.d.ts.map +1 -0
- package/types/lib/ecosystems/cppEvidence.d.ts +40 -13
- package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/cppIncludeResolver.d.ts +2 -1
- package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -1
- package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
- package/types/lib/ecosystems/gemHostProbe.d.ts +28 -0
- package/types/lib/ecosystems/gemHostProbe.d.ts.map +1 -0
- package/types/lib/ecosystems/golem.d.ts.map +1 -1
- package/types/lib/ecosystems/gradleutils.d.ts +294 -20
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
- package/types/lib/ecosystems/jsEvidence.d.ts +14 -1
- package/types/lib/ecosystems/jsEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/kosi.d.ts.map +1 -1
- package/types/lib/ecosystems/npmVersionResolution.d.ts +119 -0
- package/types/lib/ecosystems/npmVersionResolution.d.ts.map +1 -0
- package/types/lib/ecosystems/parsers-bazel.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-js.d.ts +17 -20
- package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-misc.d.ts +19 -0
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-python.d.ts +10 -0
- package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-rust.d.ts.map +1 -1
- package/types/lib/ecosystems/pythonutils.d.ts +19 -1
- package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
- package/types/lib/ecosystems/remote/huggingface.d.ts.map +1 -1
- package/types/lib/ecosystems/rubyutils.d.ts +144 -12
- package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rusi.d.ts.map +1 -1
- package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -1
- package/types/lib/ecosystems/scalasem.d.ts +58 -16
- package/types/lib/ecosystems/scalasem.d.ts.map +1 -1
- package/types/lib/ecosystems/vendoredCode.d.ts +26 -0
- package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -1
- package/types/lib/ecosystems/venvProbe.d.ts +40 -0
- package/types/lib/ecosystems/venvProbe.d.ts.map +1 -0
- package/types/lib/evinser/evinser.d.ts +120 -13
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/evinser/scalasem.d.ts.map +1 -1
- package/types/lib/helpers/agenticToolutils.d.ts +53 -19
- package/types/lib/helpers/agenticToolutils.d.ts.map +1 -1
- package/types/lib/helpers/bomSigner.d.ts +20 -3
- package/types/lib/helpers/bomSigner.d.ts.map +1 -1
- package/types/lib/helpers/jssSigner.d.ts +288 -0
- package/types/lib/helpers/jssSigner.d.ts.map +1 -0
- package/types/lib/helpers/vsixutils.d.ts +303 -19
- package/types/lib/helpers/vsixutils.d.ts.map +1 -1
- package/types/lib/inventory/agentBlueprint.d.ts +70 -0
- package/types/lib/inventory/agentBlueprint.d.ts.map +1 -0
- package/types/lib/inventory/agentToolNames.d.ts +51 -0
- package/types/lib/inventory/agentToolNames.d.ts.map +1 -0
- package/types/lib/inventory/aiCollector.d.ts.map +1 -1
- package/types/lib/inventory/aiInventory.d.ts.map +1 -1
- package/types/lib/inventory/aiProviders.d.ts +85 -0
- package/types/lib/inventory/aiProviders.d.ts.map +1 -0
- package/types/lib/inventory/atomUtils.d.ts +19 -0
- package/types/lib/inventory/atomUtils.d.ts.map +1 -1
- package/types/lib/inventory/bomUtils.d.ts.map +1 -1
- package/types/lib/inventory/brotliDecoder.d.ts +58 -0
- package/types/lib/inventory/brotliDecoder.d.ts.map +1 -0
- package/types/lib/inventory/cbomutils.d.ts +17 -0
- package/types/lib/inventory/cbomutils.d.ts.map +1 -1
- package/types/lib/inventory/ciParsers/githubActionsLock.d.ts +101 -0
- package/types/lib/inventory/ciParsers/githubActionsLock.d.ts.map +1 -0
- package/types/lib/inventory/citations.d.ts +5 -0
- package/types/lib/inventory/citations.d.ts.map +1 -1
- package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/cyclonedx20.d.ts +99 -0
- package/types/lib/inventory/cyclonedx20.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20Common.d.ts +102 -0
- package/types/lib/inventory/cyclonedx20Common.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20Derived.d.ts +74 -0
- package/types/lib/inventory/cyclonedx20Derived.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20Parties.d.ts +91 -0
- package/types/lib/inventory/cyclonedx20Parties.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20Read.d.ts +51 -0
- package/types/lib/inventory/cyclonedx20Read.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20ReadCommon.d.ts +69 -0
- package/types/lib/inventory/cyclonedx20ReadCommon.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20ReadComponents.d.ts +77 -0
- package/types/lib/inventory/cyclonedx20ReadComponents.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20ReadContext.d.ts +118 -0
- package/types/lib/inventory/cyclonedx20ReadContext.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20ReadEvidence.d.ts +40 -0
- package/types/lib/inventory/cyclonedx20ReadEvidence.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20ReadSections.d.ts +45 -0
- package/types/lib/inventory/cyclonedx20ReadSections.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20Render.d.ts +53 -0
- package/types/lib/inventory/cyclonedx20Render.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderComponents.d.ts +73 -0
- package/types/lib/inventory/cyclonedx20RenderComponents.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderDefinitions.d.ts +29 -0
- package/types/lib/inventory/cyclonedx20RenderDefinitions.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderEvidence.d.ts +110 -0
- package/types/lib/inventory/cyclonedx20RenderEvidence.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderHardware.d.ts +42 -0
- package/types/lib/inventory/cyclonedx20RenderHardware.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderPatents.d.ts +26 -0
- package/types/lib/inventory/cyclonedx20RenderPatents.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderRefIds.d.ts +55 -0
- package/types/lib/inventory/cyclonedx20RenderRefIds.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20RenderSections.d.ts +45 -0
- package/types/lib/inventory/cyclonedx20RenderSections.d.ts.map +1 -0
- package/types/lib/inventory/cyclonedx20Tables.d.ts +134 -0
- package/types/lib/inventory/cyclonedx20Tables.d.ts.map +1 -0
- package/types/lib/inventory/deps.d.ts +35 -2
- package/types/lib/inventory/deps.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +36 -1
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/dosaiParsers.d.ts.map +1 -1
- package/types/lib/inventory/dotnetRuntime.d.ts +91 -0
- package/types/lib/inventory/dotnetRuntime.d.ts.map +1 -0
- package/types/lib/inventory/ecosystemFormulationParsers.d.ts +73 -0
- package/types/lib/inventory/ecosystemFormulationParsers.d.ts.map +1 -0
- package/types/lib/inventory/envcontext.d.ts +74 -12
- package/types/lib/inventory/envcontext.d.ts.map +1 -1
- package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
- package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
- package/types/lib/inventory/gtfobins.d.ts.map +1 -1
- package/types/lib/inventory/hbom.d.ts +11 -0
- package/types/lib/inventory/hbom.d.ts.map +1 -1
- package/types/lib/inventory/hbomLoader.d.ts +54 -0
- package/types/lib/inventory/hbomLoader.d.ts.map +1 -1
- package/types/lib/inventory/hostRuntimeProbe.d.ts +44 -0
- package/types/lib/inventory/hostRuntimeProbe.d.ts.map +1 -0
- package/types/lib/inventory/jvmLocalRepos.d.ts +5 -1
- package/types/lib/inventory/jvmLocalRepos.d.ts.map +1 -1
- package/types/lib/inventory/jvmToolEnv.d.ts +12 -0
- package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
- package/types/lib/inventory/licenseEnhancer.d.ts.map +1 -1
- package/types/lib/inventory/mavenCentralMirror.d.ts +170 -0
- package/types/lib/inventory/mavenCentralMirror.d.ts.map +1 -0
- package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/nodeBuildInfo.d.ts +95 -0
- package/types/lib/inventory/nodeBuildInfo.d.ts.map +1 -0
- package/types/lib/inventory/nodeHostProbe.d.ts +15 -0
- package/types/lib/inventory/nodeHostProbe.d.ts.map +1 -0
- package/types/lib/inventory/occurrenceKey.d.ts +52 -0
- package/types/lib/inventory/occurrenceKey.d.ts.map +1 -0
- package/types/lib/inventory/osPackageResolver.d.ts +38 -0
- package/types/lib/inventory/osPackageResolver.d.ts.map +1 -1
- package/types/lib/inventory/phpBuildInfo.d.ts.map +1 -1
- package/types/lib/inventory/phpHostProbe.d.ts.map +1 -1
- package/types/lib/inventory/pqcMigration.d.ts +84 -0
- package/types/lib/inventory/pqcMigration.d.ts.map +1 -0
- package/types/lib/inventory/rubyBuildInfo.d.ts +271 -0
- package/types/lib/inventory/rubyBuildInfo.d.ts.map +1 -0
- package/types/lib/inventory/rubyHostProbe.d.ts +47 -0
- package/types/lib/inventory/rubyHostProbe.d.ts.map +1 -0
- package/types/lib/inventory/rustFormulationParser.d.ts +7 -2
- package/types/lib/inventory/rustFormulationParser.d.ts.map +1 -1
- package/types/lib/inventory/scalaCoords.d.ts.map +1 -1
- package/types/lib/inventory/scalaDedupe.d.ts +43 -0
- package/types/lib/inventory/scalaDedupe.d.ts.map +1 -0
- package/types/lib/inventory/source.d.ts +25 -0
- package/types/lib/inventory/source.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts +12 -0
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/parsers/huggingfaceManifest.d.ts +10 -1
- package/types/lib/parsers/huggingfaceManifest.d.ts.map +1 -1
- package/types/lib/parsers/huggingfaceUtils.d.ts +12 -0
- package/types/lib/parsers/huggingfaceUtils.d.ts.map +1 -1
- package/types/lib/parsers/kiroPermissions.d.ts +62 -0
- package/types/lib/parsers/kiroPermissions.d.ts.map +1 -0
- package/types/lib/parsers/largeJson.d.ts +4 -3
- package/types/lib/parsers/largeJson.d.ts.map +1 -1
- package/types/lib/parsers/vscodeExtensionId.d.ts +48 -0
- package/types/lib/parsers/vscodeExtensionId.d.ts.map +1 -0
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts +7 -0
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
- package/types/lib/stages/postgen/specVersionCompat.d.ts +25 -0
- package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
- package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
- package/types/lib/stages/pregen/pregen.d.ts +12 -8
- package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts +5 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
- package/types/lib/validator/complianceEngine.d.ts +4 -4
- package/types/lib/validator/complianceEngine.d.ts.map +1 -1
- package/types/lib/validator/complianceRules.d.ts +14 -2
- package/types/lib/validator/complianceRules.d.ts.map +1 -1
- package/types/lib/validator/index.d.ts +10 -0
- package/types/lib/validator/index.d.ts.map +1 -1
- package/types/lib/validator/reporters/json.d.ts.map +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
2
|
import { chmodSync, readdirSync, readFileSync } from "node:fs";
|
|
3
3
|
import { homedir, platform } from "node:os";
|
|
4
|
-
import { basename, join, resolve } from "node:path";
|
|
4
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
5
5
|
import process from "node:process";
|
|
6
6
|
|
|
7
7
|
import { build } from "@cdxgen/cdx-purl";
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
readEnvironmentVariable,
|
|
13
13
|
recordDecisionActivity,
|
|
14
14
|
} from "../core/activity.js";
|
|
15
|
+
import { readRegularTextFile } from "../core/boundedFile.js";
|
|
15
16
|
import {
|
|
16
17
|
commandOutputText,
|
|
17
18
|
isLedgerEnabled,
|
|
@@ -30,9 +31,13 @@ import {
|
|
|
30
31
|
noteBuildToolRateLimit,
|
|
31
32
|
} from "../inventory/buildToolRateLimit.js";
|
|
32
33
|
import { collectJarNS } from "../inventory/deps.js";
|
|
34
|
+
import { splitArtifactFileName } from "../inventory/jvmLocalRepos.js";
|
|
33
35
|
import { minimumGradleVersionForJava } from "../inventory/jvmToolEnv.js";
|
|
34
|
-
import {
|
|
36
|
+
import { gradleMirrorInitScriptArgs } from "../inventory/mavenCentralMirror.js";
|
|
37
|
+
import { applyPurl, mavenPurl, tryBuildPurl } from "../inventory/purl.js";
|
|
38
|
+
import { xml2js } from "../parsers/xml.js";
|
|
35
39
|
import { parsePkgJson } from "./parsers-js.js";
|
|
40
|
+
import { isInVendoredDir } from "./vendoredCode.js";
|
|
36
41
|
|
|
37
42
|
/**
|
|
38
43
|
* True when the pre-generation stage pinned an exact JVM build tool version
|
|
@@ -218,6 +223,50 @@ function gradleEvaluationOverrides(gradleArguments) {
|
|
|
218
223
|
return overrides;
|
|
219
224
|
}
|
|
220
225
|
|
|
226
|
+
/**
|
|
227
|
+
* Keep Gradle's task headers in the output. cdxgen splits the `properties`
|
|
228
|
+
* and `dependencies` reports by the `> Task :path` headers, which Gradle
|
|
229
|
+
* prints at the lifecycle log level, so a project that sets
|
|
230
|
+
* `org.gradle.logging.level=quiet` or `warn` in `gradle.properties` would
|
|
231
|
+
* leave nothing to split. A command-line `-D` property beats
|
|
232
|
+
* `gradle.properties`, and a log-level option such as `--info` beats the
|
|
233
|
+
* property. A log level the user chose in `GRADLE_ARGS` is left alone.
|
|
234
|
+
*
|
|
235
|
+
* @param {string[]} gradleArguments User-supplied general gradle arguments
|
|
236
|
+
* @returns {string[]} The `-D` flag to inject, or nothing
|
|
237
|
+
*/
|
|
238
|
+
function gradleLogLevelOverride(gradleArguments) {
|
|
239
|
+
const logLevelOptions = [
|
|
240
|
+
"-q",
|
|
241
|
+
"--quiet",
|
|
242
|
+
"-w",
|
|
243
|
+
"--warn",
|
|
244
|
+
"-i",
|
|
245
|
+
"--info",
|
|
246
|
+
"-d",
|
|
247
|
+
"--debug",
|
|
248
|
+
];
|
|
249
|
+
const userChose = gradleArguments.some(
|
|
250
|
+
(arg) =>
|
|
251
|
+
logLevelOptions.includes(arg) ||
|
|
252
|
+
arg.startsWith("-Dorg.gradle.logging.level="),
|
|
253
|
+
);
|
|
254
|
+
return userChose ? [] : ["-Dorg.gradle.logging.level=lifecycle"];
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Read a whitespace-separated list of Gradle arguments from an environment
|
|
259
|
+
* variable such as `GRADLE_ARGS`. Repeated, leading or trailing whitespace
|
|
260
|
+
* adds no empty argument, which Gradle would read as a task path.
|
|
261
|
+
*
|
|
262
|
+
* @param {string} name Name of the environment variable
|
|
263
|
+
* @returns {string[]} The arguments, empty when the variable is unset
|
|
264
|
+
*/
|
|
265
|
+
export function gradleArgsFromEnv(name) {
|
|
266
|
+
const value = readEnvironmentVariable(name);
|
|
267
|
+
return value ? value.split(/\s+/).filter(Boolean) : [];
|
|
268
|
+
}
|
|
269
|
+
|
|
221
270
|
/**
|
|
222
271
|
* Method to combine the general gradle arguments, the sub-commands and the sub-commands' arguments in the correct way
|
|
223
272
|
*
|
|
@@ -242,6 +291,8 @@ export function buildGradleCommandArguments(
|
|
|
242
291
|
]
|
|
243
292
|
.concat(getGradleDaemonParameter())
|
|
244
293
|
.concat(gradleEvaluationOverrides(gradleArguments))
|
|
294
|
+
.concat(gradleLogLevelOverride(gradleArguments))
|
|
295
|
+
.concat(gradleMirrorInitScriptArgs())
|
|
245
296
|
.concat(gradleArguments);
|
|
246
297
|
const maxCliArgsLength = isWin
|
|
247
298
|
? 7500 - gradleCommandLength - mainGradleArguments.join(" ").length - 2
|
|
@@ -257,7 +308,13 @@ export function buildGradleCommandArguments(
|
|
|
257
308
|
for (const gradleSubCommand of gradleSubCommands) {
|
|
258
309
|
const subCommandLength =
|
|
259
310
|
[gradleSubCommand, ...gradleSubCommandArguments].join(" ").length + 1;
|
|
260
|
-
|
|
311
|
+
// An invocation always carries at least one task: without one Gradle
|
|
312
|
+
// runs the project's default tasks.
|
|
313
|
+
if (
|
|
314
|
+
maxCliArgsLength !== -1 &&
|
|
315
|
+
remainingLength - subCommandLength < 0 &&
|
|
316
|
+
allGradleArguments.length > mainGradleArguments.length
|
|
317
|
+
) {
|
|
261
318
|
splitArgs.push(allGradleArguments);
|
|
262
319
|
allGradleArguments = [].concat(mainGradleArguments);
|
|
263
320
|
remainingLength = maxCliArgsLength;
|
|
@@ -297,16 +354,52 @@ function parseGradleTaskHeader(line) {
|
|
|
297
354
|
if (!line.startsWith("> Task :")) {
|
|
298
355
|
return undefined;
|
|
299
356
|
}
|
|
300
|
-
//
|
|
301
|
-
// follow
|
|
302
|
-
|
|
357
|
+
// A project path may contain spaces (`:my lib`), and a status such as
|
|
358
|
+
// UP-TO-DATE or FAILED may follow the task name, so the task name is the
|
|
359
|
+
// first token after the last colon. A status never contains a colon, and a
|
|
360
|
+
// CRLF line keeps its carriage return.
|
|
361
|
+
const taskPath = line.slice("> Task ".length).trim();
|
|
303
362
|
const lastColon = taskPath.lastIndexOf(":");
|
|
304
363
|
return {
|
|
305
364
|
projectPath: taskPath.slice(0, lastColon),
|
|
306
|
-
taskName: taskPath.slice(lastColon + 1),
|
|
365
|
+
taskName: taskPath.slice(lastColon + 1).split(/\s+/)[0],
|
|
307
366
|
};
|
|
308
367
|
}
|
|
309
368
|
|
|
369
|
+
/**
|
|
370
|
+
* List the dependency tasks to run for the root project and every module read
|
|
371
|
+
* from `gradle properties`.
|
|
372
|
+
*
|
|
373
|
+
* The root project is recognised by identity, never by purl. A module named
|
|
374
|
+
* after the root project that inherits its group and version has the root's
|
|
375
|
+
* purl (issue 4695), and it still needs its own `:module:dependencies` task.
|
|
376
|
+
* `root` in `modulesToSkip` drops the root task; any other entry is a module
|
|
377
|
+
* path.
|
|
378
|
+
*
|
|
379
|
+
* @param {Map<string, Object>} gradleModules Modules keyed by project path, and the root project by its name
|
|
380
|
+
* @param {Object} rootComponent The component built for the root project
|
|
381
|
+
* @param {string} gradleDepTask Name of the dependency task, usually `dependencies`
|
|
382
|
+
* @param {string[]} modulesToSkip Module paths, or `root`, to leave out
|
|
383
|
+
* @returns {string[]} Task paths in the order Gradle should run them
|
|
384
|
+
*/
|
|
385
|
+
export function gradleDependencyTasks(
|
|
386
|
+
gradleModules,
|
|
387
|
+
rootComponent,
|
|
388
|
+
gradleDepTask,
|
|
389
|
+
modulesToSkip = [],
|
|
390
|
+
) {
|
|
391
|
+
const tasks = [];
|
|
392
|
+
if (!modulesToSkip.includes("root")) {
|
|
393
|
+
tasks.push(gradleDepTask);
|
|
394
|
+
}
|
|
395
|
+
for (const [key, module] of gradleModules) {
|
|
396
|
+
if (module !== rootComponent && !modulesToSkip.includes(key)) {
|
|
397
|
+
tasks.push(`${key}:${gradleDepTask}`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return tasks;
|
|
401
|
+
}
|
|
402
|
+
|
|
310
403
|
/**
|
|
311
404
|
* Method to split the output produced by Gradle using parallel processing by project
|
|
312
405
|
*
|
|
@@ -398,27 +491,19 @@ export function parseGradleProjects(rawOutput) {
|
|
|
398
491
|
}
|
|
399
492
|
}
|
|
400
493
|
}
|
|
401
|
-
} else
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
const tmpName = projName.split(" ")[0];
|
|
407
|
-
if (tmpName.length > 1) {
|
|
408
|
-
projects.add(tmpName);
|
|
409
|
-
}
|
|
494
|
+
} else {
|
|
495
|
+
for (const marker of ["--- project ", "-> project "]) {
|
|
496
|
+
const markerIndex = l.indexOf(marker);
|
|
497
|
+
if (markerIndex === -1) {
|
|
498
|
+
continue;
|
|
410
499
|
}
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
if (projName.startsWith(":")) {
|
|
417
|
-
const tmpName = projName.split(" ")[0];
|
|
418
|
-
if (tmpName.length > 1) {
|
|
419
|
-
projects.add(tmpName);
|
|
420
|
-
}
|
|
500
|
+
const projName = gradleProjectPath(
|
|
501
|
+
l.slice(markerIndex + marker.length),
|
|
502
|
+
);
|
|
503
|
+
if (projName?.startsWith(":") && projName.length > 1) {
|
|
504
|
+
projects.add(projName);
|
|
421
505
|
}
|
|
506
|
+
break;
|
|
422
507
|
}
|
|
423
508
|
}
|
|
424
509
|
});
|
|
@@ -432,11 +517,17 @@ export function parseGradleProjects(rawOutput) {
|
|
|
432
517
|
/**
|
|
433
518
|
* Parse gradle properties output
|
|
434
519
|
*
|
|
520
|
+
* `gradlePlugin` is true when the project has the `gradlePlugin` extension,
|
|
521
|
+
* which `java-gradle-plugin`, `kotlin-dsl` and `groovy-gradle-plugin` add: the
|
|
522
|
+
* project builds Gradle plugins, as a `build-logic` build does.
|
|
523
|
+
*
|
|
435
524
|
* @param {string} rawOutput Raw string output
|
|
436
525
|
* @param {string} gradleModuleName The name (or 'path') of the module as seen from the root of the project
|
|
526
|
+
* @returns {{rootProject: string, projects: string[], metadata: Object, gradlePlugin: boolean}} The parsed properties
|
|
437
527
|
*/
|
|
438
528
|
export function parseGradleProperties(rawOutput, gradleModuleName = null) {
|
|
439
529
|
let rootProject = "root";
|
|
530
|
+
let gradlePlugin = false;
|
|
440
531
|
const projects = new Set();
|
|
441
532
|
const metadata = { group: "", version: "latest", properties: [] };
|
|
442
533
|
if (gradleModuleName) {
|
|
@@ -472,6 +563,8 @@ export function parseGradleProperties(rawOutput, gradleModuleName = null) {
|
|
|
472
563
|
metadata[tmpB[0]] = tmpB[1].trim().replace("unspecified", "latest");
|
|
473
564
|
} else if (["buildFile", "projectDir", "rootDir"].includes(tmpB[0])) {
|
|
474
565
|
metadata.properties.push({ name: tmpB[0], value: tmpB[1].trim() });
|
|
566
|
+
} else if (tmpB[0] === "gradlePlugin") {
|
|
567
|
+
gradlePlugin = true;
|
|
475
568
|
} else if (tmpB[0] === "subprojects") {
|
|
476
569
|
const spStrs = tmpB[1].replace(/[[\]']/g, "").split(", ");
|
|
477
570
|
const tmpprojects = spStrs
|
|
@@ -486,6 +579,7 @@ export function parseGradleProperties(rawOutput, gradleModuleName = null) {
|
|
|
486
579
|
rootProject,
|
|
487
580
|
projects: Array.from(projects),
|
|
488
581
|
metadata,
|
|
582
|
+
gradlePlugin,
|
|
489
583
|
};
|
|
490
584
|
}
|
|
491
585
|
|
|
@@ -511,17 +605,11 @@ export function executeParallelGradleProperties(
|
|
|
511
605
|
}
|
|
512
606
|
const gradleCmd = getGradleCommand(dir, null, options);
|
|
513
607
|
const gradleArgs = buildGradleCommandArguments(
|
|
514
|
-
extraArgs.concat(
|
|
515
|
-
readEnvironmentVariable("GRADLE_ARGS")
|
|
516
|
-
? readEnvironmentVariable("GRADLE_ARGS").split(" ")
|
|
517
|
-
: [],
|
|
518
|
-
),
|
|
608
|
+
extraArgs.concat(gradleArgsFromEnv("GRADLE_ARGS")),
|
|
519
609
|
allProjectsStr.map((project) =>
|
|
520
610
|
project ? `${project}:properties` : "properties",
|
|
521
611
|
),
|
|
522
|
-
|
|
523
|
-
? readEnvironmentVariable("GRADLE_ARGS_PROPERTIES").split(" ")
|
|
524
|
-
: [],
|
|
612
|
+
gradleArgsFromEnv("GRADLE_ARGS_PROPERTIES"),
|
|
525
613
|
gradleCmd.length,
|
|
526
614
|
);
|
|
527
615
|
const allOutputs = [];
|
|
@@ -880,19 +968,539 @@ export function recordGradleInvocationFailure(result, context = {}) {
|
|
|
880
968
|
});
|
|
881
969
|
}
|
|
882
970
|
|
|
971
|
+
/**
|
|
972
|
+
* Configurations whose dependencies never reach the packaged application:
|
|
973
|
+
* code-quality tools, coverage agents and Spring Boot's development-only
|
|
974
|
+
* dependencies.
|
|
975
|
+
*/
|
|
976
|
+
const GRADLE_TOOL_CONFIGURATIONS = new Set([
|
|
977
|
+
"checkstyle",
|
|
978
|
+
"codenarc",
|
|
979
|
+
"detekt",
|
|
980
|
+
"developmentOnly",
|
|
981
|
+
"errorprone",
|
|
982
|
+
"jacocoAgent",
|
|
983
|
+
"jacocoAnt",
|
|
984
|
+
"ktlint",
|
|
985
|
+
"lintChecks",
|
|
986
|
+
"lintClassPath",
|
|
987
|
+
"pmd",
|
|
988
|
+
"spotbugs",
|
|
989
|
+
"spotbugsPlugins",
|
|
990
|
+
]);
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* Parts of a configuration name that mark it as feeding the compiler or a
|
|
994
|
+
* build tool, such as `annotationProcessor`, Android's
|
|
995
|
+
* `debugAnnotationProcessorClasspath` or `kotlinCompilerPluginClasspathMain`.
|
|
996
|
+
*/
|
|
997
|
+
const GRADLE_TOOL_CONFIGURATION_PARTS = [
|
|
998
|
+
"annotationprocessor",
|
|
999
|
+
"buildtoolsapiclasspath",
|
|
1000
|
+
"commonizer",
|
|
1001
|
+
"compilerclasspath",
|
|
1002
|
+
"compilerpluginclasspath",
|
|
1003
|
+
];
|
|
1004
|
+
|
|
1005
|
+
/**
|
|
1006
|
+
* Properties that describe a dependency Gradle could not resolve.
|
|
1007
|
+
*/
|
|
1008
|
+
const GRADLE_FAILURE_PROPERTIES = [
|
|
1009
|
+
"cdx:gradle:resolutionFailed",
|
|
1010
|
+
"cdx:gradle:requestedVersion",
|
|
1011
|
+
];
|
|
1012
|
+
|
|
1013
|
+
/**
|
|
1014
|
+
* Markers Gradle appends to a dependency tree line, with the flag each sets.
|
|
1015
|
+
*/
|
|
1016
|
+
const GRADLE_TREE_LINE_MARKERS = [
|
|
1017
|
+
["(*)", "omitted"],
|
|
1018
|
+
["(c)", "constraint"],
|
|
1019
|
+
["(n)", "unresolvable"],
|
|
1020
|
+
["FAILED", "failed"],
|
|
1021
|
+
];
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* Read the name of the configuration a `gradle dependencies` header line
|
|
1025
|
+
* introduces, such as `runtimeClasspath - Runtime classpath of source set
|
|
1026
|
+
* 'main'.`, `api (n)`, a bare `classpath`, a Groovy name such as
|
|
1027
|
+
* `integration-test` or an internal one such as
|
|
1028
|
+
* `_internal-unified-test-platform-android-device-provider-ddmlib`.
|
|
1029
|
+
*
|
|
1030
|
+
* @param {string} line One line of the report
|
|
1031
|
+
* @returns {string|undefined} The configuration name, or undefined when the
|
|
1032
|
+
* line is not a configuration header
|
|
1033
|
+
*/
|
|
1034
|
+
function parseGradleConfigurationHeader(line) {
|
|
1035
|
+
let name = line.split(" - ")[0];
|
|
1036
|
+
if (name.endsWith(" (n)")) {
|
|
1037
|
+
name = name.slice(0, -" (n)".length);
|
|
1038
|
+
}
|
|
1039
|
+
return /^[A-Za-z_][A-Za-z0-9_.-]*$/.test(name) ? name : undefined;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
/**
|
|
1043
|
+
* Configurations, other than runtime classpaths, whose dependencies ship
|
|
1044
|
+
* with the application: `compile` and `default` in Gradle 6 and older, which
|
|
1045
|
+
* the runtime classpath extended, and the Shadow plugin's `shadow`, whose
|
|
1046
|
+
* dependencies the published artifact declares as runtime dependencies.
|
|
1047
|
+
*/
|
|
1048
|
+
const GRADLE_SHIPPED_CONFIGURATIONS = new Set(["compile", "default", "shadow"]);
|
|
1049
|
+
|
|
1050
|
+
/**
|
|
1051
|
+
* Map a Gradle configuration to the CycloneDX scope of the dependencies it
|
|
1052
|
+
* resolves, so that only what ships with the application reads as required.
|
|
1053
|
+
*
|
|
1054
|
+
* Configurations whose name starts with `_`, such as the Android Gradle
|
|
1055
|
+
* plugin's `_internal-unified-test-platform-*` ones, are the plugin's own
|
|
1056
|
+
* tooling and `excluded`. Test configurations are `optional`. Runtime
|
|
1057
|
+
* classpaths, including Android
|
|
1058
|
+
* variants such as `debugRuntimeClasspath` and Kotlin targets such as
|
|
1059
|
+
* `jvmRuntimeClasspath`, are `required`, and so are Kotlin/Native klibraries,
|
|
1060
|
+
* which are linked into the binary, and the configurations in
|
|
1061
|
+
* `GRADLE_SHIPPED_CONFIGURATIONS`. Everything else is `excluded`: annotation
|
|
1062
|
+
* processors, compiler, code-quality and other build-tool classpaths, and
|
|
1063
|
+
* compile classpaths, whose dependencies a runtime classpath also lists unless
|
|
1064
|
+
* they are compile-only, as Maven's `provided` scope is. A dependency seen in
|
|
1065
|
+
* several configurations keeps the strongest scope among them. Test and
|
|
1066
|
+
* runtime are matched as whole words of the camel-case name, so
|
|
1067
|
+
* `latestRuntimeClasspath` is a runtime classpath, not a test one.
|
|
1068
|
+
*
|
|
1069
|
+
* @param {string} configurationName Name of the configuration
|
|
1070
|
+
* @returns {string} The CycloneDX scope
|
|
1071
|
+
*/
|
|
1072
|
+
function gradleConfigurationScope(configurationName) {
|
|
1073
|
+
const lowerName = configurationName.toLowerCase();
|
|
1074
|
+
const words = configurationName
|
|
1075
|
+
.split(/[-_.]|(?=[A-Z])/)
|
|
1076
|
+
.map((word) => word.toLowerCase());
|
|
1077
|
+
if (
|
|
1078
|
+
GRADLE_TOOL_CONFIGURATIONS.has(configurationName) ||
|
|
1079
|
+
configurationName.startsWith("_") ||
|
|
1080
|
+
["kapt", "ksp"].includes(words[0]) ||
|
|
1081
|
+
GRADLE_TOOL_CONFIGURATION_PARTS.some((part) => lowerName.includes(part))
|
|
1082
|
+
) {
|
|
1083
|
+
return "excluded";
|
|
1084
|
+
}
|
|
1085
|
+
if (words.includes("test") || words.includes("tests")) {
|
|
1086
|
+
return "optional";
|
|
1087
|
+
}
|
|
1088
|
+
if (
|
|
1089
|
+
words.includes("runtime") ||
|
|
1090
|
+
words.includes("klibraries") ||
|
|
1091
|
+
GRADLE_SHIPPED_CONFIGURATIONS.has(configurationName)
|
|
1092
|
+
) {
|
|
1093
|
+
return "required";
|
|
1094
|
+
}
|
|
1095
|
+
return "excluded";
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
/**
|
|
1099
|
+
* Rank a scope so that a dependency seen in several configurations keeps the
|
|
1100
|
+
* strongest one: `required` or no scope (which CycloneDX reads as required),
|
|
1101
|
+
* then `optional`, then `excluded`.
|
|
1102
|
+
*
|
|
1103
|
+
* @param {string|undefined} scope CycloneDX scope
|
|
1104
|
+
* @returns {number} The rank, higher being stronger
|
|
1105
|
+
*/
|
|
1106
|
+
function gradleScopeRank(scope) {
|
|
1107
|
+
if (scope === "required" || scope === undefined) {
|
|
1108
|
+
return 2;
|
|
1109
|
+
}
|
|
1110
|
+
return scope === "optional" ? 1 : 0;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
/**
|
|
1114
|
+
* Read a project path from the text that follows `project ` in a dependency
|
|
1115
|
+
* tree line. Gradle 9 quotes the path (`project ':lib'`), Gradle 8 does not
|
|
1116
|
+
* (`project :lib`).
|
|
1117
|
+
*
|
|
1118
|
+
* @param {string} text The text after `project `
|
|
1119
|
+
* @returns {string|undefined} The project path, such as `:lib`
|
|
1120
|
+
*/
|
|
1121
|
+
function gradleProjectPath(text) {
|
|
1122
|
+
const trimmed = text.trim();
|
|
1123
|
+
if (trimmed.startsWith("'")) {
|
|
1124
|
+
const end = trimmed.indexOf("'", 1);
|
|
1125
|
+
return end > 1 ? trimmed.slice(1, end) : undefined;
|
|
1126
|
+
}
|
|
1127
|
+
return trimmed.split(" ")[0] || undefined;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
/**
|
|
1131
|
+
* Find the module a project path in a dependency tree names. Subprojects are
|
|
1132
|
+
* keyed by their path (`:lib`) and the root project by its name, so the root
|
|
1133
|
+
* path `:` resolves to the report's own project when that is the root, or else
|
|
1134
|
+
* to the one module keyed by a name.
|
|
1135
|
+
*
|
|
1136
|
+
* @param {Map} gradleModules Modules keyed by project path or root name
|
|
1137
|
+
* @param {string} projectPath Project path from the tree, such as `:lib` or `:`
|
|
1138
|
+
* @param {string} reportProjectName Key of the project the report belongs to
|
|
1139
|
+
* @returns {Object|undefined} The module component
|
|
1140
|
+
*/
|
|
1141
|
+
function gradleModuleForPath(gradleModules, projectPath, reportProjectName) {
|
|
1142
|
+
if (projectPath !== ":") {
|
|
1143
|
+
return gradleModules.get(projectPath);
|
|
1144
|
+
}
|
|
1145
|
+
if (!reportProjectName.startsWith(":")) {
|
|
1146
|
+
return gradleModules.get(reportProjectName);
|
|
1147
|
+
}
|
|
1148
|
+
const rootKeys = [...gradleModules.keys()].filter((k) => !k.startsWith(":"));
|
|
1149
|
+
return rootKeys.length === 1 ? gradleModules.get(rootKeys[0]) : undefined;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
/**
|
|
1153
|
+
* Pick a concrete version out of the version Gradle printed as requested. A
|
|
1154
|
+
* rich version such as `{strictly 1.0}`, `{require 2.2; reject 2.0}` or
|
|
1155
|
+
* `{prefer 2.0.9}` gives the version of its first strictly, require or prefer
|
|
1156
|
+
* clause. A range or dynamic version gives none.
|
|
1157
|
+
*
|
|
1158
|
+
* @param {string|undefined} versionText The requested version
|
|
1159
|
+
* @returns {string|undefined} The concrete version
|
|
1160
|
+
*/
|
|
1161
|
+
function gradleRequestedVersion(versionText) {
|
|
1162
|
+
if (!versionText) {
|
|
1163
|
+
return undefined;
|
|
1164
|
+
}
|
|
1165
|
+
// `1.+` and `latest.release` are dynamic and a bracketed range is a
|
|
1166
|
+
// range, but `0.92.2+1.20.1` is a concrete version with build metadata.
|
|
1167
|
+
const isConcrete = (v) =>
|
|
1168
|
+
v &&
|
|
1169
|
+
!v.endsWith("+") &&
|
|
1170
|
+
!v.startsWith("latest.") &&
|
|
1171
|
+
![..."[](),"].some((ch) => v.includes(ch));
|
|
1172
|
+
if (!versionText.startsWith("{")) {
|
|
1173
|
+
return isConcrete(versionText) ? versionText : undefined;
|
|
1174
|
+
}
|
|
1175
|
+
const body = versionText.slice(1, versionText.lastIndexOf("}"));
|
|
1176
|
+
for (const clause of body.split(";")) {
|
|
1177
|
+
const [keyword, ...rest] = clause.trim().split(" ");
|
|
1178
|
+
const value = rest.join(" ").trim();
|
|
1179
|
+
if (
|
|
1180
|
+
["strictly", "require", "prefer"].includes(keyword) &&
|
|
1181
|
+
isConcrete(value)
|
|
1182
|
+
) {
|
|
1183
|
+
return value;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
return undefined;
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
/**
|
|
1190
|
+
* Split one line of a `gradle dependencies` tree into its depth and the
|
|
1191
|
+
* dependency it names. The selected side of `->` wins over the requested
|
|
1192
|
+
* side, whether it is a version, other coordinates or a project.
|
|
1193
|
+
*
|
|
1194
|
+
* @param {string} line One line of the report, without its line ending
|
|
1195
|
+
* @returns {Object|undefined} `{ level, group, name, version,
|
|
1196
|
+
* requestedVersion, projectPath, omitted, constraint, unresolvable, failed }`,
|
|
1197
|
+
* `{ level, unknown: true }` for a tree line whose dependency cannot be
|
|
1198
|
+
* read, or undefined when the line is not a tree line
|
|
1199
|
+
*/
|
|
1200
|
+
function parseGradleTreeLine(line) {
|
|
1201
|
+
const markerIndex = line.indexOf("--- ");
|
|
1202
|
+
if (markerIndex < 1) {
|
|
1203
|
+
return undefined;
|
|
1204
|
+
}
|
|
1205
|
+
for (const ch of line.slice(0, markerIndex)) {
|
|
1206
|
+
if (!"|+\\ ".includes(ch)) {
|
|
1207
|
+
return undefined;
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
const parsed = {
|
|
1211
|
+
level: Math.floor(markerIndex / 5) + 1,
|
|
1212
|
+
omitted: false,
|
|
1213
|
+
constraint: false,
|
|
1214
|
+
unresolvable: false,
|
|
1215
|
+
failed: false,
|
|
1216
|
+
};
|
|
1217
|
+
let text = line.slice(markerIndex + "--- ".length).trim();
|
|
1218
|
+
let stripped = true;
|
|
1219
|
+
while (stripped) {
|
|
1220
|
+
stripped = false;
|
|
1221
|
+
for (const [marker, flag] of GRADLE_TREE_LINE_MARKERS) {
|
|
1222
|
+
if (text.endsWith(` ${marker}`)) {
|
|
1223
|
+
parsed[flag] = true;
|
|
1224
|
+
text = text.slice(0, -marker.length - 1).trimEnd();
|
|
1225
|
+
stripped = true;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
const [requested, selected] = text.split(" -> ");
|
|
1230
|
+
// Gradle 9.6 and later print the root project as `root project '<name>'`
|
|
1231
|
+
// where earlier versions print `project :`. Either way it is the root
|
|
1232
|
+
// project, whose path is `:`.
|
|
1233
|
+
if (requested.startsWith("root project ")) {
|
|
1234
|
+
parsed.projectPath = ":";
|
|
1235
|
+
} else if (requested.startsWith("project ")) {
|
|
1236
|
+
parsed.projectPath = gradleProjectPath(requested.slice("project ".length));
|
|
1237
|
+
} else {
|
|
1238
|
+
const [group, name, ...versionParts] = requested.split(":");
|
|
1239
|
+
if (!group || !name || group.includes(" ") || name.includes(" ")) {
|
|
1240
|
+
return { level: parsed.level, unknown: true };
|
|
1241
|
+
}
|
|
1242
|
+
parsed.group = group;
|
|
1243
|
+
parsed.name = name;
|
|
1244
|
+
parsed.requestedVersion = versionParts.join(":") || undefined;
|
|
1245
|
+
parsed.version = gradleRequestedVersion(parsed.requestedVersion);
|
|
1246
|
+
}
|
|
1247
|
+
if (selected?.startsWith("root project ")) {
|
|
1248
|
+
// A module substituted by the root project, as with
|
|
1249
|
+
// `substitute module('org.ex:main') using project(':')`.
|
|
1250
|
+
parsed.projectPath = ":";
|
|
1251
|
+
} else if (selected?.startsWith("project ")) {
|
|
1252
|
+
parsed.projectPath = gradleProjectPath(selected.slice("project ".length));
|
|
1253
|
+
} else if (selected?.includes(":")) {
|
|
1254
|
+
// The selected module replaces the requested one, version and all.
|
|
1255
|
+
const [group, name, version] = selected.split(":");
|
|
1256
|
+
parsed.group = group;
|
|
1257
|
+
parsed.name = name;
|
|
1258
|
+
parsed.version = version;
|
|
1259
|
+
parsed.requestedVersion = undefined;
|
|
1260
|
+
} else if (selected && !/\s/u.test(selected)) {
|
|
1261
|
+
// A selected version has no whitespace. Anything else is a form of the
|
|
1262
|
+
// report this parser does not know, and must not become a version.
|
|
1263
|
+
parsed.version = selected;
|
|
1264
|
+
} else if (selected) {
|
|
1265
|
+
return { level: parsed.level, unknown: true };
|
|
1266
|
+
}
|
|
1267
|
+
if (parsed.projectPath === undefined && !parsed.group) {
|
|
1268
|
+
return { level: parsed.level, unknown: true };
|
|
1269
|
+
}
|
|
1270
|
+
return parsed;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
/**
|
|
1274
|
+
* Read the properties of every project the dependency reports name that
|
|
1275
|
+
* `gradleModules` does not have yet, such as a project of an included build,
|
|
1276
|
+
* and add a module for each one whose properties task printed a report. One
|
|
1277
|
+
* Gradle invocation covers all of them.
|
|
1278
|
+
*
|
|
1279
|
+
* @param {Iterable<string>} reports Text of the `gradle dependencies` reports
|
|
1280
|
+
* @param {Map} gradleModules Modules keyed by project path, updated in place
|
|
1281
|
+
* @param {string} gradleRootPath Directory to run Gradle in
|
|
1282
|
+
* @param {Object} options CLI options
|
|
1283
|
+
* @returns {Promise<string[]>} Paths of the projects added
|
|
1284
|
+
*/
|
|
1285
|
+
export async function loadGradleModulesForReports(
|
|
1286
|
+
reports,
|
|
1287
|
+
gradleModules,
|
|
1288
|
+
gradleRootPath,
|
|
1289
|
+
options = {},
|
|
1290
|
+
) {
|
|
1291
|
+
const modulesToSkip = readEnvironmentVariable("GRADLE_SKIP_MODULES")
|
|
1292
|
+
? readEnvironmentVariable("GRADLE_SKIP_MODULES").split(",")
|
|
1293
|
+
: [];
|
|
1294
|
+
const missing = new Set();
|
|
1295
|
+
for (const report of reports) {
|
|
1296
|
+
for (const projectPath of parseGradleProjects(report).projects) {
|
|
1297
|
+
if (
|
|
1298
|
+
!gradleModules.has(projectPath) &&
|
|
1299
|
+
!modulesToSkip.includes(projectPath)
|
|
1300
|
+
) {
|
|
1301
|
+
missing.add(projectPath);
|
|
1302
|
+
}
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
if (!missing.size || !isProjectToolingAllowed(options)) {
|
|
1306
|
+
return [];
|
|
1307
|
+
}
|
|
1308
|
+
const splitPropTaskOut = splitOutputByGradleProjects(
|
|
1309
|
+
executeParallelGradleProperties(gradleRootPath, [...missing], [], options),
|
|
1310
|
+
["properties"],
|
|
1311
|
+
);
|
|
1312
|
+
const added = [];
|
|
1313
|
+
for (const projectPath of missing) {
|
|
1314
|
+
// A project whose properties task printed nothing has no metadata to
|
|
1315
|
+
// build a module from.
|
|
1316
|
+
if (!splitPropTaskOut.has(projectPath)) {
|
|
1317
|
+
continue;
|
|
1318
|
+
}
|
|
1319
|
+
const propMap = parseGradleProperties(
|
|
1320
|
+
splitPropTaskOut.get(projectPath),
|
|
1321
|
+
projectPath,
|
|
1322
|
+
);
|
|
1323
|
+
gradleModules.set(
|
|
1324
|
+
projectPath,
|
|
1325
|
+
await buildObjectForGradleModule(
|
|
1326
|
+
propMap.rootProject === "root"
|
|
1327
|
+
? projectPath.split(":").pop()
|
|
1328
|
+
: propMap.rootProject,
|
|
1329
|
+
propMap.metadata,
|
|
1330
|
+
),
|
|
1331
|
+
);
|
|
1332
|
+
added.push(projectPath);
|
|
1333
|
+
}
|
|
1334
|
+
return added;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/**
|
|
1338
|
+
* Give the components of a build-tooling report, such as that of an included
|
|
1339
|
+
* build that builds Gradle plugins, the `excluded` scope that build-tool
|
|
1340
|
+
* configurations get. `reconcileGradleComponents` later keeps a stronger
|
|
1341
|
+
* scope that another project's report gives the same component.
|
|
1342
|
+
*
|
|
1343
|
+
* @param {Object[]} pkgList Components from one project's report, updated in place
|
|
1344
|
+
* @param {Map<string, Object>} gradleModules Modules, which keep their own scope
|
|
1345
|
+
* @returns {Object[]} The same list
|
|
1346
|
+
*/
|
|
1347
|
+
export function excludeGradleBuildTooling(pkgList, gradleModules) {
|
|
1348
|
+
const modules = new Set(gradleModules.values());
|
|
1349
|
+
for (const pkg of pkgList) {
|
|
1350
|
+
if (!modules.has(pkg)) {
|
|
1351
|
+
pkg.scope = "excluded";
|
|
1352
|
+
}
|
|
1353
|
+
}
|
|
1354
|
+
return pkgList;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
/**
|
|
1358
|
+
* Reconcile the copies of a component that several Gradle projects report.
|
|
1359
|
+
* Every copy gets the strongest scope any project gave it, so a library one
|
|
1360
|
+
* module only tests with stays required when another module compiles against
|
|
1361
|
+
* it, and a dependency one project failed to resolve loses its failure
|
|
1362
|
+
* properties when another project resolved it.
|
|
1363
|
+
*
|
|
1364
|
+
* @param {Object[]} pkgList Components from all the projects, updated in place
|
|
1365
|
+
* @returns {Object[]} The same list
|
|
1366
|
+
*/
|
|
1367
|
+
export function reconcileGradleComponents(pkgList) {
|
|
1368
|
+
const strongest = new Map();
|
|
1369
|
+
const resolvedSomewhere = new Set();
|
|
1370
|
+
const isFailed = (pkg) =>
|
|
1371
|
+
pkg.properties?.some((p) => p.name === "cdx:gradle:resolutionFailed");
|
|
1372
|
+
for (const pkg of pkgList) {
|
|
1373
|
+
const key = pkg?.["bom-ref"];
|
|
1374
|
+
if (!key) {
|
|
1375
|
+
continue;
|
|
1376
|
+
}
|
|
1377
|
+
if (
|
|
1378
|
+
!strongest.has(key) ||
|
|
1379
|
+
gradleScopeRank(pkg.scope) > gradleScopeRank(strongest.get(key))
|
|
1380
|
+
) {
|
|
1381
|
+
strongest.set(key, pkg.scope);
|
|
1382
|
+
}
|
|
1383
|
+
if (!isFailed(pkg)) {
|
|
1384
|
+
resolvedSomewhere.add(key);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
for (const pkg of pkgList) {
|
|
1388
|
+
const key = pkg?.["bom-ref"];
|
|
1389
|
+
if (!key) {
|
|
1390
|
+
continue;
|
|
1391
|
+
}
|
|
1392
|
+
if (strongest.get(key) === undefined) {
|
|
1393
|
+
delete pkg.scope;
|
|
1394
|
+
} else {
|
|
1395
|
+
pkg.scope = strongest.get(key);
|
|
1396
|
+
}
|
|
1397
|
+
if (resolvedSomewhere.has(key) && isFailed(pkg)) {
|
|
1398
|
+
pkg.properties = pkg.properties.filter(
|
|
1399
|
+
(p) => !GRADLE_FAILURE_PROPERTIES.includes(p.name),
|
|
1400
|
+
);
|
|
1401
|
+
if (!pkg.properties.length) {
|
|
1402
|
+
delete pkg.properties;
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
return pkgList;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
/**
|
|
1410
|
+
* Whether a module name is one that platforms and BOMs use, such as
|
|
1411
|
+
* `jackson-bom`, `spring-boot-dependencies`, `micronaut-platform` or the AWS
|
|
1412
|
+
* SDK's `bom`.
|
|
1413
|
+
*
|
|
1414
|
+
* @param {string} name Module name
|
|
1415
|
+
* @returns {boolean} True for a platform-style name
|
|
1416
|
+
*/
|
|
1417
|
+
function isGradlePlatformName(name) {
|
|
1418
|
+
return (
|
|
1419
|
+
name === "bom" ||
|
|
1420
|
+
["-bom", "-dependencies", "-platform"].some((suffix) =>
|
|
1421
|
+
name.endsWith(suffix),
|
|
1422
|
+
)
|
|
1423
|
+
);
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
/**
|
|
1427
|
+
* Give the platforms and BOMs in one report a `pom` type and the `excluded`
|
|
1428
|
+
* scope. Gradle prints a platform like a library, with only constraints
|
|
1429
|
+
* (`(c)`) under it, but a platform is a pom that never ships. A module counts
|
|
1430
|
+
* as a platform when its name is one platforms use and the report gives it no
|
|
1431
|
+
* dependency of its own, so a jar such as `jna-platform`, which depends on
|
|
1432
|
+
* `jna`, stays a jar.
|
|
1433
|
+
*
|
|
1434
|
+
* @param {Map<string, {adep: Object, isModule: boolean}>} depsByRef Components of the report by bom-ref, updated in place
|
|
1435
|
+
* @param {Object<string, string[]>} levelTrees Edges of the report by bom-ref
|
|
1436
|
+
* @returns {Map<string, string>} The new bom-ref of each renamed component, keyed by its old one
|
|
1437
|
+
*/
|
|
1438
|
+
function markGradlePlatforms(depsByRef, levelTrees) {
|
|
1439
|
+
const renamed = new Map();
|
|
1440
|
+
for (const [bomRef, { adep, isModule }] of depsByRef) {
|
|
1441
|
+
if (
|
|
1442
|
+
isModule ||
|
|
1443
|
+
adep.qualifiers?.type !== "jar" ||
|
|
1444
|
+
!isGradlePlatformName(adep.name || "") ||
|
|
1445
|
+
levelTrees[bomRef]?.length
|
|
1446
|
+
) {
|
|
1447
|
+
continue;
|
|
1448
|
+
}
|
|
1449
|
+
const purl = tryBuildPurl({
|
|
1450
|
+
type: "maven",
|
|
1451
|
+
namespace: adep.group || null,
|
|
1452
|
+
name: adep.name,
|
|
1453
|
+
version: adep.version || null,
|
|
1454
|
+
qualifiers: { type: "pom" },
|
|
1455
|
+
});
|
|
1456
|
+
if (!purl) {
|
|
1457
|
+
continue;
|
|
1458
|
+
}
|
|
1459
|
+
adep.qualifiers = { type: "pom" };
|
|
1460
|
+
adep.scope = "excluded";
|
|
1461
|
+
applyPurl(adep, purl);
|
|
1462
|
+
renamed.set(bomRef, adep["bom-ref"]);
|
|
1463
|
+
}
|
|
1464
|
+
return renamed;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
883
1467
|
/**
|
|
884
1468
|
* Method to resolve dependencies from a gradle output
|
|
885
1469
|
*
|
|
1470
|
+
* A constraint line (`(c)`) adds neither a component nor an edge. Gradle
|
|
1471
|
+
* prints a nested constraint under the platform or module that declares it,
|
|
1472
|
+
* and only when the dependency it affects is resolved elsewhere in the tree,
|
|
1473
|
+
* where that dependency gets its component and edges. A top-level
|
|
1474
|
+
* `{strictly …}` constraint from consistent resolution can name a module the
|
|
1475
|
+
* configuration never resolves. A dependency that failed to resolve carries
|
|
1476
|
+
* `cdx:gradle:resolutionFailed`, keeps the version it was requested at when
|
|
1477
|
+
* that is a concrete version, and otherwise records the request in
|
|
1478
|
+
* `cdx:gradle:requestedVersion`. A dependency seen in several configurations
|
|
1479
|
+
* keeps the strongest scope among them. Platforms and BOMs get a `pom` type
|
|
1480
|
+
* and the `excluded` scope.
|
|
1481
|
+
*
|
|
1482
|
+
* The parser runs no Gradle task. A project the tree names but `gradleModules`
|
|
1483
|
+
* lacks gets a component built from the root project's group and version; use
|
|
1484
|
+
* `loadGradleModulesForReports` first to read such projects' properties.
|
|
1485
|
+
*
|
|
1486
|
+
* The lines Gradle prints under another project, such as `project ':lib'` in
|
|
1487
|
+
* the report of `:app`, show that project's dependencies as `:app` resolved
|
|
1488
|
+
* them, after conflict resolution. When `reportedProjects` names that project,
|
|
1489
|
+
* its own report supplies its edges, so these lines add no edge from it.
|
|
1490
|
+
*
|
|
886
1491
|
* @param {string} rawOutput Text output from gradle dependencies task
|
|
887
|
-
* @param {string} rootProjectName
|
|
888
|
-
* @param {map} gradleModules
|
|
889
|
-
* @param {string} gradleRootPath
|
|
1492
|
+
* @param {string} rootProjectName Key in `gradleModules` of the project the report belongs to
|
|
1493
|
+
* @param {map} gradleModules Gradle modules keyed by project path, and the root project by its name
|
|
1494
|
+
* @param {string} gradleRootPath Directory Gradle ran in, recorded as `cdx:gradle:GradleRootPath`
|
|
1495
|
+
* @param {Set<string>} [reportedProjects] Keys in `gradleModules` of the projects whose own reports are parsed
|
|
1496
|
+
* @returns {{pkgList: Object[], dependenciesList: Object[]}|{}} The components and dependency graph
|
|
890
1497
|
*/
|
|
891
1498
|
export async function parseGradleDep(
|
|
892
1499
|
rawOutput,
|
|
893
1500
|
rootProjectName = "root",
|
|
894
1501
|
gradleModules = new Map(),
|
|
895
1502
|
gradleRootPath = "",
|
|
1503
|
+
reportedProjects = new Set(),
|
|
896
1504
|
) {
|
|
897
1505
|
if (typeof rawOutput === "string") {
|
|
898
1506
|
// Bug: 249. Get any sub-projects refered here
|
|
@@ -907,225 +1515,238 @@ export async function parseGradleDep(
|
|
|
907
1515
|
) {
|
|
908
1516
|
rootProjectName = retMap.rootProject;
|
|
909
1517
|
}
|
|
910
|
-
let match = "";
|
|
911
1518
|
// To render dependency tree we need a root project
|
|
912
1519
|
const rootProject = gradleModules.get(rootProjectName);
|
|
1520
|
+
if (!rootProject) {
|
|
1521
|
+
thoughtLog(
|
|
1522
|
+
`The Gradle report for ${rootProjectName} belongs to no module read from 'gradle properties', so it has no parent to hang its dependency tree from.`,
|
|
1523
|
+
);
|
|
1524
|
+
return {};
|
|
1525
|
+
}
|
|
913
1526
|
const deps = [];
|
|
914
1527
|
const dependenciesList = [];
|
|
915
|
-
const
|
|
916
|
-
const
|
|
917
|
-
let last_level = 0;
|
|
918
|
-
let last_bomref = rootProject["bom-ref"];
|
|
919
|
-
const first_bomref = last_bomref;
|
|
920
|
-
let last_project_bomref = first_bomref;
|
|
1528
|
+
const depsByRef = new Map();
|
|
1529
|
+
const firstBomRef = rootProject["bom-ref"];
|
|
921
1530
|
const level_trees = {};
|
|
922
|
-
level_trees[
|
|
1531
|
+
level_trees[firstBomRef] = [];
|
|
1532
|
+
// Modules whose own report supplies their edges.
|
|
1533
|
+
const reportedModuleRefs = new Set();
|
|
1534
|
+
for (const key of reportedProjects) {
|
|
1535
|
+
const reportedRef = gradleModules.get(key)?.["bom-ref"];
|
|
1536
|
+
if (reportedRef && reportedRef !== firstBomRef) {
|
|
1537
|
+
reportedModuleRefs.add(reportedRef);
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
923
1540
|
let scope;
|
|
924
1541
|
let profileName;
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
for (const module of modulesToScan) {
|
|
943
|
-
const propMap = parseGradleProperties(
|
|
944
|
-
splitPropTaskOut.get(module),
|
|
945
|
-
module,
|
|
946
|
-
);
|
|
947
|
-
const rootSubProject = propMap.rootProject;
|
|
948
|
-
if (rootSubProject) {
|
|
949
|
-
const rootSubProjectObj = await buildObjectForGradleModule(
|
|
950
|
-
rootSubProject === "root" ? module : rootSubProject,
|
|
951
|
-
propMap.metadata,
|
|
952
|
-
);
|
|
953
|
-
gradleModules.set(module, rootSubProjectObj);
|
|
954
|
-
}
|
|
955
|
-
}
|
|
956
|
-
}
|
|
957
|
-
const subDependsOn = [];
|
|
958
|
-
for (const sd of retMap.projects) {
|
|
959
|
-
if (gradleModules.has(sd)) {
|
|
960
|
-
subDependsOn.push(gradleModules.get(sd)["bom-ref"]);
|
|
961
|
-
}
|
|
1542
|
+
// ancestors[level] is the bom-ref of the line most recently seen at that
|
|
1543
|
+
// depth; ancestors[0] is the project the report belongs to.
|
|
1544
|
+
const ancestors = [firstBomRef];
|
|
1545
|
+
const lines = rawOutput.split("\n").map((l) => l.replaceAll("\r", ""));
|
|
1546
|
+
// Modules some configuration resolved to a version, so that a failed,
|
|
1547
|
+
// versionless request for one of them adds nothing.
|
|
1548
|
+
const resolvedModules = new Set();
|
|
1549
|
+
for (const line of lines) {
|
|
1550
|
+
const parsed = parseGradleTreeLine(line);
|
|
1551
|
+
if (
|
|
1552
|
+
parsed?.group &&
|
|
1553
|
+
parsed.version !== undefined &&
|
|
1554
|
+
!parsed.failed &&
|
|
1555
|
+
!parsed.constraint &&
|
|
1556
|
+
!parsed.unresolvable
|
|
1557
|
+
) {
|
|
1558
|
+
resolvedModules.add(`${parsed.group}:${parsed.name}`);
|
|
962
1559
|
}
|
|
963
|
-
level_trees[last_bomref] = subDependsOn;
|
|
964
1560
|
}
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
1561
|
+
for (const rline of lines) {
|
|
1562
|
+
const configurationName = parseGradleConfigurationHeader(rline);
|
|
1563
|
+
if (configurationName) {
|
|
1564
|
+
ancestors.length = 1;
|
|
1565
|
+
profileName = configurationName;
|
|
1566
|
+
scope = gradleConfigurationScope(configurationName);
|
|
1567
|
+
}
|
|
1568
|
+
if (rline.trim() === "") {
|
|
1569
|
+
ancestors.length = 1;
|
|
970
1570
|
continue;
|
|
971
1571
|
}
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
1572
|
+
const parsed = parseGradleTreeLine(rline);
|
|
1573
|
+
if (!parsed) {
|
|
1574
|
+
continue;
|
|
1575
|
+
}
|
|
1576
|
+
// A line that names nothing usable still takes its place in the tree,
|
|
1577
|
+
// so the lines under it get no parent rather than a wrong one.
|
|
1578
|
+
if (parsed.unknown || parsed.unresolvable) {
|
|
1579
|
+
ancestors.length = Math.min(ancestors.length, parsed.level);
|
|
980
1580
|
continue;
|
|
981
1581
|
}
|
|
1582
|
+
const { level } = parsed;
|
|
1583
|
+
const isProject = parsed.projectPath !== undefined;
|
|
1584
|
+
if (parsed.constraint) {
|
|
1585
|
+
ancestors.length = Math.min(ancestors.length, level);
|
|
1586
|
+
continue;
|
|
1587
|
+
}
|
|
1588
|
+
// Only a dependency that failed to resolve can lack a version: Gradle
|
|
1589
|
+
// prints every resolved one with the version it selected.
|
|
982
1590
|
if (
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
1591
|
+
!isProject &&
|
|
1592
|
+
parsed.version === undefined &&
|
|
1593
|
+
(!parsed.failed ||
|
|
1594
|
+
resolvedModules.has(`${parsed.group}:${parsed.name}`))
|
|
986
1595
|
) {
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
last_bomref = last_project_bomref;
|
|
990
|
-
stack = [first_bomref];
|
|
1596
|
+
ancestors.length = Math.min(ancestors.length, level);
|
|
1597
|
+
continue;
|
|
991
1598
|
}
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1599
|
+
// A project the modules lack is named as Gradle publishes it by
|
|
1600
|
+
// default: the last segment of its path, with the root project's group
|
|
1601
|
+
// and version.
|
|
1602
|
+
const name = isProject
|
|
1603
|
+
? parsed.projectPath.split(":").pop()
|
|
1604
|
+
: parsed.name;
|
|
1605
|
+
const group = isProject ? rootProject.group : parsed.group;
|
|
1606
|
+
const version = isProject ? rootProject.version : parsed.version;
|
|
1607
|
+
const gradleModule = isProject
|
|
1608
|
+
? gradleModuleForPath(
|
|
1609
|
+
gradleModules,
|
|
1610
|
+
parsed.projectPath,
|
|
1611
|
+
rootProjectName,
|
|
1612
|
+
)
|
|
1613
|
+
: undefined;
|
|
1614
|
+
if (isProject && !gradleModule && name === "") {
|
|
1615
|
+
ancestors.length = Math.min(ancestors.length, level);
|
|
1616
|
+
continue;
|
|
1617
|
+
}
|
|
1618
|
+
let purl;
|
|
1619
|
+
let bomRef;
|
|
1620
|
+
if (gradleModule) {
|
|
1621
|
+
purl = gradleModule["purl"];
|
|
1622
|
+
bomRef = gradleModule["bom-ref"];
|
|
1623
|
+
} else {
|
|
1624
|
+
try {
|
|
1625
|
+
purl = build({
|
|
1626
|
+
type: "maven",
|
|
1627
|
+
namespace: group || null,
|
|
1628
|
+
name,
|
|
1629
|
+
version: version || null,
|
|
1630
|
+
qualifiers: { type: "jar" },
|
|
1631
|
+
});
|
|
1632
|
+
bomRef = decodeURIComponent(purl);
|
|
1633
|
+
} catch {
|
|
1634
|
+
// A purl that cannot be built keeps a name-only bom-ref so the
|
|
1635
|
+
// component still lands in the graph.
|
|
1636
|
+
purl = undefined;
|
|
1637
|
+
bomRef = name;
|
|
1000
1638
|
}
|
|
1001
1639
|
}
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
if (version !== undefined || group === "project") {
|
|
1018
|
-
// Project line has no version
|
|
1019
|
-
// For multi sub-module projects such as :module:dummy:starter the regex is producing incorrect values
|
|
1020
|
-
if (rline.includes("project ")) {
|
|
1021
|
-
const tmpA = rline.split("project ");
|
|
1022
|
-
if (tmpA && tmpA.length > 1) {
|
|
1023
|
-
group = rootProject.group;
|
|
1024
|
-
name = tmpA[1].split(" ")[0];
|
|
1025
|
-
version = undefined;
|
|
1026
|
-
}
|
|
1640
|
+
const existing = depsByRef.get(bomRef);
|
|
1641
|
+
// The project the report belongs to is the parent, not a component.
|
|
1642
|
+
if (!existing && bomRef !== firstBomRef) {
|
|
1643
|
+
let adep;
|
|
1644
|
+
if (gradleModule) {
|
|
1645
|
+
adep = gradleModule;
|
|
1646
|
+
} else {
|
|
1647
|
+
adep = {
|
|
1648
|
+
group,
|
|
1649
|
+
name: name,
|
|
1650
|
+
version,
|
|
1651
|
+
qualifiers: { type: "jar" },
|
|
1652
|
+
};
|
|
1653
|
+
if (version === undefined) {
|
|
1654
|
+
delete adep.version;
|
|
1027
1655
|
}
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
if (
|
|
1031
|
-
|
|
1032
|
-
bomRef = gradleModules.get(name)["bom-ref"];
|
|
1033
|
-
} else {
|
|
1034
|
-
try {
|
|
1035
|
-
purl = build({
|
|
1036
|
-
type: "maven",
|
|
1037
|
-
namespace:
|
|
1038
|
-
group !== "project" ? group : rootProject.group || null,
|
|
1039
|
-
name: name.replace(/^:/, ""),
|
|
1040
|
-
version:
|
|
1041
|
-
version !== undefined ? version : rootProject.version || null,
|
|
1042
|
-
qualifiers: { type: "jar" },
|
|
1043
|
-
});
|
|
1044
|
-
bomRef = decodeURIComponent(purl);
|
|
1045
|
-
} catch {
|
|
1046
|
-
// A purl that cannot be built keeps a name-only bom-ref so the
|
|
1047
|
-
// component still lands in the graph.
|
|
1048
|
-
purl = undefined;
|
|
1049
|
-
bomRef = name.replace(/^:/, "");
|
|
1050
|
-
}
|
|
1656
|
+
adep["purl"] = purl;
|
|
1657
|
+
adep["bom-ref"] = bomRef;
|
|
1658
|
+
if (scope) {
|
|
1659
|
+
adep["scope"] = scope;
|
|
1051
1660
|
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
adep = gradleModules.get(name);
|
|
1059
|
-
} else {
|
|
1060
|
-
adep = {
|
|
1061
|
-
group: group !== "project" ? group : rootProject.group,
|
|
1062
|
-
name: name,
|
|
1063
|
-
version: version !== undefined ? version : rootProject.version,
|
|
1064
|
-
qualifiers: { type: "jar" },
|
|
1065
|
-
};
|
|
1066
|
-
adep["purl"] = purl;
|
|
1067
|
-
adep["bom-ref"] = bomRef;
|
|
1068
|
-
if (scope) {
|
|
1069
|
-
adep["scope"] = scope;
|
|
1070
|
-
}
|
|
1071
|
-
adep.properties = [];
|
|
1072
|
-
if (profileName) {
|
|
1073
|
-
adep.properties.push({
|
|
1074
|
-
name: "internal:GradleProfileName",
|
|
1075
|
-
value: profileName,
|
|
1076
|
-
});
|
|
1077
|
-
}
|
|
1078
|
-
if (gradleRootPath && gradleRootPath !== ".") {
|
|
1079
|
-
adep.properties.push({
|
|
1080
|
-
name: "cdx:gradle:GradleRootPath",
|
|
1081
|
-
value: gradleRootPath,
|
|
1082
|
-
});
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
if (adep?.properties?.length === 0) {
|
|
1086
|
-
delete adep.properties;
|
|
1087
|
-
}
|
|
1088
|
-
deps.push(adep);
|
|
1661
|
+
adep.properties = [];
|
|
1662
|
+
if (profileName) {
|
|
1663
|
+
adep.properties.push({
|
|
1664
|
+
name: "internal:GradleProfileName",
|
|
1665
|
+
value: profileName,
|
|
1666
|
+
});
|
|
1089
1667
|
}
|
|
1090
|
-
if (
|
|
1091
|
-
|
|
1668
|
+
if (gradleRootPath && gradleRootPath !== ".") {
|
|
1669
|
+
adep.properties.push({
|
|
1670
|
+
name: "cdx:gradle:GradleRootPath",
|
|
1671
|
+
value: gradleRootPath,
|
|
1672
|
+
});
|
|
1092
1673
|
}
|
|
1093
|
-
if (
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1674
|
+
if (parsed.failed) {
|
|
1675
|
+
adep.properties.push({
|
|
1676
|
+
name: "cdx:gradle:resolutionFailed",
|
|
1677
|
+
value: "true",
|
|
1678
|
+
});
|
|
1679
|
+
if (
|
|
1680
|
+
parsed.requestedVersion &&
|
|
1681
|
+
parsed.requestedVersion !== version
|
|
1682
|
+
) {
|
|
1683
|
+
adep.properties.push({
|
|
1684
|
+
name: "cdx:gradle:requestedVersion",
|
|
1685
|
+
value: parsed.requestedVersion.slice(0, 256),
|
|
1686
|
+
});
|
|
1106
1687
|
}
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
if (adep?.properties?.length === 0) {
|
|
1691
|
+
delete adep.properties;
|
|
1692
|
+
}
|
|
1693
|
+
depsByRef.set(bomRef, { adep, isModule: !!gradleModule });
|
|
1694
|
+
deps.push(adep);
|
|
1695
|
+
} else if (existing && !existing.isModule) {
|
|
1696
|
+
const { adep } = existing;
|
|
1697
|
+
if (gradleScopeRank(scope) > gradleScopeRank(adep.scope)) {
|
|
1698
|
+
if (scope) {
|
|
1699
|
+
adep.scope = scope;
|
|
1107
1700
|
} else {
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1701
|
+
delete adep.scope;
|
|
1702
|
+
}
|
|
1703
|
+
const profileProp = adep.properties?.find(
|
|
1704
|
+
(p) => p.name === "internal:GradleProfileName",
|
|
1705
|
+
);
|
|
1706
|
+
if (profileProp && profileName) {
|
|
1707
|
+
profileProp.value = profileName;
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
// A dependency one configuration failed to resolve and another
|
|
1711
|
+
// resolved is not a failed dependency.
|
|
1712
|
+
if (!parsed.failed && adep.properties) {
|
|
1713
|
+
adep.properties = adep.properties.filter(
|
|
1714
|
+
(p) => !GRADLE_FAILURE_PROPERTIES.includes(p.name),
|
|
1715
|
+
);
|
|
1716
|
+
if (!adep.properties.length) {
|
|
1717
|
+
delete adep.properties;
|
|
1119
1718
|
}
|
|
1120
|
-
last_level = level;
|
|
1121
|
-
last_bomref = bomRef;
|
|
1122
1719
|
}
|
|
1123
1720
|
}
|
|
1721
|
+
if (!level_trees[bomRef]) {
|
|
1722
|
+
level_trees[bomRef] = [];
|
|
1723
|
+
}
|
|
1724
|
+
// A line whose parent was skipped has no known parent and gets no edge.
|
|
1725
|
+
const parentRef =
|
|
1726
|
+
ancestors.length >= level ? ancestors[level - 1] : undefined;
|
|
1727
|
+
if (
|
|
1728
|
+
parentRef &&
|
|
1729
|
+
parentRef !== bomRef &&
|
|
1730
|
+
!reportedModuleRefs.has(parentRef)
|
|
1731
|
+
) {
|
|
1732
|
+
const cnodes = level_trees[parentRef] || [];
|
|
1733
|
+
if (!cnodes.includes(bomRef)) {
|
|
1734
|
+
cnodes.push(bomRef);
|
|
1735
|
+
}
|
|
1736
|
+
level_trees[parentRef] = cnodes;
|
|
1737
|
+
}
|
|
1738
|
+
ancestors.length = Math.min(ancestors.length, level);
|
|
1739
|
+
if (ancestors.length === level) {
|
|
1740
|
+
ancestors.push(bomRef);
|
|
1741
|
+
}
|
|
1124
1742
|
}
|
|
1743
|
+
const renamedRefs = markGradlePlatforms(depsByRef, level_trees);
|
|
1125
1744
|
for (const lk of Object.keys(level_trees)) {
|
|
1126
1745
|
dependenciesList.push({
|
|
1127
|
-
ref: lk,
|
|
1128
|
-
dependsOn: [
|
|
1746
|
+
ref: renamedRefs.get(lk) || lk,
|
|
1747
|
+
dependsOn: [
|
|
1748
|
+
...new Set(level_trees[lk].map((ref) => renamedRefs.get(ref) || ref)),
|
|
1749
|
+
].sort(),
|
|
1129
1750
|
});
|
|
1130
1751
|
}
|
|
1131
1752
|
return {
|
|
@@ -1830,8 +2451,16 @@ function catalogCoordinate(entry) {
|
|
|
1830
2451
|
}
|
|
1831
2452
|
|
|
1832
2453
|
/**
|
|
1833
|
-
*
|
|
1834
|
-
*
|
|
2454
|
+
* Gradle rich-version syntax that names no single version: a bracketed range,
|
|
2455
|
+
* an interval, a `!!` or `{strictly}` marker, a `->` transition, or a `..`
|
|
2456
|
+
* separator. A version text matching this is a declaration about versions, not
|
|
2457
|
+
* one version.
|
|
2458
|
+
*/
|
|
2459
|
+
const GRADLE_RICH_VERSION_PATTERN = /[[\](),!]|->|\.\./u;
|
|
2460
|
+
|
|
2461
|
+
/**
|
|
2462
|
+
* Resolve the version of one catalog entry.
|
|
2463
|
+
*
|
|
1835
2464
|
* An entry may pin `version = "1.2.3"` directly or reference the versions
|
|
1836
2465
|
* table with `version.ref = "alias"`. Version references that are missing, or
|
|
1837
2466
|
* that carry Gradle rich-version syntax (ranges, `!!`, separators), resolve
|
|
@@ -1851,8 +2480,1059 @@ function resolveCatalogVersion(entry, versions) {
|
|
|
1851
2480
|
if (typeof raw !== "string" || !raw) {
|
|
1852
2481
|
return undefined;
|
|
1853
2482
|
}
|
|
1854
|
-
if (
|
|
2483
|
+
if (GRADLE_RICH_VERSION_PATTERN.test(raw)) {
|
|
1855
2484
|
return undefined;
|
|
1856
2485
|
}
|
|
1857
2486
|
return raw;
|
|
1858
2487
|
}
|
|
2488
|
+
|
|
2489
|
+
/**
|
|
2490
|
+
* The largest Gradle lockfile read. Real lockfiles of large multi-project
|
|
2491
|
+
* builds stay well under a megabyte; the file is untrusted input, so a larger
|
|
2492
|
+
* one is skipped whole rather than read in part.
|
|
2493
|
+
*/
|
|
2494
|
+
const GRADLE_LOCKFILE_MAX_BYTES = 8 * 1024 * 1024;
|
|
2495
|
+
|
|
2496
|
+
/**
|
|
2497
|
+
* The most lines read from one Gradle lockfile. Lines past the cap are
|
|
2498
|
+
* ignored.
|
|
2499
|
+
*/
|
|
2500
|
+
const GRADLE_LOCKFILE_MAX_LINES = 100000;
|
|
2501
|
+
|
|
2502
|
+
/**
|
|
2503
|
+
* Gradle 6+ lockfiles that lock a buildscript classpath rather than the
|
|
2504
|
+
* project's own configurations.
|
|
2505
|
+
*/
|
|
2506
|
+
const GRADLE_BUILD_TOOLING_LOCKFILES = new Set([
|
|
2507
|
+
"buildscript-gradle.lockfile",
|
|
2508
|
+
"settings-gradle.lockfile",
|
|
2509
|
+
]);
|
|
2510
|
+
|
|
2511
|
+
const GRADLE_LOCKFILE_SUFFIX = ".lockfile";
|
|
2512
|
+
|
|
2513
|
+
/**
|
|
2514
|
+
* Work out which kind of Gradle lockfile a path names.
|
|
2515
|
+
*
|
|
2516
|
+
* Gradle 6 and later write one `gradle.lockfile` per project, plus
|
|
2517
|
+
* `buildscript-gradle.lockfile` and `settings-gradle.lockfile` for the
|
|
2518
|
+
* buildscript classpaths. Gradle 4.8 to 5.x wrote one
|
|
2519
|
+
* `gradle/dependency-locks/<configuration>.lockfile` per configuration, with
|
|
2520
|
+
* the buildscript configurations prefixed `buildscript-`.
|
|
2521
|
+
*
|
|
2522
|
+
* @param {string} lockFile Path to the lockfile
|
|
2523
|
+
* @returns {{legacy: boolean, buildTooling: boolean, configuration: (string|undefined)}|undefined}
|
|
2524
|
+
* The kind, or undefined when the name is not a lockfile name
|
|
2525
|
+
*/
|
|
2526
|
+
function gradleLockfileKind(lockFile) {
|
|
2527
|
+
const fileName = basename(lockFile);
|
|
2528
|
+
if (!fileName.endsWith(GRADLE_LOCKFILE_SUFFIX)) {
|
|
2529
|
+
return undefined;
|
|
2530
|
+
}
|
|
2531
|
+
const parentDir = dirname(lockFile);
|
|
2532
|
+
if (
|
|
2533
|
+
basename(parentDir) === "dependency-locks" &&
|
|
2534
|
+
basename(dirname(parentDir)) === "gradle"
|
|
2535
|
+
) {
|
|
2536
|
+
const configuration = fileName.slice(0, -GRADLE_LOCKFILE_SUFFIX.length);
|
|
2537
|
+
if (!configuration) {
|
|
2538
|
+
return undefined;
|
|
2539
|
+
}
|
|
2540
|
+
return {
|
|
2541
|
+
legacy: true,
|
|
2542
|
+
buildTooling: configuration.startsWith("buildscript-"),
|
|
2543
|
+
configuration,
|
|
2544
|
+
};
|
|
2545
|
+
}
|
|
2546
|
+
return {
|
|
2547
|
+
legacy: false,
|
|
2548
|
+
buildTooling: GRADLE_BUILD_TOOLING_LOCKFILES.has(fileName),
|
|
2549
|
+
configuration: undefined,
|
|
2550
|
+
};
|
|
2551
|
+
}
|
|
2552
|
+
|
|
2553
|
+
/**
|
|
2554
|
+
* Read a `group:name:version` coordinate from a lockfile entry.
|
|
2555
|
+
*
|
|
2556
|
+
* Gradle writes only resolved versions to a lockfile, so a version that is
|
|
2557
|
+
* blank, a range, a dynamic selector (`1.+`, `latest.release`) or any other
|
|
2558
|
+
* rich-version syntax means the line was not written by Gradle and is
|
|
2559
|
+
* skipped.
|
|
2560
|
+
*
|
|
2561
|
+
* @param {string} text Coordinate text, already trimmed
|
|
2562
|
+
* @returns {{group: string, name: string, version: string}|undefined}
|
|
2563
|
+
* The coordinate, or undefined for anything else
|
|
2564
|
+
*/
|
|
2565
|
+
function parseGradleLockCoordinate(text) {
|
|
2566
|
+
if (!text || /[\s=]/u.test(text)) {
|
|
2567
|
+
return undefined;
|
|
2568
|
+
}
|
|
2569
|
+
const parts = text.split(":");
|
|
2570
|
+
if (parts.length !== 3 || parts.some((part) => !part)) {
|
|
2571
|
+
return undefined;
|
|
2572
|
+
}
|
|
2573
|
+
const [group, name, version] = parts;
|
|
2574
|
+
// Gradle writes only resolved versions to a lockfile, so a rich version or
|
|
2575
|
+
// a dynamic selector (`1.+`, `2.*`, `latest.release`) means the line was
|
|
2576
|
+
// not written by Gradle. The rich-version rejection is the one
|
|
2577
|
+
// `resolveCatalogVersion` applies; a trailing `+` and a `*` are the
|
|
2578
|
+
// dynamic selectors.
|
|
2579
|
+
if (
|
|
2580
|
+
version.startsWith("latest.") ||
|
|
2581
|
+
version.endsWith("+") ||
|
|
2582
|
+
version.includes("*") ||
|
|
2583
|
+
GRADLE_RICH_VERSION_PATTERN.test(version)
|
|
2584
|
+
) {
|
|
2585
|
+
return undefined;
|
|
2586
|
+
}
|
|
2587
|
+
return { group, name, version };
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
/**
|
|
2591
|
+
* Parse one Gradle dependency lockfile.
|
|
2592
|
+
*
|
|
2593
|
+
* Both layouts are read. A Gradle 6+ `gradle.lockfile` has one
|
|
2594
|
+
* `group:name:version=configuration,configuration` line per module and an
|
|
2595
|
+
* `empty=` line naming the configurations that resolved nothing. A legacy
|
|
2596
|
+
* `gradle/dependency-locks/<configuration>.lockfile` has one
|
|
2597
|
+
* `group:name:version` line per module, and the file name is the
|
|
2598
|
+
* configuration.
|
|
2599
|
+
*
|
|
2600
|
+
* The scope of a module is the strongest that `gradleConfigurationScope`
|
|
2601
|
+
* gives any of its configurations, so a module locked for both
|
|
2602
|
+
* `testRuntimeClasspath` and `runtimeClasspath` is `required`. Everything in
|
|
2603
|
+
* a buildscript or settings lockfile is build tooling and `excluded`.
|
|
2604
|
+
*
|
|
2605
|
+
* A lockfile records resolved versions, so components carry a purl identity
|
|
2606
|
+
* with confidence 1 and the lockfile path as the evidence value. The purl and
|
|
2607
|
+
* bom-ref match what `parseGradleDep` builds for the same coordinate, so a
|
|
2608
|
+
* component reads the same whether Gradle ran or not. A lockfile is flat: it
|
|
2609
|
+
* says nothing about which module depends on which, so no dependency graph is
|
|
2610
|
+
* returned.
|
|
2611
|
+
*
|
|
2612
|
+
* The file is untrusted input. It is read only when it is a regular file no
|
|
2613
|
+
* larger than 8 MiB, at most 100000 lines are read, and a malformed line
|
|
2614
|
+
* costs only itself. Line content never reaches a property.
|
|
2615
|
+
*
|
|
2616
|
+
* @param {string} lockFile Path to the lockfile
|
|
2617
|
+
* @param {Object} [options] Parse options
|
|
2618
|
+
* @param {string} [options.rootDir] Directory the lockfile must resolve inside
|
|
2619
|
+
* @returns {Object[]} Components, one per locked module
|
|
2620
|
+
*/
|
|
2621
|
+
export function parseGradleLockfile(lockFile, options = {}) {
|
|
2622
|
+
const kind = gradleLockfileKind(lockFile);
|
|
2623
|
+
if (!kind) {
|
|
2624
|
+
return [];
|
|
2625
|
+
}
|
|
2626
|
+
const text = readRegularTextFile(
|
|
2627
|
+
lockFile,
|
|
2628
|
+
GRADLE_LOCKFILE_MAX_BYTES,
|
|
2629
|
+
options?.rootDir,
|
|
2630
|
+
);
|
|
2631
|
+
if (text === undefined) {
|
|
2632
|
+
if (safeExistsSync(lockFile)) {
|
|
2633
|
+
console.warn(
|
|
2634
|
+
`Skipping ${lockFile}: it is not a regular file inside the project, or is larger than ${GRADLE_LOCKFILE_MAX_BYTES} bytes.`,
|
|
2635
|
+
);
|
|
2636
|
+
}
|
|
2637
|
+
return [];
|
|
2638
|
+
}
|
|
2639
|
+
const body = text.startsWith("\uFEFF") ? text.slice(1) : text;
|
|
2640
|
+
const pkgsByRef = new Map();
|
|
2641
|
+
let skippedLines = 0;
|
|
2642
|
+
let lineStart = 0;
|
|
2643
|
+
let lineCount = 0;
|
|
2644
|
+
while (lineStart <= body.length) {
|
|
2645
|
+
if (lineCount === GRADLE_LOCKFILE_MAX_LINES) {
|
|
2646
|
+
if (lineStart < body.length) {
|
|
2647
|
+
thoughtLog(
|
|
2648
|
+
`${lockFile} has more than ${GRADLE_LOCKFILE_MAX_LINES} lines; only the first ${GRADLE_LOCKFILE_MAX_LINES} are read.`,
|
|
2649
|
+
);
|
|
2650
|
+
}
|
|
2651
|
+
break;
|
|
2652
|
+
}
|
|
2653
|
+
let lineEnd = body.indexOf("\n", lineStart);
|
|
2654
|
+
if (lineEnd === -1) {
|
|
2655
|
+
lineEnd = body.length;
|
|
2656
|
+
}
|
|
2657
|
+
const line = body.slice(lineStart, lineEnd).trim();
|
|
2658
|
+
lineStart = lineEnd + 1;
|
|
2659
|
+
lineCount++;
|
|
2660
|
+
if (!line || line.startsWith("#")) {
|
|
2661
|
+
continue;
|
|
2662
|
+
}
|
|
2663
|
+
let coordinateText = line;
|
|
2664
|
+
let configurations = [kind.configuration];
|
|
2665
|
+
if (!kind.legacy) {
|
|
2666
|
+
const separator = line.indexOf("=");
|
|
2667
|
+
if (separator === -1) {
|
|
2668
|
+
skippedLines++;
|
|
2669
|
+
continue;
|
|
2670
|
+
}
|
|
2671
|
+
coordinateText = line.slice(0, separator).trim();
|
|
2672
|
+
if (coordinateText === "empty") {
|
|
2673
|
+
continue;
|
|
2674
|
+
}
|
|
2675
|
+
configurations = line
|
|
2676
|
+
.slice(separator + 1)
|
|
2677
|
+
.split(",")
|
|
2678
|
+
.map((configuration) => configuration.trim())
|
|
2679
|
+
.filter(Boolean);
|
|
2680
|
+
}
|
|
2681
|
+
const coordinate = parseGradleLockCoordinate(coordinateText);
|
|
2682
|
+
if (!coordinate || !configurations.length) {
|
|
2683
|
+
skippedLines++;
|
|
2684
|
+
continue;
|
|
2685
|
+
}
|
|
2686
|
+
const { group, name, version } = coordinate;
|
|
2687
|
+
const purl = mavenPurl(group, name, version, { type: "jar" });
|
|
2688
|
+
if (!purl) {
|
|
2689
|
+
skippedLines++;
|
|
2690
|
+
continue;
|
|
2691
|
+
}
|
|
2692
|
+
let scope = "excluded";
|
|
2693
|
+
if (!kind.buildTooling) {
|
|
2694
|
+
for (const configuration of configurations) {
|
|
2695
|
+
const configurationScope = gradleConfigurationScope(configuration);
|
|
2696
|
+
if (gradleScopeRank(configurationScope) > gradleScopeRank(scope)) {
|
|
2697
|
+
scope = configurationScope;
|
|
2698
|
+
}
|
|
2699
|
+
}
|
|
2700
|
+
}
|
|
2701
|
+
const bomRef = decodeURIComponent(purl);
|
|
2702
|
+
const existing = pkgsByRef.get(bomRef);
|
|
2703
|
+
if (existing) {
|
|
2704
|
+
if (gradleScopeRank(scope) > gradleScopeRank(existing.scope)) {
|
|
2705
|
+
existing.scope = scope;
|
|
2706
|
+
}
|
|
2707
|
+
continue;
|
|
2708
|
+
}
|
|
2709
|
+
pkgsByRef.set(bomRef, {
|
|
2710
|
+
group,
|
|
2711
|
+
name,
|
|
2712
|
+
version,
|
|
2713
|
+
qualifiers: { type: "jar" },
|
|
2714
|
+
type: "library",
|
|
2715
|
+
scope,
|
|
2716
|
+
purl,
|
|
2717
|
+
"bom-ref": bomRef,
|
|
2718
|
+
properties: [{ name: "internal:SrcFile", value: lockFile }],
|
|
2719
|
+
evidence: {
|
|
2720
|
+
identity: {
|
|
2721
|
+
field: "purl",
|
|
2722
|
+
confidence: 1,
|
|
2723
|
+
concludedValue: purl,
|
|
2724
|
+
methods: [
|
|
2725
|
+
{
|
|
2726
|
+
technique: "manifest-analysis",
|
|
2727
|
+
confidence: 1,
|
|
2728
|
+
value: lockFile,
|
|
2729
|
+
},
|
|
2730
|
+
],
|
|
2731
|
+
},
|
|
2732
|
+
},
|
|
2733
|
+
});
|
|
2734
|
+
}
|
|
2735
|
+
if (skippedLines) {
|
|
2736
|
+
thoughtLog(
|
|
2737
|
+
`Skipped ${skippedLines} malformed or unresolved line(s) in ${lockFile}.`,
|
|
2738
|
+
);
|
|
2739
|
+
}
|
|
2740
|
+
return [...pkgsByRef.values()];
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
/**
|
|
2744
|
+
* Parse several Gradle lockfiles of one build and merge the result.
|
|
2745
|
+
*
|
|
2746
|
+
* Every project of a multi-project build has its own lockfile, so the same
|
|
2747
|
+
* module often appears in several of them with different configurations. The
|
|
2748
|
+
* copies collapse into one component with the strongest scope any lockfile
|
|
2749
|
+
* gave it (see `reconcileGradleComponents`), and its evidence lists every
|
|
2750
|
+
* lockfile that locked it. The first lockfile that locked a module stays its
|
|
2751
|
+
* `internal:SrcFile`.
|
|
2752
|
+
*
|
|
2753
|
+
* @param {string[]} lockFiles Paths to the lockfiles
|
|
2754
|
+
* @param {Object} [options] Parse options, as for `parseGradleLockfile`
|
|
2755
|
+
* @returns {Object[]} Components, one per locked module
|
|
2756
|
+
*/
|
|
2757
|
+
export function parseGradleLockfiles(lockFiles, options = {}) {
|
|
2758
|
+
const allPkgs = [];
|
|
2759
|
+
for (const lockFile of lockFiles || []) {
|
|
2760
|
+
allPkgs.push(...parseGradleLockfile(lockFile, options));
|
|
2761
|
+
}
|
|
2762
|
+
reconcileGradleComponents(allPkgs);
|
|
2763
|
+
const pkgsByRef = new Map();
|
|
2764
|
+
for (const pkg of allPkgs) {
|
|
2765
|
+
const existing = pkgsByRef.get(pkg["bom-ref"]);
|
|
2766
|
+
if (!existing) {
|
|
2767
|
+
pkgsByRef.set(pkg["bom-ref"], pkg);
|
|
2768
|
+
continue;
|
|
2769
|
+
}
|
|
2770
|
+
const methods = existing.evidence.identity.methods;
|
|
2771
|
+
for (const method of pkg.evidence.identity.methods) {
|
|
2772
|
+
if (!methods.some((m) => m.value === method.value)) {
|
|
2773
|
+
methods.push(method);
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
return [...pkgsByRef.values()];
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
/**
|
|
2781
|
+
* The build files that mark a directory as a Gradle project of its own. A
|
|
2782
|
+
* lockfile is accepted only beside one of these, so a lockfile copied into a
|
|
2783
|
+
* notes folder or a build's output is not read as an inventory source.
|
|
2784
|
+
*/
|
|
2785
|
+
const GRADLE_PROJECT_MARKERS = [
|
|
2786
|
+
"build.gradle",
|
|
2787
|
+
"build.gradle.kts",
|
|
2788
|
+
"settings.gradle",
|
|
2789
|
+
"settings.gradle.kts",
|
|
2790
|
+
];
|
|
2791
|
+
|
|
2792
|
+
/**
|
|
2793
|
+
* Find the Gradle dependency lockfiles of one build.
|
|
2794
|
+
*
|
|
2795
|
+
* Both layouts are searched below `rootPath`, recursively even when
|
|
2796
|
+
* `options.multiProject` is off: every subproject of a Gradle build locks its
|
|
2797
|
+
* own configurations, and the subprojects belong to the same build. The
|
|
2798
|
+
* Gradle 6+ names are `gradle.lockfile`, `buildscript-gradle.lockfile` and
|
|
2799
|
+
* `settings-gradle.lockfile`; the Gradle 4.8 to 5.x layout is
|
|
2800
|
+
* `gradle/dependency-locks/<configuration>.lockfile`.
|
|
2801
|
+
*
|
|
2802
|
+
* A lockfile is accepted only when the project directory that owns it holds a
|
|
2803
|
+
* Gradle build file, and never when it sits under `build/`, `.gradle/`,
|
|
2804
|
+
* `node_modules/` or a vendored directory. `getAllFiles` already leaves
|
|
2805
|
+
* `.git`, `.hg`, `.chen` and `node_modules` out; `build/` and `.gradle/` are
|
|
2806
|
+
* Gradle's own output and cache directories, which a lockfile in them cannot
|
|
2807
|
+
* belong to the project's source of truth.
|
|
2808
|
+
*
|
|
2809
|
+
* Custom `dependencyLocking.lockFile` paths, the Nebula `dependencies.lock`
|
|
2810
|
+
* and Palantir `versions.lock` formats are not looked for.
|
|
2811
|
+
*
|
|
2812
|
+
* @param {string} rootPath Directory to search
|
|
2813
|
+
* @param {Object} [options] CLI options
|
|
2814
|
+
* @returns {string[]} Lockfile paths, in a stable order
|
|
2815
|
+
*/
|
|
2816
|
+
export function findGradleLockfiles(rootPath, options = {}) {
|
|
2817
|
+
const lockFiles = new Set();
|
|
2818
|
+
for (const pattern of [
|
|
2819
|
+
"**/{gradle,buildscript-gradle,settings-gradle}.lockfile",
|
|
2820
|
+
"**/gradle/dependency-locks/*.lockfile",
|
|
2821
|
+
]) {
|
|
2822
|
+
for (const lockFile of getAllFiles(rootPath, pattern, options)) {
|
|
2823
|
+
// Only the segments below the scan root count: a scan root that sits
|
|
2824
|
+
// under a directory named `build` still has lockfiles of its own.
|
|
2825
|
+
const segments = relative(rootPath, lockFile).split(/[\\/]/);
|
|
2826
|
+
if (
|
|
2827
|
+
segments.some((segment) =>
|
|
2828
|
+
["build", ".gradle", "node_modules"].includes(segment),
|
|
2829
|
+
)
|
|
2830
|
+
) {
|
|
2831
|
+
continue;
|
|
2832
|
+
}
|
|
2833
|
+
if (isInVendoredDir(rootPath, lockFile)) {
|
|
2834
|
+
continue;
|
|
2835
|
+
}
|
|
2836
|
+
const kind = gradleLockfileKind(lockFile);
|
|
2837
|
+
if (!kind) {
|
|
2838
|
+
continue;
|
|
2839
|
+
}
|
|
2840
|
+
// The project directory is the lockfile's own directory, and the
|
|
2841
|
+
// directory above `gradle` for a legacy dependency-locks file.
|
|
2842
|
+
const projectDir = kind.legacy
|
|
2843
|
+
? dirname(dirname(dirname(lockFile)))
|
|
2844
|
+
: dirname(lockFile);
|
|
2845
|
+
if (
|
|
2846
|
+
!GRADLE_PROJECT_MARKERS.some((marker) =>
|
|
2847
|
+
safeExistsSync(join(projectDir, marker)),
|
|
2848
|
+
)
|
|
2849
|
+
) {
|
|
2850
|
+
continue;
|
|
2851
|
+
}
|
|
2852
|
+
lockFiles.add(lockFile);
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
return [...lockFiles].sort();
|
|
2856
|
+
}
|
|
2857
|
+
|
|
2858
|
+
/**
|
|
2859
|
+
* The most bytes read from a settings script when looking for the root
|
|
2860
|
+
* project name.
|
|
2861
|
+
*/
|
|
2862
|
+
const GRADLE_SETTINGS_MAX_BYTES = 1024 * 1024;
|
|
2863
|
+
|
|
2864
|
+
/**
|
|
2865
|
+
* Read the root project name a Gradle settings script assigns with
|
|
2866
|
+
* `rootProject.name = '<name>'` or `rootProject.name = "<name>"`, without
|
|
2867
|
+
* running Gradle.
|
|
2868
|
+
*
|
|
2869
|
+
* `settings.gradle` is read before `settings.gradle.kts`, and the last
|
|
2870
|
+
* assignment in the file wins, as it does in Gradle. Only a plain string
|
|
2871
|
+
* literal is accepted: a value built from a variable, a template
|
|
2872
|
+
* (`"${x}"`) or a method call is not something a static read can resolve.
|
|
2873
|
+
*
|
|
2874
|
+
* @param {string} gradleRootPath Root directory of the Gradle build
|
|
2875
|
+
* @returns {string|undefined} The root project name, or undefined when no
|
|
2876
|
+
* settings script assigns one as a plain literal
|
|
2877
|
+
*/
|
|
2878
|
+
export function readGradleRootProjectName(gradleRootPath) {
|
|
2879
|
+
for (const settingsName of ["settings.gradle", "settings.gradle.kts"]) {
|
|
2880
|
+
const settingsFile = join(gradleRootPath, settingsName);
|
|
2881
|
+
if (!safeExistsSync(settingsFile)) {
|
|
2882
|
+
continue;
|
|
2883
|
+
}
|
|
2884
|
+
const text = readRegularTextFile(
|
|
2885
|
+
settingsFile,
|
|
2886
|
+
GRADLE_SETTINGS_MAX_BYTES,
|
|
2887
|
+
gradleRootPath,
|
|
2888
|
+
);
|
|
2889
|
+
if (!text) {
|
|
2890
|
+
continue;
|
|
2891
|
+
}
|
|
2892
|
+
let rootName;
|
|
2893
|
+
for (const rawLine of text.split("\n")) {
|
|
2894
|
+
const line = rawLine.trim();
|
|
2895
|
+
if (!line.startsWith("rootProject.name")) {
|
|
2896
|
+
continue;
|
|
2897
|
+
}
|
|
2898
|
+
let rest = line.slice("rootProject.name".length).trim();
|
|
2899
|
+
if (!rest.startsWith("=")) {
|
|
2900
|
+
continue;
|
|
2901
|
+
}
|
|
2902
|
+
rest = rest.slice(1).trim();
|
|
2903
|
+
const commentIndex = rest.indexOf("//");
|
|
2904
|
+
if (commentIndex !== -1) {
|
|
2905
|
+
rest = rest.slice(0, commentIndex).trim();
|
|
2906
|
+
}
|
|
2907
|
+
if (rest.endsWith(";")) {
|
|
2908
|
+
rest = rest.slice(0, -1).trim();
|
|
2909
|
+
}
|
|
2910
|
+
const quote = rest.charAt(0);
|
|
2911
|
+
if (
|
|
2912
|
+
rest.length < 3 ||
|
|
2913
|
+
!["'", '"'].includes(quote) ||
|
|
2914
|
+
!rest.endsWith(quote)
|
|
2915
|
+
) {
|
|
2916
|
+
continue;
|
|
2917
|
+
}
|
|
2918
|
+
const value = rest.slice(1, -1).trim();
|
|
2919
|
+
if (
|
|
2920
|
+
!value ||
|
|
2921
|
+
[..."'\"$\\"].some((ch) => value.includes(ch)) ||
|
|
2922
|
+
value.includes("/")
|
|
2923
|
+
) {
|
|
2924
|
+
continue;
|
|
2925
|
+
}
|
|
2926
|
+
rootName = value;
|
|
2927
|
+
}
|
|
2928
|
+
if (rootName) {
|
|
2929
|
+
return rootName;
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
return undefined;
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
/**
|
|
2936
|
+
* The largest gradle/verification-metadata.xml read. Real files of large
|
|
2937
|
+
* builds stay around half a megabyte; the file is untrusted input, so a
|
|
2938
|
+
* larger one is skipped whole rather than read in part.
|
|
2939
|
+
*/
|
|
2940
|
+
const GRADLE_VERIFICATION_METADATA_MAX_BYTES = 32 * 1024 * 1024;
|
|
2941
|
+
|
|
2942
|
+
/**
|
|
2943
|
+
* The most components read from one verification-metadata.xml. Components
|
|
2944
|
+
* past the cap are ignored.
|
|
2945
|
+
*/
|
|
2946
|
+
const GRADLE_VERIFICATION_METADATA_MAX_COMPONENTS = 200000;
|
|
2947
|
+
|
|
2948
|
+
/**
|
|
2949
|
+
* The most artifacts read for one component. A real entry names the jar, the
|
|
2950
|
+
* pom and the module metadata; a coordinate listing many more artifacts was
|
|
2951
|
+
* not written by Gradle.
|
|
2952
|
+
*/
|
|
2953
|
+
const GRADLE_VERIFICATION_METADATA_MAX_ARTIFACTS = 64;
|
|
2954
|
+
|
|
2955
|
+
/**
|
|
2956
|
+
* Checksum element of the verification schema, the CycloneDX hash algorithm
|
|
2957
|
+
* it maps to, and the exact hex digest length that algorithm produces. A
|
|
2958
|
+
* digest of any other length was not written by Gradle.
|
|
2959
|
+
*/
|
|
2960
|
+
const VERIFICATION_HASH_ALGORITHMS = [
|
|
2961
|
+
{ element: "sha512", alg: "SHA-512", length: 128 },
|
|
2962
|
+
{ element: "sha256", alg: "SHA-256", length: 64 },
|
|
2963
|
+
{ element: "sha1", alg: "SHA-1", length: 40 },
|
|
2964
|
+
{ element: "md5", alg: "MD5", length: 32 },
|
|
2965
|
+
];
|
|
2966
|
+
|
|
2967
|
+
const HEX_DIGEST_PATTERN = /^[0-9a-f]+$/iu;
|
|
2968
|
+
|
|
2969
|
+
/**
|
|
2970
|
+
* A `pgp value` is either a 16-character long key id or a 40-character v4
|
|
2971
|
+
* fingerprint. Anything else is not a key id.
|
|
2972
|
+
*/
|
|
2973
|
+
const PGP_KEY_ID_LENGTHS = new Set([16, 40]);
|
|
2974
|
+
|
|
2975
|
+
/**
|
|
2976
|
+
* The build stamp a timestamped SNAPSHOT file carries in place of
|
|
2977
|
+
* `-SNAPSHOT`, for example `-20240401.123456-1`.
|
|
2978
|
+
*/
|
|
2979
|
+
const SNAPSHOT_STAMP_PATTERN = /^-\d{8}\.\d{6}-\d+/u;
|
|
2980
|
+
|
|
2981
|
+
/**
|
|
2982
|
+
* Children of one element in the compact XML form. A child that occurs once
|
|
2983
|
+
* parses as an object and a repeated child as an array; this accepts both.
|
|
2984
|
+
*
|
|
2985
|
+
* @param {object|object[]|undefined} value Compact-form child or children
|
|
2986
|
+
* @returns {object[]} The children as a list
|
|
2987
|
+
*/
|
|
2988
|
+
function verificationElementList(value) {
|
|
2989
|
+
if (value === undefined || value === null) {
|
|
2990
|
+
return [];
|
|
2991
|
+
}
|
|
2992
|
+
return Array.isArray(value) ? value : [value];
|
|
2993
|
+
}
|
|
2994
|
+
|
|
2995
|
+
/**
|
|
2996
|
+
* One named child of an element in the compact XML form.
|
|
2997
|
+
*
|
|
2998
|
+
* A document whose root element carries a namespace prefix keeps that prefix
|
|
2999
|
+
* on the name of every element below it, so the child name is looked up with
|
|
3000
|
+
* the root's prefix in front of it. Attributes are never prefixed: the
|
|
3001
|
+
* schema's attributes are unqualified even when the elements are not.
|
|
3002
|
+
*
|
|
3003
|
+
* @param {object|undefined} element Compact-form element
|
|
3004
|
+
* @param {string} name Child element name without a prefix
|
|
3005
|
+
* @param {string} [prefix] Namespace prefix of the root element, with its
|
|
3006
|
+
* colon
|
|
3007
|
+
* @returns {object[]} The children as a list
|
|
3008
|
+
*/
|
|
3009
|
+
function verificationChildList(element, name, prefix = "") {
|
|
3010
|
+
return verificationElementList(element?.[`${prefix}${name}`]);
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
/**
|
|
3014
|
+
* Read the `true` or `false` text of a configuration flag. A flag that is
|
|
3015
|
+
* absent, or whose text is neither, reads as undefined so the caller cannot
|
|
3016
|
+
* tell a written `false` from an unreadable one.
|
|
3017
|
+
*
|
|
3018
|
+
* @param {object|undefined} element Compact-form flag element
|
|
3019
|
+
* @returns {boolean|undefined} The flag value, or undefined
|
|
3020
|
+
*/
|
|
3021
|
+
function verificationBoolean(element) {
|
|
3022
|
+
const value = typeof element?._ === "string" ? element._.trim() : "";
|
|
3023
|
+
if (value === "true") {
|
|
3024
|
+
return true;
|
|
3025
|
+
}
|
|
3026
|
+
if (value === "false") {
|
|
3027
|
+
return false;
|
|
3028
|
+
}
|
|
3029
|
+
return undefined;
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
/**
|
|
3033
|
+
* Split an artifact file name into its classifier and extension, accepting
|
|
3034
|
+
* the timestamped file names Gradle writes for SNAPSHOT versions, for which
|
|
3035
|
+
* the build stamp replaces `-SNAPSHOT` in the file name only.
|
|
3036
|
+
*
|
|
3037
|
+
* @param {string} fileName File name from the verification metadata
|
|
3038
|
+
* @param {string} name Artifact ID of the coordinate
|
|
3039
|
+
* @param {string} version Version of the coordinate
|
|
3040
|
+
* @returns {{classifier: string, extension: string}|undefined} Undefined when
|
|
3041
|
+
* the name does not belong to the coordinate
|
|
3042
|
+
*/
|
|
3043
|
+
function splitVerificationArtifactName(fileName, name, version) {
|
|
3044
|
+
const split = splitArtifactFileName(fileName, name, version);
|
|
3045
|
+
if (split) {
|
|
3046
|
+
return split;
|
|
3047
|
+
}
|
|
3048
|
+
if (!version.endsWith("-SNAPSHOT")) {
|
|
3049
|
+
return undefined;
|
|
3050
|
+
}
|
|
3051
|
+
const base = version.slice(0, -"-SNAPSHOT".length);
|
|
3052
|
+
if (!fileName.startsWith(`${name}-${base}`)) {
|
|
3053
|
+
return undefined;
|
|
3054
|
+
}
|
|
3055
|
+
const stamp = SNAPSHOT_STAMP_PATTERN.exec(
|
|
3056
|
+
fileName.substring(`${name}-${base}`.length),
|
|
3057
|
+
);
|
|
3058
|
+
if (!stamp) {
|
|
3059
|
+
return undefined;
|
|
3060
|
+
}
|
|
3061
|
+
return splitArtifactFileName(fileName, name, `${base}${stamp[0]}`);
|
|
3062
|
+
}
|
|
3063
|
+
|
|
3064
|
+
/**
|
|
3065
|
+
* Keep a checksum value only when it is a hex digest of the exact length its
|
|
3066
|
+
* algorithm produces. The `origin` and `reason` attributes, and the
|
|
3067
|
+
* `also-trust` alternates, are never read.
|
|
3068
|
+
*
|
|
3069
|
+
* @param {string} alg CycloneDX hash algorithm name
|
|
3070
|
+
* @param {number} length Expected hex digest length
|
|
3071
|
+
* @param {object|undefined} element Compact-form checksum element
|
|
3072
|
+
* @returns {{alg: string, content: string}|undefined} The hash, or undefined
|
|
3073
|
+
*/
|
|
3074
|
+
function verificationHash(alg, length, element) {
|
|
3075
|
+
const value = element?.$?.value;
|
|
3076
|
+
if (typeof value !== "string" || value.length !== length) {
|
|
3077
|
+
return undefined;
|
|
3078
|
+
}
|
|
3079
|
+
if (!HEX_DIGEST_PATTERN.test(value)) {
|
|
3080
|
+
return undefined;
|
|
3081
|
+
}
|
|
3082
|
+
return { alg, content: value.toLowerCase() };
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3085
|
+
/**
|
|
3086
|
+
* Keep a `pgp value` only when it is a long key id or a v4 fingerprint, in
|
|
3087
|
+
* the upper-case form the verification schema itself uses.
|
|
3088
|
+
*
|
|
3089
|
+
* @param {object|undefined} element Compact-form pgp element
|
|
3090
|
+
* @returns {string|undefined} The key id, or undefined
|
|
3091
|
+
*/
|
|
3092
|
+
function verificationPgpKeyId(element) {
|
|
3093
|
+
const value = element?.$?.value;
|
|
3094
|
+
if (typeof value !== "string") {
|
|
3095
|
+
return undefined;
|
|
3096
|
+
}
|
|
3097
|
+
const keyId = value.trim();
|
|
3098
|
+
if (
|
|
3099
|
+
!PGP_KEY_ID_LENGTHS.has(keyId.length) ||
|
|
3100
|
+
!HEX_DIGEST_PATTERN.test(keyId)
|
|
3101
|
+
) {
|
|
3102
|
+
return undefined;
|
|
3103
|
+
}
|
|
3104
|
+
return keyId.toUpperCase();
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
/**
|
|
3108
|
+
* Parse a `gradle/verification-metadata.xml` file.
|
|
3109
|
+
*
|
|
3110
|
+
* The file is untrusted input. It records what Gradle verified when
|
|
3111
|
+
* `gradle --write-verification-metadata` last ran, which does not prove that
|
|
3112
|
+
* the artifacts in this BOM match: the file travels with the source, and
|
|
3113
|
+
* nothing here re-checks the digests. Trust rules (`trusted-artifacts`,
|
|
3114
|
+
* `trusted-keys`, `ignored-keys`) may be regular expressions, so they are
|
|
3115
|
+
* only counted, never evaluated, and no free text of the file (an `origin`, a
|
|
3116
|
+
* `reason`, a key-server URI) reaches the result. Only checksum digests,
|
|
3117
|
+
* PGP key ids and the configuration flags survive.
|
|
3118
|
+
*
|
|
3119
|
+
* The read is bounded: the file must be a regular file no larger than
|
|
3120
|
+
* 32 MiB inside `options.rootDir`, at most 200000 components and 64
|
|
3121
|
+
* artifacts per component are read, and a malformed file yields undefined
|
|
3122
|
+
* rather than an error.
|
|
3123
|
+
*
|
|
3124
|
+
* @param {string} metadataFile Path to verification-metadata.xml
|
|
3125
|
+
* @param {Object} [options] Parse options
|
|
3126
|
+
* @param {string} [options.rootDir] Directory the file must resolve inside
|
|
3127
|
+
* @returns {{configuration: {verifyMetadata: (boolean|undefined), verifySignatures: (boolean|undefined), trustedKeyCount: number, trustedArtifactRuleCount: number, ignoredKeyCount: number, keyServerCount: number}, components: Map<string, {group: string, name: string, version: string, artifacts: {fileName: string, classifier: string, extension: string, hashes: {alg: string, content: string}[], pgpKeyIds: string[]}[]}>}|undefined}
|
|
3128
|
+
* The parsed metadata, or undefined when the file could not be read
|
|
3129
|
+
*/
|
|
3130
|
+
export function parseGradleVerificationMetadata(metadataFile, options = {}) {
|
|
3131
|
+
const text = readRegularTextFile(
|
|
3132
|
+
metadataFile,
|
|
3133
|
+
GRADLE_VERIFICATION_METADATA_MAX_BYTES,
|
|
3134
|
+
options?.rootDir,
|
|
3135
|
+
);
|
|
3136
|
+
if (text === undefined) {
|
|
3137
|
+
if (safeExistsSync(metadataFile)) {
|
|
3138
|
+
console.warn(
|
|
3139
|
+
`Skipping ${metadataFile}: it is not a regular file inside the project, or is larger than ${GRADLE_VERIFICATION_METADATA_MAX_BYTES} bytes.`,
|
|
3140
|
+
);
|
|
3141
|
+
}
|
|
3142
|
+
return undefined;
|
|
3143
|
+
}
|
|
3144
|
+
const body = text.startsWith("\uFEFF") ? text.slice(1) : text;
|
|
3145
|
+
let parsed;
|
|
3146
|
+
try {
|
|
3147
|
+
parsed = xml2js(body, {
|
|
3148
|
+
compact: true,
|
|
3149
|
+
ignoreComment: true,
|
|
3150
|
+
ignoreDoctype: true,
|
|
3151
|
+
ignoreDeclaration: true,
|
|
3152
|
+
textKey: "_",
|
|
3153
|
+
attributesKey: "$",
|
|
3154
|
+
});
|
|
3155
|
+
} catch (err) {
|
|
3156
|
+
thoughtLog(
|
|
3157
|
+
`${metadataFile} is not well-formed XML, so it was skipped: ${
|
|
3158
|
+
err?.message || err
|
|
3159
|
+
}`,
|
|
3160
|
+
);
|
|
3161
|
+
return undefined;
|
|
3162
|
+
}
|
|
3163
|
+
// Gradle writes the root element in its own namespace, which a default
|
|
3164
|
+
// namespace leaves off element names and a prefix keeps; both are accepted.
|
|
3165
|
+
// A prefixed document keeps the prefix on the name of every element below
|
|
3166
|
+
// the root, so the prefix is carried into every child lookup below.
|
|
3167
|
+
const rootKey = Object.keys(parsed).find(
|
|
3168
|
+
(key) =>
|
|
3169
|
+
key === "verification-metadata" || key.endsWith(":verification-metadata"),
|
|
3170
|
+
);
|
|
3171
|
+
const root = verificationElementList(
|
|
3172
|
+
rootKey ? parsed[rootKey] : undefined,
|
|
3173
|
+
)[0];
|
|
3174
|
+
if (!root) {
|
|
3175
|
+
thoughtLog(
|
|
3176
|
+
`${metadataFile} has no verification-metadata root element, so it was skipped.`,
|
|
3177
|
+
);
|
|
3178
|
+
return undefined;
|
|
3179
|
+
}
|
|
3180
|
+
const prefix = rootKey?.includes(":")
|
|
3181
|
+
? rootKey.slice(0, rootKey.indexOf(":") + 1)
|
|
3182
|
+
: "";
|
|
3183
|
+
const configurationElement = verificationChildList(
|
|
3184
|
+
root,
|
|
3185
|
+
"configuration",
|
|
3186
|
+
prefix,
|
|
3187
|
+
)[0];
|
|
3188
|
+
const componentsElement = verificationChildList(
|
|
3189
|
+
root,
|
|
3190
|
+
"components",
|
|
3191
|
+
prefix,
|
|
3192
|
+
)[0];
|
|
3193
|
+
const trustedKeysElement = verificationChildList(
|
|
3194
|
+
configurationElement,
|
|
3195
|
+
"trusted-keys",
|
|
3196
|
+
prefix,
|
|
3197
|
+
)[0];
|
|
3198
|
+
const trustedArtifactsElement = verificationChildList(
|
|
3199
|
+
configurationElement,
|
|
3200
|
+
"trusted-artifacts",
|
|
3201
|
+
prefix,
|
|
3202
|
+
)[0];
|
|
3203
|
+
const ignoredKeysElement = verificationChildList(
|
|
3204
|
+
configurationElement,
|
|
3205
|
+
"ignored-keys",
|
|
3206
|
+
prefix,
|
|
3207
|
+
)[0];
|
|
3208
|
+
const keyServersElement = verificationChildList(
|
|
3209
|
+
configurationElement,
|
|
3210
|
+
"key-servers",
|
|
3211
|
+
prefix,
|
|
3212
|
+
)[0];
|
|
3213
|
+
const components = new Map();
|
|
3214
|
+
let skippedArtifacts = 0;
|
|
3215
|
+
const componentList = verificationChildList(
|
|
3216
|
+
componentsElement,
|
|
3217
|
+
"component",
|
|
3218
|
+
prefix,
|
|
3219
|
+
);
|
|
3220
|
+
for (const componentElement of componentList) {
|
|
3221
|
+
if (components.size === GRADLE_VERIFICATION_METADATA_MAX_COMPONENTS) {
|
|
3222
|
+
thoughtLog(
|
|
3223
|
+
`${metadataFile} lists more than ${GRADLE_VERIFICATION_METADATA_MAX_COMPONENTS} components; only the first ${GRADLE_VERIFICATION_METADATA_MAX_COMPONENTS} are read.`,
|
|
3224
|
+
);
|
|
3225
|
+
break;
|
|
3226
|
+
}
|
|
3227
|
+
const attributes = componentElement?.$ || {};
|
|
3228
|
+
const group =
|
|
3229
|
+
typeof attributes.group === "string" ? attributes.group.trim() : "";
|
|
3230
|
+
const name =
|
|
3231
|
+
typeof attributes.name === "string" ? attributes.name.trim() : "";
|
|
3232
|
+
const version =
|
|
3233
|
+
typeof attributes.version === "string" ? attributes.version.trim() : "";
|
|
3234
|
+
// A coordinate without all three parts, or one no valid purl can be
|
|
3235
|
+
// built for, can never match a component of this BOM.
|
|
3236
|
+
if (!group || !name || !version) {
|
|
3237
|
+
continue;
|
|
3238
|
+
}
|
|
3239
|
+
if (!mavenPurl(group, name, version)) {
|
|
3240
|
+
continue;
|
|
3241
|
+
}
|
|
3242
|
+
const artifacts = [];
|
|
3243
|
+
const artifactList = verificationChildList(
|
|
3244
|
+
componentElement,
|
|
3245
|
+
"artifact",
|
|
3246
|
+
prefix,
|
|
3247
|
+
);
|
|
3248
|
+
if (artifactList.length > GRADLE_VERIFICATION_METADATA_MAX_ARTIFACTS) {
|
|
3249
|
+
skippedArtifacts +=
|
|
3250
|
+
artifactList.length - GRADLE_VERIFICATION_METADATA_MAX_ARTIFACTS;
|
|
3251
|
+
}
|
|
3252
|
+
for (const artifactElement of artifactList.slice(
|
|
3253
|
+
0,
|
|
3254
|
+
GRADLE_VERIFICATION_METADATA_MAX_ARTIFACTS,
|
|
3255
|
+
)) {
|
|
3256
|
+
const fileName =
|
|
3257
|
+
typeof artifactElement?.$?.name === "string"
|
|
3258
|
+
? artifactElement.$.name.trim()
|
|
3259
|
+
: "";
|
|
3260
|
+
const split = fileName
|
|
3261
|
+
? splitVerificationArtifactName(fileName, name, version)
|
|
3262
|
+
: undefined;
|
|
3263
|
+
if (!split) {
|
|
3264
|
+
skippedArtifacts++;
|
|
3265
|
+
continue;
|
|
3266
|
+
}
|
|
3267
|
+
const hashes = [];
|
|
3268
|
+
for (const { element, alg, length } of VERIFICATION_HASH_ALGORITHMS) {
|
|
3269
|
+
const hash = verificationHash(
|
|
3270
|
+
alg,
|
|
3271
|
+
length,
|
|
3272
|
+
verificationChildList(artifactElement, element, prefix)[0],
|
|
3273
|
+
);
|
|
3274
|
+
if (hash) {
|
|
3275
|
+
hashes.push(hash);
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
const pgpKeyIds = [];
|
|
3279
|
+
for (const pgpElement of verificationChildList(
|
|
3280
|
+
artifactElement,
|
|
3281
|
+
"pgp",
|
|
3282
|
+
prefix,
|
|
3283
|
+
)) {
|
|
3284
|
+
const keyId = verificationPgpKeyId(pgpElement);
|
|
3285
|
+
if (keyId && !pgpKeyIds.includes(keyId)) {
|
|
3286
|
+
pgpKeyIds.push(keyId);
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
pgpKeyIds.sort();
|
|
3290
|
+
artifacts.push({
|
|
3291
|
+
fileName,
|
|
3292
|
+
classifier: split.classifier,
|
|
3293
|
+
extension: split.extension,
|
|
3294
|
+
hashes,
|
|
3295
|
+
pgpKeyIds,
|
|
3296
|
+
});
|
|
3297
|
+
}
|
|
3298
|
+
const key = `${group}:${name}:${version}`;
|
|
3299
|
+
if (!components.has(key)) {
|
|
3300
|
+
components.set(key, { group, name, version, artifacts });
|
|
3301
|
+
}
|
|
3302
|
+
}
|
|
3303
|
+
if (skippedArtifacts) {
|
|
3304
|
+
thoughtLog(
|
|
3305
|
+
`Skipped ${skippedArtifacts} artifact element(s) whose file name does not belong to their coordinate in ${metadataFile}.`,
|
|
3306
|
+
);
|
|
3307
|
+
}
|
|
3308
|
+
return {
|
|
3309
|
+
configuration: {
|
|
3310
|
+
verifyMetadata: verificationBoolean(
|
|
3311
|
+
verificationChildList(
|
|
3312
|
+
configurationElement,
|
|
3313
|
+
"verify-metadata",
|
|
3314
|
+
prefix,
|
|
3315
|
+
)[0],
|
|
3316
|
+
),
|
|
3317
|
+
verifySignatures: verificationBoolean(
|
|
3318
|
+
verificationChildList(
|
|
3319
|
+
configurationElement,
|
|
3320
|
+
"verify-signatures",
|
|
3321
|
+
prefix,
|
|
3322
|
+
)[0],
|
|
3323
|
+
),
|
|
3324
|
+
trustedKeyCount: verificationChildList(
|
|
3325
|
+
trustedKeysElement,
|
|
3326
|
+
"trusted-key",
|
|
3327
|
+
prefix,
|
|
3328
|
+
).length,
|
|
3329
|
+
trustedArtifactRuleCount: verificationChildList(
|
|
3330
|
+
trustedArtifactsElement,
|
|
3331
|
+
"trust",
|
|
3332
|
+
prefix,
|
|
3333
|
+
).length,
|
|
3334
|
+
ignoredKeyCount: verificationChildList(
|
|
3335
|
+
ignoredKeysElement,
|
|
3336
|
+
"ignored-key",
|
|
3337
|
+
prefix,
|
|
3338
|
+
).length,
|
|
3339
|
+
keyServerCount: verificationChildList(
|
|
3340
|
+
keyServersElement,
|
|
3341
|
+
"key-server",
|
|
3342
|
+
prefix,
|
|
3343
|
+
).length,
|
|
3344
|
+
},
|
|
3345
|
+
components,
|
|
3346
|
+
};
|
|
3347
|
+
}
|
|
3348
|
+
|
|
3349
|
+
/**
|
|
3350
|
+
* Find the gradle/verification-metadata.xml of one Gradle build.
|
|
3351
|
+
*
|
|
3352
|
+
* Gradle reads one verification file per build, from the root build's
|
|
3353
|
+
* `gradle` directory; included builds' copies are ignored, so nothing is
|
|
3354
|
+
* searched below the root. The file is accepted only when the directory is a
|
|
3355
|
+
* Gradle project of its own, so a file copied into a nested folder is not
|
|
3356
|
+
* read as an inventory source.
|
|
3357
|
+
*
|
|
3358
|
+
* @param {string} gradleRootPath Root directory of the Gradle build
|
|
3359
|
+
* @returns {string|undefined} The metadata file path, or undefined
|
|
3360
|
+
*/
|
|
3361
|
+
export function findGradleVerificationMetadata(gradleRootPath) {
|
|
3362
|
+
const metadataFile = join(
|
|
3363
|
+
gradleRootPath,
|
|
3364
|
+
"gradle",
|
|
3365
|
+
"verification-metadata.xml",
|
|
3366
|
+
);
|
|
3367
|
+
if (!safeExistsSync(metadataFile)) {
|
|
3368
|
+
return undefined;
|
|
3369
|
+
}
|
|
3370
|
+
if (
|
|
3371
|
+
!GRADLE_PROJECT_MARKERS.some((marker) =>
|
|
3372
|
+
safeExistsSync(join(gradleRootPath, marker)),
|
|
3373
|
+
)
|
|
3374
|
+
) {
|
|
3375
|
+
return undefined;
|
|
3376
|
+
}
|
|
3377
|
+
return metadataFile;
|
|
3378
|
+
}
|
|
3379
|
+
|
|
3380
|
+
/**
|
|
3381
|
+
* The artifact of a coordinate whose hashes describe the component itself:
|
|
3382
|
+
* the `.pom` artifact for a platform or BOM component, else the main jar,
|
|
3383
|
+
* else the main aar. Sources, javadoc and other classifiers, and the
|
|
3384
|
+
* `.module` and `.pom` hashes of a jar component, are never used.
|
|
3385
|
+
*
|
|
3386
|
+
* @param {object} component Parsed verification metadata component
|
|
3387
|
+
* @param {boolean} wantsPom True for a component with purl qualifier type=pom
|
|
3388
|
+
* @returns {object|undefined} The artifact, or undefined
|
|
3389
|
+
*/
|
|
3390
|
+
export function gradleVerificationMainArtifact(component, wantsPom) {
|
|
3391
|
+
const wanted = wantsPom ? "pom" : "jar";
|
|
3392
|
+
const main = component.artifacts.find(
|
|
3393
|
+
(artifact) => !artifact.classifier && artifact.extension === wanted,
|
|
3394
|
+
);
|
|
3395
|
+
if (main || wantsPom) {
|
|
3396
|
+
return main;
|
|
3397
|
+
}
|
|
3398
|
+
return component.artifacts.find(
|
|
3399
|
+
(artifact) => !artifact.classifier && artifact.extension === "aar",
|
|
3400
|
+
);
|
|
3401
|
+
}
|
|
3402
|
+
|
|
3403
|
+
/**
|
|
3404
|
+
* Merge the checksums and PGP key ids a verification-metadata.xml records
|
|
3405
|
+
* into the components of a package list.
|
|
3406
|
+
*
|
|
3407
|
+
* A digest of the artifact itself, computed from the jar in the local cache
|
|
3408
|
+
* or found by another collector, is the stronger claim, so the recorded
|
|
3409
|
+
* digests only fill the algorithms a component still lacks. Where the file
|
|
3410
|
+
* records a different digest for an algorithm the component already has,
|
|
3411
|
+
* the recorded digest never wins: the component keeps its own, and the
|
|
3412
|
+
* component gains the cdx:gradle:verificationMismatch=true property so the
|
|
3413
|
+
* disagreement is visible. A released Maven version does not change after
|
|
3414
|
+
* publishing, so a stale file cannot explain a mismatch: a stale file names
|
|
3415
|
+
* old versions, which match nothing. The likely causes are an artifact
|
|
3416
|
+
* republished on a mirror or internal repository, or a corrupted or
|
|
3417
|
+
* tampered cache. SNAPSHOT versions and digests that are not hex are not
|
|
3418
|
+
* compared.
|
|
3419
|
+
*
|
|
3420
|
+
* Every matched component also gains two properties naming the verification
|
|
3421
|
+
* methods and the PGP key ids of its artifact. The evidence of the
|
|
3422
|
+
* component is left alone: a component found by a Gradle run or a lockfile
|
|
3423
|
+
* keeps the identity evidence of that source, and only the components the
|
|
3424
|
+
* verification-metadata fallback built carry the file as their evidence.
|
|
3425
|
+
*
|
|
3426
|
+
* @param {Object[]} pkgList Components to enrich in place
|
|
3427
|
+
* @param {Object} metadata Parsed metadata, as parseGradleVerificationMetadata
|
|
3428
|
+
* returns
|
|
3429
|
+
* @param {string} metadataFile Path to the verification-metadata.xml, named
|
|
3430
|
+
* in the debug reasoning
|
|
3431
|
+
* @returns {number} The number of components this call changed
|
|
3432
|
+
*/
|
|
3433
|
+
export function applyGradleVerificationMetadata(
|
|
3434
|
+
pkgList,
|
|
3435
|
+
metadata,
|
|
3436
|
+
metadataFile,
|
|
3437
|
+
) {
|
|
3438
|
+
if (!metadata?.components?.size) {
|
|
3439
|
+
return 0;
|
|
3440
|
+
}
|
|
3441
|
+
let changedCount = 0;
|
|
3442
|
+
for (const pkg of pkgList || []) {
|
|
3443
|
+
if (!pkg?.purl?.startsWith("pkg:maven/")) {
|
|
3444
|
+
continue;
|
|
3445
|
+
}
|
|
3446
|
+
const component = metadata.components.get(
|
|
3447
|
+
`${pkg.group}:${pkg.name}:${pkg.version}`,
|
|
3448
|
+
);
|
|
3449
|
+
if (!component) {
|
|
3450
|
+
continue;
|
|
3451
|
+
}
|
|
3452
|
+
const artifact = gradleVerificationMainArtifact(
|
|
3453
|
+
component,
|
|
3454
|
+
pkg.qualifiers?.type === "pom",
|
|
3455
|
+
);
|
|
3456
|
+
if (!artifact) {
|
|
3457
|
+
continue;
|
|
3458
|
+
}
|
|
3459
|
+
const hashesBefore = pkg.hashes;
|
|
3460
|
+
const propertiesBefore = pkg.properties;
|
|
3461
|
+
// A SNAPSHOT version names a moving artifact: the file can record
|
|
3462
|
+
// several timestamped builds of it while the cache holds the newest, so
|
|
3463
|
+
// a differing digest there says nothing about tampering.
|
|
3464
|
+
const comparable = !String(pkg.version || "").endsWith("-SNAPSHOT");
|
|
3465
|
+
// The merged list keeps the order the component's own hashes came in
|
|
3466
|
+
// and appends the recorded ones in the schema's algorithm order.
|
|
3467
|
+
const hashes = Array.isArray(pkg.hashes) ? [...pkg.hashes] : [];
|
|
3468
|
+
let mismatch = false;
|
|
3469
|
+
for (const recorded of artifact.hashes) {
|
|
3470
|
+
const existing = hashes.find((hash) => hash.alg === recorded.alg);
|
|
3471
|
+
if (existing) {
|
|
3472
|
+
// Other collectors may carry an upper-case or base64 digest; only a
|
|
3473
|
+
// hex digest can be compared with the recorded one.
|
|
3474
|
+
const existingContent =
|
|
3475
|
+
typeof existing.content === "string" ? existing.content : "";
|
|
3476
|
+
if (
|
|
3477
|
+
comparable &&
|
|
3478
|
+
HEX_DIGEST_PATTERN.test(existingContent) &&
|
|
3479
|
+
existingContent.toLowerCase() !== recorded.content
|
|
3480
|
+
) {
|
|
3481
|
+
mismatch = true;
|
|
3482
|
+
thoughtLog(
|
|
3483
|
+
`${metadataFile} records a different ${recorded.alg} digest for ${pkg.group}:${pkg.name}:${pkg.version} than the artifact carries; the recorded digest is flagged, not applied.`,
|
|
3484
|
+
);
|
|
3485
|
+
}
|
|
3486
|
+
continue;
|
|
3487
|
+
}
|
|
3488
|
+
hashes.push({ alg: recorded.alg, content: recorded.content });
|
|
3489
|
+
}
|
|
3490
|
+
if (hashes.length && hashes.length !== (pkg.hashes?.length || 0)) {
|
|
3491
|
+
pkg.hashes = hashes;
|
|
3492
|
+
}
|
|
3493
|
+
const methods = new Set();
|
|
3494
|
+
for (const { element, alg } of VERIFICATION_HASH_ALGORITHMS) {
|
|
3495
|
+
if (artifact.hashes.some((hash) => hash.alg === alg)) {
|
|
3496
|
+
methods.add(element);
|
|
3497
|
+
}
|
|
3498
|
+
}
|
|
3499
|
+
if (artifact.pgpKeyIds.length) {
|
|
3500
|
+
methods.add("pgp");
|
|
3501
|
+
}
|
|
3502
|
+
const properties = pkg.properties || [];
|
|
3503
|
+
if (
|
|
3504
|
+
methods.size &&
|
|
3505
|
+
!properties.some((p) => p.name === "cdx:gradle:verificationMethods")
|
|
3506
|
+
) {
|
|
3507
|
+
properties.push({
|
|
3508
|
+
name: "cdx:gradle:verificationMethods",
|
|
3509
|
+
value: [...methods].sort().join(","),
|
|
3510
|
+
});
|
|
3511
|
+
}
|
|
3512
|
+
if (
|
|
3513
|
+
artifact.pgpKeyIds.length &&
|
|
3514
|
+
!properties.some((p) => p.name === "cdx:gradle:pgpKeyIds")
|
|
3515
|
+
) {
|
|
3516
|
+
properties.push({
|
|
3517
|
+
name: "cdx:gradle:pgpKeyIds",
|
|
3518
|
+
value: [...new Set(artifact.pgpKeyIds)].sort().slice(0, 10).join(","),
|
|
3519
|
+
});
|
|
3520
|
+
}
|
|
3521
|
+
if (
|
|
3522
|
+
mismatch &&
|
|
3523
|
+
!properties.some((p) => p.name === "cdx:gradle:verificationMismatch")
|
|
3524
|
+
) {
|
|
3525
|
+
properties.push({
|
|
3526
|
+
name: "cdx:gradle:verificationMismatch",
|
|
3527
|
+
value: "true",
|
|
3528
|
+
});
|
|
3529
|
+
}
|
|
3530
|
+
if (properties.length !== (pkg.properties?.length || 0)) {
|
|
3531
|
+
pkg.properties = properties;
|
|
3532
|
+
}
|
|
3533
|
+
if (pkg.hashes !== hashesBefore || pkg.properties !== propertiesBefore) {
|
|
3534
|
+
changedCount++;
|
|
3535
|
+
}
|
|
3536
|
+
}
|
|
3537
|
+
return changedCount;
|
|
3538
|
+
}
|