@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,5 +1,13 @@
|
|
|
1
|
-
import { lstatSync, readFileSync, statSync } from "node:fs";
|
|
2
|
-
import {
|
|
1
|
+
import { lstatSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
2
|
+
import {
|
|
3
|
+
basename,
|
|
4
|
+
dirname,
|
|
5
|
+
isAbsolute,
|
|
6
|
+
join,
|
|
7
|
+
relative,
|
|
8
|
+
resolve,
|
|
9
|
+
sep,
|
|
10
|
+
} from "node:path";
|
|
3
11
|
import process from "node:process";
|
|
4
12
|
|
|
5
13
|
import { DEBUG_MODE, readEnvironmentVariable } from "../core/activity.js";
|
|
@@ -8,12 +16,12 @@ import { PROJECT_TYPE_ALIASES } from "../core/env.js";
|
|
|
8
16
|
import {
|
|
9
17
|
getAllFiles,
|
|
10
18
|
getTmpDir,
|
|
11
|
-
safeCopyFileSync,
|
|
12
19
|
safeExistsSync,
|
|
13
20
|
safeMkdirSync,
|
|
14
21
|
safeMkdtempSync,
|
|
15
22
|
safeRmSync,
|
|
16
23
|
safeSpawnSync,
|
|
24
|
+
safeWriteSync,
|
|
17
25
|
} from "../core/fs.js";
|
|
18
26
|
import { dirNameStr } from "../core/paths.js";
|
|
19
27
|
import { resolvePluginSourceDir } from "../core/pluginRun.js";
|
|
@@ -22,6 +30,7 @@ import { isProjectToolingAllowed } from "../core/projectTooling.js";
|
|
|
22
30
|
import { parsetoolsFile } from "../inventory/atomUtils.js";
|
|
23
31
|
import { applyAlgorithmProperties } from "../inventory/cryptoAlgorithmFamily.js";
|
|
24
32
|
import { getJarClasses } from "../inventory/deps.js";
|
|
33
|
+
import { occurrenceLocationKey } from "../inventory/occurrenceKey.js";
|
|
25
34
|
import {
|
|
26
35
|
parseScalaArtifact,
|
|
27
36
|
scalaCoordinateKey,
|
|
@@ -147,22 +156,25 @@ function scalasemTimeoutMs(options = {}) {
|
|
|
147
156
|
const TIMEOUT_GRACE_MS = 30_000;
|
|
148
157
|
|
|
149
158
|
// The build files that identify a Scala project, for a source argument that
|
|
150
|
-
// names the working directory relative to itself
|
|
159
|
+
// names the working directory relative to itself, and that a persisted report
|
|
160
|
+
// must be newer than. sbt 0.13 builds may define everything under project/.
|
|
151
161
|
const SCALA_BUILD_MARKERS = [
|
|
152
162
|
"build.sbt",
|
|
153
163
|
"build.sc",
|
|
154
164
|
"build.mill",
|
|
155
165
|
"pom.xml",
|
|
156
166
|
"project.scala",
|
|
167
|
+
join("project", "build.properties"),
|
|
157
168
|
];
|
|
158
169
|
|
|
159
170
|
/**
|
|
160
171
|
* Where the scalasem report is persisted. An absolute --semantics-slices-file
|
|
161
172
|
* keeps its directory; anything else lands in the evinse output directory, the
|
|
162
|
-
* same rule the atom slice writer follows.
|
|
173
|
+
* same rule the atom slice writer follows. Without either there is nowhere to
|
|
174
|
+
* keep it, and the report lives only as long as the run.
|
|
163
175
|
*
|
|
164
176
|
* @param {Object} options CLI options carrying `output` and `semanticsSlicesFile`.
|
|
165
|
-
* @returns {string} Absolute report path.
|
|
177
|
+
* @returns {string|undefined} Absolute report path.
|
|
166
178
|
*/
|
|
167
179
|
export function scalasemOutputFile(options = {}) {
|
|
168
180
|
const requested = options.semanticsSlicesFile || "semantics.slices.json";
|
|
@@ -171,21 +183,18 @@ export function scalasemOutputFile(options = {}) {
|
|
|
171
183
|
if (isAbsolute(requested)) {
|
|
172
184
|
return requested;
|
|
173
185
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
outputDir = isDir ? resolvedOutput : dirname(resolvedOutput);
|
|
186
|
-
} else {
|
|
187
|
-
outputDir = safeMkdtempSync(join(getTmpDir(), "scalasem-"));
|
|
186
|
+
if (!options.output) {
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
const resolvedOutput = resolve(options.output);
|
|
190
|
+
let isDir = false;
|
|
191
|
+
try {
|
|
192
|
+
isDir =
|
|
193
|
+
safeExistsSync(resolvedOutput) && lstatSync(resolvedOutput).isDirectory();
|
|
194
|
+
} catch (_err) {
|
|
195
|
+
isDir = false;
|
|
188
196
|
}
|
|
197
|
+
const outputDir = isDir ? resolvedOutput : dirname(resolvedOutput);
|
|
189
198
|
return join(outputDir, basename(requested));
|
|
190
199
|
}
|
|
191
200
|
|
|
@@ -224,6 +233,18 @@ export function isV1SemanticsSlice(parsed) {
|
|
|
224
233
|
return Object.keys(parsed).some((key) => key.endsWith(".scala"));
|
|
225
234
|
}
|
|
226
235
|
|
|
236
|
+
// Whether a path lies below a directory. A prefix test fails for a root
|
|
237
|
+
// directory (`/`, `C:\\`), whose path already ends in the separator.
|
|
238
|
+
function isInside(directory, path) {
|
|
239
|
+
const rel = relative(directory, path);
|
|
240
|
+
return (
|
|
241
|
+
Boolean(rel) &&
|
|
242
|
+
rel !== ".." &&
|
|
243
|
+
!rel.startsWith(`..${sep}`) &&
|
|
244
|
+
!isAbsolute(rel)
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
227
248
|
/**
|
|
228
249
|
* Whether a version 1 slice describes this project: it names no project, so
|
|
229
250
|
* every source file it lists must exist under the project directory.
|
|
@@ -234,15 +255,104 @@ function isV1SliceOfProject(slice, projectDir) {
|
|
|
234
255
|
files.length > 0 &&
|
|
235
256
|
files.every((file) => {
|
|
236
257
|
const path = resolve(projectDir, file);
|
|
237
|
-
return
|
|
258
|
+
return isInside(projectDir, path) && safeExistsSync(path);
|
|
238
259
|
})
|
|
239
260
|
);
|
|
240
261
|
}
|
|
241
262
|
|
|
263
|
+
// The run settings a report depends on. cdxgen records them in the report it
|
|
264
|
+
// keeps, so a later run reuses that report only when it would ask for the
|
|
265
|
+
// same: tests included or not, the build run or not.
|
|
266
|
+
const RUN_SETTINGS_KEY = "cdxgenRunSettings";
|
|
267
|
+
|
|
268
|
+
function scalasemRunSettings(options) {
|
|
269
|
+
return {
|
|
270
|
+
build: isProjectToolingAllowed(options),
|
|
271
|
+
includeTests: Boolean(
|
|
272
|
+
options.scalasemIncludeTests && !options.requiredOnly,
|
|
273
|
+
),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function canonicalPath(path) {
|
|
278
|
+
try {
|
|
279
|
+
return realpathSync(path);
|
|
280
|
+
} catch (_err) {
|
|
281
|
+
return resolve(path);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Whether two paths name one directory, through symbolic links such as
|
|
286
|
+
// /var and /private/var, and in any case on Windows.
|
|
287
|
+
function isSamePath(left, right) {
|
|
288
|
+
const a = canonicalPath(left);
|
|
289
|
+
const b = canonicalPath(right);
|
|
290
|
+
return process.platform === "win32"
|
|
291
|
+
? a.toLowerCase() === b.toLowerCase()
|
|
292
|
+
: a === b;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function modifiedAt(file) {
|
|
296
|
+
try {
|
|
297
|
+
return statSync(file).mtimeMs;
|
|
298
|
+
} catch (_err) {
|
|
299
|
+
return undefined;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Why a persisted report cannot stand in for a new run: it describes another
|
|
305
|
+
* project, it predates the input BOM, cdxgen kept it from a run with other
|
|
306
|
+
* settings, or a build file or a source it describes changed or went away
|
|
307
|
+
* since it was written.
|
|
308
|
+
*
|
|
309
|
+
* @returns {string|undefined} The reason, or undefined when it can be reused.
|
|
310
|
+
*/
|
|
311
|
+
function reportReuseProblem(report, reportFile, analysisDir, options) {
|
|
312
|
+
const projectPath = report._meta?.projectPath;
|
|
313
|
+
if (!projectPath || !isSamePath(projectPath, analysisDir)) {
|
|
314
|
+
return "it describes another project";
|
|
315
|
+
}
|
|
316
|
+
if (!isNewerThan(reportFile, options.input)) {
|
|
317
|
+
return "it is older than the input BOM";
|
|
318
|
+
}
|
|
319
|
+
const recorded = report._meta[RUN_SETTINGS_KEY];
|
|
320
|
+
const wanted = scalasemRunSettings(options);
|
|
321
|
+
if (
|
|
322
|
+
recorded &&
|
|
323
|
+
(recorded.build !== wanted.build ||
|
|
324
|
+
recorded.includeTests !== wanted.includeTests)
|
|
325
|
+
) {
|
|
326
|
+
return "it was made with other settings";
|
|
327
|
+
}
|
|
328
|
+
const writtenAt = modifiedAt(reportFile);
|
|
329
|
+
if (writtenAt === undefined) {
|
|
330
|
+
return "its modification time is unknown";
|
|
331
|
+
}
|
|
332
|
+
for (const marker of SCALA_BUILD_MARKERS) {
|
|
333
|
+
const changedAt = modifiedAt(join(analysisDir, marker));
|
|
334
|
+
if (changedAt !== undefined && changedAt > writtenAt) {
|
|
335
|
+
return `${marker} changed since`;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
for (const key of Object.keys(report)) {
|
|
339
|
+
const file = isReportFileKey(key) ? resolve(analysisDir, key) : undefined;
|
|
340
|
+
if (!file || !isInside(analysisDir, file)) {
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
const changedAt = modifiedAt(file);
|
|
344
|
+
if (changedAt === undefined || changedAt > writtenAt) {
|
|
345
|
+
return `${key} changed since`;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return undefined;
|
|
349
|
+
}
|
|
350
|
+
|
|
242
351
|
/**
|
|
243
352
|
* Run scalasem over a Scala project and return its report, reusing a
|
|
244
|
-
* persisted version 2 report that belongs to this project
|
|
245
|
-
* the
|
|
353
|
+
* persisted version 2 report that belongs to this project, is newer than the
|
|
354
|
+
* input BOM and than the build files and sources it describes, and, when
|
|
355
|
+
* cdxgen kept it, was made with the settings this run asks for.
|
|
246
356
|
*
|
|
247
357
|
* Failures are visible rather than silent: the reason is printed once, the
|
|
248
358
|
* run's diagnostics reach the caller as `cdx:scalasem:diagnostic` properties,
|
|
@@ -280,10 +390,18 @@ export function analyzeScalaProject(src, options = {}) {
|
|
|
280
390
|
for (const candidate of candidates) {
|
|
281
391
|
const existing = readScalasemReport(candidate);
|
|
282
392
|
if (existing?._meta?.schemaVersion === "scalasem/2") {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
393
|
+
const problem = reportReuseProblem(
|
|
394
|
+
existing,
|
|
395
|
+
candidate,
|
|
396
|
+
analysisDir,
|
|
397
|
+
options,
|
|
398
|
+
);
|
|
399
|
+
if (problem && DEBUG_MODE) {
|
|
400
|
+
console.log(
|
|
401
|
+
`Not reusing the scalasem report "${candidate}": ${problem}.`,
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
if (!problem) {
|
|
287
405
|
if (DEBUG_MODE) {
|
|
288
406
|
console.log(`Reusing the scalasem report "${candidate}".`);
|
|
289
407
|
}
|
|
@@ -340,13 +458,38 @@ function runScalasem(analysisDir, options, requestedFile, v1Slice, v1File) {
|
|
|
340
458
|
// earlier run (or another project's run) had left there, and a report
|
|
341
459
|
// written within the millisecond the run started passed for fresh.
|
|
342
460
|
const runDir = safeMkdtempSync(join(getTmpDir(), "scalasem-"));
|
|
461
|
+
try {
|
|
462
|
+
return runScalasemIn(
|
|
463
|
+
runDir,
|
|
464
|
+
scalasem,
|
|
465
|
+
analysisDir,
|
|
466
|
+
options,
|
|
467
|
+
requestedFile,
|
|
468
|
+
v1Slice,
|
|
469
|
+
v1File,
|
|
470
|
+
);
|
|
471
|
+
} finally {
|
|
472
|
+
safeRmSync(runDir, { recursive: true, force: true });
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function runScalasemIn(
|
|
477
|
+
runDir,
|
|
478
|
+
scalasem,
|
|
479
|
+
analysisDir,
|
|
480
|
+
options,
|
|
481
|
+
requestedFile,
|
|
482
|
+
v1Slice,
|
|
483
|
+
v1File,
|
|
484
|
+
) {
|
|
343
485
|
const runReportFile = join(runDir, "scalasem.slices.json");
|
|
344
486
|
const args = [scalasem, analysisDir, runReportFile];
|
|
487
|
+
const settings = scalasemRunSettings(options);
|
|
345
488
|
// The build runs the project's sbt definitions
|
|
346
|
-
if (!
|
|
489
|
+
if (!settings.build) {
|
|
347
490
|
args.push("--no-build");
|
|
348
491
|
}
|
|
349
|
-
if (
|
|
492
|
+
if (settings.includeTests) {
|
|
350
493
|
args.push("--include-tests");
|
|
351
494
|
}
|
|
352
495
|
if (DEBUG_MODE) {
|
|
@@ -357,17 +500,24 @@ function runScalasem(analysisDir, options, requestedFile, v1Slice, v1File) {
|
|
|
357
500
|
// scalasem enforces the limit itself, so the builds it started stop with
|
|
358
501
|
// it; cdxgen's own timeout is the backstop. Naming cdxgen as the supervisor
|
|
359
502
|
// stops scalasem when cdxgen is killed.
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
503
|
+
// A spawn that throws, such as a command the allow list refuses, degrades
|
|
504
|
+
// the evidence like any other run that could not start.
|
|
505
|
+
let result;
|
|
506
|
+
try {
|
|
507
|
+
result = safeSpawnSync(process.execPath, args, {
|
|
508
|
+
cwd: analysisDir,
|
|
509
|
+
shell: false,
|
|
510
|
+
killSignal: "SIGKILL",
|
|
511
|
+
timeout: timeoutMs + Math.min(TIMEOUT_GRACE_MS, timeoutMs),
|
|
512
|
+
env: {
|
|
513
|
+
...process.env,
|
|
514
|
+
ATOM_PARENT_PID: String(process.pid),
|
|
515
|
+
SCALASEM_TIMEOUT: String(timeoutMs),
|
|
516
|
+
},
|
|
517
|
+
});
|
|
518
|
+
} catch (err) {
|
|
519
|
+
result = { error: err };
|
|
520
|
+
}
|
|
371
521
|
const elapsedMs = Date.now() - startedAt;
|
|
372
522
|
const wroteReport = safeExistsSync(runReportFile);
|
|
373
523
|
const written = wroteReport ? readScalasemReport(runReportFile) : undefined;
|
|
@@ -431,7 +581,9 @@ function runScalasem(analysisDir, options, requestedFile, v1Slice, v1File) {
|
|
|
431
581
|
),
|
|
432
582
|
];
|
|
433
583
|
}
|
|
434
|
-
const summary = summarizeDiagnostics(
|
|
584
|
+
const summary = summarizeDiagnostics(
|
|
585
|
+
degradingDiagnostics(report._meta.diagnostics),
|
|
586
|
+
);
|
|
435
587
|
if (summary) {
|
|
436
588
|
console.warn(
|
|
437
589
|
`scalasem: the report is degraded: ${summary}. The cdx:scalasem:diagnostic:* metadata properties of the BOM say where.`,
|
|
@@ -443,19 +595,26 @@ function runScalasem(analysisDir, options, requestedFile, v1Slice, v1File) {
|
|
|
443
595
|
}
|
|
444
596
|
// Only a usable report replaces the one at the requested path, and a
|
|
445
597
|
// version 1 slice there stays as the user left it.
|
|
598
|
+
// The kept copy records the settings of this run, so a later run reuses it
|
|
599
|
+
// only when it asks for the same.
|
|
446
600
|
let reportFile;
|
|
447
|
-
if (usable && v1File !== requestedFile) {
|
|
601
|
+
if (usable && requestedFile && v1File !== requestedFile) {
|
|
448
602
|
try {
|
|
449
603
|
safeMkdirSync(dirname(requestedFile), { recursive: true });
|
|
450
|
-
|
|
451
|
-
|
|
604
|
+
safeWriteSync(
|
|
605
|
+
requestedFile,
|
|
606
|
+
JSON.stringify({
|
|
607
|
+
...report,
|
|
608
|
+
_meta: { ...report._meta, [RUN_SETTINGS_KEY]: settings },
|
|
609
|
+
}),
|
|
610
|
+
);
|
|
611
|
+
reportFile = safeExistsSync(requestedFile) ? requestedFile : undefined;
|
|
452
612
|
} catch (err) {
|
|
453
613
|
console.warn(
|
|
454
614
|
`scalasem: the report could not be kept at ${requestedFile} (${err.message}); this run still uses it.`,
|
|
455
615
|
);
|
|
456
616
|
}
|
|
457
617
|
}
|
|
458
|
-
safeRmSync(runDir, { recursive: true, force: true });
|
|
459
618
|
return {
|
|
460
619
|
report,
|
|
461
620
|
reportFile,
|
|
@@ -464,6 +623,21 @@ function runScalasem(analysisDir, options, requestedFile, v1Slice, v1File) {
|
|
|
464
623
|
};
|
|
465
624
|
}
|
|
466
625
|
|
|
626
|
+
// Diagnostics scalasem counts on a healthy run: symbols its semantic pass
|
|
627
|
+
// could not resolve, and stale build outputs it correctly skipped. They say
|
|
628
|
+
// the analysis is incomplete, not that it is degraded, so they stay out of
|
|
629
|
+
// the BOM and out of the degraded flag.
|
|
630
|
+
const INFORMATIONAL_DIAGNOSTIC_CODES = new Set([
|
|
631
|
+
"unresolved-symbols",
|
|
632
|
+
"stale-output",
|
|
633
|
+
]);
|
|
634
|
+
|
|
635
|
+
function degradingDiagnostics(diagnostics = []) {
|
|
636
|
+
return (diagnostics || []).filter(
|
|
637
|
+
(diagnostic) => !INFORMATIONAL_DIAGNOSTIC_CODES.has(diagnostic?.code),
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
|
|
467
641
|
function summarizeDiagnostics(diagnostics = []) {
|
|
468
642
|
const byCode = new Map();
|
|
469
643
|
for (const diagnostic of diagnostics || []) {
|
|
@@ -490,8 +664,9 @@ function summarizeDiagnostics(diagnostics = []) {
|
|
|
490
664
|
*/
|
|
491
665
|
export function scalasemMetadataProperties(
|
|
492
666
|
report,
|
|
493
|
-
|
|
667
|
+
allDiagnostics = report?._meta?.diagnostics || [],
|
|
494
668
|
) {
|
|
669
|
+
const diagnostics = degradingDiagnostics(allDiagnostics);
|
|
495
670
|
const properties = [];
|
|
496
671
|
const meta = report?._meta || {};
|
|
497
672
|
appendUniqueProperty(
|
|
@@ -553,20 +728,23 @@ export function sortedCsv(values) {
|
|
|
553
728
|
|
|
554
729
|
/**
|
|
555
730
|
* The platform a purl's artifact name targets: `_sjs1` is Scala.js,
|
|
556
|
-
* `_native0.x` is Scala Native, and neither is the JVM.
|
|
731
|
+
* `_native0.x` is Scala Native, and neither is the JVM. The platform suffix
|
|
732
|
+
* counts whether the Scala binary suffix follows it (`upickle_sjs1_3`, the
|
|
733
|
+
* published name a jar scan keeps) or not (`upickle_sjs1`, the sbt name).
|
|
557
734
|
*
|
|
558
735
|
* @param {string} purl Package URL.
|
|
559
736
|
* @returns {"jvm"|"js"|"native"} Platform key.
|
|
560
737
|
*/
|
|
561
738
|
export function scalaPurlPlatform(purl) {
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
739
|
+
return scalaNamePlatform(purlPurlName(purl));
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function scalaNamePlatform(name) {
|
|
743
|
+
const { platformSuffix } = parseScalaArtifact(name);
|
|
744
|
+
if (platformSuffix.startsWith("_sjs")) {
|
|
567
745
|
return "js";
|
|
568
746
|
}
|
|
569
|
-
if (
|
|
747
|
+
if (platformSuffix.startsWith("_native")) {
|
|
570
748
|
return "native";
|
|
571
749
|
}
|
|
572
750
|
return "jvm";
|
|
@@ -584,6 +762,20 @@ function purlPurlName(purl) {
|
|
|
584
762
|
}
|
|
585
763
|
}
|
|
586
764
|
|
|
765
|
+
// A compiler-generated class: a `$`-separated part that is empty past the
|
|
766
|
+
// top-level name (`$$`) or numeric, or one of the anonymous and lambda markers.
|
|
767
|
+
function isSyntheticClassName(symbol, dollar) {
|
|
768
|
+
return symbol
|
|
769
|
+
.slice(dollar + 1)
|
|
770
|
+
.split("$")
|
|
771
|
+
.some(
|
|
772
|
+
(part, index, parts) =>
|
|
773
|
+
(part === "" && index < parts.length - 1) ||
|
|
774
|
+
/^\d+$/.test(part) ||
|
|
775
|
+
["anon", "anonfun", "Lambda", "lambda"].includes(part),
|
|
776
|
+
);
|
|
777
|
+
}
|
|
778
|
+
|
|
587
779
|
/**
|
|
588
780
|
* The class and package index that joins scalasem symbols to component purls.
|
|
589
781
|
*
|
|
@@ -633,11 +825,22 @@ export class ScalaJoinIndex {
|
|
|
633
825
|
if (!symbol || symbol.includes(" ") || symbol.includes("/")) {
|
|
634
826
|
continue;
|
|
635
827
|
}
|
|
636
|
-
|
|
637
|
-
|
|
828
|
+
// A `$` inside the simple name separates nested classes; one that starts
|
|
829
|
+
// it belongs to an encoded operator name (`$colon$colon` for `::`).
|
|
830
|
+
const simpleName = symbol.lastIndexOf(".") + 1;
|
|
831
|
+
const dollar = symbol.startsWith("$", simpleName)
|
|
832
|
+
? -1
|
|
833
|
+
: symbol.indexOf("$", simpleName);
|
|
638
834
|
const topLevel = dollar > 0 ? symbol.slice(0, dollar) : symbol;
|
|
835
|
+
// Anonymous classes and lambdas (`Foo$$anon$1`, `Foo$$anonfun$bar$1`,
|
|
836
|
+
// `Foo$$Lambda$12`) are no names a source can reference; their outer
|
|
837
|
+
// class speaks for them.
|
|
838
|
+
const synthetic = dollar > 0 && isSyntheticClassName(symbol, dollar);
|
|
839
|
+
if (!synthetic) {
|
|
840
|
+
indexed.push(symbol);
|
|
841
|
+
}
|
|
639
842
|
this.addSymbol(this.classPurls, topLevel, purl, source);
|
|
640
|
-
if (dollar > 0) {
|
|
843
|
+
if (dollar > 0 && !synthetic) {
|
|
641
844
|
const nested = symbol.replaceAll("$", ".").replace(/\.+$/, "");
|
|
642
845
|
if (nested !== topLevel) {
|
|
643
846
|
this.addSymbol(this.classPurls, nested, purl, source);
|
|
@@ -734,15 +937,25 @@ export class ScalaJoinIndex {
|
|
|
734
937
|
* sits in. A package the project also fills is ambiguous and joins nothing,
|
|
735
938
|
* and `javax.` names join only through an exact class, since the JDK ships
|
|
736
939
|
* most of them.
|
|
940
|
+
*
|
|
941
|
+
* The package is the part of the symbol before its first type name, which
|
|
942
|
+
* starts with a capital letter (`io.circe` of `io.circe.Json.fromString`). A
|
|
943
|
+
* symbol without one names a package (`io.circe`) or a member of one, such
|
|
944
|
+
* as a package object member (`io.circe.syntax.asJson`). Parent packages
|
|
945
|
+
* never answer: a missing `io.circe.generic` is not circe-core.
|
|
737
946
|
*/
|
|
738
947
|
packageOwners(symbol) {
|
|
739
948
|
if (symbol.startsWith(JDK_EXTENSION_PREFIX)) {
|
|
740
949
|
return undefined;
|
|
741
950
|
}
|
|
742
951
|
const parts = symbol.replace(/\$+$/, "").split(".");
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
952
|
+
const firstType = parts.findIndex((part) => /^[A-Z]/.test(part));
|
|
953
|
+
const packages =
|
|
954
|
+
firstType >= 0
|
|
955
|
+
? [parts.slice(0, firstType).join(".")]
|
|
956
|
+
: [parts.join("."), parts.slice(0, -1).join(".")];
|
|
957
|
+
for (const name of packages) {
|
|
958
|
+
const owners = name ? this.packagePurls.get(name) : undefined;
|
|
746
959
|
if (owners?.size) {
|
|
747
960
|
return this.projectPackages.has(name) ? undefined : owners;
|
|
748
961
|
}
|
|
@@ -875,41 +1088,83 @@ function componentBinaryVersions(component, coordinate) {
|
|
|
875
1088
|
);
|
|
876
1089
|
}
|
|
877
1090
|
|
|
1091
|
+
// Which of two components standing for the same library is the one evidence
|
|
1092
|
+
// attaches to: the plain purl over one with qualifiers, then the shorter, then
|
|
1093
|
+
// the lexicographically first, so the choice never depends on BOM order.
|
|
1094
|
+
function preferredComponent(current, candidate) {
|
|
1095
|
+
const rank = (component) => [
|
|
1096
|
+
component.purl.includes("?") ? 1 : 0,
|
|
1097
|
+
component.purl.length,
|
|
1098
|
+
];
|
|
1099
|
+
const [currentQualified, currentLength] = rank(current);
|
|
1100
|
+
const [candidateQualified, candidateLength] = rank(candidate);
|
|
1101
|
+
if (currentQualified !== candidateQualified) {
|
|
1102
|
+
return candidateQualified < currentQualified ? candidate : current;
|
|
1103
|
+
}
|
|
1104
|
+
if (currentLength !== candidateLength) {
|
|
1105
|
+
return candidateLength < currentLength ? candidate : current;
|
|
1106
|
+
}
|
|
1107
|
+
return candidate.purl < current.purl ? candidate : current;
|
|
1108
|
+
}
|
|
1109
|
+
|
|
878
1110
|
/**
|
|
879
1111
|
* Index the BOM components by their normalized Scala coordinates, both with
|
|
880
1112
|
* and without the version, so a classpath jar, a namespace property and a map
|
|
881
1113
|
* entry of the same library meet on one component. Each entry keeps the Scala
|
|
882
|
-
* binary
|
|
883
|
-
* artifacts of one library in a Maven build stay apart.
|
|
1114
|
+
* binary versions the component was built for, so the `_2.13` and `_3`
|
|
1115
|
+
* artifacts of one library in a Maven build stay apart. Components that share
|
|
1116
|
+
* a normalized coordinate and were built for an overlapping set of Scala
|
|
1117
|
+
* binary versions are one library listed more than once (a repository_url
|
|
1118
|
+
* qualifier, a `_3` suffix beside the sbt name built for 2.13 and 3, a stale
|
|
1119
|
+
* duplicate), so they share one entry, which the preferred component of the
|
|
1120
|
+
* group stands for.
|
|
884
1121
|
*/
|
|
885
1122
|
export function componentCoordinateIndex(components = []) {
|
|
886
|
-
const
|
|
887
|
-
const withoutVersion = new Map();
|
|
1123
|
+
const groupsByKey = new Map();
|
|
888
1124
|
for (const component of components) {
|
|
889
1125
|
const coordinate = scalaCoordinateOfPurl(component?.purl);
|
|
890
1126
|
if (!coordinate) {
|
|
891
1127
|
continue;
|
|
892
1128
|
}
|
|
893
|
-
const
|
|
894
|
-
|
|
895
|
-
|
|
1129
|
+
const binaryVersions = componentBinaryVersions(component, coordinate);
|
|
1130
|
+
const groups = groupsByKey.get(coordinate.key) || [];
|
|
1131
|
+
groupsByKey.set(coordinate.key, groups);
|
|
1132
|
+
// Every group this component overlaps becomes one with it.
|
|
1133
|
+
const overlapping = groups.filter((group) =>
|
|
1134
|
+
binaryVersions.size
|
|
1135
|
+
? [...binaryVersions].some((version) =>
|
|
1136
|
+
group.binaryVersions.has(version),
|
|
1137
|
+
)
|
|
1138
|
+
: !group.binaryVersions.size,
|
|
1139
|
+
);
|
|
1140
|
+
const merged = {
|
|
896
1141
|
component,
|
|
897
|
-
binaryVersions:
|
|
1142
|
+
binaryVersions: new Set(binaryVersions),
|
|
898
1143
|
};
|
|
899
|
-
|
|
900
|
-
|
|
1144
|
+
for (const group of overlapping) {
|
|
1145
|
+
merged.component = preferredComponent(merged.component, group.component);
|
|
1146
|
+
for (const version of group.binaryVersions) {
|
|
1147
|
+
merged.binaryVersions.add(version);
|
|
1148
|
+
}
|
|
1149
|
+
groups.splice(groups.indexOf(group), 1);
|
|
901
1150
|
}
|
|
902
|
-
|
|
903
|
-
|
|
1151
|
+
groups.push(merged);
|
|
1152
|
+
}
|
|
1153
|
+
const byVersion = new Map();
|
|
1154
|
+
const withoutVersion = new Map();
|
|
1155
|
+
for (const [key, groups] of groupsByKey) {
|
|
1156
|
+
const [type, group, artifactBase, platformSuffix] = key.split(":");
|
|
1157
|
+
byVersion.set(key, groups);
|
|
1158
|
+
const versionless = scalaCoordinateKey({
|
|
904
1159
|
type,
|
|
905
1160
|
group,
|
|
906
1161
|
artifactBase,
|
|
907
1162
|
platformSuffix,
|
|
908
1163
|
});
|
|
909
|
-
|
|
910
|
-
withoutVersion.
|
|
911
|
-
|
|
912
|
-
|
|
1164
|
+
withoutVersion.set(versionless, [
|
|
1165
|
+
...(withoutVersion.get(versionless) || []),
|
|
1166
|
+
...groups,
|
|
1167
|
+
]);
|
|
913
1168
|
}
|
|
914
1169
|
return { byVersion, withoutVersion };
|
|
915
1170
|
}
|
|
@@ -944,11 +1199,126 @@ function pickByBinaryVersion(entries, binaryVersion) {
|
|
|
944
1199
|
* @returns {{component: Object, exact: boolean}|undefined} The component, and
|
|
945
1200
|
* whether its version is the one on the classpath.
|
|
946
1201
|
*/
|
|
947
|
-
export function matchClasspathEntry(
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1202
|
+
export function matchClasspathEntry(entry, coordinateIndex, platform) {
|
|
1203
|
+
const direct =
|
|
1204
|
+
entry?.group && entry?.artifact
|
|
1205
|
+
? matchCoordinates(entry, coordinateIndex, platform)
|
|
1206
|
+
: undefined;
|
|
1207
|
+
if (direct?.component || direct?.ambiguous) {
|
|
1208
|
+
return direct.component ? direct : undefined;
|
|
1209
|
+
}
|
|
1210
|
+
// The report reads coordinates from a Maven repository layout and can take
|
|
1211
|
+
// directories above the group for part of it (`maven.io.circe` for jars
|
|
1212
|
+
// under `<repository>/maven/io/circe/`), and reads none from the Gradle and
|
|
1213
|
+
// Ivy cache layouts. The coordinates the jar's own path spells are tried
|
|
1214
|
+
// then, and must still meet a component on artifact and version.
|
|
1215
|
+
for (const coordinates of jarPathCoordinates(entry)) {
|
|
1216
|
+
const byPath = matchCoordinates(
|
|
1217
|
+
{ ...entry, ...coordinates },
|
|
1218
|
+
coordinateIndex,
|
|
1219
|
+
platform,
|
|
1220
|
+
);
|
|
1221
|
+
if (byPath?.exact) {
|
|
1222
|
+
return byPath;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
return undefined;
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
/**
|
|
1229
|
+
* The coordinates a classpath jar's path spells, for the layouts the report
|
|
1230
|
+
* does not read itself, most specific first:
|
|
1231
|
+
*
|
|
1232
|
+
* - a Maven layout `<group dirs>/<artifact>/<version>/<file>` whose group is
|
|
1233
|
+
* a dotted tail of the group the report recorded, so a group is only ever
|
|
1234
|
+
* shortened by the directories the report took for it, never guessed
|
|
1235
|
+
* - the Gradle cache `files-2.1/<group>/<artifact>/<version>/<sha1>/<file>`
|
|
1236
|
+
* - an Ivy cache `<org>/<artifact>/jars/<artifact>-<version>.jar`, as sbt 0.13
|
|
1237
|
+
* and sbt 1.0 to 1.2 keep it, and an Ivy local repository
|
|
1238
|
+
* `<org>/<artifact>/<version>/jars/<artifact>.jar`
|
|
1239
|
+
*
|
|
1240
|
+
* @param {Object} entry Classpath entry with `path` and, when the report read
|
|
1241
|
+
* them, `group`, `artifact` and `version`.
|
|
1242
|
+
* @returns {{group: string, artifact: string, version: string}[]}
|
|
1243
|
+
*/
|
|
1244
|
+
export function jarPathCoordinates(entry) {
|
|
1245
|
+
if (!entry?.path) {
|
|
1246
|
+
return [];
|
|
1247
|
+
}
|
|
1248
|
+
const segments = String(entry.path).split(/[\\/]/).filter(Boolean);
|
|
1249
|
+
const total = segments.length;
|
|
1250
|
+
const file = segments[total - 1] || "";
|
|
1251
|
+
if (total < 4 || !file.toLowerCase().endsWith(".jar")) {
|
|
1252
|
+
return [];
|
|
1253
|
+
}
|
|
1254
|
+
const found = [];
|
|
1255
|
+
if (entry.group && entry.artifact && entry.version) {
|
|
1256
|
+
const artifactIndex = total - 3;
|
|
1257
|
+
if (
|
|
1258
|
+
segments[artifactIndex] === entry.artifact &&
|
|
1259
|
+
segments[artifactIndex + 1] === entry.version
|
|
1260
|
+
) {
|
|
1261
|
+
const groupParts = entry.group.split(".");
|
|
1262
|
+
for (let start = 1; start < groupParts.length; start++) {
|
|
1263
|
+
const tail = groupParts.slice(start);
|
|
1264
|
+
const directories = segments.slice(
|
|
1265
|
+
artifactIndex - tail.length,
|
|
1266
|
+
artifactIndex,
|
|
1267
|
+
);
|
|
1268
|
+
if (
|
|
1269
|
+
artifactIndex - tail.length >= 0 &&
|
|
1270
|
+
directories.join("/") === tail.join("/")
|
|
1271
|
+
) {
|
|
1272
|
+
found.push({
|
|
1273
|
+
group: tail.join("."),
|
|
1274
|
+
artifact: entry.artifact,
|
|
1275
|
+
version: entry.version,
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
const gradle = segments.lastIndexOf("files-2.1");
|
|
1282
|
+
if (gradle >= 0 && gradle + 5 === total - 1) {
|
|
1283
|
+
found.push({
|
|
1284
|
+
group: segments[gradle + 1],
|
|
1285
|
+
artifact: segments[gradle + 2],
|
|
1286
|
+
version: segments[gradle + 3],
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
const stem = file.slice(0, -".jar".length);
|
|
1290
|
+
if (["jars", "bundles"].includes(segments[total - 2])) {
|
|
1291
|
+
// Ivy local: <org>/<artifact>/<version>/jars/<artifact>.jar
|
|
1292
|
+
if (total >= 5 && stem === segments[total - 4]) {
|
|
1293
|
+
found.push({
|
|
1294
|
+
group: segments[total - 5],
|
|
1295
|
+
artifact: segments[total - 4],
|
|
1296
|
+
version: segments[total - 3],
|
|
1297
|
+
});
|
|
1298
|
+
} else if (stem.startsWith(`${segments[total - 3]}-`)) {
|
|
1299
|
+
// Ivy cache: <org>/<artifact>/jars/<artifact>-<version>.jar
|
|
1300
|
+
found.push({
|
|
1301
|
+
group: segments[total - 4],
|
|
1302
|
+
artifact: segments[total - 3],
|
|
1303
|
+
version: stem.slice(segments[total - 3].length + 1),
|
|
1304
|
+
});
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
return found.filter(
|
|
1308
|
+
(coordinates) =>
|
|
1309
|
+
coordinates.group &&
|
|
1310
|
+
coordinates.artifact &&
|
|
1311
|
+
coordinates.version &&
|
|
1312
|
+
(coordinates.group !== entry.group ||
|
|
1313
|
+
coordinates.artifact !== entry.artifact ||
|
|
1314
|
+
coordinates.version !== entry.version),
|
|
1315
|
+
);
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
// The component a set of coordinates names: `{component, exact}` for a single
|
|
1319
|
+
// owner, `{ambiguous: true}` when several components answer, undefined when
|
|
1320
|
+
// none does.
|
|
1321
|
+
function matchCoordinates(entry, { byVersion, withoutVersion }, platform) {
|
|
952
1322
|
const parsed = parseScalaArtifact(entry?.artifact);
|
|
953
1323
|
const parts = {
|
|
954
1324
|
group: entry?.group,
|
|
@@ -963,7 +1333,7 @@ export function matchClasspathEntry(
|
|
|
963
1333
|
return { component: exact[0].component, exact: true };
|
|
964
1334
|
}
|
|
965
1335
|
if (exact.length > 1) {
|
|
966
|
-
return
|
|
1336
|
+
return { ambiguous: true };
|
|
967
1337
|
}
|
|
968
1338
|
const candidates = pickByBinaryVersion(
|
|
969
1339
|
withoutVersion.get(scalaCoordinateKey(parts)),
|
|
@@ -973,9 +1343,10 @@ export function matchClasspathEntry(
|
|
|
973
1343
|
(candidate) => scalaPurlPlatform(candidate.component.purl) === platform,
|
|
974
1344
|
);
|
|
975
1345
|
const pool = matching.length ? matching : candidates;
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1346
|
+
if (pool.length === 1) {
|
|
1347
|
+
return { component: pool[0].component, exact: false };
|
|
1348
|
+
}
|
|
1349
|
+
return pool.length > 1 ? { ambiguous: true } : undefined;
|
|
979
1350
|
}
|
|
980
1351
|
|
|
981
1352
|
/**
|
|
@@ -1043,8 +1414,14 @@ export async function buildScalaJoinIndex(report, components, jarNSMapping) {
|
|
|
1043
1414
|
}
|
|
1044
1415
|
}
|
|
1045
1416
|
for (const mapPurl of Object.keys(jarNSMapping || {})) {
|
|
1046
|
-
|
|
1047
|
-
|
|
1417
|
+
// Maps written before every producer used `{ namespaces }` hold the
|
|
1418
|
+
// newline-joined namespaces of a jar-scanned component as a string.
|
|
1419
|
+
const value = jarNSMapping[mapPurl];
|
|
1420
|
+
const namespaces =
|
|
1421
|
+
typeof value === "string"
|
|
1422
|
+
? value.split("\n").filter(Boolean)
|
|
1423
|
+
: value?.namespaces;
|
|
1424
|
+
if (!Array.isArray(namespaces) || !namespaces.length) {
|
|
1048
1425
|
continue;
|
|
1049
1426
|
}
|
|
1050
1427
|
const segments = /^pkg:[^/]+\/(?:([^/@?]+)\/)?([^@?]+)@([^?#]*)/.exec(
|
|
@@ -1063,7 +1440,7 @@ export async function buildScalaJoinIndex(report, components, jarNSMapping) {
|
|
|
1063
1440
|
scalaPurlPlatform(mapPurl),
|
|
1064
1441
|
);
|
|
1065
1442
|
if (matched) {
|
|
1066
|
-
index.addNamespaces(matched.component.purl,
|
|
1443
|
+
index.addNamespaces(matched.component.purl, namespaces, 3);
|
|
1067
1444
|
}
|
|
1068
1445
|
}
|
|
1069
1446
|
return index;
|
|
@@ -1310,10 +1687,11 @@ export function scalasemCryptoOidKey(name, keySize, mode, curve, padding) {
|
|
|
1310
1687
|
return undefined;
|
|
1311
1688
|
}
|
|
1312
1689
|
if (SCALASEM_AES_NAMES.has(name)) {
|
|
1690
|
+
const upperMode = mode?.toUpperCase();
|
|
1313
1691
|
const modeKey =
|
|
1314
|
-
SCALASEM_AES_MODE_KEYS[
|
|
1692
|
+
SCALASEM_AES_MODE_KEYS[upperMode] ||
|
|
1315
1693
|
SCALASEM_AES_MODE_KEYS[name.split("-")[1]];
|
|
1316
|
-
const wrap = name === "AES-KW" ||
|
|
1694
|
+
const wrap = name === "AES-KW" || upperMode === "KW";
|
|
1317
1695
|
if (keySize && (modeKey !== undefined || wrap)) {
|
|
1318
1696
|
const sizedKey = wrap ? `aes${keySize}-wrap` : `aes${keySize}-${modeKey}`;
|
|
1319
1697
|
const hit = SCALASEM_OID_KEYS.get(sizedKey.toLowerCase());
|
|
@@ -1456,6 +1834,7 @@ export async function collectScalasemEvidence(
|
|
|
1456
1834
|
componentPropertiesMap,
|
|
1457
1835
|
metadataProperties,
|
|
1458
1836
|
options,
|
|
1837
|
+
allowedPurlsByModule,
|
|
1459
1838
|
);
|
|
1460
1839
|
addJsModuleEvidence(
|
|
1461
1840
|
report,
|
|
@@ -1606,7 +1985,7 @@ function addUsageEvidence(
|
|
|
1606
1985
|
let added = false;
|
|
1607
1986
|
for (const [file, lines] of files) {
|
|
1608
1987
|
if (depth < lines.length && locations.size < cap) {
|
|
1609
|
-
locations.add(
|
|
1988
|
+
locations.add(occurrenceLocationKey(file, lines[depth]));
|
|
1610
1989
|
added = true;
|
|
1611
1990
|
}
|
|
1612
1991
|
}
|
|
@@ -1788,7 +2167,17 @@ function stackTargetPurls(sink, fileEntry, joinIndex, platform, allowed) {
|
|
|
1788
2167
|
return [...purls];
|
|
1789
2168
|
}
|
|
1790
2169
|
|
|
2170
|
+
// The package of a class the report names. A class nested in an object reads
|
|
2171
|
+
// `Outer$.Inner`, so the package ends before the first part that is a type:
|
|
2172
|
+
// one that carries a `$` or starts with a capital letter.
|
|
1791
2173
|
function packageOf(className) {
|
|
2174
|
+
const parts = String(className || "").split(".");
|
|
2175
|
+
const firstType = parts.findIndex(
|
|
2176
|
+
(part) => part.includes("$") || /^[A-Z]/.test(part),
|
|
2177
|
+
);
|
|
2178
|
+
if (firstType > 0) {
|
|
2179
|
+
return parts.slice(0, firstType).join(".");
|
|
2180
|
+
}
|
|
1792
2181
|
const text = String(className || "").replace(/\$+$/, "");
|
|
1793
2182
|
const dot = text.lastIndexOf(".");
|
|
1794
2183
|
return dot > 0 ? text.slice(0, dot) : "";
|
|
@@ -1848,9 +2237,22 @@ function providerAliases(purl, components = []) {
|
|
|
1848
2237
|
/**
|
|
1849
2238
|
* The component purl of the library a crypto finding names as its provider.
|
|
1850
2239
|
* Maven coordinates match on group and base artifact; the short aliases cover
|
|
1851
|
-
* the providers the rules emit for libraries without a coordinate.
|
|
2240
|
+
* the providers the rules emit for libraries without a coordinate. Of several
|
|
2241
|
+
* matching components, one on the referencing module's classpath wins, then
|
|
2242
|
+
* the artifact whose name extends the provider's least (`bcprov-jdk18on`
|
|
2243
|
+
* over `bcprov-ext-jdk18on`), then the first purl, so the choice never
|
|
2244
|
+
* depends on the order of the BOM.
|
|
2245
|
+
*
|
|
2246
|
+
* @param {string} provider Provider the finding names.
|
|
2247
|
+
* @param {Object[]} components BOM components.
|
|
2248
|
+
* @param {Set<string>} [classpathPurls] Purls on the module's classpath.
|
|
2249
|
+
* @returns {string|undefined} Component purl.
|
|
1852
2250
|
*/
|
|
1853
|
-
export function scalasemProviderPurl(
|
|
2251
|
+
export function scalasemProviderPurl(
|
|
2252
|
+
provider,
|
|
2253
|
+
components = [],
|
|
2254
|
+
classpathPurls = undefined,
|
|
2255
|
+
) {
|
|
1854
2256
|
if (!provider) {
|
|
1855
2257
|
return undefined;
|
|
1856
2258
|
}
|
|
@@ -1865,6 +2267,7 @@ export function scalasemProviderPurl(provider, components = []) {
|
|
|
1865
2267
|
return undefined;
|
|
1866
2268
|
}
|
|
1867
2269
|
const base = parseScalaArtifact(artifactPrefix).artifactBase;
|
|
2270
|
+
const matches = [];
|
|
1868
2271
|
for (const component of components) {
|
|
1869
2272
|
if (!component?.purl) {
|
|
1870
2273
|
continue;
|
|
@@ -1877,10 +2280,23 @@ export function scalasemProviderPurl(provider, components = []) {
|
|
|
1877
2280
|
}
|
|
1878
2281
|
const name = parseScalaArtifact(decodeSafe(segments[2])).artifactBase;
|
|
1879
2282
|
if (name === base || name.startsWith(`${base}-`)) {
|
|
1880
|
-
|
|
2283
|
+
matches.push({ purl: component.purl, name });
|
|
1881
2284
|
}
|
|
1882
2285
|
}
|
|
1883
|
-
|
|
2286
|
+
const rank = (match) => [
|
|
2287
|
+
classpathPurls?.has(match.purl) ? 0 : 1,
|
|
2288
|
+
match.name.length,
|
|
2289
|
+
];
|
|
2290
|
+
matches.sort((left, right) => {
|
|
2291
|
+
const [leftClasspath, leftLength] = rank(left);
|
|
2292
|
+
const [rightClasspath, rightLength] = rank(right);
|
|
2293
|
+
return (
|
|
2294
|
+
leftClasspath - rightClasspath ||
|
|
2295
|
+
leftLength - rightLength ||
|
|
2296
|
+
(left.purl < right.purl ? -1 : left.purl > right.purl ? 1 : 0)
|
|
2297
|
+
);
|
|
2298
|
+
});
|
|
2299
|
+
return matches[0]?.purl;
|
|
1884
2300
|
}
|
|
1885
2301
|
|
|
1886
2302
|
// Primitives a mode of operation applies to. RSA spells `RSA/ECB/...` in JCA,
|
|
@@ -1965,6 +2381,7 @@ function addCryptoEvidence(
|
|
|
1965
2381
|
componentPropertiesMap,
|
|
1966
2382
|
metadataProperties,
|
|
1967
2383
|
options = {},
|
|
2384
|
+
allowedPurlsByModule = new Map(),
|
|
1968
2385
|
) {
|
|
1969
2386
|
const includeTests = options.scalasemIncludeTests === true;
|
|
1970
2387
|
const nativeLibraryBySymbol = new Map();
|
|
@@ -1979,11 +2396,17 @@ function addCryptoEvidence(
|
|
|
1979
2396
|
if (scope === "test" && !includeTests) {
|
|
1980
2397
|
continue;
|
|
1981
2398
|
}
|
|
1982
|
-
const location =
|
|
2399
|
+
const location = finding.line
|
|
2400
|
+
? `${finding.file || ""}#${finding.line}`
|
|
2401
|
+
: finding.file || "";
|
|
1983
2402
|
const nativeLibrary = finding.kind?.startsWith("native")
|
|
1984
2403
|
? nativeLibraryBySymbol.get(finding.name)
|
|
1985
2404
|
: undefined;
|
|
1986
|
-
const providerPurl = scalasemProviderPurl(
|
|
2405
|
+
const providerPurl = scalasemProviderPurl(
|
|
2406
|
+
finding.provider,
|
|
2407
|
+
components,
|
|
2408
|
+
allowedPurlsFor(report[finding.file], allowedPurlsByModule),
|
|
2409
|
+
);
|
|
1987
2410
|
const asset =
|
|
1988
2411
|
finding.resolution === "unresolved"
|
|
1989
2412
|
? undefined
|
|
@@ -2054,12 +2477,18 @@ function addCryptoEvidence(
|
|
|
2054
2477
|
}
|
|
2055
2478
|
}
|
|
2056
2479
|
|
|
2480
|
+
// `file#line` as an occurrence. A location without a line, or whose `#` is
|
|
2481
|
+
// part of the file name, is the file alone.
|
|
2057
2482
|
function parseLocationString(location) {
|
|
2058
2483
|
const separator = location.lastIndexOf("#");
|
|
2059
|
-
const
|
|
2484
|
+
const suffix = separator >= 0 ? location.slice(separator + 1) : "";
|
|
2485
|
+
if (!/^\d+$/.test(suffix)) {
|
|
2486
|
+
return { location };
|
|
2487
|
+
}
|
|
2488
|
+
const line = Number.parseInt(suffix, 10);
|
|
2060
2489
|
return {
|
|
2061
2490
|
location: location.slice(0, separator),
|
|
2062
|
-
...(
|
|
2491
|
+
...(line > 0 ? { line } : {}),
|
|
2063
2492
|
};
|
|
2064
2493
|
}
|
|
2065
2494
|
|
|
@@ -2187,6 +2616,191 @@ function cryptoAssetProperties(finding, nativeLibrary) {
|
|
|
2187
2616
|
return properties;
|
|
2188
2617
|
}
|
|
2189
2618
|
|
|
2619
|
+
// The characters that end a host list: a path, URL parameters, and the
|
|
2620
|
+
// `;key=value` properties some JDBC drivers take.
|
|
2621
|
+
const ADDRESS_END = ["/", "?", "#", ";"];
|
|
2622
|
+
|
|
2623
|
+
const HOST_PATTERN = /^(?:\[[0-9A-Fa-f:.]+\]|[A-Za-z0-9._%-]+)(?::\d+)?$/;
|
|
2624
|
+
|
|
2625
|
+
function indexOfAny(text, characters, from = 0) {
|
|
2626
|
+
let found = -1;
|
|
2627
|
+
for (const character of characters) {
|
|
2628
|
+
const index = text.indexOf(character, from);
|
|
2629
|
+
if (index >= 0 && (found < 0 || index < found)) {
|
|
2630
|
+
found = index;
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
return found;
|
|
2634
|
+
}
|
|
2635
|
+
|
|
2636
|
+
function cutAt(text, characters) {
|
|
2637
|
+
const index = indexOfAny(text, characters);
|
|
2638
|
+
return index >= 0 ? text.slice(0, index) : text;
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2641
|
+
// `host`, `host:port`, `[ipv6]:port`, or a comma separated list of those.
|
|
2642
|
+
function isHostList(text) {
|
|
2643
|
+
return (
|
|
2644
|
+
Boolean(text) && text.split(",").every((host) => HOST_PATTERN.test(host))
|
|
2645
|
+
);
|
|
2646
|
+
}
|
|
2647
|
+
|
|
2648
|
+
/**
|
|
2649
|
+
* Where the userinfo in front of a host ends, or -1 when there is none. A
|
|
2650
|
+
* password may hold any of `/ ? # ; @ =`, so an `@` ends the userinfo unless a
|
|
2651
|
+
* well-formed host list is followed by parameters that hold it
|
|
2652
|
+
* (`host:1433;user=a@b`). Of several `@`, the last one before the first
|
|
2653
|
+
* parameter ends it, and the last one of all when that leaves none: a host the
|
|
2654
|
+
* split gets wrong costs a service name, a split that keeps a password leaks it.
|
|
2655
|
+
*
|
|
2656
|
+
* @param {string} rest The text after `//`, or after a JDBC subprotocol.
|
|
2657
|
+
* @returns {number} Index of the `@` that ends the userinfo.
|
|
2658
|
+
*/
|
|
2659
|
+
function userinfoEnd(rest) {
|
|
2660
|
+
const firstAt = rest.indexOf("@");
|
|
2661
|
+
if (firstAt < 0) {
|
|
2662
|
+
return -1;
|
|
2663
|
+
}
|
|
2664
|
+
const end = indexOfAny(rest, ADDRESS_END);
|
|
2665
|
+
if (
|
|
2666
|
+
end >= 0 &&
|
|
2667
|
+
end < firstAt &&
|
|
2668
|
+
isHostList(rest.slice(0, end)) &&
|
|
2669
|
+
rest.slice(end + 1, firstAt).includes("=")
|
|
2670
|
+
) {
|
|
2671
|
+
return -1;
|
|
2672
|
+
}
|
|
2673
|
+
const firstEquals = rest.indexOf("=");
|
|
2674
|
+
const beforeParameters =
|
|
2675
|
+
firstEquals >= 0 ? rest.lastIndexOf("@", firstEquals) : -1;
|
|
2676
|
+
return beforeParameters >= 0 ? beforeParameters : rest.lastIndexOf("@");
|
|
2677
|
+
}
|
|
2678
|
+
|
|
2679
|
+
/**
|
|
2680
|
+
* The `host[:port]` list of a parenthesized address: MySQL
|
|
2681
|
+
* `address=(host=h)(port=3306)` and `(host=h,port=3306,user=u)`, Oracle
|
|
2682
|
+
* `(DESCRIPTION=(ADDRESS=(HOST=h)(PORT=1521)))`. Only host and port values are
|
|
2683
|
+
* read, so a user or password beside them never leaves.
|
|
2684
|
+
*/
|
|
2685
|
+
function parenthesizedHosts(text) {
|
|
2686
|
+
const hosts = [];
|
|
2687
|
+
const ports = [];
|
|
2688
|
+
for (const group of text.split(/[()]/)) {
|
|
2689
|
+
for (const pair of group.split(",")) {
|
|
2690
|
+
const equals = pair.indexOf("=");
|
|
2691
|
+
if (equals <= 0) {
|
|
2692
|
+
continue;
|
|
2693
|
+
}
|
|
2694
|
+
const key = pair.slice(0, equals).trim().toLowerCase();
|
|
2695
|
+
const value = pair.slice(equals + 1).trim();
|
|
2696
|
+
if (key === "host" && HOST_PATTERN.test(value)) {
|
|
2697
|
+
hosts.push(value);
|
|
2698
|
+
} else if (key === "port" && /^\d+$/.test(value)) {
|
|
2699
|
+
ports[hosts.length ? hosts.length - 1 : 0] ??= value;
|
|
2700
|
+
}
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
return hosts.map((host, index) =>
|
|
2704
|
+
ports[index] && !host.includes(":") ? `${host}:${ports[index]}` : host,
|
|
2705
|
+
);
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2708
|
+
// The hosts and database path of an authority, after its userinfo is gone.
|
|
2709
|
+
function parseAuthority(text) {
|
|
2710
|
+
const parenthesized = text.startsWith("(") || /^[A-Za-z]+=\(/.test(text);
|
|
2711
|
+
if (parenthesized) {
|
|
2712
|
+
const close = text.lastIndexOf(")");
|
|
2713
|
+
const after = close >= 0 ? text.slice(close + 1) : "";
|
|
2714
|
+
return {
|
|
2715
|
+
hosts: parenthesizedHosts(text.slice(0, close + 1)),
|
|
2716
|
+
path: after.startsWith("/") ? cutAt(after, ["?", "#", ";", ":"]) : "",
|
|
2717
|
+
};
|
|
2718
|
+
}
|
|
2719
|
+
const end = indexOfAny(text, ADDRESS_END);
|
|
2720
|
+
const hostPart = end >= 0 ? text.slice(0, end) : text;
|
|
2721
|
+
const hosts = isHostList(hostPart) ? hostPart.split(",") : [];
|
|
2722
|
+
// DB2 and Informix append `:key=value;` properties to the database name.
|
|
2723
|
+
const path =
|
|
2724
|
+
hosts.length && end >= 0 && text[end] === "/"
|
|
2725
|
+
? cutAt(text.slice(end), ["?", "#", ";", ":"])
|
|
2726
|
+
: "";
|
|
2727
|
+
return { hosts, path };
|
|
2728
|
+
}
|
|
2729
|
+
|
|
2730
|
+
/**
|
|
2731
|
+
* Split a JDBC URL into its subprotocol, hosts and database path, dropping
|
|
2732
|
+
* anything that may carry credentials: userinfo, the `user/password@` prefix
|
|
2733
|
+
* Oracle takes, URL parameters, and the `;key=value` and `:key=value`
|
|
2734
|
+
* properties some drivers take. Host values that do not look like hosts are
|
|
2735
|
+
* dropped too, rather than passed on.
|
|
2736
|
+
*
|
|
2737
|
+
* @param {string} value JDBC URL.
|
|
2738
|
+
* @returns {{scheme: string, hosts: string[], address: string}|undefined}
|
|
2739
|
+
* The first subprotocol, the hosts, and the address rebuilt from those
|
|
2740
|
+
* parts; undefined for anything but a JDBC URL.
|
|
2741
|
+
*/
|
|
2742
|
+
function parseJdbcUrl(value) {
|
|
2743
|
+
const text = String(value || "").trim();
|
|
2744
|
+
if (!/^jdbc:/i.test(text)) {
|
|
2745
|
+
return undefined;
|
|
2746
|
+
}
|
|
2747
|
+
let rest = text.slice(5);
|
|
2748
|
+
const tokens = [];
|
|
2749
|
+
for (
|
|
2750
|
+
let token = /^[A-Za-z][\w+.-]*:/.exec(rest);
|
|
2751
|
+
token && !rest.startsWith("//");
|
|
2752
|
+
token = /^[A-Za-z][\w+.-]*:/.exec(rest)
|
|
2753
|
+
) {
|
|
2754
|
+
tokens.push(token[0]);
|
|
2755
|
+
rest = rest.slice(token[0].length);
|
|
2756
|
+
}
|
|
2757
|
+
const scheme = (tokens[0] || "jdbc:").slice(0, -1).toLowerCase();
|
|
2758
|
+
if (rest.startsWith("//")) {
|
|
2759
|
+
const authority = rest.slice(2);
|
|
2760
|
+
const { hosts, path } = parseAuthority(
|
|
2761
|
+
authority.slice(userinfoEnd(authority) + 1),
|
|
2762
|
+
);
|
|
2763
|
+
return {
|
|
2764
|
+
scheme,
|
|
2765
|
+
hosts,
|
|
2766
|
+
address: `jdbc:${tokens.join("")}//${hosts.join(",")}${path}`,
|
|
2767
|
+
};
|
|
2768
|
+
}
|
|
2769
|
+
const at = userinfoEnd(rest);
|
|
2770
|
+
if (at < 0) {
|
|
2771
|
+
return {
|
|
2772
|
+
scheme,
|
|
2773
|
+
hosts: [],
|
|
2774
|
+
address: `jdbc:${tokens.join("")}${cutAt(rest, ["?", "#", ";"])}`,
|
|
2775
|
+
};
|
|
2776
|
+
}
|
|
2777
|
+
// `user/password@host:port:sid`: a word before the credentials can read as
|
|
2778
|
+
// a subprotocol (`jdbc:db:user:password@host`), so only the driver keeps
|
|
2779
|
+
// its tokens, and Oracle its driver type.
|
|
2780
|
+
const kept = tokens.slice(0, scheme === "oracle" ? 2 : 1).join("");
|
|
2781
|
+
const target = rest.slice(at + 1);
|
|
2782
|
+
if (target.startsWith("//")) {
|
|
2783
|
+
const { hosts, path } = parseAuthority(target.slice(2));
|
|
2784
|
+
return {
|
|
2785
|
+
scheme,
|
|
2786
|
+
hosts,
|
|
2787
|
+
address: `jdbc:${kept}@//${hosts.join(",")}${path}`,
|
|
2788
|
+
};
|
|
2789
|
+
}
|
|
2790
|
+
if (target.startsWith("(")) {
|
|
2791
|
+
const hosts = parenthesizedHosts(target);
|
|
2792
|
+
return { scheme, hosts, address: `jdbc:${kept}@${hosts.join(",")}` };
|
|
2793
|
+
}
|
|
2794
|
+
const address = cutAt(target, ["?", "#", ";", "/"]);
|
|
2795
|
+
const host = address.split(":")[0];
|
|
2796
|
+
const hosts = HOST_PATTERN.test(host) ? [host] : [];
|
|
2797
|
+
return {
|
|
2798
|
+
scheme,
|
|
2799
|
+
hosts,
|
|
2800
|
+
address: `jdbc:${kept}@${hosts.length ? address : ""}`,
|
|
2801
|
+
};
|
|
2802
|
+
}
|
|
2803
|
+
|
|
2190
2804
|
/**
|
|
2191
2805
|
* A JDBC address without what may carry credentials: userinfo, the Oracle
|
|
2192
2806
|
* `user/password@` prefix, URL parameters and the `;key=value` properties
|
|
@@ -2197,49 +2811,58 @@ function cryptoAssetProperties(finding, nativeLibrary) {
|
|
|
2197
2811
|
* @returns {string} The address part.
|
|
2198
2812
|
*/
|
|
2199
2813
|
export function sanitizeJdbcUrl(value) {
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
}
|
|
2210
|
-
}
|
|
2211
|
-
const slashes = text.indexOf("//");
|
|
2212
|
-
if (slashes >= 0) {
|
|
2213
|
-
const authorityEnd = text.indexOf("/", slashes + 2);
|
|
2214
|
-
const authority = text.slice(
|
|
2215
|
-
slashes + 2,
|
|
2216
|
-
authorityEnd >= 0 ? authorityEnd : undefined,
|
|
2217
|
-
);
|
|
2218
|
-
const at = authority.lastIndexOf("@");
|
|
2219
|
-
if (at >= 0) {
|
|
2220
|
-
text =
|
|
2221
|
-
text.slice(0, slashes + 2) +
|
|
2222
|
-
authority.slice(at + 1) +
|
|
2223
|
-
(authorityEnd >= 0 ? text.slice(authorityEnd) : "");
|
|
2224
|
-
}
|
|
2225
|
-
}
|
|
2226
|
-
// Oracle thin: jdbc:oracle:thin:user/password@host:port:sid
|
|
2227
|
-
const thin = /^(jdbc:oracle:thin:)[^@]*@/i.exec(text);
|
|
2228
|
-
if (thin) {
|
|
2229
|
-
text = `${thin[1]}@${text.slice(thin[0].length)}`;
|
|
2814
|
+
const parsed = parseJdbcUrl(value);
|
|
2815
|
+
return parsed ? parsed.address : String(value || "").trim();
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
// The host name of `host`, `host:port` or `[ipv6]:port`.
|
|
2819
|
+
function hostName(host) {
|
|
2820
|
+
if (host.startsWith("[")) {
|
|
2821
|
+
const close = host.indexOf("]");
|
|
2822
|
+
return close > 0 ? host.slice(1, close) : undefined;
|
|
2230
2823
|
}
|
|
2231
|
-
return
|
|
2824
|
+
return host.split(":")[0] || undefined;
|
|
2232
2825
|
}
|
|
2233
2826
|
|
|
2234
2827
|
function jdbcServiceName(value) {
|
|
2235
|
-
const
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2828
|
+
const { scheme, hosts } = parseJdbcUrl(value);
|
|
2829
|
+
return [scheme, hosts.length ? hostName(hosts[0]) : undefined]
|
|
2830
|
+
.filter(Boolean)
|
|
2831
|
+
.join("-");
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
// A host value as `host[:port]`, without a scheme, userinfo, path or
|
|
2835
|
+
// parameters, or undefined when what is left is not a host.
|
|
2836
|
+
function bareHost(value) {
|
|
2837
|
+
let text = String(value || "").trim();
|
|
2838
|
+
const scheme = /^[A-Za-z][A-Za-z0-9+.-]*:\/\//.exec(text);
|
|
2839
|
+
if (scheme) {
|
|
2840
|
+
text = text.slice(scheme[0].length);
|
|
2841
|
+
}
|
|
2842
|
+
text = cutAt(text.slice(userinfoEnd(text) + 1), ADDRESS_END);
|
|
2843
|
+
return HOST_PATTERN.test(text) ? text : undefined;
|
|
2844
|
+
}
|
|
2845
|
+
|
|
2846
|
+
// The `host[:port]` a URL names. The userinfo is dropped before the URL is
|
|
2847
|
+
// parsed: a password holding `/` would otherwise end the authority early and
|
|
2848
|
+
// leave the user name to read as the host.
|
|
2849
|
+
function urlHost(value) {
|
|
2850
|
+
const separator = value.indexOf("://");
|
|
2851
|
+
if (separator <= 0) {
|
|
2852
|
+
return undefined;
|
|
2853
|
+
}
|
|
2854
|
+
const rest = value.slice(separator + 3);
|
|
2855
|
+
const withoutUserinfo = rest.slice(userinfoEnd(rest) + 1);
|
|
2856
|
+
try {
|
|
2857
|
+
const host = new URL(`${value.slice(0, separator)}://${withoutUserinfo}`)
|
|
2858
|
+
.host;
|
|
2859
|
+
if (host) {
|
|
2860
|
+
return host;
|
|
2861
|
+
}
|
|
2862
|
+
} catch (_err) {
|
|
2863
|
+
// Not a URL the WHATWG parser takes; the host is read by hand.
|
|
2241
2864
|
}
|
|
2242
|
-
return
|
|
2865
|
+
return bareHost(withoutUserinfo);
|
|
2243
2866
|
}
|
|
2244
2867
|
|
|
2245
2868
|
/**
|
|
@@ -2259,13 +2882,11 @@ export function scalasemServiceName(service) {
|
|
|
2259
2882
|
return jdbcServiceName(value);
|
|
2260
2883
|
}
|
|
2261
2884
|
if (service?.host) {
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
try {
|
|
2265
|
-
return new URL(value).hostname || undefined;
|
|
2266
|
-
} catch (_err) {
|
|
2267
|
-
return undefined;
|
|
2885
|
+
const host = bareHost(service.host);
|
|
2886
|
+
return host ? hostName(host) : undefined;
|
|
2268
2887
|
}
|
|
2888
|
+
const host = urlHost(value);
|
|
2889
|
+
return host ? hostName(host) : undefined;
|
|
2269
2890
|
}
|
|
2270
2891
|
|
|
2271
2892
|
/**
|
|
@@ -2287,15 +2908,12 @@ export function scalasemServiceEndpoint(service) {
|
|
|
2287
2908
|
return sanitizeJdbcUrl(value);
|
|
2288
2909
|
}
|
|
2289
2910
|
if (service?.host) {
|
|
2290
|
-
return service.host;
|
|
2291
|
-
}
|
|
2292
|
-
try {
|
|
2293
|
-
const parsed = new URL(value);
|
|
2294
|
-
return `${parsed.protocol}//${parsed.host}`;
|
|
2295
|
-
} catch (_err) {
|
|
2296
|
-
const hostMatch = /:\/\/([^/?#@]+@)?([^/?#]+)/.exec(value);
|
|
2297
|
-
return hostMatch ? `${value.split("://")[0]}://${hostMatch[2]}` : undefined;
|
|
2911
|
+
return bareHost(service.host);
|
|
2298
2912
|
}
|
|
2913
|
+
const host = urlHost(value);
|
|
2914
|
+
return host
|
|
2915
|
+
? `${value.slice(0, value.indexOf("://")).toLowerCase()}://${host}`
|
|
2916
|
+
: undefined;
|
|
2299
2917
|
}
|
|
2300
2918
|
|
|
2301
2919
|
function serviceLocation(service) {
|
|
@@ -2459,7 +3077,10 @@ export async function collectScalaJsNpmComponents(
|
|
|
2459
3077
|
) {
|
|
2460
3078
|
const isScalaJs = pkgList.some((pkg) => {
|
|
2461
3079
|
const name = pkg?.name || purlPurlName(pkg?.purl) || "";
|
|
2462
|
-
return
|
|
3080
|
+
return (
|
|
3081
|
+
scalaNamePlatform(name) === "js" ||
|
|
3082
|
+
parseScalaArtifact(name).artifactBase === "scalajs-library"
|
|
3083
|
+
);
|
|
2463
3084
|
});
|
|
2464
3085
|
const empty = { components: [], roots: [], dependencies: [] };
|
|
2465
3086
|
if (!isScalaJs) {
|
|
@@ -2548,6 +3169,24 @@ function npmNameFromPurl(purl) {
|
|
|
2548
3169
|
}
|
|
2549
3170
|
}
|
|
2550
3171
|
|
|
3172
|
+
function nodeModulesDepth(component) {
|
|
3173
|
+
const location = (component.properties || []).find(
|
|
3174
|
+
(property) => property.name === "internal:LocalNodeModulesPath",
|
|
3175
|
+
)?.value;
|
|
3176
|
+
return location
|
|
3177
|
+
? location.split("node_modules/").length - 1
|
|
3178
|
+
: Number.POSITIVE_INFINITY;
|
|
3179
|
+
}
|
|
3180
|
+
|
|
3181
|
+
function isPreferredNpmCopy(candidate, known) {
|
|
3182
|
+
const candidateDepth = nodeModulesDepth(candidate);
|
|
3183
|
+
const knownDepth = nodeModulesDepth(known);
|
|
3184
|
+
if (candidateDepth !== knownDepth) {
|
|
3185
|
+
return candidateDepth < knownDepth;
|
|
3186
|
+
}
|
|
3187
|
+
return candidate.purl < known.purl;
|
|
3188
|
+
}
|
|
3189
|
+
|
|
2551
3190
|
/**
|
|
2552
3191
|
* Attribute the report's JavaScript module imports to the npm components the
|
|
2553
3192
|
* build bundles. A module no component owns is reported on the project
|
|
@@ -2564,10 +3203,17 @@ function addJsModuleEvidence(
|
|
|
2564
3203
|
if (!jsModules.length) {
|
|
2565
3204
|
return;
|
|
2566
3205
|
}
|
|
3206
|
+
// Of several versions of one package, the one a bare import resolves to is
|
|
3207
|
+
// the hoisted copy, the shallowest under node_modules; the first purl
|
|
3208
|
+
// breaks a tie, so the choice never depends on the order of the BOM.
|
|
2567
3209
|
const npmByName = new Map();
|
|
2568
3210
|
for (const component of components) {
|
|
2569
3211
|
const name = npmNameFromPurl(component?.purl);
|
|
2570
|
-
if (name) {
|
|
3212
|
+
if (!name) {
|
|
3213
|
+
continue;
|
|
3214
|
+
}
|
|
3215
|
+
const known = npmByName.get(name);
|
|
3216
|
+
if (!known || isPreferredNpmCopy(component, known)) {
|
|
2571
3217
|
npmByName.set(name, component);
|
|
2572
3218
|
}
|
|
2573
3219
|
}
|