@cdxgen/cdxgen 13.3.0 → 13.5.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 +12 -9
- package/bin/cdxgen-secure.js +15 -0
- package/bin/cdxgen.js +115 -31
- package/bin/evinse.js +23 -1
- package/bin/hbom.js +15 -7
- package/bin/repl.js +115 -0
- package/bin/sign.js +45 -15
- package/bin/tracebom.js +5 -1
- package/bin/validate.js +84 -18
- package/bin/verify.js +155 -86
- package/data/cbomosdb-queries.json +13 -1
- package/data/component-tags.json +8 -0
- package/data/helpers/init.gradle +46 -21
- package/data/queries-darwin.json +25 -1
- package/data/queries-win.json +19 -1
- package/data/queries.json +41 -0
- package/data/remediations.json +344 -1
- package/data/rules/cbom-security.yaml +235 -4
- package/data/rules/dependency-sources.yaml +67 -0
- package/data/rules/go-binaries.yaml +46 -0
- package/data/rules/golem-go.yaml +80 -0
- package/data/rules/hbom-compliance.yaml +40 -22
- package/data/rules/hbom-performance.yaml +7 -1
- package/data/rules/host-topology.yaml +1 -1
- package/data/rules/obom-runtime.yaml +31 -2
- package/data/rules/php-runtime.yaml +143 -0
- package/data/rules/scala-evidence.yaml +115 -0
- package/lib/audit/index.js +52 -6
- package/lib/cli/bomAssembly.js +28 -3
- package/lib/cli/cliOptions.js +110 -3
- package/lib/cli/index.js +186 -58
- package/lib/cli/jsBom.js +50 -29
- package/lib/cli/jvmBom.js +779 -146
- package/lib/cli/managedBom.js +1021 -261
- package/lib/cli/nativeBom.js +364 -58
- package/lib/core/activity.js +12 -0
- package/lib/core/env.js +47 -17
- package/lib/core/fs.js +31 -21
- package/lib/core/httpClient.js +233 -69
- package/lib/core/pluginRun.js +184 -0
- package/lib/core/processTree.js +100 -0
- package/lib/core/projectConfig.js +1 -0
- package/lib/core/projectTooling.js +97 -0
- package/lib/core/propertySanitizer.js +72 -0
- package/lib/core/runState.js +39 -0
- package/lib/core/toml.js +72 -0
- package/lib/ecosystems/asarutils.js +4 -2
- package/lib/ecosystems/bunutils.js +5 -7
- package/lib/ecosystems/cargoCache.js +231 -8
- package/lib/ecosystems/cmakeResolver.js +10 -21
- package/lib/ecosystems/cppBuildContext.js +631 -0
- package/lib/ecosystems/cppEvidence.js +337 -22
- package/lib/ecosystems/cppIncludeResolver.js +294 -0
- package/lib/ecosystems/denoutils.js +93 -66
- package/lib/ecosystems/ecosystems.js +1967 -788
- package/lib/ecosystems/golem.js +578 -18
- package/lib/ecosystems/gradleutils.js +340 -46
- package/lib/ecosystems/jsEvidence.js +85 -24
- package/lib/ecosystems/kosi.js +21 -4
- package/lib/ecosystems/npmutils.js +64 -0
- package/lib/ecosystems/parsers-dotnet.js +668 -153
- package/lib/ecosystems/parsers-elm.js +46 -28
- package/lib/ecosystems/parsers-gleam.js +3 -4
- package/lib/ecosystems/parsers-go.js +1283 -297
- package/lib/ecosystems/parsers-js.js +92 -26
- package/lib/ecosystems/parsers-julia.js +2 -3
- package/lib/ecosystems/parsers-jvm.js +463 -29
- package/lib/ecosystems/parsers-misc.js +405 -93
- package/lib/ecosystems/parsers-python.js +152 -20
- package/lib/ecosystems/parsers-rust.js +4 -4
- package/lib/ecosystems/parsers-terraform.js +1573 -19
- package/lib/ecosystems/pythonutils.js +102 -6
- package/lib/ecosystems/remote/huggingface.js +3 -0
- package/lib/ecosystems/rubyutils.js +73 -20
- package/lib/ecosystems/rusi.js +32 -9
- package/lib/ecosystems/sbtutils.js +243 -187
- package/lib/ecosystems/scalacliutils.js +343 -0
- package/lib/ecosystems/scalasem.js +2620 -0
- package/lib/ecosystems/terraformRegistry.js +288 -0
- package/lib/ecosystems/utils.js +2 -0
- package/lib/ecosystems/vendoredCode.js +156 -0
- package/lib/evinser/evinser.js +695 -78
- package/lib/evinser/swiftsem.js +68 -5
- package/lib/helpers/agenticToolutils.js +2 -2
- package/lib/helpers/bomSigner.js +576 -128
- package/lib/inventory/agentSkillSpec.js +17 -0
- package/lib/inventory/aiCollector.js +4 -1
- package/lib/inventory/analyzer.js +110 -127
- package/lib/inventory/apkInstalledDb.js +243 -0
- package/lib/inventory/atomUtils.js +204 -48
- package/lib/inventory/auditCategories.js +10 -3
- package/lib/inventory/buildToolRateLimit.js +144 -0
- package/lib/inventory/cacheCommands.js +2 -1
- package/lib/inventory/cacheDir.js +52 -9
- package/lib/inventory/certificates.js +2371 -0
- package/lib/inventory/cmakeBuildDirs.js +254 -0
- package/lib/inventory/communityAiConfigParser.js +4 -9
- package/lib/inventory/deps.js +507 -128
- package/lib/inventory/display.js +200 -3
- package/lib/inventory/dosai.js +747 -101
- package/lib/inventory/dosaiParsers.js +310 -15
- package/lib/inventory/dynamic.js +96 -8
- package/lib/inventory/envcontext.js +168 -5
- package/lib/inventory/evidenceUtils.js +590 -132
- package/lib/inventory/fetchBatch.js +574 -38
- package/lib/inventory/fetchDiskCache.js +597 -0
- package/lib/inventory/forgeEnricher.js +14 -5
- package/lib/inventory/formulationParsers.js +3 -2
- package/lib/inventory/goBuildInfo.js +140 -0
- package/lib/inventory/hbom.js +67 -7
- package/lib/inventory/hbomAnalysis.js +16 -0
- package/lib/inventory/hostTopology.js +21 -0
- package/lib/inventory/jvmLocalRepos.js +917 -0
- package/lib/inventory/mcpConfigParser.js +2 -3
- package/lib/inventory/osPackagePurl.js +244 -0
- package/lib/inventory/osPackageResolver.js +89 -87
- package/lib/inventory/osinfo.js +79 -31
- package/lib/inventory/osqueryTransform.js +114 -5
- package/lib/inventory/parentSelection.js +146 -0
- package/lib/inventory/phpBuildInfo.js +487 -0
- package/lib/inventory/phpHostProbe.js +641 -0
- package/lib/inventory/rootfsCerts.js +426 -0
- package/lib/inventory/rustFormulationParser.js +3 -4
- package/lib/inventory/scalaCoords.js +153 -0
- package/lib/inventory/source.js +215 -11
- package/lib/inventory/spdx.js +245 -0
- package/lib/inventory/traceRunner.js +93 -8
- package/lib/inventory/tracedCrypto.js +423 -0
- package/lib/managers/binary.js +529 -183
- package/lib/managers/docker.js +199 -44
- package/lib/managers/oci.js +24 -2
- package/lib/parsers/cmakeCache.js +69 -0
- package/lib/parsers/cmakeLists.js +16 -2
- package/lib/parsers/cmakePresets.js +400 -0
- package/lib/parsers/compileCommands.js +557 -0
- package/lib/parsers/email.js +277 -0
- package/lib/parsers/largeJson.js +34 -2
- package/lib/parsers/strictJson.js +322 -0
- package/lib/parsers/terraformConfig.js +1073 -0
- package/lib/parsers/terraformSource.js +652 -0
- package/lib/server/server.js +209 -1
- package/lib/stages/postgen/annotator.js +17 -1
- package/lib/stages/postgen/introspection/markers.js +1 -1
- package/lib/stages/postgen/introspection/reflect.js +4 -1
- package/lib/stages/postgen/postgen.js +20 -6
- package/lib/stages/postgen/ruleEngine.js +58 -1
- package/lib/stages/postgen/specVersionCompat.js +62 -0
- package/lib/stages/pregen/pregen.js +117 -5
- package/lib/validator/bomValidator.js +26 -5
- package/lib/validator/generated/runtime.mjs +13 -0
- package/lib/validator/generated/validate-1.6.mjs +26 -7
- package/lib/validator/generated/validate-1.7.mjs +31 -12
- package/lib/validator/generated/validate-2.0.mjs +30 -11
- package/lib/validator/index.js +25 -10
- package/lib/validator/reporters/json.js +1 -0
- package/package.json +23 -23
- package/types/bin/cdxgen-secure.d.ts +3 -0
- package/types/bin/cdxgen-secure.d.ts.map +1 -0
- package/types/bin/repl.d.ts.map +1 -1
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/cli/bomAssembly.d.ts +9 -2
- package/types/lib/cli/bomAssembly.d.ts.map +1 -1
- package/types/lib/cli/cliOptions.d.ts +43 -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.map +1 -1
- package/types/lib/cli/jvmBom.d.ts +44 -1
- package/types/lib/cli/jvmBom.d.ts.map +1 -1
- package/types/lib/cli/managedBom.d.ts +7 -4
- package/types/lib/cli/managedBom.d.ts.map +1 -1
- package/types/lib/cli/nativeBom.d.ts +3 -1
- package/types/lib/cli/nativeBom.d.ts.map +1 -1
- package/types/lib/core/activity.d.ts.map +1 -1
- package/types/lib/core/env.d.ts +27 -2
- package/types/lib/core/env.d.ts.map +1 -1
- package/types/lib/core/fs.d.ts.map +1 -1
- package/types/lib/core/httpClient.d.ts +13 -8
- package/types/lib/core/httpClient.d.ts.map +1 -1
- package/types/lib/core/pluginRun.d.ts +97 -0
- package/types/lib/core/pluginRun.d.ts.map +1 -0
- package/types/lib/core/processTree.d.ts +36 -0
- package/types/lib/core/processTree.d.ts.map +1 -0
- package/types/lib/core/projectConfig.d.ts.map +1 -1
- package/types/lib/core/projectTooling.d.ts +41 -0
- package/types/lib/core/projectTooling.d.ts.map +1 -0
- package/types/lib/core/propertySanitizer.d.ts +26 -0
- package/types/lib/core/propertySanitizer.d.ts.map +1 -1
- package/types/lib/core/runState.d.ts +29 -0
- package/types/lib/core/runState.d.ts.map +1 -0
- package/types/lib/core/toml.d.ts +20 -0
- package/types/lib/core/toml.d.ts.map +1 -0
- package/types/lib/ecosystems/asarutils.d.ts.map +1 -1
- package/types/lib/ecosystems/bunutils.d.ts.map +1 -1
- package/types/lib/ecosystems/cargoCache.d.ts +12 -0
- package/types/lib/ecosystems/cargoCache.d.ts.map +1 -1
- package/types/lib/ecosystems/cmakeResolver.d.ts +5 -3
- package/types/lib/ecosystems/cmakeResolver.d.ts.map +1 -1
- package/types/lib/ecosystems/cppBuildContext.d.ts +55 -0
- package/types/lib/ecosystems/cppBuildContext.d.ts.map +1 -0
- package/types/lib/ecosystems/cppEvidence.d.ts +19 -3
- package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/cppIncludeResolver.d.ts +92 -0
- package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -0
- package/types/lib/ecosystems/denoutils.d.ts +9 -3
- package/types/lib/ecosystems/denoutils.d.ts.map +1 -1
- package/types/lib/ecosystems/ecosystems.d.ts +62 -13
- package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
- package/types/lib/ecosystems/golem.d.ts +81 -1
- package/types/lib/ecosystems/golem.d.ts.map +1 -1
- package/types/lib/ecosystems/gradleutils.d.ts +59 -7
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -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/npmutils.d.ts +15 -0
- package/types/lib/ecosystems/npmutils.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-dotnet.d.ts +155 -5
- package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-elm.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-gleam.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-go.d.ts +119 -5
- package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-js.d.ts +6 -1
- package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-julia.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-jvm.d.ts +74 -3
- package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-misc.d.ts +21 -59
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-python.d.ts +57 -6
- package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-terraform.d.ts +38 -21
- package/types/lib/ecosystems/parsers-terraform.d.ts.map +1 -1
- package/types/lib/ecosystems/pythonutils.d.ts +26 -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 +21 -1
- package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rusi.d.ts.map +1 -1
- package/types/lib/ecosystems/sbtutils.d.ts +88 -2
- package/types/lib/ecosystems/sbtutils.d.ts.map +1 -1
- package/types/lib/ecosystems/scalacliutils.d.ts +56 -0
- package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -0
- package/types/lib/ecosystems/scalasem.d.ts +311 -0
- package/types/lib/ecosystems/scalasem.d.ts.map +1 -0
- package/types/lib/ecosystems/terraformRegistry.d.ts +9 -0
- package/types/lib/ecosystems/terraformRegistry.d.ts.map +1 -0
- package/types/lib/ecosystems/utils.d.ts +2 -2
- package/types/lib/ecosystems/utils.d.ts.map +1 -1
- package/types/lib/ecosystems/vendoredCode.d.ts +32 -0
- package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -0
- package/types/lib/evinser/evinser.d.ts +100 -8
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/evinser/swiftsem.d.ts +10 -0
- package/types/lib/evinser/swiftsem.d.ts.map +1 -1
- package/types/lib/helpers/bomSigner.d.ts +80 -4
- package/types/lib/helpers/bomSigner.d.ts.map +1 -1
- package/types/lib/inventory/agentSkillSpec.d.ts +13 -0
- package/types/lib/inventory/agentSkillSpec.d.ts.map +1 -0
- package/types/lib/inventory/aiCollector.d.ts.map +1 -1
- package/types/lib/inventory/analyzer.d.ts.map +1 -1
- package/types/lib/inventory/apkInstalledDb.d.ts +42 -0
- package/types/lib/inventory/apkInstalledDb.d.ts.map +1 -0
- package/types/lib/inventory/atomUtils.d.ts +96 -10
- package/types/lib/inventory/atomUtils.d.ts.map +1 -1
- package/types/lib/inventory/auditCategories.d.ts +3 -0
- package/types/lib/inventory/auditCategories.d.ts.map +1 -1
- package/types/lib/inventory/buildToolRateLimit.d.ts +38 -0
- package/types/lib/inventory/buildToolRateLimit.d.ts.map +1 -0
- package/types/lib/inventory/cacheCommands.d.ts +2 -1
- package/types/lib/inventory/cacheCommands.d.ts.map +1 -1
- package/types/lib/inventory/cacheDir.d.ts +12 -4
- package/types/lib/inventory/cacheDir.d.ts.map +1 -1
- package/types/lib/inventory/certificates.d.ts +205 -0
- package/types/lib/inventory/certificates.d.ts.map +1 -0
- package/types/lib/inventory/cmakeBuildDirs.d.ts +58 -0
- package/types/lib/inventory/cmakeBuildDirs.d.ts.map +1 -0
- package/types/lib/inventory/communityAiConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/deps.d.ts +61 -2
- package/types/lib/inventory/deps.d.ts.map +1 -1
- package/types/lib/inventory/display.d.ts +18 -0
- package/types/lib/inventory/display.d.ts.map +1 -1
- package/types/lib/inventory/dosai.d.ts +202 -14
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/dosaiParsers.d.ts +53 -7
- package/types/lib/inventory/dosaiParsers.d.ts.map +1 -1
- package/types/lib/inventory/dynamic.d.ts +16 -1
- package/types/lib/inventory/dynamic.d.ts.map +1 -1
- package/types/lib/inventory/envcontext.d.ts +40 -2
- package/types/lib/inventory/envcontext.d.ts.map +1 -1
- package/types/lib/inventory/evidenceUtils.d.ts +48 -14
- package/types/lib/inventory/evidenceUtils.d.ts.map +1 -1
- package/types/lib/inventory/fetchBatch.d.ts +83 -14
- package/types/lib/inventory/fetchBatch.d.ts.map +1 -1
- package/types/lib/inventory/fetchDiskCache.d.ts +169 -0
- package/types/lib/inventory/fetchDiskCache.d.ts.map +1 -0
- package/types/lib/inventory/forgeEnricher.d.ts.map +1 -1
- package/types/lib/inventory/formulationParsers.d.ts.map +1 -1
- package/types/lib/inventory/goBuildInfo.d.ts +10 -0
- package/types/lib/inventory/goBuildInfo.d.ts.map +1 -0
- package/types/lib/inventory/hbom.d.ts.map +1 -1
- package/types/lib/inventory/hbomAnalysis.d.ts +10 -0
- package/types/lib/inventory/hbomAnalysis.d.ts.map +1 -1
- package/types/lib/inventory/hostTopology.d.ts.map +1 -1
- package/types/lib/inventory/jvmLocalRepos.d.ts +178 -0
- package/types/lib/inventory/jvmLocalRepos.d.ts.map +1 -0
- package/types/lib/inventory/mcpConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/osPackagePurl.d.ts +99 -0
- package/types/lib/inventory/osPackagePurl.d.ts.map +1 -0
- package/types/lib/inventory/osPackageResolver.d.ts +6 -2
- package/types/lib/inventory/osPackageResolver.d.ts.map +1 -1
- package/types/lib/inventory/osinfo.d.ts +31 -6
- package/types/lib/inventory/osinfo.d.ts.map +1 -1
- package/types/lib/inventory/osqueryTransform.d.ts +51 -0
- package/types/lib/inventory/osqueryTransform.d.ts.map +1 -1
- package/types/lib/inventory/parentSelection.d.ts +34 -0
- package/types/lib/inventory/parentSelection.d.ts.map +1 -0
- package/types/lib/inventory/phpBuildInfo.d.ts +142 -0
- package/types/lib/inventory/phpBuildInfo.d.ts.map +1 -0
- package/types/lib/inventory/phpHostProbe.d.ts +35 -0
- package/types/lib/inventory/phpHostProbe.d.ts.map +1 -0
- package/types/lib/inventory/rootfsCerts.d.ts +27 -0
- package/types/lib/inventory/rootfsCerts.d.ts.map +1 -0
- package/types/lib/inventory/rustFormulationParser.d.ts.map +1 -1
- package/types/lib/inventory/scalaCoords.d.ts +80 -0
- package/types/lib/inventory/scalaCoords.d.ts.map +1 -0
- package/types/lib/inventory/source.d.ts +28 -0
- package/types/lib/inventory/source.d.ts.map +1 -1
- package/types/lib/inventory/spdx.d.ts +24 -0
- package/types/lib/inventory/spdx.d.ts.map +1 -1
- package/types/lib/inventory/traceRunner.d.ts +16 -1
- package/types/lib/inventory/traceRunner.d.ts.map +1 -1
- package/types/lib/inventory/tracedCrypto.d.ts +64 -0
- package/types/lib/inventory/tracedCrypto.d.ts.map +1 -0
- package/types/lib/managers/binary.d.ts +10 -0
- 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/managers/oci.d.ts +3 -1
- package/types/lib/managers/oci.d.ts.map +1 -1
- package/types/lib/parsers/cmakeCache.d.ts +16 -0
- package/types/lib/parsers/cmakeCache.d.ts.map +1 -1
- package/types/lib/parsers/cmakeLists.d.ts +2 -1
- package/types/lib/parsers/cmakeLists.d.ts.map +1 -1
- package/types/lib/parsers/cmakePresets.d.ts +78 -0
- package/types/lib/parsers/cmakePresets.d.ts.map +1 -0
- package/types/lib/parsers/compileCommands.d.ts +115 -0
- package/types/lib/parsers/compileCommands.d.ts.map +1 -0
- package/types/lib/parsers/email.d.ts +19 -0
- package/types/lib/parsers/email.d.ts.map +1 -0
- package/types/lib/parsers/largeJson.d.ts +13 -0
- package/types/lib/parsers/largeJson.d.ts.map +1 -1
- package/types/lib/parsers/strictJson.d.ts +46 -0
- package/types/lib/parsers/strictJson.d.ts.map +1 -0
- package/types/lib/parsers/terraformConfig.d.ts +70 -0
- package/types/lib/parsers/terraformConfig.d.ts.map +1 -0
- package/types/lib/parsers/terraformSource.d.ts +66 -0
- package/types/lib/parsers/terraformSource.d.ts.map +1 -0
- package/types/lib/server/server.d.ts +8 -0
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
- package/types/lib/stages/postgen/introspection/reflect.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts +7 -4
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
- package/types/lib/stages/postgen/specVersionCompat.d.ts.map +1 -1
- package/types/lib/stages/pregen/pregen.d.ts +14 -1
- package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
- package/types/lib/validator/index.d.ts +8 -0
- package/types/lib/validator/index.d.ts.map +1 -1
- package/types/lib/validator/reporters/json.d.ts.map +1 -1
- package/lib/third-party/arborist/snapshots/calc-dep-flags.test.cjs +0 -462
|
@@ -1,24 +1,17 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
existsSync,
|
|
5
|
-
lstatSync,
|
|
6
|
-
readdirSync,
|
|
7
|
-
readFileSync,
|
|
8
|
-
statSync,
|
|
9
|
-
} from "node:fs";
|
|
10
|
-
import { homedir } from "node:os";
|
|
2
|
+
import { constants, lstatSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import { homedir, tmpdir } from "node:os";
|
|
11
4
|
import {
|
|
12
5
|
delimiter as _delimiter,
|
|
13
6
|
sep as _sep,
|
|
14
7
|
basename,
|
|
15
|
-
|
|
8
|
+
isAbsolute,
|
|
16
9
|
join,
|
|
17
10
|
relative,
|
|
11
|
+
resolve,
|
|
18
12
|
} from "node:path";
|
|
19
13
|
|
|
20
14
|
import { build } from "@cdxgen/cdx-purl";
|
|
21
|
-
import { globSync } from "glob";
|
|
22
15
|
import StreamZip from "node-stream-zip";
|
|
23
16
|
import {
|
|
24
17
|
clean,
|
|
@@ -32,48 +25,68 @@ import {
|
|
|
32
25
|
import {
|
|
33
26
|
cdxgenAgent,
|
|
34
27
|
DEBUG_MODE,
|
|
28
|
+
isSecureMode,
|
|
35
29
|
readEnvironmentVariable,
|
|
36
30
|
} from "../core/activity.js";
|
|
37
31
|
import {
|
|
38
32
|
SEARCH_MAVEN_ORG,
|
|
39
33
|
shouldFetchLicense,
|
|
40
34
|
shouldFetchPackageMetadata,
|
|
35
|
+
shouldFetchRegistryProvenance,
|
|
41
36
|
shouldFetchVCS,
|
|
42
37
|
} from "../core/env.js";
|
|
43
38
|
import {
|
|
44
39
|
checksumFile,
|
|
45
40
|
getAllFiles,
|
|
41
|
+
getTmpDir,
|
|
46
42
|
multiChecksumFile,
|
|
47
43
|
safeCopyFileSync,
|
|
48
44
|
safeExistsSync,
|
|
49
45
|
safeExtractArchive,
|
|
46
|
+
safeMkdtempSync,
|
|
50
47
|
safeRmSync,
|
|
51
48
|
} from "../core/fs.js";
|
|
52
49
|
import { thoughtLog } from "../core/logger.js";
|
|
53
50
|
import { mapWithConcurrency } from "../core/parallel.js";
|
|
54
51
|
import { isWin } from "../core/paths.js";
|
|
52
|
+
import { registerRunReset } from "../core/runState.js";
|
|
55
53
|
import { vendorAliases } from "../core/state.js";
|
|
56
|
-
import { isGitHubUrl, modulePathHost } from "../core/urls.js";
|
|
54
|
+
import { isGitHubUrl, modulePathHost, parseUrl } from "../core/urls.js";
|
|
57
55
|
import {
|
|
58
56
|
collectJarNS,
|
|
59
57
|
getPomPropertiesFromMavenDir,
|
|
60
58
|
inferJarGroupFromManifest,
|
|
59
|
+
isDocumentationJar,
|
|
61
60
|
parseJarManifest,
|
|
61
|
+
parseManifestLicenseList,
|
|
62
62
|
parsePomXml,
|
|
63
|
+
readEmbeddedMavenDescriptor,
|
|
63
64
|
trimJarGroupSuffix,
|
|
64
65
|
} from "../inventory/deps.js";
|
|
65
66
|
import {
|
|
67
|
+
gatedGet,
|
|
68
|
+
isHostCircuitOpen,
|
|
69
|
+
openHostCircuit,
|
|
66
70
|
prefetchEnabled,
|
|
67
71
|
prefetchedResponse,
|
|
68
72
|
prefetchJson,
|
|
69
73
|
withHostRateLimit,
|
|
70
74
|
} from "../inventory/fetchBatch.js";
|
|
75
|
+
import {
|
|
76
|
+
findLocalMavenArtifact,
|
|
77
|
+
findMavenCoordinatesBySha1,
|
|
78
|
+
inferMavenCoordinatesFromPath,
|
|
79
|
+
} from "../inventory/jvmLocalRepos.js";
|
|
71
80
|
import {
|
|
72
81
|
applyPurl,
|
|
73
82
|
genericPurl,
|
|
74
83
|
nugetPurl,
|
|
75
84
|
pypiPurl,
|
|
76
85
|
} from "../inventory/purl.js";
|
|
86
|
+
import {
|
|
87
|
+
publishedArtifactId,
|
|
88
|
+
scalaCoordinateOfPurl,
|
|
89
|
+
} from "../inventory/scalaCoords.js";
|
|
77
90
|
import {
|
|
78
91
|
findLicenseId,
|
|
79
92
|
guessLicenseId,
|
|
@@ -85,6 +98,9 @@ import {
|
|
|
85
98
|
localCargoMetadataEnabled,
|
|
86
99
|
readCargoCacheMetadata,
|
|
87
100
|
} from "./cargoCache.js";
|
|
101
|
+
import { normalizeNpmRegistryUrl, resolveNpmLicense } from "./npmutils.js";
|
|
102
|
+
import { parseNuspecData } from "./parsers-dotnet.js";
|
|
103
|
+
import { isDefaultPypiRegistry } from "./pylockutils.js";
|
|
88
104
|
import {
|
|
89
105
|
applyPypiClassifierMetadata,
|
|
90
106
|
applyPypiModuleNames,
|
|
@@ -95,26 +111,63 @@ import {
|
|
|
95
111
|
collectPypiRegistryProvenanceProperties,
|
|
96
112
|
} from "./registryProvenance.js";
|
|
97
113
|
|
|
98
|
-
|
|
99
|
-
|
|
114
|
+
/**
|
|
115
|
+
* No longer used. Registry answers are kept per ecosystem and for one run; this
|
|
116
|
+
* object stays exported for callers that imported it.
|
|
117
|
+
*
|
|
118
|
+
* @deprecated
|
|
119
|
+
*/
|
|
120
|
+
export const metadata_cache = {};
|
|
121
|
+
|
|
122
|
+
// Registry answers kept for the rest of a run, one map per ecosystem, so that a
|
|
123
|
+
// package several lockfiles name is asked about once and one ecosystem's parser
|
|
124
|
+
// cannot empty another's. They are emptied when the run ends.
|
|
125
|
+
/** npm packument URL to the packument. */
|
|
126
|
+
const npmDocuments = new Map();
|
|
127
|
+
/** pkg.go.dev page URL to the licences or the repository URL read from it. */
|
|
128
|
+
const goPkgAnswers = new Map();
|
|
129
|
+
/** `name|version` to the NuGet catalog leaf, or `{error}` for a failed lookup. */
|
|
130
|
+
const nugetEntries = new Map();
|
|
131
|
+
|
|
132
|
+
registerRunReset("ecosystems:registry-answers", () => {
|
|
133
|
+
npmDocuments.clear();
|
|
134
|
+
goPkgAnswers.clear();
|
|
135
|
+
nugetEntries.clear();
|
|
136
|
+
repoLicensePrefetch = new Map();
|
|
137
|
+
goPkgPrefetch = new Map();
|
|
138
|
+
});
|
|
100
139
|
|
|
101
140
|
// Speed up lookup namespaces for a given jar
|
|
102
141
|
|
|
103
|
-
//
|
|
104
|
-
|
|
105
|
-
const MAX_SEARCH_MAVEN_ORG_ERRORS = 1;
|
|
142
|
+
// Maven Central's search API, used to identify a jar by its SHA-1.
|
|
143
|
+
const MAVEN_SEARCH_HOST = "central.sonatype.com";
|
|
106
144
|
|
|
107
|
-
// circuit breaker for get repo license
|
|
145
|
+
// circuit breaker for get repo license. After five failures the lookups pause
|
|
146
|
+
// for fifteen minutes, so a long-running server recovers once the API does.
|
|
108
147
|
let get_repo_license_errors = 0;
|
|
148
|
+
let get_repo_license_paused_until = 0;
|
|
109
149
|
const MAX_GET_REPO_LICENSE_ERRORS = 5;
|
|
150
|
+
const GET_REPO_LICENSE_PAUSE_MS = 15 * 60 * 1000;
|
|
110
151
|
|
|
111
152
|
/**
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
153
|
+
* Whether repository licence lookups are currently paused.
|
|
154
|
+
*
|
|
155
|
+
* @returns {boolean}
|
|
115
156
|
*/
|
|
116
|
-
|
|
117
|
-
|
|
157
|
+
function repoLicenseLookupsPaused() {
|
|
158
|
+
if (get_repo_license_errors < MAX_GET_REPO_LICENSE_ERRORS) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
if (!get_repo_license_paused_until) {
|
|
162
|
+
get_repo_license_paused_until = Date.now() + GET_REPO_LICENSE_PAUSE_MS;
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
if (Date.now() < get_repo_license_paused_until) {
|
|
166
|
+
return true;
|
|
167
|
+
}
|
|
168
|
+
get_repo_license_errors = 0;
|
|
169
|
+
get_repo_license_paused_until = 0;
|
|
170
|
+
return false;
|
|
118
171
|
}
|
|
119
172
|
|
|
120
173
|
/**
|
|
@@ -182,75 +235,286 @@ function npmRegistryKey(p) {
|
|
|
182
235
|
return p.name;
|
|
183
236
|
}
|
|
184
237
|
|
|
238
|
+
// The hosts of the public npm registry. A lockfile `resolved` URL on any other
|
|
239
|
+
// host was served by a mirror or a private registry.
|
|
240
|
+
const DEFAULT_NPM_REGISTRY_HOSTS = new Set([
|
|
241
|
+
"registry.npmjs.org",
|
|
242
|
+
"registry.npmjs.com",
|
|
243
|
+
"registry.yarnpkg.com",
|
|
244
|
+
]);
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Whether a URL points at the public npm registry or at the registry the
|
|
248
|
+
* caller resolves against.
|
|
249
|
+
*
|
|
250
|
+
* @param {string} url Registry or tarball URL.
|
|
251
|
+
* @param {string} defaultRegistryUrl The registry the caller resolves against.
|
|
252
|
+
* @returns {boolean}
|
|
253
|
+
*/
|
|
254
|
+
function isDefaultNpmRegistry(url, defaultRegistryUrl) {
|
|
255
|
+
const host = parseUrl(url)?.host;
|
|
256
|
+
return (
|
|
257
|
+
!!host &&
|
|
258
|
+
(DEFAULT_NPM_REGISTRY_HOSTS.has(host) ||
|
|
259
|
+
host === parseUrl(defaultRegistryUrl)?.host)
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* The registry that served a tarball. Registries lay tarballs out as
|
|
265
|
+
* `<registry>/<name>/-/<file>`, so the registry is everything before the name.
|
|
266
|
+
*
|
|
267
|
+
* @param {string} tarballUrl Lockfile `resolved` URL.
|
|
268
|
+
* @param {string} fullName Package name, with its scope.
|
|
269
|
+
* @returns {string|undefined} Registry base URL, or undefined when the URL has
|
|
270
|
+
* another layout.
|
|
271
|
+
*/
|
|
272
|
+
function npmTarballRegistry(tarballUrl, fullName) {
|
|
273
|
+
if (!tarballUrl.startsWith("https://") && !tarballUrl.startsWith("http://")) {
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
const index = tarballUrl.indexOf(`/${fullName}/-/`);
|
|
277
|
+
return index > 0 ? tarballUrl.slice(0, index) : undefined;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Whether a lockfile `resolved` URL names a git source rather than a tarball.
|
|
282
|
+
*
|
|
283
|
+
* @param {string} url Resolved URL.
|
|
284
|
+
* @returns {boolean}
|
|
285
|
+
*/
|
|
286
|
+
function isNpmGitSource(url) {
|
|
287
|
+
return (
|
|
288
|
+
url.startsWith("git+") ||
|
|
289
|
+
url.startsWith("git://") ||
|
|
290
|
+
url.startsWith("git@") ||
|
|
291
|
+
url.startsWith("ssh://")
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* The registry to look an npm package up at, or undefined when no registry
|
|
297
|
+
* lookup may be made for it.
|
|
298
|
+
*
|
|
299
|
+
* A public registry cannot hold the project itself, a workspace or link
|
|
300
|
+
* member, a file or git source, or a package another registry serves. Sending
|
|
301
|
+
* those names out leaks them and imports a look-alike's metadata, so the
|
|
302
|
+
* component keeps whatever the lockfile and the manifests already say.
|
|
303
|
+
*
|
|
304
|
+
* A scope that .npmrc maps to another registry, or whose tarball another
|
|
305
|
+
* registry served, is looked up at that registry. In secure mode cdxgen does
|
|
306
|
+
* not contact a host the scanned project names, so such a package is not
|
|
307
|
+
* looked up at all.
|
|
308
|
+
*
|
|
309
|
+
* @param {Object} p Package object carrying the properties the npm parsers record.
|
|
310
|
+
* @param {string} defaultRegistryUrl The registry the caller resolves against.
|
|
311
|
+
* @param {Object} [npmrcConfig] Merged .npmrc configuration, for scope mappings.
|
|
312
|
+
* @param {boolean} [secureMode] Whether secure mode is on.
|
|
313
|
+
* @returns {string|undefined} Registry base URL, or undefined to skip.
|
|
314
|
+
*/
|
|
315
|
+
function npmLookupRegistry(p, defaultRegistryUrl, npmrcConfig, secureMode) {
|
|
316
|
+
if (p.type === "application") {
|
|
317
|
+
return undefined;
|
|
318
|
+
}
|
|
319
|
+
const props = {};
|
|
320
|
+
for (const prop of p.properties || []) {
|
|
321
|
+
props[prop.name] = prop.value;
|
|
322
|
+
}
|
|
323
|
+
if (
|
|
324
|
+
props["cdx:npm:isWorkspace"] === "true" ||
|
|
325
|
+
props["cdx:npm:isLink"] === "true" ||
|
|
326
|
+
props["cdx:npm:isRegistryDependency"] === "false" ||
|
|
327
|
+
props["cdx:npm:resolvedPath"]
|
|
328
|
+
) {
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
331
|
+
const manifestSourceType = props["cdx:npm:manifestSourceType"];
|
|
332
|
+
if (manifestSourceType && manifestSourceType !== "registry") {
|
|
333
|
+
return undefined;
|
|
334
|
+
}
|
|
335
|
+
const resolvedUrl =
|
|
336
|
+
props["internal:ResolvedUrl"] ||
|
|
337
|
+
p.externalReferences?.find((ref) => ref.type === "distribution")?.url;
|
|
338
|
+
if (resolvedUrl && isNpmGitSource(resolvedUrl)) {
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
if (p.group) {
|
|
342
|
+
const scope = p.group.startsWith("@") ? p.group : `@${p.group}`;
|
|
343
|
+
const scopeRegistry = normalizeNpmRegistryUrl(
|
|
344
|
+
npmrcConfig?.[`${scope}:registry`],
|
|
345
|
+
);
|
|
346
|
+
let registry;
|
|
347
|
+
if (scopeRegistry) {
|
|
348
|
+
registry = scopeRegistry;
|
|
349
|
+
} else if (resolvedUrl) {
|
|
350
|
+
registry = isDefaultNpmRegistry(resolvedUrl, defaultRegistryUrl)
|
|
351
|
+
? defaultRegistryUrl
|
|
352
|
+
: npmTarballRegistry(resolvedUrl, `${scope}/${p.name}`);
|
|
353
|
+
}
|
|
354
|
+
if (registry && !isDefaultNpmRegistry(registry, defaultRegistryUrl)) {
|
|
355
|
+
return secureMode ? undefined : registry;
|
|
356
|
+
}
|
|
357
|
+
if (resolvedUrl && !registry) {
|
|
358
|
+
// Served by another registry, but not in a layout that names it.
|
|
359
|
+
return undefined;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return defaultRegistryUrl;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Whether every field the registry is asked for is already on the component.
|
|
367
|
+
*
|
|
368
|
+
* A lockfile or an installed package.json that already states the licence, the
|
|
369
|
+
* description and the repository has answered the question a packument would be
|
|
370
|
+
* asked, so the package is not looked up at all. A licence the project gave is
|
|
371
|
+
* never replaced by the registry's view even when a lookup happens for the
|
|
372
|
+
* other fields.
|
|
373
|
+
*
|
|
374
|
+
* @param {Object} p Package object
|
|
375
|
+
* @returns {boolean}
|
|
376
|
+
*/
|
|
377
|
+
function npmMetadataComplete(p) {
|
|
378
|
+
// A lockfile records the repository as a vcs external reference rather than
|
|
379
|
+
// the repository field the registry path sets, so both count.
|
|
380
|
+
const hasRepository =
|
|
381
|
+
p.repository?.url ||
|
|
382
|
+
(typeof p.repository === "string" && p.repository) ||
|
|
383
|
+
(p.externalReferences || []).some((ref) => ref.type === "vcs" && ref.url);
|
|
384
|
+
return Boolean(p.license && p.description && hasRepository);
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* The license an npm registry document declares for a version, read like the
|
|
389
|
+
* manifest it came from: the registry still serves the legacy `licenses` array
|
|
390
|
+
* of packages such as fuzzy 0.1.3 (issue 4466). The version's own manifest
|
|
391
|
+
* wins over the document-level field.
|
|
392
|
+
*
|
|
393
|
+
* @param {Object} body Registry document (packument).
|
|
394
|
+
* @param {string} version Version of the package.
|
|
395
|
+
* @returns {string|Object|Array|undefined} The declared license, if any.
|
|
396
|
+
*/
|
|
397
|
+
function npmRegistryLicense(body, version) {
|
|
398
|
+
return (
|
|
399
|
+
resolveNpmLicense(body?.versions?.[version]) || resolveNpmLicense(body)
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
185
403
|
/**
|
|
186
404
|
* Method to retrieve metadata for npm packages by querying npmjs
|
|
187
405
|
*
|
|
406
|
+
* Packages a public registry cannot hold are skipped: the project itself,
|
|
407
|
+
* workspace and link members, file and git sources, and scopes served by
|
|
408
|
+
* another registry. See {@link npmLookupRegistry}. A package whose licence,
|
|
409
|
+
* description and repository the project or lockfile already supplied is left
|
|
410
|
+
* alone as well: the registry answers only what is still missing, and never
|
|
411
|
+
* overwrites a licence that is already set. The registry's licence is read
|
|
412
|
+
* with its legacy `licenses` field, and only a package for which neither the
|
|
413
|
+
* package nor the registry names one falls back to its repository's licence.
|
|
414
|
+
*
|
|
188
415
|
* @param {Array} pkgList Package list
|
|
416
|
+
* @param {string} [registryUrl] Registry to query instead of NPM_URL
|
|
417
|
+
* @param {Object} [options] Options
|
|
418
|
+
* @param {Object} [options.npmrcConfig] Merged .npmrc configuration, used to
|
|
419
|
+
* resolve a scope's own registry
|
|
420
|
+
* @param {boolean} [options.secureMode] Whether secure mode is on. Defaults to
|
|
421
|
+
* the process-wide setting.
|
|
189
422
|
*/
|
|
190
|
-
export async function getNpmMetadata(pkgList, registryUrl) {
|
|
423
|
+
export async function getNpmMetadata(pkgList, registryUrl, options = {}) {
|
|
191
424
|
const NPM_URL =
|
|
192
425
|
registryUrl ||
|
|
193
426
|
readEnvironmentVariable("NPM_URL") ||
|
|
194
427
|
"https://registry.npmjs.org/";
|
|
428
|
+
const secureMode = options.secureMode ?? isSecureMode;
|
|
195
429
|
const cdepList = [];
|
|
430
|
+
// The registry for every package is decided up front, so the batch and the
|
|
431
|
+
// loop below cannot disagree about who is skipped and who is asked where.
|
|
432
|
+
const lookupUrls = new Map();
|
|
433
|
+
// Provenance is never held locally, so when it is wanted every package is
|
|
434
|
+
// still asked about; the local fields survive the round either way.
|
|
435
|
+
const wantsProvenance = shouldFetchRegistryProvenance();
|
|
436
|
+
for (const p of pkgList) {
|
|
437
|
+
if (!wantsProvenance && npmMetadataComplete(p)) {
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
const registry = npmLookupRegistry(
|
|
441
|
+
p,
|
|
442
|
+
NPM_URL,
|
|
443
|
+
options.npmrcConfig,
|
|
444
|
+
secureMode,
|
|
445
|
+
);
|
|
446
|
+
if (registry) {
|
|
447
|
+
const base = registry.endsWith("/") ? registry : `${registry}/`;
|
|
448
|
+
lookupUrls.set(p, base + npmRegistryKey(p));
|
|
449
|
+
}
|
|
450
|
+
}
|
|
196
451
|
// Every URL this loop needs is known up front, so they are fetched
|
|
197
452
|
// concurrently before the loop rather than one at a time inside it. The
|
|
198
453
|
// derivation below is unchanged and still reads `body`; only where `body`
|
|
199
|
-
// comes from differs.
|
|
454
|
+
// comes from differs. A packument this run already holds, from another
|
|
455
|
+
// lockfile or an earlier pass, is not fetched again.
|
|
200
456
|
const prefetched = await prefetchJson(
|
|
201
457
|
prefetchEnabled()
|
|
202
|
-
?
|
|
458
|
+
? [...lookupUrls.values()]
|
|
459
|
+
.filter((url) => !npmDocuments.has(url))
|
|
460
|
+
.map((url) => ({ url }))
|
|
203
461
|
: [],
|
|
204
462
|
);
|
|
205
463
|
// The licence fallback calls getRepoLicense with a URL that only exists once
|
|
206
464
|
// the registry document has arrived, so it needs a second batched round. The
|
|
207
465
|
// documents are already in hand here, which is why this can be done without a
|
|
208
466
|
// second pass over the network for the registry itself.
|
|
209
|
-
if (prefetched.size) {
|
|
467
|
+
if (prefetched.size || npmDocuments.size) {
|
|
210
468
|
const repoUrls = [];
|
|
211
469
|
for (const p of pkgList) {
|
|
212
|
-
const
|
|
213
|
-
|
|
470
|
+
const url = lookupUrls.get(p);
|
|
471
|
+
const entry = url ? prefetched.get(url) : undefined;
|
|
472
|
+
const body = entry?.ok ? entry.body : npmDocuments.get(url);
|
|
473
|
+
if (!body) {
|
|
214
474
|
continue;
|
|
215
475
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
476
|
+
if (
|
|
477
|
+
!p.license &&
|
|
478
|
+
!npmRegistryLicense(body, p.version) &&
|
|
479
|
+
body?.repository?.url
|
|
480
|
+
) {
|
|
219
481
|
repoUrls.push(body.repository.url);
|
|
220
482
|
}
|
|
221
483
|
}
|
|
222
484
|
await prefetchRepoLicenses(repoUrls);
|
|
223
485
|
}
|
|
224
486
|
for (const p of pkgList) {
|
|
487
|
+
const url = lookupUrls.get(p);
|
|
488
|
+
if (!url) {
|
|
489
|
+
cdepList.push(p);
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
225
492
|
try {
|
|
226
|
-
const key = npmRegistryKey(p);
|
|
227
493
|
// Namespace the cache by registry so packages resolved from a
|
|
228
494
|
// non-default registry (e.g. jsr's npm mirror at npm.jsr.io) do not
|
|
229
495
|
// collide with identically-named packages on the default registry.
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (metadata_cache[cacheKey]) {
|
|
233
|
-
body = metadata_cache[cacheKey];
|
|
234
|
-
} else {
|
|
235
|
-
const url = NPM_URL + key;
|
|
496
|
+
let body = npmDocuments.get(url);
|
|
497
|
+
if (!body) {
|
|
236
498
|
const res =
|
|
237
499
|
prefetchedResponse(prefetched, url) ||
|
|
238
|
-
(await
|
|
239
|
-
responseType: "json",
|
|
240
|
-
}));
|
|
500
|
+
(await gatedGet(url, { responseType: "json" }));
|
|
241
501
|
body = res.body;
|
|
242
|
-
|
|
502
|
+
npmDocuments.set(url, body);
|
|
243
503
|
}
|
|
504
|
+
// The registry fills only what the project, the lockfile or an
|
|
505
|
+
// installed manifest left open, so a local answer survives.
|
|
244
506
|
p.description =
|
|
245
|
-
|
|
507
|
+
p.description ||
|
|
508
|
+
body.versions?.[p.version]?.description ||
|
|
509
|
+
body.description;
|
|
246
510
|
p.license =
|
|
247
|
-
|
|
248
|
-
body.
|
|
511
|
+
p.license ||
|
|
512
|
+
npmRegistryLicense(body, p.version) ||
|
|
249
513
|
(await getRepoLicense(body.repository?.url, undefined));
|
|
250
|
-
if (body.repository?.url) {
|
|
514
|
+
if (body.repository?.url && !p.repository) {
|
|
251
515
|
p.repository = { url: body.repository.url };
|
|
252
516
|
}
|
|
253
|
-
if (body.homepage) {
|
|
517
|
+
if (body.homepage && !p.homepage) {
|
|
254
518
|
p.homepage = { url: body.homepage };
|
|
255
519
|
}
|
|
256
520
|
// Capture the resolved tarball as a distribution external reference when
|
|
@@ -295,445 +559,604 @@ export async function getNpmMetadata(pkgList, registryUrl) {
|
|
|
295
559
|
* @returns {Object|null} Object containing jarPath, sha1, and pomPath, or null
|
|
296
560
|
*/
|
|
297
561
|
export function findLocalMvnArtifact(group, name, version) {
|
|
298
|
-
|
|
562
|
+
const found = findLocalMavenArtifact(group, name, version);
|
|
563
|
+
if (!found) {
|
|
299
564
|
return null;
|
|
300
565
|
}
|
|
566
|
+
return {
|
|
567
|
+
jarPath: found.jarPath || null,
|
|
568
|
+
sha1: found.sha1 || null,
|
|
569
|
+
pomPath: found.pomPath || null,
|
|
570
|
+
};
|
|
571
|
+
}
|
|
301
572
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
573
|
+
// Options for reading a POM with xml2js, shared by every POM reader here.
|
|
574
|
+
const POM_XML_OPTIONS = {
|
|
575
|
+
compact: true,
|
|
576
|
+
spaces: 4,
|
|
577
|
+
textKey: "_",
|
|
578
|
+
attributesKey: "$",
|
|
579
|
+
commentKey: "value",
|
|
580
|
+
};
|
|
309
581
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
if (existsSync(gradlePath)) {
|
|
313
|
-
try {
|
|
314
|
-
const subdirs = readdirSync(gradlePath);
|
|
315
|
-
let jarPath = null;
|
|
316
|
-
let sha1 = null;
|
|
317
|
-
let pomPath = null;
|
|
318
|
-
for (const subdir of subdirs) {
|
|
319
|
-
const subdirPath = join(gradlePath, subdir);
|
|
320
|
-
if (statSync(subdirPath).isDirectory()) {
|
|
321
|
-
const files = readdirSync(subdirPath);
|
|
322
|
-
for (const file of files) {
|
|
323
|
-
if (
|
|
324
|
-
file.endsWith(".jar") &&
|
|
325
|
-
!file.endsWith("-sources.jar") &&
|
|
326
|
-
!file.endsWith("-javadoc.jar")
|
|
327
|
-
) {
|
|
328
|
-
jarPath = join(subdirPath, file);
|
|
329
|
-
sha1 = subdir;
|
|
330
|
-
} else if (file.endsWith(".pom")) {
|
|
331
|
-
pomPath = join(subdirPath, file);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
if (jarPath || pomPath) {
|
|
337
|
-
return { jarPath, sha1, pomPath };
|
|
338
|
-
}
|
|
339
|
-
} catch (_err) {
|
|
340
|
-
// ignore
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
}
|
|
582
|
+
// A POM chain is walked at most this deep. Real hierarchies stop well short.
|
|
583
|
+
const MAX_POM_PARENT_DEPTH = 8;
|
|
344
584
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
585
|
+
// Groups published to Google's Maven repository rather than Maven Central.
|
|
586
|
+
const GOOGLE_MAVEN_GROUP_PREFIXES = [
|
|
587
|
+
"android.arch.",
|
|
588
|
+
"androidx.",
|
|
589
|
+
"com.android.",
|
|
590
|
+
"com.google.ads.mediation",
|
|
591
|
+
"com.google.android.",
|
|
592
|
+
"com.google.ar",
|
|
593
|
+
"com.google.firebase",
|
|
594
|
+
"com.google.gms",
|
|
595
|
+
"com.google.mlkit",
|
|
596
|
+
"com.google.testing.platform",
|
|
597
|
+
];
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* A repository base URL with exactly one trailing slash, ready for
|
|
601
|
+
* {@link composePomXmlUrl}.
|
|
602
|
+
*
|
|
603
|
+
* @param {string} url Repository URL.
|
|
604
|
+
* @returns {string} Normalised URL.
|
|
605
|
+
*/
|
|
606
|
+
function withTrailingSlash(url) {
|
|
607
|
+
return url.endsWith("/") ? url : `${url}/`;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* The repository a package's POM should be fetched from, or undefined when no
|
|
612
|
+
* public repository can hold it: snapshots never reach Maven Central, the
|
|
613
|
+
* project's own modules are not published, and a file:// repository is local.
|
|
614
|
+
*
|
|
615
|
+
* @param {Object} p Package.
|
|
616
|
+
* @param {{MAVEN_CENTRAL_URL: string, ANDROID_MAVEN_URL: string}} repos Repository base URLs.
|
|
617
|
+
* @param {{skipPurls?: Set<string>}} context Packages the caller knows are its own.
|
|
618
|
+
* @returns {string|undefined} Repository base URL.
|
|
619
|
+
*/
|
|
620
|
+
function mavenRepositoryFor(p, repos, context) {
|
|
621
|
+
const group = p.group || "";
|
|
622
|
+
if (!group || !p.name || !p.version || p.version.endsWith("-SNAPSHOT")) {
|
|
623
|
+
return undefined;
|
|
624
|
+
}
|
|
625
|
+
if (
|
|
626
|
+
context?.skipPurls?.has(p.purl) ||
|
|
627
|
+
context?.skipPurls?.has(p["bom-ref"])
|
|
628
|
+
) {
|
|
629
|
+
return undefined;
|
|
630
|
+
}
|
|
631
|
+
let repositoryUrl;
|
|
632
|
+
const query = p.purl?.includes("?") ? p.purl.split("?")[1] : "";
|
|
633
|
+
if (query) {
|
|
634
|
+
repositoryUrl = new URLSearchParams(query.split("#")[0]).get(
|
|
635
|
+
"repository_url",
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
if (repositoryUrl?.startsWith("file:")) {
|
|
639
|
+
return undefined;
|
|
640
|
+
}
|
|
641
|
+
let repositoryHost;
|
|
642
|
+
try {
|
|
643
|
+
repositoryHost = repositoryUrl ? new URL(repositoryUrl).hostname : "";
|
|
644
|
+
} catch (_err) {
|
|
645
|
+
repositoryHost = "";
|
|
646
|
+
}
|
|
647
|
+
if (
|
|
648
|
+
["maven.google.com", "dl.google.com"].includes(repositoryHost) ||
|
|
649
|
+
group.includes("android") ||
|
|
650
|
+
GOOGLE_MAVEN_GROUP_PREFIXES.some(
|
|
651
|
+
(prefix) => group === prefix || group.startsWith(prefix),
|
|
652
|
+
)
|
|
653
|
+
) {
|
|
654
|
+
return repos.ANDROID_MAVEN_URL;
|
|
365
655
|
}
|
|
656
|
+
return repos.MAVEN_CENTRAL_URL;
|
|
657
|
+
}
|
|
366
658
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
homedir(),
|
|
379
|
-
"AppData",
|
|
380
|
-
"Local",
|
|
381
|
-
"Coursier",
|
|
382
|
-
"Cache",
|
|
383
|
-
"v1",
|
|
384
|
-
);
|
|
385
|
-
}
|
|
386
|
-
} else {
|
|
387
|
-
cacheRoot = join(homedir(), ".cache", "coursier", "v1");
|
|
388
|
-
}
|
|
659
|
+
/**
|
|
660
|
+
* Whether a package already carries licence information, in either the
|
|
661
|
+
* `license` field cdxgen fills or the CycloneDX `licenses` array that the
|
|
662
|
+
* cyclonedx-maven-plugin and Quarkus emit.
|
|
663
|
+
*
|
|
664
|
+
* @param {Object} p Package.
|
|
665
|
+
* @returns {boolean}
|
|
666
|
+
*/
|
|
667
|
+
function hasMavenLicense(p) {
|
|
668
|
+
if (Array.isArray(p.license)) {
|
|
669
|
+
return p.license.some(Boolean);
|
|
389
670
|
}
|
|
671
|
+
return Boolean(p.license) || Boolean(p.licenses?.length);
|
|
672
|
+
}
|
|
390
673
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
!match.endsWith("-javadoc.jar")
|
|
404
|
-
) {
|
|
405
|
-
const localPath = join(cacheRoot, match);
|
|
406
|
-
if (existsSync(localPath)) {
|
|
407
|
-
jarPath = localPath;
|
|
408
|
-
}
|
|
409
|
-
} else if (match.endsWith(".pom")) {
|
|
410
|
-
const localPath = join(cacheRoot, match);
|
|
411
|
-
if (existsSync(localPath)) {
|
|
412
|
-
pomPath = localPath;
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
if (jarPath || pomPath) {
|
|
417
|
-
return { jarPath, pomPath };
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
} catch (_err) {
|
|
421
|
-
// ignore
|
|
422
|
-
}
|
|
674
|
+
/**
|
|
675
|
+
* Set the licences a jar manifest's Bundle-License names on a package: one
|
|
676
|
+
* value as is, several as a list.
|
|
677
|
+
*
|
|
678
|
+
* @param {Object} p Package.
|
|
679
|
+
* @param {string[]|undefined} licenses Values from parseManifestLicenseList.
|
|
680
|
+
*/
|
|
681
|
+
function applyManifestLicenses(p, licenses) {
|
|
682
|
+
if (licenses?.length === 1) {
|
|
683
|
+
p.license = licenses[0];
|
|
684
|
+
} else if (licenses?.length > 1) {
|
|
685
|
+
p.license = licenses;
|
|
423
686
|
}
|
|
687
|
+
}
|
|
424
688
|
|
|
425
|
-
|
|
689
|
+
/**
|
|
690
|
+
* Collapse the whitespace a POM description carries over from XML indentation.
|
|
691
|
+
*
|
|
692
|
+
* @param {string} description Raw description.
|
|
693
|
+
* @returns {string} Tidied description.
|
|
694
|
+
*/
|
|
695
|
+
function tidyPomDescription(description) {
|
|
696
|
+
return description
|
|
697
|
+
.replace(/[ \t]+/g, " ")
|
|
698
|
+
.replace(/^[ \t]+|[ \t]+$/gm, "")
|
|
699
|
+
.replace(/\n\s*\n/g, "\n")
|
|
700
|
+
.trim();
|
|
426
701
|
}
|
|
427
702
|
|
|
428
703
|
/**
|
|
429
|
-
*
|
|
704
|
+
* The licence a POM names in a comment ahead of its project element, as some
|
|
705
|
+
* projects do instead of declaring `<licenses>`.
|
|
430
706
|
*
|
|
431
|
-
* @param {
|
|
432
|
-
* @
|
|
433
|
-
|
|
707
|
+
* @param {string} pomXml POM text.
|
|
708
|
+
* @returns {string|undefined} Licence id.
|
|
709
|
+
*/
|
|
710
|
+
function licenseFromPomComment(pomXml) {
|
|
711
|
+
const match = /<!--([\s\S]*?)-->[\s\n]*<project/m.exec(pomXml || "");
|
|
712
|
+
return match?.[1] ? findLicenseId(match[1].trim()) : undefined;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
/**
|
|
716
|
+
* Fill publisher, description, repository and licence from a merged POM,
|
|
717
|
+
* keeping any value the package already has.
|
|
434
718
|
*
|
|
435
|
-
* @
|
|
719
|
+
* @param {Object} p Package.
|
|
720
|
+
* @param {Object} pomJson Merged POM (xml2js compact form).
|
|
721
|
+
* @param {string} [ownPomXml] The package's own POM text, for a licence comment.
|
|
722
|
+
* @returns {void}
|
|
436
723
|
*/
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
force = false,
|
|
441
|
-
) {
|
|
442
|
-
const MAVEN_CENTRAL_URL =
|
|
443
|
-
readEnvironmentVariable("MAVEN_CENTRAL_URL") ||
|
|
444
|
-
"https://repo1.maven.org/maven2/";
|
|
445
|
-
const ANDROID_MAVEN_URL =
|
|
446
|
-
readEnvironmentVariable("ANDROID_MAVEN_URL") || "https://maven.google.com/";
|
|
447
|
-
const cdepList = [];
|
|
448
|
-
if (!pkgList?.length) {
|
|
449
|
-
return pkgList;
|
|
724
|
+
function applyPomMetadata(p, pomJson, ownPomXml) {
|
|
725
|
+
if (!pomJson) {
|
|
726
|
+
return;
|
|
450
727
|
}
|
|
451
|
-
if (
|
|
452
|
-
|
|
728
|
+
if (!p.publisher && pomJson.organization?.name?._) {
|
|
729
|
+
p.publisher = pomJson.organization.name._;
|
|
453
730
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
// inside fetchPomXmlAsJson) depend on the direct POM's content and stay
|
|
457
|
-
// serial. The conditions below mirror the loop's skip checks so the batch
|
|
458
|
-
// does not request anything the loop would skip.
|
|
459
|
-
const batchUrls = [];
|
|
460
|
-
const seenPomUrls = new Set();
|
|
461
|
-
for (const p of pkgList) {
|
|
462
|
-
const group = p.group || "";
|
|
463
|
-
if (
|
|
464
|
-
!p.version ||
|
|
465
|
-
(group && p.name && p.version && !shouldFetchLicense() && !force) ||
|
|
466
|
-
(group && p.name && p.version && p.license)
|
|
467
|
-
) {
|
|
468
|
-
continue;
|
|
469
|
-
}
|
|
470
|
-
if (!group || group === "") {
|
|
471
|
-
continue;
|
|
472
|
-
}
|
|
473
|
-
const urlPrefix =
|
|
474
|
-
group.indexOf("android") !== -1 ? ANDROID_MAVEN_URL : MAVEN_CENTRAL_URL;
|
|
475
|
-
const pomUrl = composePomXmlUrl({
|
|
476
|
-
urlPrefix,
|
|
477
|
-
group,
|
|
478
|
-
name: p.name,
|
|
479
|
-
version: p.version,
|
|
480
|
-
});
|
|
481
|
-
if (!seenPomUrls.has(pomUrl)) {
|
|
482
|
-
seenPomUrls.add(pomUrl);
|
|
483
|
-
batchUrls.push({ url: pomUrl, responseType: "text" });
|
|
484
|
-
}
|
|
731
|
+
if (!p.description && pomJson.description?._) {
|
|
732
|
+
p.description = tidyPomDescription(pomJson.description._);
|
|
485
733
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
repoUrls.push(scmUrl);
|
|
496
|
-
}
|
|
734
|
+
if (!p.repository && pomJson.scm?.url?._) {
|
|
735
|
+
p.repository = { url: pomJson.scm.url._ };
|
|
736
|
+
}
|
|
737
|
+
if (!hasMavenLicense(p)) {
|
|
738
|
+
const license =
|
|
739
|
+
parseLicenseEntryOrArrayFromPomXml(pomJson.licenses?.license) ||
|
|
740
|
+
licenseFromPomComment(ownPomXml);
|
|
741
|
+
if (license) {
|
|
742
|
+
p.license = license;
|
|
497
743
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Read a POM from the local Maven, Gradle or Coursier cache.
|
|
749
|
+
*
|
|
750
|
+
* @param {string} group groupId.
|
|
751
|
+
* @param {string} name artifactId.
|
|
752
|
+
* @param {string} version Version.
|
|
753
|
+
* @returns {string|undefined} POM text.
|
|
754
|
+
*/
|
|
755
|
+
function readLocalPom(group, name, version) {
|
|
756
|
+
const pomPath = findLocalMavenArtifact(group, name, version)?.pomPath;
|
|
757
|
+
if (!pomPath) {
|
|
758
|
+
return undefined;
|
|
504
759
|
}
|
|
505
760
|
try {
|
|
506
|
-
return
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
});
|
|
510
|
-
} finally {
|
|
511
|
-
// Each body is read once, by the loop below. Holding them past that only
|
|
512
|
-
// grows the process, which matters in server mode where one process serves
|
|
513
|
-
// many scans. Only the documents this call fetched are dropped, so a scan
|
|
514
|
-
// running alongside this one keeps its own.
|
|
515
|
-
for (const pomUrl of ownPomUrls) {
|
|
516
|
-
prefetchedPoms.delete(pomUrl);
|
|
517
|
-
}
|
|
761
|
+
return readFileSync(pomPath, "utf-8");
|
|
762
|
+
} catch (_err) {
|
|
763
|
+
return undefined;
|
|
518
764
|
}
|
|
519
765
|
}
|
|
520
766
|
|
|
521
767
|
/**
|
|
522
|
-
*
|
|
523
|
-
* by {@link getMvnMetadata}'s batch round.
|
|
768
|
+
* Load a POM and its parents and merge them child over parent.
|
|
524
769
|
*
|
|
525
|
-
*
|
|
526
|
-
*
|
|
527
|
-
*
|
|
528
|
-
*
|
|
529
|
-
*
|
|
530
|
-
*
|
|
531
|
-
*
|
|
770
|
+
* Every level is read from the local caches first; a build has always
|
|
771
|
+
* downloaded the parents it needed. A level missing locally is fetched only
|
|
772
|
+
* when `remote` is set, and at most `remoteParents` parent levels are fetched,
|
|
773
|
+
* so a cache miss costs no more requests than it always did. The walk stops
|
|
774
|
+
* once licence, organization and scm are all known. A missing parent ends the
|
|
775
|
+
* walk without discarding what the child and nearer parents declared.
|
|
776
|
+
*
|
|
777
|
+
* @param {{urlPrefix?: string, group: string, name: string, version: string}} coordinates
|
|
778
|
+
* @param {{remote?: boolean, remoteParents?: number}} [opts]
|
|
779
|
+
* @returns {Promise<{merged: Object, ownXml: string}|undefined>}
|
|
532
780
|
*/
|
|
533
|
-
async function
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
jarNSMapping,
|
|
537
|
-
force,
|
|
538
|
-
{ MAVEN_CENTRAL_URL, ANDROID_MAVEN_URL },
|
|
781
|
+
async function loadPomChain(
|
|
782
|
+
coordinates,
|
|
783
|
+
{ remote = false, remoteParents = 1 } = {},
|
|
539
784
|
) {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
},
|
|
554
|
-
],
|
|
555
|
-
},
|
|
556
|
-
};
|
|
557
|
-
}
|
|
558
|
-
if (jarNSMapping[p.purl].hashes && !p?.hashes?.length) {
|
|
559
|
-
p.hashes = jarNSMapping[p.purl].hashes;
|
|
560
|
-
}
|
|
561
|
-
if (
|
|
562
|
-
jarNSMapping[p.purl].namespaces &&
|
|
563
|
-
jarNSMapping[p.purl].namespaces.length
|
|
564
|
-
) {
|
|
565
|
-
if (!p.properties) {
|
|
566
|
-
p.properties = [];
|
|
567
|
-
}
|
|
568
|
-
p.properties.push({
|
|
569
|
-
name: "internal:Namespaces",
|
|
570
|
-
value: jarNSMapping[p.purl].namespaces.join("\n"),
|
|
571
|
-
});
|
|
785
|
+
const chain = [];
|
|
786
|
+
let current = coordinates;
|
|
787
|
+
let remoteParentAttempts = 0;
|
|
788
|
+
for (let depth = 0; current && depth < MAX_POM_PARENT_DEPTH; depth++) {
|
|
789
|
+
let xml = readLocalPom(current.group, current.name, current.version);
|
|
790
|
+
if (
|
|
791
|
+
!xml &&
|
|
792
|
+
remote &&
|
|
793
|
+
coordinates.urlPrefix &&
|
|
794
|
+
(depth === 0 || remoteParentAttempts < remoteParents)
|
|
795
|
+
) {
|
|
796
|
+
if (depth > 0) {
|
|
797
|
+
remoteParentAttempts++;
|
|
572
798
|
}
|
|
799
|
+
xml = await fetchRemotePomXml({
|
|
800
|
+
urlPrefix: coordinates.urlPrefix,
|
|
801
|
+
group: current.group,
|
|
802
|
+
name: current.name,
|
|
803
|
+
version: current.version,
|
|
804
|
+
});
|
|
573
805
|
}
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
if (localArtifact.jarPath && (!p.hashes || p.hashes.length === 0)) {
|
|
577
|
-
try {
|
|
578
|
-
const hashValues = await multiChecksumFile(
|
|
579
|
-
["md5", "sha1", "sha256", "sha512"],
|
|
580
|
-
localArtifact.jarPath,
|
|
581
|
-
);
|
|
582
|
-
p.hashes = [
|
|
583
|
-
{ alg: "MD5", content: hashValues["md5"] },
|
|
584
|
-
{ alg: "SHA-1", content: hashValues["sha1"] },
|
|
585
|
-
{ alg: "SHA-256", content: hashValues["sha256"] },
|
|
586
|
-
{ alg: "SHA-512", content: hashValues["sha512"] },
|
|
587
|
-
];
|
|
588
|
-
} catch (_err) {
|
|
589
|
-
if (localArtifact.sha1) {
|
|
590
|
-
p.hashes = [{ alg: "SHA-1", content: localArtifact.sha1 }];
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
if (localArtifact.pomPath) {
|
|
595
|
-
try {
|
|
596
|
-
const xmlData = readFileSync(localArtifact.pomPath, "utf-8");
|
|
597
|
-
const pomData = parsePomXml(xmlData);
|
|
598
|
-
if (pomData) {
|
|
599
|
-
if (!p.publisher && pomData.organization?.name?._) {
|
|
600
|
-
p.publisher = pomData.organization.name._;
|
|
601
|
-
}
|
|
602
|
-
if (!p.description && pomData.description) {
|
|
603
|
-
p.description = pomData.description
|
|
604
|
-
.replace(/[ \t]+/g, " ")
|
|
605
|
-
.replace(/^[ \t]+|[ \t]+$/gm, "")
|
|
606
|
-
.replace(/\n\s*\n/g, "\n")
|
|
607
|
-
.trim();
|
|
608
|
-
}
|
|
609
|
-
if (!p.repository && pomData.scm) {
|
|
610
|
-
p.repository = { url: pomData.scm };
|
|
611
|
-
}
|
|
612
|
-
if (!p.license) {
|
|
613
|
-
const parsedLicense = parseLicenseEntryOrArrayFromPomXml(
|
|
614
|
-
pomData.licenses,
|
|
615
|
-
);
|
|
616
|
-
if (parsedLicense) {
|
|
617
|
-
p.license = parsedLicense;
|
|
618
|
-
} else {
|
|
619
|
-
const licenseRegex = /<!--([\s\S]*?)-->[\s\n]*<project/m;
|
|
620
|
-
const match = licenseRegex.exec(xmlData);
|
|
621
|
-
if (match?.[1]) {
|
|
622
|
-
const commentLicense = findLicenseId(match[1].trim());
|
|
623
|
-
if (commentLicense) {
|
|
624
|
-
p.license = commentLicense;
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
}
|
|
630
|
-
} catch (_err) {
|
|
631
|
-
// ignore
|
|
632
|
-
}
|
|
633
|
-
}
|
|
806
|
+
if (!xml) {
|
|
807
|
+
break;
|
|
634
808
|
}
|
|
635
|
-
|
|
636
|
-
|
|
809
|
+
let json;
|
|
810
|
+
try {
|
|
811
|
+
json = xml2js(xml, POM_XML_OPTIONS).project;
|
|
812
|
+
} catch (_err) {
|
|
813
|
+
json = undefined;
|
|
814
|
+
}
|
|
815
|
+
if (!json) {
|
|
816
|
+
break;
|
|
817
|
+
}
|
|
818
|
+
chain.push({ json, xml });
|
|
819
|
+
const merged = mergePomChain(chain);
|
|
637
820
|
if (
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
821
|
+
parseLicenseEntryOrArrayFromPomXml(merged.licenses?.license) &&
|
|
822
|
+
merged.organization?.name?._ &&
|
|
823
|
+
merged.scm?.url?._
|
|
641
824
|
) {
|
|
642
|
-
|
|
825
|
+
break;
|
|
826
|
+
}
|
|
827
|
+
const parent = json.parent;
|
|
828
|
+
current =
|
|
829
|
+
parent?.groupId?._ && parent?.artifactId?._ && parent?.version?._
|
|
830
|
+
? {
|
|
831
|
+
group: parent.groupId._,
|
|
832
|
+
name: parent.artifactId._,
|
|
833
|
+
version: parent.version._,
|
|
834
|
+
}
|
|
835
|
+
: undefined;
|
|
836
|
+
}
|
|
837
|
+
if (!chain.length) {
|
|
838
|
+
return undefined;
|
|
839
|
+
}
|
|
840
|
+
return { merged: mergePomChain(chain), ownXml: chain[0].xml };
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* Merge POM levels so that nearer levels win, as Maven inheritance does for
|
|
845
|
+
* the elements cdxgen reads.
|
|
846
|
+
*
|
|
847
|
+
* @param {Array<{json: Object}>} chain Levels, child first.
|
|
848
|
+
* @returns {Object} Merged POM.
|
|
849
|
+
*/
|
|
850
|
+
function mergePomChain(chain) {
|
|
851
|
+
let merged = {};
|
|
852
|
+
for (let i = chain.length - 1; i >= 0; i--) {
|
|
853
|
+
merged = { ...merged, ...chain[i].json };
|
|
854
|
+
}
|
|
855
|
+
return merged;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* A lookup of the jar namespace entry for a package. The jar data is keyed by the
|
|
860
|
+
* POM coordinates of the jar, while sbt (and the Mill and scala-cli paths)
|
|
861
|
+
* emit components with the Scala binary suffix stripped and a repository_url
|
|
862
|
+
* qualifier. Both spellings resolve to the same qualifier-free coordinate
|
|
863
|
+
* key, with the exact purl still winning when it matches. A cache can hold
|
|
864
|
+
* the same library for several Scala versions (`foo_2.13` and `foo_3`); the
|
|
865
|
+
* jar whose binary version matches the component's cdx:scala:compilerVersion
|
|
866
|
+
* speaks for it, and when that cannot be told only the namespaces, which the
|
|
867
|
+
* cross builds share, are taken.
|
|
868
|
+
*
|
|
869
|
+
* @param {Object} jarNSMapping Namespace and hash data from jar analysis.
|
|
870
|
+
* @returns {(p: Object) => {entry?: Object, exact?: Boolean}} Entry lookup.
|
|
871
|
+
*/
|
|
872
|
+
function scalaJarEntryLookup(jarNSMapping) {
|
|
873
|
+
const coordinateIndex = new Map();
|
|
874
|
+
for (const apurl of Object.keys(jarNSMapping || {})) {
|
|
875
|
+
const coordinate = scalaCoordinateOfPurl(apurl);
|
|
876
|
+
if (!coordinate) {
|
|
643
877
|
continue;
|
|
644
878
|
}
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
879
|
+
if (!coordinateIndex.has(coordinate.key)) {
|
|
880
|
+
coordinateIndex.set(coordinate.key, new Map());
|
|
881
|
+
}
|
|
882
|
+
const byBinaryVersion = coordinateIndex.get(coordinate.key);
|
|
883
|
+
const binaryVersion = coordinate.binaryVersion || "";
|
|
884
|
+
if (!byBinaryVersion.has(binaryVersion)) {
|
|
885
|
+
byBinaryVersion.set(binaryVersion, apurl);
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
return (pkg) => {
|
|
889
|
+
if (!pkg.purl) {
|
|
890
|
+
return undefined;
|
|
891
|
+
}
|
|
892
|
+
if (jarNSMapping[pkg.purl]) {
|
|
893
|
+
return { entry: jarNSMapping[pkg.purl], exact: true };
|
|
894
|
+
}
|
|
895
|
+
const byBinaryVersion = coordinateIndex.get(
|
|
896
|
+
scalaCoordinateOfPurl(pkg.purl)?.key,
|
|
897
|
+
);
|
|
898
|
+
if (!byBinaryVersion) {
|
|
899
|
+
return undefined;
|
|
900
|
+
}
|
|
901
|
+
const wanted = pkg.properties?.find(
|
|
902
|
+
(prop) => prop.name === "cdx:scala:compilerVersion",
|
|
903
|
+
)?.value;
|
|
904
|
+
if (wanted && byBinaryVersion.has(wanted)) {
|
|
905
|
+
return { entry: jarNSMapping[byBinaryVersion.get(wanted)], exact: true };
|
|
906
|
+
}
|
|
907
|
+
const candidates = [...byBinaryVersion.values()];
|
|
908
|
+
return {
|
|
909
|
+
entry: jarNSMapping[candidates[0]],
|
|
910
|
+
exact: candidates.length === 1,
|
|
911
|
+
};
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
/**
|
|
916
|
+
* Enrich one package from data already on disk: the jar namespace map, the
|
|
917
|
+
* POM that jar analysis parsed, the local jar's hashes and the local POM
|
|
918
|
+
* chain. No request is made.
|
|
919
|
+
*
|
|
920
|
+
* @param {Object} p Package.
|
|
921
|
+
* @param {Function} jarNSEntryFor Jar namespace entry lookup for a package.
|
|
922
|
+
* @returns {Promise<void>}
|
|
923
|
+
*/
|
|
924
|
+
async function enrichMvnPackageLocally(p, jarNSEntryFor) {
|
|
925
|
+
const { entry: nsEntry, exact } = jarNSEntryFor(p) || {};
|
|
926
|
+
if (nsEntry) {
|
|
927
|
+
// A jar copied into a temporary directory, as Maven --deep copies the
|
|
928
|
+
// dependencies, is deleted with the scan, and its path would make the BOM
|
|
929
|
+
// differ between two runs. The identity the dependency tree gave the
|
|
930
|
+
// package stands.
|
|
931
|
+
if (nsEntry.jarFile && exact && !isInsideTmpDir(nsEntry.jarFile)) {
|
|
932
|
+
p.evidence = {
|
|
933
|
+
identity: {
|
|
934
|
+
field: "purl",
|
|
935
|
+
confidence: 0.8,
|
|
936
|
+
methods: [
|
|
937
|
+
{
|
|
938
|
+
technique: "binary-analysis",
|
|
939
|
+
confidence: 0.8,
|
|
940
|
+
value: nsEntry.jarFile,
|
|
941
|
+
},
|
|
942
|
+
],
|
|
943
|
+
},
|
|
944
|
+
};
|
|
945
|
+
}
|
|
946
|
+
if (nsEntry.hashes && exact && !p?.hashes?.length) {
|
|
947
|
+
p.hashes = nsEntry.hashes;
|
|
649
948
|
}
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
949
|
+
if (nsEntry.namespaces?.length) {
|
|
950
|
+
if (!p.properties) {
|
|
951
|
+
p.properties = [];
|
|
952
|
+
}
|
|
953
|
+
p.properties.push({
|
|
954
|
+
name: "internal:Namespaces",
|
|
955
|
+
value: nsEntry.namespaces.join("\n"),
|
|
956
|
+
});
|
|
654
957
|
}
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
};
|
|
661
|
-
try {
|
|
662
|
-
if (DEBUG_MODE) {
|
|
663
|
-
console.log(
|
|
664
|
-
`Querying ${pomMetadata.urlPrefix} for '${group}/${p.name}@${p.version}' ${composePomXmlUrl(
|
|
665
|
-
pomMetadata,
|
|
666
|
-
)}`,
|
|
667
|
-
);
|
|
958
|
+
// The POM jar analysis already parsed.
|
|
959
|
+
const nsPom = nsEntry.pom;
|
|
960
|
+
if (nsPom) {
|
|
961
|
+
if (!p.publisher && nsPom.organization?.name?._) {
|
|
962
|
+
p.publisher = nsPom.organization.name._;
|
|
668
963
|
}
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
: "";
|
|
680
|
-
if (bodyJson?.scm?.url) {
|
|
681
|
-
p.repository = { url: bodyJson.scm.url._ };
|
|
964
|
+
if (!p.description && nsPom.description) {
|
|
965
|
+
p.description = tidyPomDescription(nsPom.description);
|
|
966
|
+
}
|
|
967
|
+
if (!p.repository && nsPom.scm) {
|
|
968
|
+
p.repository = { url: nsPom.scm };
|
|
969
|
+
}
|
|
970
|
+
if (!hasMavenLicense(p)) {
|
|
971
|
+
const license = parseLicenseEntryOrArrayFromPomXml(nsPom.licenses);
|
|
972
|
+
if (license) {
|
|
973
|
+
p.license = license;
|
|
682
974
|
}
|
|
683
|
-
p.license =
|
|
684
|
-
parseLicenseEntryOrArrayFromPomXml(bodyJson?.licenses?.license) ||
|
|
685
|
-
(await extractLicenseCommentFromPomXml(pomMetadata)) ||
|
|
686
|
-
(await getRepoLicense(p.repository?.url, undefined));
|
|
687
975
|
}
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
976
|
+
}
|
|
977
|
+
// The jar's manifest names its licences even when no POM was parsed. A
|
|
978
|
+
// licence the package already has always wins.
|
|
979
|
+
if (!hasMavenLicense(p)) {
|
|
980
|
+
applyManifestLicenses(p, nsEntry.manifestLicenses);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
// Scala components are named without their Scala binary suffix, while the
|
|
984
|
+
// published artifact carries it. The published name is looked up first and
|
|
985
|
+
// the component name is the local fallback; a disk read costs nothing.
|
|
986
|
+
const publishedName = publishedArtifactId(p);
|
|
987
|
+
const primaryArtifact = findLocalMvnArtifact(
|
|
988
|
+
p.group,
|
|
989
|
+
publishedName,
|
|
990
|
+
p.version,
|
|
991
|
+
);
|
|
992
|
+
const localArtifact =
|
|
993
|
+
primaryArtifact ||
|
|
994
|
+
(publishedName !== p.name
|
|
995
|
+
? findLocalMvnArtifact(p.group, p.name, p.version)
|
|
996
|
+
: null);
|
|
997
|
+
const pomChainName = primaryArtifact ? publishedName : p.name;
|
|
998
|
+
if (localArtifact?.jarPath && (!p.hashes || p.hashes.length === 0)) {
|
|
999
|
+
try {
|
|
1000
|
+
const hashValues = await multiChecksumFile(
|
|
1001
|
+
["md5", "sha1", "sha256", "sha512"],
|
|
1002
|
+
localArtifact.jarPath,
|
|
1003
|
+
);
|
|
1004
|
+
p.hashes = [
|
|
1005
|
+
{ alg: "MD5", content: hashValues["md5"] },
|
|
1006
|
+
{ alg: "SHA-1", content: hashValues["sha1"] },
|
|
1007
|
+
{ alg: "SHA-256", content: hashValues["sha256"] },
|
|
1008
|
+
{ alg: "SHA-512", content: hashValues["sha512"] },
|
|
1009
|
+
];
|
|
1010
|
+
} catch (_err) {
|
|
1011
|
+
if (localArtifact.sha1) {
|
|
1012
|
+
p.hashes = [{ alg: "SHA-1", content: localArtifact.sha1 }];
|
|
694
1013
|
}
|
|
695
|
-
} finally {
|
|
696
|
-
cdepList.push(p);
|
|
697
1014
|
}
|
|
698
1015
|
}
|
|
699
|
-
|
|
1016
|
+
if (localArtifact?.pomPath) {
|
|
1017
|
+
const chain = await loadPomChain(
|
|
1018
|
+
{ group: p.group, name: pomChainName, version: p.version },
|
|
1019
|
+
{ remote: false },
|
|
1020
|
+
);
|
|
1021
|
+
applyPomMetadata(p, chain?.merged, chain?.ownXml);
|
|
1022
|
+
}
|
|
700
1023
|
}
|
|
701
1024
|
|
|
702
1025
|
/**
|
|
703
|
-
*
|
|
1026
|
+
* Method to retrieve metadata for maven packages, from the local caches first
|
|
1027
|
+
* and from Maven Central only for what remains.
|
|
1028
|
+
*
|
|
1029
|
+
* Every package is first enriched from data on disk. Only when license
|
|
1030
|
+
* fetching is enabled (FETCH_LICENSE) or `force` is set are the packages
|
|
1031
|
+
* still without a licence looked up remotely, and then only those a public
|
|
1032
|
+
* repository can hold.
|
|
704
1033
|
*
|
|
705
|
-
*
|
|
706
|
-
*
|
|
707
|
-
*
|
|
708
|
-
*
|
|
1034
|
+
* @param {Array} pkgList Package list
|
|
1035
|
+
* @param {Object} jarNSMapping Jar Namespace mapping object
|
|
1036
|
+
* @param {Boolean} force Force fetching of license
|
|
1037
|
+
* @param {{skipPurls?: Set<string>}} [context] Purls or bom-refs of the
|
|
1038
|
+
* project's own modules, which are never looked up remotely.
|
|
709
1039
|
*
|
|
710
|
-
* @
|
|
711
|
-
* @returns {String|undefined} The scm URL to prefetch a licence for, if any.
|
|
1040
|
+
* @returns {Array} Updated package list
|
|
712
1041
|
*/
|
|
713
|
-
function
|
|
714
|
-
|
|
715
|
-
|
|
1042
|
+
export async function getMvnMetadata(
|
|
1043
|
+
pkgList,
|
|
1044
|
+
jarNSMapping = {},
|
|
1045
|
+
force = false,
|
|
1046
|
+
context = {},
|
|
1047
|
+
) {
|
|
1048
|
+
if (!pkgList?.length) {
|
|
1049
|
+
return pkgList;
|
|
716
1050
|
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
}
|
|
727
|
-
|
|
1051
|
+
const repos = {
|
|
1052
|
+
MAVEN_CENTRAL_URL: withTrailingSlash(
|
|
1053
|
+
readEnvironmentVariable("MAVEN_CENTRAL_URL") ||
|
|
1054
|
+
"https://repo1.maven.org/maven2/",
|
|
1055
|
+
),
|
|
1056
|
+
ANDROID_MAVEN_URL: withTrailingSlash(
|
|
1057
|
+
readEnvironmentVariable("ANDROID_MAVEN_URL") ||
|
|
1058
|
+
"https://maven.google.com/",
|
|
1059
|
+
),
|
|
1060
|
+
};
|
|
1061
|
+
const jarNSEntryFor = scalaJarEntryLookup(jarNSMapping);
|
|
1062
|
+
for (const p of pkgList) {
|
|
1063
|
+
await enrichMvnPackageLocally(p, jarNSEntryFor);
|
|
728
1064
|
}
|
|
729
|
-
if (
|
|
730
|
-
return
|
|
1065
|
+
if (!shouldFetchLicense() && !force) {
|
|
1066
|
+
return pkgList;
|
|
731
1067
|
}
|
|
732
|
-
const
|
|
733
|
-
|
|
734
|
-
|
|
1068
|
+
const remoteItems = [];
|
|
1069
|
+
for (const p of pkgList) {
|
|
1070
|
+
if (hasMavenLicense(p)) {
|
|
1071
|
+
continue;
|
|
1072
|
+
}
|
|
1073
|
+
const urlPrefix = mavenRepositoryFor(p, repos, context);
|
|
1074
|
+
if (urlPrefix) {
|
|
1075
|
+
// The remote round asks only for the published name: the component
|
|
1076
|
+
// name has no POM where the artifact is suffixed.
|
|
1077
|
+
remoteItems.push({
|
|
1078
|
+
p,
|
|
1079
|
+
coordinates: {
|
|
1080
|
+
urlPrefix,
|
|
1081
|
+
group: p.group,
|
|
1082
|
+
name: publishedArtifactId(p),
|
|
1083
|
+
version: p.version,
|
|
1084
|
+
},
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
if (!remoteItems.length) {
|
|
1089
|
+
return pkgList;
|
|
1090
|
+
}
|
|
1091
|
+
if (DEBUG_MODE) {
|
|
1092
|
+
console.log(
|
|
1093
|
+
`About to query maven for ${remoteItems.length} of ${pkgList.length} packages`,
|
|
1094
|
+
);
|
|
1095
|
+
}
|
|
1096
|
+
// The direct POMs not on disk are fetched in one batched round. Parent URLs
|
|
1097
|
+
// depend on the direct POM's content, so parents are read in the loop below.
|
|
1098
|
+
const batchUrls = [];
|
|
1099
|
+
const seenPomUrls = new Set();
|
|
1100
|
+
for (const { coordinates } of remoteItems) {
|
|
1101
|
+
const pomUrl = composePomXmlUrl(coordinates);
|
|
1102
|
+
if (
|
|
1103
|
+
seenPomUrls.has(pomUrl) ||
|
|
1104
|
+
readLocalPom(coordinates.group, coordinates.name, coordinates.version)
|
|
1105
|
+
) {
|
|
1106
|
+
continue;
|
|
1107
|
+
}
|
|
1108
|
+
seenPomUrls.add(pomUrl);
|
|
1109
|
+
batchUrls.push({ url: pomUrl, responseType: "text" });
|
|
1110
|
+
}
|
|
1111
|
+
const ownPomUrls = [];
|
|
1112
|
+
if (batchUrls.length) {
|
|
1113
|
+
for (const [key, value] of await prefetchJson(batchUrls)) {
|
|
1114
|
+
prefetchedPoms.set(key, value);
|
|
1115
|
+
ownPomUrls.push(key);
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
try {
|
|
1119
|
+
const needRepoLicense = [];
|
|
1120
|
+
for (const item of remoteItems) {
|
|
1121
|
+
try {
|
|
1122
|
+
const chain = await loadPomChain(item.coordinates, {
|
|
1123
|
+
remote: true,
|
|
1124
|
+
remoteParents: 1,
|
|
1125
|
+
});
|
|
1126
|
+
applyPomMetadata(item.p, chain?.merged, chain?.ownXml);
|
|
1127
|
+
} catch (err) {
|
|
1128
|
+
if (DEBUG_MODE) {
|
|
1129
|
+
console.log(
|
|
1130
|
+
`An error occurred when trying to fetch metadata for ${item.p.group}/${item.p.name}@${item.p.version}`,
|
|
1131
|
+
err,
|
|
1132
|
+
);
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
if (!hasMavenLicense(item.p) && item.p.repository?.url) {
|
|
1136
|
+
needRepoLicense.push(item.p);
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
// The repository licence is the last resort, asked for only once the POM
|
|
1140
|
+
// and all of its parents have been read, and batched.
|
|
1141
|
+
if (needRepoLicense.length) {
|
|
1142
|
+
await prefetchRepoLicenses(needRepoLicense.map((p) => p.repository.url));
|
|
1143
|
+
for (const p of needRepoLicense) {
|
|
1144
|
+
const license = await getRepoLicense(p.repository.url, undefined);
|
|
1145
|
+
if (license) {
|
|
1146
|
+
p.license = license;
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
} finally {
|
|
1151
|
+
// Each body is read once, by the loop above. Holding them past that only
|
|
1152
|
+
// grows the process, which matters in server mode where one process serves
|
|
1153
|
+
// many scans. Only the documents this call fetched are dropped, so a scan
|
|
1154
|
+
// running alongside this one keeps its own.
|
|
1155
|
+
for (const pomUrl of ownPomUrls) {
|
|
1156
|
+
prefetchedPoms.delete(pomUrl);
|
|
1157
|
+
}
|
|
735
1158
|
}
|
|
736
|
-
return
|
|
1159
|
+
return pkgList;
|
|
737
1160
|
}
|
|
738
1161
|
|
|
739
1162
|
/**
|
|
@@ -752,7 +1175,10 @@ export function composePomXmlUrl({ urlPrefix, group, name, version }) {
|
|
|
752
1175
|
}
|
|
753
1176
|
|
|
754
1177
|
/**
|
|
755
|
-
* Method to fetch pom.xml data and parse it to JSON
|
|
1178
|
+
* Method to fetch pom.xml data and parse it to JSON, merged with its parents.
|
|
1179
|
+
*
|
|
1180
|
+
* Each level is read from the local caches first. When a parent cannot be
|
|
1181
|
+
* found, the child and any nearer parents are still returned.
|
|
756
1182
|
*
|
|
757
1183
|
* @param {String} urlPrefix
|
|
758
1184
|
* @param {String} group
|
|
@@ -762,76 +1188,101 @@ export function composePomXmlUrl({ urlPrefix, group, name, version }) {
|
|
|
762
1188
|
* @return {Object|undefined}
|
|
763
1189
|
*/
|
|
764
1190
|
export async function fetchPomXmlAsJson({ urlPrefix, group, name, version }) {
|
|
765
|
-
const
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
1191
|
+
const chain = await loadPomChain(
|
|
1192
|
+
{
|
|
1193
|
+
urlPrefix: urlPrefix ? withTrailingSlash(urlPrefix) : undefined,
|
|
1194
|
+
group,
|
|
1195
|
+
name,
|
|
1196
|
+
version,
|
|
1197
|
+
},
|
|
1198
|
+
{ remote: Boolean(urlPrefix), remoteParents: 1 },
|
|
1199
|
+
);
|
|
1200
|
+
return chain?.merged;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
/**
|
|
1204
|
+
* Fetch one document through the batch pool, so that it shares the pool's rate
|
|
1205
|
+
* gate, failure memo and disk cache, and make a gated request of its own only
|
|
1206
|
+
* when the pool could not reach the host.
|
|
1207
|
+
*
|
|
1208
|
+
* @param {string} url Document URL.
|
|
1209
|
+
* @param {"json"|"text"} [responseType] How to decode the body.
|
|
1210
|
+
* @returns {Promise<{body: *}>} The response.
|
|
1211
|
+
* @throws {Error} When the server answered with an error status.
|
|
1212
|
+
*/
|
|
1213
|
+
async function pooledDocument(url, responseType = "json") {
|
|
1214
|
+
if (prefetchEnabled()) {
|
|
1215
|
+
const res = prefetchedResponse(
|
|
1216
|
+
await prefetchJson([{ url, responseType }]),
|
|
1217
|
+
url,
|
|
1218
|
+
);
|
|
1219
|
+
if (res) {
|
|
1220
|
+
return res;
|
|
786
1221
|
}
|
|
787
|
-
const parentJson = xml2js(parentXml, options).project;
|
|
788
|
-
return { ...parentJson, ...pomJson };
|
|
789
1222
|
}
|
|
790
|
-
return
|
|
1223
|
+
return await gatedGet(url, { responseType });
|
|
791
1224
|
}
|
|
792
1225
|
|
|
793
1226
|
/**
|
|
794
1227
|
* Prefetched POM documents, populated by `getMvnMetadata` before its loop and
|
|
795
|
-
* read by `
|
|
796
|
-
*
|
|
797
|
-
* `extractLicenseCommentFromPomXml`, and threading a map through all of them
|
|
798
|
-
* would be a wide change for no gain.
|
|
1228
|
+
* read by `fetchRemotePomXml`. Module state rather than a parameter because
|
|
1229
|
+
* the POM readers are reached from several exported entry points.
|
|
799
1230
|
*
|
|
800
1231
|
* Only the *direct* POMs — the ones whose URLs are known before the loop — are
|
|
801
|
-
* prefetched.
|
|
802
|
-
*
|
|
803
|
-
* remain serial.
|
|
1232
|
+
* prefetched. Parent URLs depend on the direct POM's content, so they are not
|
|
1233
|
+
* known up front and are read serially.
|
|
804
1234
|
*/
|
|
805
1235
|
const prefetchedPoms = new Map();
|
|
806
1236
|
|
|
807
1237
|
/**
|
|
808
|
-
*
|
|
809
|
-
*
|
|
810
|
-
*
|
|
811
|
-
* @param {String} group
|
|
812
|
-
* @param {String} name
|
|
813
|
-
* @param {String} version
|
|
1238
|
+
* Fetch a POM from a remote repository, using a prefetched copy when there is
|
|
1239
|
+
* one. A POM that answered 404 or 410 is not asked for again this run, so
|
|
1240
|
+
* packages sharing a missing parent ask once.
|
|
814
1241
|
*
|
|
815
|
-
* @
|
|
1242
|
+
* @param {{urlPrefix: string, group: string, name: string, version: string}} coordinates
|
|
1243
|
+
* @returns {Promise<string|undefined>} POM text.
|
|
816
1244
|
*/
|
|
817
|
-
|
|
1245
|
+
async function fetchRemotePomXml({ urlPrefix, group, name, version }) {
|
|
1246
|
+
if (!urlPrefix || !group || !name || !version) {
|
|
1247
|
+
return undefined;
|
|
1248
|
+
}
|
|
818
1249
|
const fullUrl = composePomXmlUrl({ urlPrefix, group, name, version });
|
|
819
|
-
//
|
|
820
|
-
//
|
|
821
|
-
//
|
|
822
|
-
//
|
|
823
|
-
// and still counts against the same budget as the batch round, so it queues
|
|
824
|
-
// behind the same per-host gate rather than a second, ungoverned one.
|
|
1250
|
+
// Parent POMs, whose URLs are known only once the child is read, are
|
|
1251
|
+
// fetched one at a time but still through the pool: they count against
|
|
1252
|
+
// Maven Central's budget, and a release POM never changes, so the pool's
|
|
1253
|
+
// disk cache keeps it for every later run.
|
|
825
1254
|
try {
|
|
826
1255
|
const res =
|
|
827
1256
|
prefetchedResponse(prefetchedPoms, fullUrl) ||
|
|
828
|
-
(await
|
|
1257
|
+
(await pooledDocument(fullUrl, "text"));
|
|
829
1258
|
return res.body;
|
|
830
1259
|
} catch (_err) {
|
|
831
1260
|
return undefined;
|
|
832
1261
|
}
|
|
833
1262
|
}
|
|
834
1263
|
|
|
1264
|
+
/**
|
|
1265
|
+
* Method to fetch pom.xml data, from the local caches when present.
|
|
1266
|
+
*
|
|
1267
|
+
* @param {String} urlPrefix
|
|
1268
|
+
* @param {String} group
|
|
1269
|
+
* @param {String} name
|
|
1270
|
+
* @param {String} version
|
|
1271
|
+
*
|
|
1272
|
+
* @return {Promise<String>}
|
|
1273
|
+
*/
|
|
1274
|
+
export async function fetchPomXml({ urlPrefix, group, name, version }) {
|
|
1275
|
+
return (
|
|
1276
|
+
readLocalPom(group, name, version) ||
|
|
1277
|
+
(await fetchRemotePomXml({
|
|
1278
|
+
urlPrefix: urlPrefix ? withTrailingSlash(urlPrefix) : undefined,
|
|
1279
|
+
group,
|
|
1280
|
+
name,
|
|
1281
|
+
version,
|
|
1282
|
+
}))
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
835
1286
|
/**
|
|
836
1287
|
* Method extract single or multiple license entries that might appear in pom.xml
|
|
837
1288
|
*
|
|
@@ -865,12 +1316,9 @@ export async function extractLicenseCommentFromPomXml({
|
|
|
865
1316
|
name,
|
|
866
1317
|
version,
|
|
867
1318
|
}) {
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
if (match?.[1]) {
|
|
872
|
-
return findLicenseId(match[1].trim());
|
|
873
|
-
}
|
|
1319
|
+
return licenseFromPomComment(
|
|
1320
|
+
await fetchPomXml({ urlPrefix, group, name, version }),
|
|
1321
|
+
);
|
|
874
1322
|
}
|
|
875
1323
|
|
|
876
1324
|
/**
|
|
@@ -905,6 +1353,63 @@ export function guessPypiMatchingVersion(versionsList, versionSpecifiers) {
|
|
|
905
1353
|
return maxSatisfying(versionsList, clean(versionSpecifiers, { loose: true }));
|
|
906
1354
|
}
|
|
907
1355
|
|
|
1356
|
+
/**
|
|
1357
|
+
* Whether PyPI, or the index PYPI_URL points at, can hold the package.
|
|
1358
|
+
*
|
|
1359
|
+
* The Python parsers record where each package came from. A virtual workspace
|
|
1360
|
+
* root, an editable install, and a git, URL or path source are not index
|
|
1361
|
+
* releases; Poetry records a private index the same way, as the package's
|
|
1362
|
+
* source URL. A package from another index is asked only when PYPI_URL points
|
|
1363
|
+
* at that index's server. Asking PyPI about any of them leaks the name and
|
|
1364
|
+
* attaches a look-alike's licence.
|
|
1365
|
+
*
|
|
1366
|
+
* @param {Object} p Package object with the properties the Python parsers record.
|
|
1367
|
+
* @param {string} pypiUrl The JSON API base cdxgen asks.
|
|
1368
|
+
* @returns {boolean}
|
|
1369
|
+
*/
|
|
1370
|
+
function canPypiServe(p, pypiUrl) {
|
|
1371
|
+
const props = {};
|
|
1372
|
+
for (const prop of p.properties || []) {
|
|
1373
|
+
props[prop.name] = prop.value;
|
|
1374
|
+
}
|
|
1375
|
+
if (props["internal:virtual_path"]) {
|
|
1376
|
+
return false;
|
|
1377
|
+
}
|
|
1378
|
+
const sourceType = props["cdx:pypi:manifestSourceType"];
|
|
1379
|
+
if (
|
|
1380
|
+
sourceType &&
|
|
1381
|
+
!(
|
|
1382
|
+
sourceType === "url" &&
|
|
1383
|
+
isDefaultPypiRegistry(props["cdx:pypi:manifestSource"])
|
|
1384
|
+
)
|
|
1385
|
+
) {
|
|
1386
|
+
return false;
|
|
1387
|
+
}
|
|
1388
|
+
if ((p.externalReferences || []).some((ref) => ref.type === "vcs")) {
|
|
1389
|
+
return false;
|
|
1390
|
+
}
|
|
1391
|
+
// The registry property is recorded only for a non-default index.
|
|
1392
|
+
const registry = props["cdx:pypi:registry"];
|
|
1393
|
+
if (registry && !isDefaultPypiRegistry(registry)) {
|
|
1394
|
+
const host = parseUrl(registry)?.host;
|
|
1395
|
+
return !!host && host === parseUrl(pypiUrl)?.host;
|
|
1396
|
+
}
|
|
1397
|
+
return true;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
/**
|
|
1401
|
+
* Whether a package already carries what a PyPI lookup would be asked for: a
|
|
1402
|
+
* licence and a version from the lockfile or an installed distribution. When
|
|
1403
|
+
* registry provenance is wanted it is still asked, since nothing local holds
|
|
1404
|
+
* that.
|
|
1405
|
+
*
|
|
1406
|
+
* @param {Object} p Package object.
|
|
1407
|
+
* @returns {boolean}
|
|
1408
|
+
*/
|
|
1409
|
+
function pypiAnsweredLocally(p) {
|
|
1410
|
+
return !!(p.license && p.version) && !shouldFetchRegistryProvenance();
|
|
1411
|
+
}
|
|
1412
|
+
|
|
908
1413
|
/**
|
|
909
1414
|
* The PyPI path for a package, or `undefined` when the package will not be
|
|
910
1415
|
* queried at all.
|
|
@@ -915,14 +1420,19 @@ export function guessPypiMatchingVersion(versionsList, versionSpecifiers) {
|
|
|
915
1420
|
* JS path never makes, which is both wasted work and a divergence.
|
|
916
1421
|
*
|
|
917
1422
|
* @param {Object} p Package object.
|
|
1423
|
+
* @param {string} pypiUrl The JSON API base cdxgen asks.
|
|
918
1424
|
* @returns {string|undefined} Path to append to PYPI_URL.
|
|
919
1425
|
*/
|
|
920
|
-
function pyUrlAddition(p) {
|
|
1426
|
+
function pyUrlAddition(p, pypiUrl) {
|
|
921
1427
|
if (!p?.name) {
|
|
922
1428
|
return undefined;
|
|
923
1429
|
}
|
|
1430
|
+
// A package PyPI cannot hold is not queried.
|
|
1431
|
+
if (!canPypiServe(p, pypiUrl)) {
|
|
1432
|
+
return undefined;
|
|
1433
|
+
}
|
|
924
1434
|
// A URL as a name, or a package that already has both fields, is not queried.
|
|
925
|
-
if (p.name.includes("https") || (p
|
|
1435
|
+
if (p.name.includes("https") || pypiAnsweredLocally(p)) {
|
|
926
1436
|
return undefined;
|
|
927
1437
|
}
|
|
928
1438
|
// Extras (`requests[security]`) are not part of the PyPI path.
|
|
@@ -932,6 +1442,35 @@ function pyUrlAddition(p) {
|
|
|
932
1442
|
: `${name}/json`;
|
|
933
1443
|
}
|
|
934
1444
|
|
|
1445
|
+
/**
|
|
1446
|
+
* Whether a failed PyPI request means the project or version does not exist,
|
|
1447
|
+
* as opposed to a rate limit, a timeout or a server error, which say nothing
|
|
1448
|
+
* about the name.
|
|
1449
|
+
*
|
|
1450
|
+
* @param {Error} err Error from the agent or the batch prefetch.
|
|
1451
|
+
* @returns {boolean} True for 404 and 410.
|
|
1452
|
+
*/
|
|
1453
|
+
function isPypiNotFound(err) {
|
|
1454
|
+
const status = err?.statusCode ?? err?.response?.statusCode;
|
|
1455
|
+
return status === 404 || status === 410;
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
/**
|
|
1459
|
+
* Fetch a PyPI JSON document, from the batch prefetch when it holds one.
|
|
1460
|
+
* getPyMetadata runs more than once over the same packages, and a URL that
|
|
1461
|
+
* answered 404 or 410 fails at once on every later pass of the run.
|
|
1462
|
+
*
|
|
1463
|
+
* @param {string} url PyPI JSON API URL.
|
|
1464
|
+
* @param {Map} [prefetched] Results of the batch prefetch.
|
|
1465
|
+
* @returns {Promise<Object>} Response with `body`.
|
|
1466
|
+
*/
|
|
1467
|
+
async function getPypiDocument(url, prefetched) {
|
|
1468
|
+
return (
|
|
1469
|
+
prefetchedResponse(prefetched, url) ||
|
|
1470
|
+
(await gatedGet(url, { responseType: "json" }))
|
|
1471
|
+
);
|
|
1472
|
+
}
|
|
1473
|
+
|
|
935
1474
|
/**
|
|
936
1475
|
* Method to retrieve metadata for python packages by querying pypi
|
|
937
1476
|
*
|
|
@@ -951,7 +1490,7 @@ export async function getPyMetadata(pkgList, fetchDepsInfo) {
|
|
|
951
1490
|
const prefetched = await prefetchJson(
|
|
952
1491
|
prefetchEnabled()
|
|
953
1492
|
? pkgList
|
|
954
|
-
.map((p) => pyUrlAddition(p))
|
|
1493
|
+
.map((p) => pyUrlAddition(p, PYPI_URL))
|
|
955
1494
|
.filter(Boolean)
|
|
956
1495
|
.map((addition) => ({ url: PYPI_URL + addition }))
|
|
957
1496
|
: [],
|
|
@@ -965,8 +1504,13 @@ export async function getPyMetadata(pkgList, fetchDepsInfo) {
|
|
|
965
1504
|
// map, so they are available without a registry round trip and are attached
|
|
966
1505
|
// before the skip below rather than after it.
|
|
967
1506
|
applyPypiModuleNames(p);
|
|
1507
|
+
// A package PyPI cannot hold keeps the answers the lockfile gave it.
|
|
1508
|
+
if (!canPypiServe(p, PYPI_URL)) {
|
|
1509
|
+
cdepList.push(p);
|
|
1510
|
+
continue;
|
|
1511
|
+
}
|
|
968
1512
|
// If the package name has a url or already includes license and version skip it
|
|
969
|
-
if (p.name.includes("https") || (p
|
|
1513
|
+
if (p.name.includes("https") || pypiAnsweredLocally(p)) {
|
|
970
1514
|
cdepList.push(p);
|
|
971
1515
|
continue;
|
|
972
1516
|
}
|
|
@@ -983,30 +1527,36 @@ export async function getPyMetadata(pkgList, fetchDepsInfo) {
|
|
|
983
1527
|
url_addition = `${p.name}/json`;
|
|
984
1528
|
}
|
|
985
1529
|
try {
|
|
986
|
-
res =
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
});
|
|
1530
|
+
res = await getPypiDocument(PYPI_URL + url_addition, prefetched);
|
|
1531
|
+
} catch (err) {
|
|
1532
|
+
// Only a missing project means the name may be the Django plugin's.
|
|
1533
|
+
// A rate limit, a timeout or a server error says nothing about the
|
|
1534
|
+
// name, and renaming the package then would give it another
|
|
1535
|
+
// project's identity.
|
|
1536
|
+
if (!isPypiNotFound(err)) {
|
|
1537
|
+
throw err;
|
|
1538
|
+
}
|
|
1539
|
+
res = await getPypiDocument(`${PYPI_URL}django-${url_addition}`);
|
|
996
1540
|
p.name = `django-${p.name}`;
|
|
997
1541
|
}
|
|
998
1542
|
const body = res.body;
|
|
1543
|
+
// The registry fills only what the lockfile and the installed
|
|
1544
|
+
// distribution left open, so a local answer survives the round.
|
|
1545
|
+
let registryAuthor;
|
|
999
1546
|
if (body.info.author && body.info.author.trim() !== "") {
|
|
1000
1547
|
if (body.info.author_email && body.info.author_email.trim() !== "") {
|
|
1001
|
-
|
|
1548
|
+
registryAuthor = `${body.info.author.trim()} <${body.info.author_email.trim()}>`;
|
|
1002
1549
|
} else {
|
|
1003
|
-
|
|
1550
|
+
registryAuthor = body.info.author.trim();
|
|
1004
1551
|
}
|
|
1005
1552
|
} else if (
|
|
1006
1553
|
body.info.author_email &&
|
|
1007
1554
|
body.info.author_email.trim() !== ""
|
|
1008
1555
|
) {
|
|
1009
|
-
|
|
1556
|
+
registryAuthor = body.info.author_email.trim();
|
|
1557
|
+
}
|
|
1558
|
+
if (registryAuthor && !p.author) {
|
|
1559
|
+
p.author = registryAuthor;
|
|
1010
1560
|
}
|
|
1011
1561
|
if (
|
|
1012
1562
|
p.name !== body.info?.name &&
|
|
@@ -1014,36 +1564,40 @@ export async function getPyMetadata(pkgList, fetchDepsInfo) {
|
|
|
1014
1564
|
) {
|
|
1015
1565
|
p.name = body.info.name;
|
|
1016
1566
|
}
|
|
1017
|
-
p.description
|
|
1018
|
-
|
|
1567
|
+
if (!p.description) {
|
|
1568
|
+
p.description = body.info.summary;
|
|
1569
|
+
}
|
|
1570
|
+
const registryLicenses = [];
|
|
1019
1571
|
if (body.info.classifiers) {
|
|
1020
1572
|
for (const c of body.info.classifiers) {
|
|
1021
1573
|
if (c.startsWith("License :: ")) {
|
|
1022
1574
|
const licenseName = c.split("::").slice(-1)[0].trim();
|
|
1023
1575
|
const licenseId = findLicenseId(licenseName);
|
|
1024
|
-
if (licenseId && !
|
|
1025
|
-
|
|
1576
|
+
if (licenseId && !registryLicenses.includes(licenseId)) {
|
|
1577
|
+
registryLicenses.push(licenseId);
|
|
1026
1578
|
}
|
|
1027
1579
|
}
|
|
1028
1580
|
}
|
|
1029
1581
|
applyPypiClassifierMetadata(p, body.info.classifiers);
|
|
1030
1582
|
}
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1583
|
+
for (const declared of [
|
|
1584
|
+
body.info.license,
|
|
1585
|
+
body.info.license_expression,
|
|
1586
|
+
]) {
|
|
1587
|
+
const licenseId = declared ? findLicenseId(declared) : undefined;
|
|
1588
|
+
if (licenseId && !registryLicenses.includes(licenseId)) {
|
|
1589
|
+
registryLicenses.push(licenseId);
|
|
1035
1590
|
}
|
|
1036
1591
|
}
|
|
1037
|
-
if (
|
|
1038
|
-
|
|
1039
|
-
if (licenseId && !p.license.includes(licenseId)) {
|
|
1040
|
-
p.license.push(licenseId);
|
|
1041
|
-
}
|
|
1592
|
+
if (!(Array.isArray(p.license) ? p.license.length : p.license)) {
|
|
1593
|
+
p.license = registryLicenses;
|
|
1042
1594
|
}
|
|
1043
1595
|
if (body.info.home_page) {
|
|
1044
1596
|
if (body.info.home_page.includes("git")) {
|
|
1045
|
-
p.repository
|
|
1046
|
-
|
|
1597
|
+
if (!p.repository) {
|
|
1598
|
+
p.repository = { url: body.info.home_page };
|
|
1599
|
+
}
|
|
1600
|
+
} else if (!p.homepage) {
|
|
1047
1601
|
p.homepage = { url: body.info.home_page };
|
|
1048
1602
|
}
|
|
1049
1603
|
}
|
|
@@ -1481,7 +2035,13 @@ let repoLicensePrefetch = new Map();
|
|
|
1481
2035
|
* @returns {Promise<void>}
|
|
1482
2036
|
*/
|
|
1483
2037
|
export async function prefetchRepoLicenses(repoUrls) {
|
|
1484
|
-
|
|
2038
|
+
// While the lookups are paused, getRepoLicense asks nothing, so neither
|
|
2039
|
+
// does the batch.
|
|
2040
|
+
if (
|
|
2041
|
+
!prefetchEnabled() ||
|
|
2042
|
+
!Array.isArray(repoUrls) ||
|
|
2043
|
+
repoLicenseLookupsPaused()
|
|
2044
|
+
) {
|
|
1485
2045
|
return;
|
|
1486
2046
|
}
|
|
1487
2047
|
const requests = [];
|
|
@@ -1495,7 +2055,16 @@ export async function prefetchRepoLicenses(repoUrls) {
|
|
|
1495
2055
|
continue;
|
|
1496
2056
|
}
|
|
1497
2057
|
const licenseUrl = `${apiUrl}/license`;
|
|
1498
|
-
|
|
2058
|
+
// A licence this run already has an answer for, a licence or a 404, is
|
|
2059
|
+
// read from the earlier round. A lookup that failed, for instance on a
|
|
2060
|
+
// spent quota, is asked again.
|
|
2061
|
+
const known = repoLicensePrefetch.get(licenseUrl);
|
|
2062
|
+
if (
|
|
2063
|
+
seen.has(licenseUrl) ||
|
|
2064
|
+
known?.ok ||
|
|
2065
|
+
known?.status === 404 ||
|
|
2066
|
+
known?.status === 410
|
|
2067
|
+
) {
|
|
1499
2068
|
continue;
|
|
1500
2069
|
}
|
|
1501
2070
|
seen.add(licenseUrl);
|
|
@@ -1542,7 +2111,7 @@ export async function getRepoLicense(repoUrl, repoMetadata) {
|
|
|
1542
2111
|
}
|
|
1543
2112
|
const apiUrl = toGitHubApiUrl(repoUrl, repoMetadata);
|
|
1544
2113
|
// Perform github lookups
|
|
1545
|
-
if (apiUrl &&
|
|
2114
|
+
if (apiUrl && !repoLicenseLookupsPaused()) {
|
|
1546
2115
|
const licenseUrl = `${apiUrl}/license`;
|
|
1547
2116
|
const headers = {};
|
|
1548
2117
|
if (readEnvironmentVariable("GITHUB_TOKEN")) {
|
|
@@ -1552,9 +2121,9 @@ export async function getRepoLicense(repoUrl, repoMetadata) {
|
|
|
1552
2121
|
try {
|
|
1553
2122
|
const res =
|
|
1554
2123
|
prefetchedResponse(repoLicensePrefetch, licenseUrl) ||
|
|
1555
|
-
(await
|
|
2124
|
+
(await gatedGet(licenseUrl, {
|
|
1556
2125
|
responseType: "json",
|
|
1557
|
-
headers
|
|
2126
|
+
headers,
|
|
1558
2127
|
}));
|
|
1559
2128
|
if (res?.body) {
|
|
1560
2129
|
const license = res.body.license;
|
|
@@ -1581,20 +2150,18 @@ export async function getRepoLicense(repoUrl, repoMetadata) {
|
|
|
1581
2150
|
}
|
|
1582
2151
|
}
|
|
1583
2152
|
} catch (err) {
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
);
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
get_repo_license_errors++;
|
|
1597
|
-
}
|
|
2153
|
+
// A repository without a licence file answers 404. A spent quota is a
|
|
2154
|
+
// 429, or a 403 with nothing remaining; the request layer pauses
|
|
2155
|
+
// api.github.com until the quota resets and says so once, with the
|
|
2156
|
+
// GITHUB_TOKEN advice. Any other failure counts towards pausing these
|
|
2157
|
+
// lookups.
|
|
2158
|
+
const status = err?.response?.statusCode ?? err?.statusCode;
|
|
2159
|
+
const quotaSpent =
|
|
2160
|
+
status === 429 ||
|
|
2161
|
+
(status === 403 &&
|
|
2162
|
+
String(err?.response?.headers?.["x-ratelimit-remaining"]) === "0");
|
|
2163
|
+
if (status !== 404 && !quotaSpent) {
|
|
2164
|
+
get_repo_license_errors++;
|
|
1598
2165
|
}
|
|
1599
2166
|
}
|
|
1600
2167
|
}
|
|
@@ -1638,12 +2205,21 @@ export async function prefetchGoPkgMetadata(modules) {
|
|
|
1638
2205
|
if (!module?.name) {
|
|
1639
2206
|
continue;
|
|
1640
2207
|
}
|
|
2208
|
+
const fullName =
|
|
2209
|
+
module.fullName || getGoPkgFullName(module.group, module.name);
|
|
2210
|
+
// A module the environment marks private is never requested.
|
|
2211
|
+
// getGoPkgLicense applies the same test, so the batch cannot ask for a
|
|
2212
|
+
// module the serial path would skip.
|
|
2213
|
+
if (isPrivateGoModule(fullName)) {
|
|
2214
|
+
continue;
|
|
2215
|
+
}
|
|
1641
2216
|
const pkgUrl = getGoPkgUrl(module);
|
|
1642
2217
|
// The licence tab and the module page are distinct documents, read by
|
|
1643
2218
|
// getGoPkgLicense and getGoPkgVCSUrl respectively. Both are requested only
|
|
1644
2219
|
// when the corresponding flag is on, so the batch mirrors that.
|
|
1645
2220
|
const wanted = [];
|
|
1646
|
-
|
|
2221
|
+
// A licence notice read on this machine answers the licence tab.
|
|
2222
|
+
if (shouldFetchLicense() && !module.licenseKnown) {
|
|
1647
2223
|
wanted.push(`${pkgUrl}?tab=licenses`);
|
|
1648
2224
|
}
|
|
1649
2225
|
// getGoPkgVCSUrl derives github.com and gitlab.com URLs without asking
|
|
@@ -1652,7 +2228,7 @@ export async function prefetchGoPkgMetadata(modules) {
|
|
|
1652
2228
|
wanted.push(pkgUrl);
|
|
1653
2229
|
}
|
|
1654
2230
|
for (const url of wanted) {
|
|
1655
|
-
if (seen.has(url) ||
|
|
2231
|
+
if (seen.has(url) || goPkgAnswers.has(url)) {
|
|
1656
2232
|
continue;
|
|
1657
2233
|
}
|
|
1658
2234
|
seen.add(url);
|
|
@@ -1682,15 +2258,21 @@ export function resetGoPkgPrefetch() {
|
|
|
1682
2258
|
*/
|
|
1683
2259
|
export async function getGoPkgLicense(repoMetadata) {
|
|
1684
2260
|
const group = repoMetadata.group;
|
|
2261
|
+
// A module the environment marks private is never sent to the public site.
|
|
2262
|
+
const fullName =
|
|
2263
|
+
repoMetadata.fullName ||
|
|
2264
|
+
getGoPkgFullName(repoMetadata.group, repoMetadata.name);
|
|
2265
|
+
if (isPrivateGoModule(fullName)) {
|
|
2266
|
+
return undefined;
|
|
2267
|
+
}
|
|
1685
2268
|
const pkgUrl = `${getGoPkgUrl(repoMetadata)}?tab=licenses`;
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
return metadata_cache[pkgUrl];
|
|
2269
|
+
if (goPkgAnswers.has(pkgUrl)) {
|
|
2270
|
+
return goPkgAnswers.get(pkgUrl);
|
|
1689
2271
|
}
|
|
1690
2272
|
try {
|
|
1691
2273
|
const res =
|
|
1692
2274
|
prefetchedResponse(goPkgPrefetch, pkgUrl) ||
|
|
1693
|
-
(await
|
|
2275
|
+
(await gatedGet(pkgUrl, { responseType: "text" }));
|
|
1694
2276
|
if (res?.body) {
|
|
1695
2277
|
const licenses = extractLicenseText(res.body);
|
|
1696
2278
|
const licenseIds = licenses.split(", ");
|
|
@@ -1713,7 +2295,7 @@ export async function getGoPkgLicense(repoMetadata) {
|
|
|
1713
2295
|
licList.push(alicense);
|
|
1714
2296
|
}
|
|
1715
2297
|
}
|
|
1716
|
-
|
|
2298
|
+
goPkgAnswers.set(pkgUrl, licList);
|
|
1717
2299
|
return licList;
|
|
1718
2300
|
}
|
|
1719
2301
|
} catch (_err) {
|
|
@@ -1730,23 +2312,31 @@ export async function getGoPkgLicense(repoMetadata) {
|
|
|
1730
2312
|
*
|
|
1731
2313
|
* @param {String} group Package group
|
|
1732
2314
|
* @param {String} name Package name
|
|
2315
|
+
* @param {String} [version] Package version, part of the page URL
|
|
1733
2316
|
*/
|
|
1734
|
-
export async function getGoPkgVCSUrl(group, name) {
|
|
2317
|
+
export async function getGoPkgVCSUrl(group, name, version) {
|
|
1735
2318
|
const fullName = getGoPkgFullName(group, name);
|
|
1736
2319
|
if (isDirectlyResolvableGoVCS({ group, name })) {
|
|
1737
2320
|
return `https://${fullName}`;
|
|
1738
2321
|
}
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
2322
|
+
// The repository URL of a private module follows from a github.com or
|
|
2323
|
+
// gitlab.com path without a request, but pkg.go.dev is never asked.
|
|
2324
|
+
if (isPrivateGoModule(fullName)) {
|
|
2325
|
+
return undefined;
|
|
2326
|
+
}
|
|
2327
|
+
// The versioned page matches the URL the prefetch requested, so a prefetched
|
|
2328
|
+
// document is found in the cache.
|
|
2329
|
+
const pkgUrl = getGoPkgUrl({ fullName, version });
|
|
2330
|
+
if (goPkgAnswers.has(pkgUrl)) {
|
|
2331
|
+
return goPkgAnswers.get(pkgUrl);
|
|
1742
2332
|
}
|
|
1743
2333
|
try {
|
|
1744
2334
|
const res =
|
|
1745
2335
|
prefetchedResponse(goPkgPrefetch, pkgUrl) ||
|
|
1746
|
-
(await
|
|
2336
|
+
(await gatedGet(pkgUrl, { responseType: "text" }));
|
|
1747
2337
|
if (res?.body) {
|
|
1748
2338
|
const vcs = extractRepoUrl(res.body);
|
|
1749
|
-
|
|
2339
|
+
goPkgAnswers.set(pkgUrl, vcs);
|
|
1750
2340
|
return vcs;
|
|
1751
2341
|
}
|
|
1752
2342
|
} catch (_err) {
|
|
@@ -1773,17 +2363,154 @@ function isDirectlyResolvableGoVCS(pkgMetadata) {
|
|
|
1773
2363
|
}
|
|
1774
2364
|
|
|
1775
2365
|
/**
|
|
1776
|
-
* Method to get go pkg url (go.dev site).
|
|
2366
|
+
* Method to get go pkg url (go.dev site).
|
|
2367
|
+
*
|
|
2368
|
+
* The version is part of the URL whenever it is known, so pkg.go.dev answers
|
|
2369
|
+
* for the release the project pinned rather than for the latest one.
|
|
2370
|
+
*
|
|
2371
|
+
* @param {Object} pkgMetadata pkg metadata
|
|
2372
|
+
*/
|
|
2373
|
+
function getGoPkgUrl(pkgMetadata) {
|
|
2374
|
+
const pkgUrlPrefix =
|
|
2375
|
+
readEnvironmentVariable("GO_PKG_URL") || "https://pkg.go.dev/";
|
|
2376
|
+
const fullName =
|
|
2377
|
+
pkgMetadata.fullName ||
|
|
2378
|
+
getGoPkgFullName(pkgMetadata.group, pkgMetadata.name);
|
|
2379
|
+
const version = pkgMetadata.version ? `@${pkgMetadata.version}` : "";
|
|
2380
|
+
return pkgUrlPrefix + fullName + version;
|
|
2381
|
+
}
|
|
2382
|
+
|
|
2383
|
+
/**
|
|
2384
|
+
* Match one path element of a Go module pattern against one element of a
|
|
2385
|
+
* module path, the way Go's `path.Match` does: `*` and `?` do not cross a `/`,
|
|
2386
|
+
* and a bracketed character class is allowed.
|
|
2387
|
+
*
|
|
2388
|
+
* @param {string} pattern One pattern element
|
|
2389
|
+
* @param {string} name One module path element
|
|
2390
|
+
* @returns {boolean}
|
|
2391
|
+
*/
|
|
2392
|
+
function matchGoPathElement(pattern, name) {
|
|
2393
|
+
let p = 0;
|
|
2394
|
+
let n = 0;
|
|
2395
|
+
let starP = -1;
|
|
2396
|
+
let starN = -1;
|
|
2397
|
+
while (n < name.length) {
|
|
2398
|
+
if (p < pattern.length) {
|
|
2399
|
+
const c = pattern[p];
|
|
2400
|
+
if (c === "*") {
|
|
2401
|
+
starP = p;
|
|
2402
|
+
starN = n;
|
|
2403
|
+
p++;
|
|
2404
|
+
continue;
|
|
2405
|
+
}
|
|
2406
|
+
if (c === "?") {
|
|
2407
|
+
p++;
|
|
2408
|
+
n++;
|
|
2409
|
+
continue;
|
|
2410
|
+
}
|
|
2411
|
+
if (c === "[") {
|
|
2412
|
+
let i = p + 1;
|
|
2413
|
+
let negate = false;
|
|
2414
|
+
if (i < pattern.length && "^".indexOf(pattern[i]) > -1) {
|
|
2415
|
+
negate = pattern[i] === "^";
|
|
2416
|
+
i++;
|
|
2417
|
+
}
|
|
2418
|
+
let matched = false;
|
|
2419
|
+
let first = true;
|
|
2420
|
+
while (i < pattern.length && (first || pattern[i] !== "]")) {
|
|
2421
|
+
first = false;
|
|
2422
|
+
let lo = pattern[i];
|
|
2423
|
+
if (lo === "\\" && i + 1 < pattern.length) {
|
|
2424
|
+
i++;
|
|
2425
|
+
lo = pattern[i];
|
|
2426
|
+
}
|
|
2427
|
+
let hi = lo;
|
|
2428
|
+
if (
|
|
2429
|
+
i + 2 < pattern.length &&
|
|
2430
|
+
pattern[i + 1] === "-" &&
|
|
2431
|
+
pattern[i + 2] !== "]"
|
|
2432
|
+
) {
|
|
2433
|
+
hi = pattern[i + 2];
|
|
2434
|
+
i += 2;
|
|
2435
|
+
}
|
|
2436
|
+
if (name[n] >= lo && name[n] <= hi) {
|
|
2437
|
+
matched = true;
|
|
2438
|
+
}
|
|
2439
|
+
i++;
|
|
2440
|
+
}
|
|
2441
|
+
if (i < pattern.length && pattern[i] === "]") {
|
|
2442
|
+
if (matched !== negate) {
|
|
2443
|
+
p = i + 1;
|
|
2444
|
+
n++;
|
|
2445
|
+
continue;
|
|
2446
|
+
}
|
|
2447
|
+
} else {
|
|
2448
|
+
// An unterminated class matches nothing rather than silently
|
|
2449
|
+
// swallowing the rest of the pattern.
|
|
2450
|
+
return false;
|
|
2451
|
+
}
|
|
2452
|
+
} else if (c === "\\" && p + 1 < pattern.length) {
|
|
2453
|
+
if (pattern[p + 1] === name[n]) {
|
|
2454
|
+
p += 2;
|
|
2455
|
+
n++;
|
|
2456
|
+
continue;
|
|
2457
|
+
}
|
|
2458
|
+
} else if (c === name[n]) {
|
|
2459
|
+
p++;
|
|
2460
|
+
n++;
|
|
2461
|
+
continue;
|
|
2462
|
+
}
|
|
2463
|
+
}
|
|
2464
|
+
if (starP !== -1) {
|
|
2465
|
+
p = starP + 1;
|
|
2466
|
+
starN++;
|
|
2467
|
+
n = starN;
|
|
2468
|
+
continue;
|
|
2469
|
+
}
|
|
2470
|
+
return false;
|
|
2471
|
+
}
|
|
2472
|
+
while (p < pattern.length && pattern[p] === "*") {
|
|
2473
|
+
p++;
|
|
2474
|
+
}
|
|
2475
|
+
return p === pattern.length;
|
|
2476
|
+
}
|
|
2477
|
+
|
|
2478
|
+
/**
|
|
2479
|
+
* Whether a module is private and must not be sent to pkg.go.dev.
|
|
1777
2480
|
*
|
|
1778
|
-
*
|
|
2481
|
+
* `GOPRIVATE`, and `GONOPROXY`/`GONOSUMDB` when they are set, hold
|
|
2482
|
+
* comma-separated globs. Like Go's `module.MatchPrefixPatterns`, a glob with N
|
|
2483
|
+
* path elements is matched against the first N elements of the module path,
|
|
2484
|
+
* so `example.com/org` matches `example.com/org/tool`. Sending a private
|
|
2485
|
+
* module to a public site leaks its path, and the site cannot hold it anyway.
|
|
2486
|
+
*
|
|
2487
|
+
* @param {string} modulePath Full module path
|
|
2488
|
+
* @returns {boolean}
|
|
1779
2489
|
*/
|
|
1780
|
-
function
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
2490
|
+
function isPrivateGoModule(modulePath) {
|
|
2491
|
+
if (!modulePath) {
|
|
2492
|
+
return false;
|
|
2493
|
+
}
|
|
2494
|
+
const targetParts = modulePath.split("/");
|
|
2495
|
+
for (const name of ["GOPRIVATE", "GONOPROXY", "GONOSUMDB"]) {
|
|
2496
|
+
for (const rawGlob of (readEnvironmentVariable(name) || "").split(",")) {
|
|
2497
|
+
const trimmed = rawGlob.trim();
|
|
2498
|
+
const glob = trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
|
|
2499
|
+
if (!glob) {
|
|
2500
|
+
continue;
|
|
2501
|
+
}
|
|
2502
|
+
const globParts = glob.split("/");
|
|
2503
|
+
if (globParts.length > targetParts.length) {
|
|
2504
|
+
continue;
|
|
2505
|
+
}
|
|
2506
|
+
if (
|
|
2507
|
+
globParts.every((part, i) => matchGoPathElement(part, targetParts[i]))
|
|
2508
|
+
) {
|
|
2509
|
+
return true;
|
|
2510
|
+
}
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
return false;
|
|
1787
2514
|
}
|
|
1788
2515
|
|
|
1789
2516
|
/**
|
|
@@ -1796,6 +2523,35 @@ function getGoPkgFullName(group, name) {
|
|
|
1796
2523
|
return group && group !== "." && group !== name ? `${group}/${name}` : name;
|
|
1797
2524
|
}
|
|
1798
2525
|
|
|
2526
|
+
/**
|
|
2527
|
+
* Whether crates.io can hold the crate.
|
|
2528
|
+
*
|
|
2529
|
+
* The Cargo.lock parser records a `cdx:cargo:sourceKind` for every crate whose
|
|
2530
|
+
* source is not plain crates.io: the workspace and path crates, the project
|
|
2531
|
+
* itself, git crates, and crates an alternate registry serves. Asking crates.io
|
|
2532
|
+
* about those attaches a same-named crate's metadata to them, so only a crate
|
|
2533
|
+
* crates.io actually serves is looked up.
|
|
2534
|
+
*
|
|
2535
|
+
* @param {Object} p Package object with the parser's source properties.
|
|
2536
|
+
* @returns {boolean}
|
|
2537
|
+
*/
|
|
2538
|
+
function canCratesIoServe(p) {
|
|
2539
|
+
let sourceKind;
|
|
2540
|
+
let alternateRegistry = false;
|
|
2541
|
+
for (const prop of p.properties || []) {
|
|
2542
|
+
if (prop.name === "cdx:cargo:sourceKind") {
|
|
2543
|
+
sourceKind = prop.value;
|
|
2544
|
+
} else if (prop.name === "cdx:cargo:alternateRegistry") {
|
|
2545
|
+
alternateRegistry = prop.value === "true";
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
if (!sourceKind) {
|
|
2549
|
+
// The public registry is the norm and goes unrecorded.
|
|
2550
|
+
return true;
|
|
2551
|
+
}
|
|
2552
|
+
return sourceKind === "registry" && !alternateRegistry;
|
|
2553
|
+
}
|
|
2554
|
+
|
|
1799
2555
|
/**
|
|
1800
2556
|
* Method to retrieve metadata for rust packages by querying crates
|
|
1801
2557
|
*
|
|
@@ -1806,7 +2562,13 @@ function getGoPkgFullName(group, name) {
|
|
|
1806
2562
|
*
|
|
1807
2563
|
* What it does not hold is publisher identity, so a caller that needs the
|
|
1808
2564
|
* publisher-drift and release-cadence signals — the predictive audit does —
|
|
1809
|
-
* passes `preferLocalCache: false` and takes the slower registry path.
|
|
2565
|
+
* passes `preferLocalCache: false` and takes the slower registry path. That is
|
|
2566
|
+
* also the only caller that needs the crate's owners, so the `/owners` request
|
|
2567
|
+
* is made for it alone.
|
|
2568
|
+
*
|
|
2569
|
+
* Crates a public registry cannot hold are skipped: the project itself, path
|
|
2570
|
+
* and git crates, and crates from an alternate registry. See
|
|
2571
|
+
* {@link canCratesIoServe}.
|
|
1810
2572
|
*
|
|
1811
2573
|
* @param {Array} pkgList Package list
|
|
1812
2574
|
* @param {Object} [options] Options
|
|
@@ -1823,12 +2585,26 @@ export async function getCratesMetadata(pkgList, options = {}) {
|
|
|
1823
2585
|
// reads; only the crates it cannot account for reach the network.
|
|
1824
2586
|
const servedLocally = new Set();
|
|
1825
2587
|
const preferLocalCache = options.preferLocalCache !== false;
|
|
2588
|
+
// The owners listing carries no licence, so only a caller that wants the
|
|
2589
|
+
// publisher identity behind it pays for the second request.
|
|
2590
|
+
const wantsOwners = !preferLocalCache;
|
|
1826
2591
|
if (preferLocalCache && localCargoMetadataEnabled()) {
|
|
1827
2592
|
for (const p of pkgList) {
|
|
1828
2593
|
if (!p?.name || !p?.version || p.version === "workspace") {
|
|
1829
2594
|
continue;
|
|
1830
2595
|
}
|
|
1831
|
-
|
|
2596
|
+
// A crate crates.io does not hold must not take a same-named crate's
|
|
2597
|
+
// place in the local registry cache either.
|
|
2598
|
+
if (!canCratesIoServe(p)) {
|
|
2599
|
+
continue;
|
|
2600
|
+
}
|
|
2601
|
+
// A crate a caller already filled from its own local source, such as a
|
|
2602
|
+
// .crate archive the scan itself read, has nothing left to ask for.
|
|
2603
|
+
if (p.license && p.description) {
|
|
2604
|
+
servedLocally.add(p);
|
|
2605
|
+
continue;
|
|
2606
|
+
}
|
|
2607
|
+
if (await applyCargoCacheMetadata(p)) {
|
|
1832
2608
|
servedLocally.add(p);
|
|
1833
2609
|
}
|
|
1834
2610
|
}
|
|
@@ -1843,17 +2619,21 @@ export async function getCratesMetadata(pkgList, options = {}) {
|
|
|
1843
2619
|
p?.name &&
|
|
1844
2620
|
p?.version &&
|
|
1845
2621
|
p.version !== "workspace" &&
|
|
2622
|
+
canCratesIoServe(p) &&
|
|
1846
2623
|
!servedLocally.has(p),
|
|
1847
2624
|
);
|
|
1848
|
-
// Two URLs per crate — the crate document and
|
|
1849
|
-
//
|
|
1850
|
-
//
|
|
2625
|
+
// Two URLs per crate — the crate document and, for the caller that wants
|
|
2626
|
+
// publisher identity, its owners — fetched for the whole list at once. The
|
|
2627
|
+
// `workspace` and source guards below are mirrored here so the batch does
|
|
2628
|
+
// not request anything the loop would have skipped.
|
|
1851
2629
|
const prefetched = await prefetchJson(
|
|
1852
2630
|
prefetchEnabled()
|
|
1853
|
-
? needsRegistry.flatMap((p) =>
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
2631
|
+
? needsRegistry.flatMap((p) => {
|
|
2632
|
+
const crateUrl = CRATES_URL + p.name;
|
|
2633
|
+
return wantsOwners
|
|
2634
|
+
? [{ url: crateUrl }, { url: `${crateUrl}/owners` }]
|
|
2635
|
+
: [{ url: crateUrl }];
|
|
2636
|
+
})
|
|
1857
2637
|
: [],
|
|
1858
2638
|
);
|
|
1859
2639
|
for (const p of pkgList) {
|
|
@@ -1862,6 +2642,10 @@ export async function getCratesMetadata(pkgList, options = {}) {
|
|
|
1862
2642
|
cdepList.push(p);
|
|
1863
2643
|
continue;
|
|
1864
2644
|
}
|
|
2645
|
+
if (!canCratesIoServe(p)) {
|
|
2646
|
+
cdepList.push(p);
|
|
2647
|
+
continue;
|
|
2648
|
+
}
|
|
1865
2649
|
if (servedLocally.has(p)) {
|
|
1866
2650
|
cdepList.push(p);
|
|
1867
2651
|
continue;
|
|
@@ -1876,32 +2660,32 @@ export async function getCratesMetadata(pkgList, options = {}) {
|
|
|
1876
2660
|
// this process issued it.
|
|
1877
2661
|
const res =
|
|
1878
2662
|
prefetchedResponse(prefetched, crateUrl) ||
|
|
1879
|
-
(await
|
|
1880
|
-
cdxgenAgent.get(crateUrl, {
|
|
1881
|
-
responseType: "json",
|
|
1882
|
-
}),
|
|
1883
|
-
));
|
|
2663
|
+
(await gatedGet(crateUrl, { responseType: "json" }));
|
|
1884
2664
|
let ownersRes;
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
}
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
2665
|
+
if (wantsOwners) {
|
|
2666
|
+
try {
|
|
2667
|
+
const ownersUrl = `${crateUrl}/owners`;
|
|
2668
|
+
ownersRes =
|
|
2669
|
+
prefetchedResponse(prefetched, ownersUrl) ||
|
|
2670
|
+
(await gatedGet(ownersUrl, { responseType: "json" }));
|
|
2671
|
+
} catch (_err) {
|
|
2672
|
+
ownersRes = undefined;
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2675
|
+
// A crate whose version crates.io does not list was not published
|
|
2676
|
+
// there: a path or git crate, or one from another registry, that
|
|
2677
|
+
// shares a crates.io name. Nothing in that document describes it.
|
|
2678
|
+
const versionToUse = res?.body?.versions?.find(
|
|
2679
|
+
(aversion) => aversion.num === p.version,
|
|
2680
|
+
);
|
|
2681
|
+
if (!versionToUse) {
|
|
2682
|
+
if (DEBUG_MODE) {
|
|
2683
|
+
console.log(
|
|
2684
|
+
`crates.io has no version ${p.version} of ${p.name}; leaving it unchanged.`,
|
|
2685
|
+
);
|
|
1904
2686
|
}
|
|
2687
|
+
cdepList.push(p);
|
|
2688
|
+
continue;
|
|
1905
2689
|
}
|
|
1906
2690
|
const body = res.body.crate;
|
|
1907
2691
|
p.description = body.description;
|
|
@@ -1914,10 +2698,6 @@ export async function getCratesMetadata(pkgList, options = {}) {
|
|
|
1914
2698
|
if (body.homepage && body.homepage !== body.repository) {
|
|
1915
2699
|
p.homepage = { url: body.homepage };
|
|
1916
2700
|
}
|
|
1917
|
-
// Use the latest version if none specified
|
|
1918
|
-
if (!p.version) {
|
|
1919
|
-
p.version = body.newest_version;
|
|
1920
|
-
}
|
|
1921
2701
|
if (!p._integrity && versionToUse.checksum) {
|
|
1922
2702
|
p._integrity = normalizeCargoIntegrity(versionToUse.checksum);
|
|
1923
2703
|
}
|
|
@@ -1977,12 +2757,17 @@ export async function getCratesMetadata(pkgList, options = {}) {
|
|
|
1977
2757
|
* @param {Object} p Package component to enrich in place.
|
|
1978
2758
|
* @returns {boolean} true when crates.io can be skipped for this crate.
|
|
1979
2759
|
*/
|
|
1980
|
-
function applyCargoCacheMetadata(p) {
|
|
1981
|
-
const local = readCargoCacheMetadata(p.name, p.version);
|
|
1982
|
-
if (!local?.license) {
|
|
2760
|
+
async function applyCargoCacheMetadata(p) {
|
|
2761
|
+
const local = await readCargoCacheMetadata(p.name, p.version);
|
|
2762
|
+
if (!local?.license && !local?.licenses) {
|
|
1983
2763
|
return false;
|
|
1984
2764
|
}
|
|
1985
|
-
|
|
2765
|
+
if (local.license) {
|
|
2766
|
+
p.license = local.license;
|
|
2767
|
+
}
|
|
2768
|
+
if (local.licenses && !p.licenses) {
|
|
2769
|
+
p.licenses = local.licenses;
|
|
2770
|
+
}
|
|
1986
2771
|
if (local.description) {
|
|
1987
2772
|
p.description = local.description;
|
|
1988
2773
|
}
|
|
@@ -2040,14 +2825,47 @@ function applyCargoCacheMetadata(p) {
|
|
|
2040
2825
|
return true;
|
|
2041
2826
|
}
|
|
2042
2827
|
|
|
2828
|
+
// pub.dev also answers under its old name, which lockfiles written before
|
|
2829
|
+
// Dart 2.19 record for every hosted package.
|
|
2830
|
+
const PUB_DEV_HOSTS = new Set(["pub.dev", "pub.dartlang.org"]);
|
|
2831
|
+
|
|
2832
|
+
/**
|
|
2833
|
+
* The registry a hosted Dart package records, or undefined when that is
|
|
2834
|
+
* pub.dev or the registry cdxgen is configured to ask.
|
|
2835
|
+
*
|
|
2836
|
+
* @param {Object} p Package object with the pubspec.lock properties.
|
|
2837
|
+
* @param {string} pubDevUrl The registry cdxgen asks by default.
|
|
2838
|
+
* @returns {string|undefined}
|
|
2839
|
+
*/
|
|
2840
|
+
function pubHostedRegistry(p, pubDevUrl) {
|
|
2841
|
+
const registry = p.properties?.find(
|
|
2842
|
+
(prop) => prop.name === "cdx:pub:registry" && prop.value,
|
|
2843
|
+
)?.value;
|
|
2844
|
+
const host = parseUrl(registry)?.host;
|
|
2845
|
+
if (!host || PUB_DEV_HOSTS.has(host) || host === parseUrl(pubDevUrl)?.host) {
|
|
2846
|
+
return undefined;
|
|
2847
|
+
}
|
|
2848
|
+
return registry;
|
|
2849
|
+
}
|
|
2850
|
+
|
|
2043
2851
|
/**
|
|
2044
2852
|
* Method to retrieve metadata for dart packages by querying pub.dev
|
|
2045
2853
|
*
|
|
2854
|
+
* A hosted package whose lockfile names another registry is looked up there;
|
|
2855
|
+
* the score endpoint that carries the licence tag is pub.dev's, so a private
|
|
2856
|
+
* registry is asked only for the package document. In secure mode cdxgen does
|
|
2857
|
+
* not contact a registry the scanned project names, so such a package is not
|
|
2858
|
+
* looked up at all.
|
|
2859
|
+
*
|
|
2046
2860
|
* @param {Array} pkgList Package list
|
|
2861
|
+
* @param {Object} [options] Options
|
|
2862
|
+
* @param {boolean} [options.secureMode] Whether secure mode is on. Defaults to
|
|
2863
|
+
* the process-wide setting.
|
|
2047
2864
|
*/
|
|
2048
|
-
export async function getDartMetadata(pkgList) {
|
|
2865
|
+
export async function getDartMetadata(pkgList, options = {}) {
|
|
2049
2866
|
const PUB_DEV_URL =
|
|
2050
2867
|
readEnvironmentVariable("PUB_DEV_URL") || "https://pub.dev";
|
|
2868
|
+
const secureMode = options.secureMode ?? isSecureMode;
|
|
2051
2869
|
const PUB_LICENSE_REGEX = /^license:/i;
|
|
2052
2870
|
const OPTIONS = {
|
|
2053
2871
|
responseType: "json",
|
|
@@ -2055,31 +2873,50 @@ export async function getDartMetadata(pkgList) {
|
|
|
2055
2873
|
Accept: PUB_ACCEPT,
|
|
2056
2874
|
},
|
|
2057
2875
|
};
|
|
2876
|
+
const apiBase = (url) => (url.endsWith("/") ? url.slice(0, -1) : url);
|
|
2058
2877
|
|
|
2059
2878
|
const cdepList = [];
|
|
2879
|
+
// Where each package is asked, decided once so the batch and the loop agree.
|
|
2060
2880
|
// pub.dev needs the package document and its score document; both carry the
|
|
2061
|
-
// vendor Accept header, which is part of the cache key on the Rust side.
|
|
2881
|
+
// vendor Accept header, which is part of the cache key on the Rust side. A
|
|
2882
|
+
// package hosted elsewhere is asked only for its package document.
|
|
2883
|
+
const lookups = new Map();
|
|
2884
|
+
for (const p of pkgList) {
|
|
2885
|
+
const registry = pubHostedRegistry(p, PUB_DEV_URL);
|
|
2886
|
+
if (registry && secureMode) {
|
|
2887
|
+
continue;
|
|
2888
|
+
}
|
|
2889
|
+
lookups.set(p, {
|
|
2890
|
+
registry,
|
|
2891
|
+
url: `${apiBase(registry || PUB_DEV_URL)}/api/packages/${p.name}/versions/${p.version}`,
|
|
2892
|
+
});
|
|
2893
|
+
}
|
|
2062
2894
|
const prefetched = await prefetchJson(
|
|
2063
2895
|
prefetchEnabled()
|
|
2064
|
-
?
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2896
|
+
? [...lookups.values()].flatMap(({ registry, url }) =>
|
|
2897
|
+
registry
|
|
2898
|
+
? [{ url, accept: PUB_ACCEPT }]
|
|
2899
|
+
: [
|
|
2900
|
+
{ url, accept: PUB_ACCEPT },
|
|
2901
|
+
{ url: `${url}/score`, accept: PUB_ACCEPT },
|
|
2902
|
+
],
|
|
2903
|
+
)
|
|
2071
2904
|
: [],
|
|
2072
2905
|
);
|
|
2073
2906
|
for (const p of pkgList) {
|
|
2907
|
+
const lookup = lookups.get(p);
|
|
2908
|
+
if (!lookup) {
|
|
2909
|
+
cdepList.push(p);
|
|
2910
|
+
continue;
|
|
2911
|
+
}
|
|
2074
2912
|
try {
|
|
2913
|
+
const { registry, url: PUB_PACKAGE_URL } = lookup;
|
|
2075
2914
|
if (DEBUG_MODE) {
|
|
2076
|
-
console.log(`Querying ${PUB_DEV_URL} for ${p.name}`);
|
|
2915
|
+
console.log(`Querying ${registry || PUB_DEV_URL} for ${p.name}`);
|
|
2077
2916
|
}
|
|
2078
|
-
const PUB_PACKAGE_URL = `${PUB_DEV_URL}/api/packages/${p.name}/versions/${p.version}`;
|
|
2079
|
-
const PUB_PACKAGE_SCORE_URL = `${PUB_PACKAGE_URL}/score`;
|
|
2080
2917
|
const res =
|
|
2081
2918
|
prefetchedResponse(prefetched, PUB_PACKAGE_URL) ||
|
|
2082
|
-
(await
|
|
2919
|
+
(await gatedGet(PUB_PACKAGE_URL, OPTIONS));
|
|
2083
2920
|
if (res?.body) {
|
|
2084
2921
|
const pubspec = res.body.pubspec;
|
|
2085
2922
|
p.description = pubspec.description;
|
|
@@ -2089,18 +2926,21 @@ export async function getDartMetadata(pkgList) {
|
|
|
2089
2926
|
if (pubspec.homepage) {
|
|
2090
2927
|
p.homepage = { url: pubspec.homepage };
|
|
2091
2928
|
}
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
|
|
2929
|
+
if (!registry) {
|
|
2930
|
+
const PUB_PACKAGE_SCORE_URL = `${PUB_PACKAGE_URL}/score`;
|
|
2931
|
+
const score =
|
|
2932
|
+
prefetchedResponse(prefetched, PUB_PACKAGE_SCORE_URL) ||
|
|
2933
|
+
(await gatedGet(PUB_PACKAGE_SCORE_URL, OPTIONS));
|
|
2934
|
+
if (score?.body) {
|
|
2935
|
+
const tags = score.body.tags;
|
|
2936
|
+
const license = tags.find((tag) => PUB_LICENSE_REGEX.test(tag));
|
|
2937
|
+
if (license) {
|
|
2938
|
+
p.license = spdxLicenses.find(
|
|
2939
|
+
(spdxLicense) =>
|
|
2940
|
+
spdxLicense.toLowerCase() ===
|
|
2941
|
+
license.replace(PUB_LICENSE_REGEX, "").toLowerCase(),
|
|
2942
|
+
);
|
|
2943
|
+
}
|
|
2104
2944
|
}
|
|
2105
2945
|
}
|
|
2106
2946
|
cdepList.push(p);
|
|
@@ -2151,6 +2991,60 @@ export function normalizeCargoIntegrity(integrity) {
|
|
|
2151
2991
|
return undefined;
|
|
2152
2992
|
}
|
|
2153
2993
|
|
|
2994
|
+
/**
|
|
2995
|
+
* The POM stored next to a jar, as in the Maven repository layout.
|
|
2996
|
+
*
|
|
2997
|
+
* @param {string} jarPath Path to a jar.
|
|
2998
|
+
* @returns {string|undefined} Path to `<name>.pom` when it exists.
|
|
2999
|
+
*/
|
|
3000
|
+
function siblingPomFile(jarPath) {
|
|
3001
|
+
if (!jarPath?.endsWith(".jar")) {
|
|
3002
|
+
return undefined;
|
|
3003
|
+
}
|
|
3004
|
+
const pomPath = `${jarPath.slice(0, -".jar".length)}.pom`;
|
|
3005
|
+
return safeExistsSync(pomPath) ? pomPath : undefined;
|
|
3006
|
+
}
|
|
3007
|
+
|
|
3008
|
+
/**
|
|
3009
|
+
* Read the groupId, artifactId and version a POM declares, inheriting the
|
|
3010
|
+
* groupId and version from its parent element when they are omitted.
|
|
3011
|
+
*
|
|
3012
|
+
* @param {string} pomPath Path to a POM.
|
|
3013
|
+
* @returns {{groupId: string, artifactId: string, version: string}|undefined}
|
|
3014
|
+
*/
|
|
3015
|
+
function readPomCoordinates(pomPath) {
|
|
3016
|
+
try {
|
|
3017
|
+
const pomData = parsePomXml(readFileSync(pomPath, "utf-8"));
|
|
3018
|
+
if (pomData?.groupId && pomData.artifactId && pomData.version) {
|
|
3019
|
+
return {
|
|
3020
|
+
groupId: pomData.groupId,
|
|
3021
|
+
artifactId: pomData.artifactId,
|
|
3022
|
+
version: pomData.version,
|
|
3023
|
+
};
|
|
3024
|
+
}
|
|
3025
|
+
} catch (_err) {
|
|
3026
|
+
// An unreadable POM contributes nothing.
|
|
3027
|
+
}
|
|
3028
|
+
return undefined;
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
/**
|
|
3032
|
+
* Whether a directory lies inside cdxgen's temp directory or the OS temp
|
|
3033
|
+
* directory. Cleanup in {@link extractJarArchive} is limited to such paths.
|
|
3034
|
+
*
|
|
3035
|
+
* @param {string} dir Directory to check.
|
|
3036
|
+
* @returns {boolean}
|
|
3037
|
+
*/
|
|
3038
|
+
function isInsideTmpDir(dir) {
|
|
3039
|
+
if (!dir) {
|
|
3040
|
+
return false;
|
|
3041
|
+
}
|
|
3042
|
+
return [getTmpDir(), tmpdir()].some((root) => {
|
|
3043
|
+
const rel = relative(resolve(root), resolve(dir));
|
|
3044
|
+
return !!rel && !rel.startsWith("..") && !isAbsolute(rel);
|
|
3045
|
+
});
|
|
3046
|
+
}
|
|
3047
|
+
|
|
2154
3048
|
/**
|
|
2155
3049
|
* Method to extract a war or ear file
|
|
2156
3050
|
*
|
|
@@ -2167,23 +3061,12 @@ export async function extractJarArchive(jarFile, tempDir, jarNSMapping = {}) {
|
|
|
2167
3061
|
// reported, as file components with zero-confidence identity evidence.
|
|
2168
3062
|
const unresolvedJars = [];
|
|
2169
3063
|
const fname = basename(jarFile);
|
|
2170
|
-
|
|
2171
|
-
//
|
|
2172
|
-
|
|
2173
|
-
//
|
|
2174
|
-
|
|
2175
|
-
// For now check for presence of both .pom and MANIFEST.MF files.
|
|
2176
|
-
if (jarFile.endsWith(".jar")) {
|
|
2177
|
-
pomname = jarFile.replace(".jar", ".pom");
|
|
2178
|
-
}
|
|
3064
|
+
// A POM next to the archive, as in the Maven repository layout, names its
|
|
3065
|
+
// coordinates. It is read where it is. The archive itself is always copied
|
|
3066
|
+
// into tempDir and extracted there, so nothing is ever written to or removed
|
|
3067
|
+
// from the directory being scanned.
|
|
3068
|
+
const siblingPom = siblingPomFile(jarFile);
|
|
2179
3069
|
if (
|
|
2180
|
-
pomname &&
|
|
2181
|
-
safeExistsSync(pomname) &&
|
|
2182
|
-
manifestname &&
|
|
2183
|
-
safeExistsSync(manifestname)
|
|
2184
|
-
) {
|
|
2185
|
-
tempDir = dirname(jarFile);
|
|
2186
|
-
} else if (
|
|
2187
3070
|
!safeExistsSync(join(tempDir, fname)) &&
|
|
2188
3071
|
safeExistsSync(jarFile) &&
|
|
2189
3072
|
lstatSync(jarFile).isFile()
|
|
@@ -2191,6 +3074,8 @@ export async function extractJarArchive(jarFile, tempDir, jarNSMapping = {}) {
|
|
|
2191
3074
|
// Only copy if the file doesn't exist
|
|
2192
3075
|
safeCopyFileSync(jarFile, join(tempDir, fname), constants.COPYFILE_FICLONE);
|
|
2193
3076
|
}
|
|
3077
|
+
const outerCopy = join(tempDir, fname);
|
|
3078
|
+
let outerExtracted = false;
|
|
2194
3079
|
const env = {
|
|
2195
3080
|
...process.env,
|
|
2196
3081
|
};
|
|
@@ -2206,11 +3091,11 @@ export async function extractJarArchive(jarFile, tempDir, jarNSMapping = {}) {
|
|
|
2206
3091
|
jarFile.endsWith(".hpi") ||
|
|
2207
3092
|
jarFile.endsWith(".jar")
|
|
2208
3093
|
) {
|
|
2209
|
-
if (safeExistsSync(
|
|
3094
|
+
if (safeExistsSync(outerCopy)) {
|
|
2210
3095
|
try {
|
|
2211
|
-
const zip = new StreamZip.async({ file:
|
|
3096
|
+
const zip = new StreamZip.async({ file: outerCopy });
|
|
2212
3097
|
const extracted = await safeExtractArchive(
|
|
2213
|
-
|
|
3098
|
+
outerCopy,
|
|
2214
3099
|
tempDir,
|
|
2215
3100
|
async () => {
|
|
2216
3101
|
await zip.extract(null, tempDir);
|
|
@@ -2220,6 +3105,7 @@ export async function extractJarArchive(jarFile, tempDir, jarNSMapping = {}) {
|
|
|
2220
3105
|
if (!extracted) {
|
|
2221
3106
|
return pkgList;
|
|
2222
3107
|
}
|
|
3108
|
+
outerExtracted = true;
|
|
2223
3109
|
} catch (e) {
|
|
2224
3110
|
console.log("Unable to extract %s. Skipping.", join(tempDir, fname), e);
|
|
2225
3111
|
return pkgList;
|
|
@@ -2250,32 +3136,41 @@ export async function extractJarArchive(jarFile, tempDir, jarNSMapping = {}) {
|
|
|
2250
3136
|
}
|
|
2251
3137
|
continue;
|
|
2252
3138
|
}
|
|
2253
|
-
pomname = jf.replace(".jar", ".pom");
|
|
2254
3139
|
const jarname = basename(jf);
|
|
2255
|
-
// Ignore test jars
|
|
3140
|
+
// Ignore test, sources and javadoc jars: none of them is a runtime
|
|
3141
|
+
// component, and identifying them only costs lookups.
|
|
2256
3142
|
if (
|
|
2257
3143
|
jarname.endsWith("-tests.jar") ||
|
|
2258
|
-
jarname.endsWith("-test-sources.jar")
|
|
3144
|
+
jarname.endsWith("-test-sources.jar") ||
|
|
3145
|
+
isDocumentationJar(jarname)
|
|
2259
3146
|
) {
|
|
2260
3147
|
if (DEBUG_MODE) {
|
|
2261
3148
|
console.log(`Skipping tests jar ${jarname}`);
|
|
2262
3149
|
}
|
|
2263
3150
|
continue;
|
|
2264
3151
|
}
|
|
2265
|
-
const
|
|
3152
|
+
const isOuterArchive = jf === outerCopy || jf === jarFile;
|
|
3153
|
+
// The outer archive is already extracted into tempDir. Every nested
|
|
3154
|
+
// archive gets a directory of its own, so the META-INF of the war or of
|
|
3155
|
+
// a previously read jar can never be mistaken for this jar's.
|
|
3156
|
+
const reuseOuter = jf === outerCopy && outerExtracted;
|
|
3157
|
+
const workDir = reuseOuter
|
|
3158
|
+
? tempDir
|
|
3159
|
+
: safeMkdtempSync(join(tempDir, "nested-"));
|
|
3160
|
+
const manifestDir = join(workDir, "META-INF");
|
|
2266
3161
|
const manifestFile = join(manifestDir, "MANIFEST.MF");
|
|
2267
3162
|
const mavenDir = join(manifestDir, "maven");
|
|
2268
3163
|
let jarResult = {
|
|
2269
3164
|
status: 1,
|
|
2270
3165
|
};
|
|
2271
|
-
if (
|
|
3166
|
+
if (reuseOuter) {
|
|
2272
3167
|
jarResult = { status: 0 };
|
|
2273
|
-
} else {
|
|
3168
|
+
} else if (workDir) {
|
|
2274
3169
|
// Unzip natively
|
|
2275
3170
|
try {
|
|
2276
3171
|
const zip = new StreamZip.async({ file: jf });
|
|
2277
|
-
const extracted = await safeExtractArchive(jf,
|
|
2278
|
-
await zip.extract(null,
|
|
3172
|
+
const extracted = await safeExtractArchive(jf, workDir, async () => {
|
|
3173
|
+
await zip.extract(null, workDir);
|
|
2279
3174
|
});
|
|
2280
3175
|
await zip.close();
|
|
2281
3176
|
jarResult = { status: extracted ? 0 : 1 };
|
|
@@ -2290,36 +3185,79 @@ export async function extractJarArchive(jarFile, tempDir, jarNSMapping = {}) {
|
|
|
2290
3185
|
// When maven descriptor is available take group, name and version from pom.properties
|
|
2291
3186
|
// META-INF/maven/${groupId}/${artifactId}/pom.properties
|
|
2292
3187
|
// see https://maven.apache.org/shared/maven-archiver/index.html
|
|
2293
|
-
const pomProperties = getPomPropertiesFromMavenDir(mavenDir);
|
|
3188
|
+
const pomProperties = getPomPropertiesFromMavenDir(mavenDir, jarname);
|
|
2294
3189
|
let group = pomProperties["groupId"];
|
|
2295
3190
|
let name = pomProperties["artifactId"];
|
|
2296
3191
|
let version = pomProperties["version"];
|
|
2297
3192
|
let confidence = 0.5;
|
|
2298
3193
|
let technique = "manifest-analysis";
|
|
3194
|
+
let classifier;
|
|
3195
|
+
// A POM stored next to the archive names the same coordinates.
|
|
3196
|
+
const identityPom = isOuterArchive ? siblingPom : siblingPomFile(jf);
|
|
3197
|
+
if ((!group || !name || !version) && identityPom) {
|
|
3198
|
+
const pomData = readPomCoordinates(identityPom);
|
|
3199
|
+
if (pomData) {
|
|
3200
|
+
group = pomData.groupId;
|
|
3201
|
+
name = pomData.artifactId;
|
|
3202
|
+
version = pomData.version;
|
|
3203
|
+
}
|
|
3204
|
+
}
|
|
3205
|
+
// A jar inside a local Maven repository or Gradle cache is named by
|
|
3206
|
+
// where it sits.
|
|
3207
|
+
if ((!group || !name || !version) && isOuterArchive) {
|
|
3208
|
+
const coordinates = inferMavenCoordinatesFromPath(jarFile);
|
|
3209
|
+
if (coordinates?.extension === "jar") {
|
|
3210
|
+
group = coordinates.group;
|
|
3211
|
+
name = coordinates.name;
|
|
3212
|
+
version = coordinates.version;
|
|
3213
|
+
classifier = coordinates.classifier;
|
|
3214
|
+
technique = "filename";
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3217
|
+
// A jar that was copied out of a local cache is known by its SHA-1,
|
|
3218
|
+
// which answers what the Maven Central search would be asked.
|
|
3219
|
+
let sha;
|
|
3220
|
+
if (!group || !name || !version) {
|
|
3221
|
+
sha = await checksumFile("sha1", jf);
|
|
3222
|
+
const indexed = findMavenCoordinatesBySha1(sha);
|
|
3223
|
+
if (indexed) {
|
|
3224
|
+
group = indexed.group;
|
|
3225
|
+
name = indexed.name;
|
|
3226
|
+
version = indexed.version;
|
|
3227
|
+
classifier = indexed.classifier;
|
|
3228
|
+
technique = "hash-comparison";
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
2299
3231
|
if (
|
|
2300
3232
|
(!group || !name || !version) &&
|
|
2301
3233
|
SEARCH_MAVEN_ORG &&
|
|
2302
|
-
|
|
3234
|
+
!isHostCircuitOpen(MAVEN_SEARCH_HOST)
|
|
2303
3235
|
) {
|
|
2304
3236
|
try {
|
|
2305
|
-
|
|
2306
|
-
const searchurl = `https
|
|
3237
|
+
sha = sha || (await checksumFile("sha1", jf));
|
|
3238
|
+
const searchurl = `https://${MAVEN_SEARCH_HOST}/solrsearch/select?q=1:%22${sha}%22&rows=20&wt=json`;
|
|
2307
3239
|
// One Solr query per unidentified jar, against Central's origin
|
|
2308
3240
|
// rather than its CDN. A directory of jars issues these back to
|
|
2309
3241
|
// back, so they go through the same per-host gate the batch pool
|
|
2310
|
-
// uses (500 ms, serial) instead of a second, ungoverned path.
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
3242
|
+
// uses (500 ms, serial) instead of a second, ungoverned path. A
|
|
3243
|
+
// repeated hash is answered from the response cache without
|
|
3244
|
+
// waiting for that gate.
|
|
3245
|
+
const res = await withHostRateLimit(
|
|
3246
|
+
searchurl,
|
|
3247
|
+
(gate) =>
|
|
3248
|
+
cdxgenAgent.get(searchurl, {
|
|
3249
|
+
...gate,
|
|
3250
|
+
responseType: "json",
|
|
3251
|
+
timeout: {
|
|
3252
|
+
lookup: 1000,
|
|
3253
|
+
connect: 5000,
|
|
3254
|
+
secureConnect: 5000,
|
|
3255
|
+
socket: 1000,
|
|
3256
|
+
send: 10000,
|
|
3257
|
+
response: 1000,
|
|
3258
|
+
},
|
|
3259
|
+
}),
|
|
3260
|
+
{ deferGate: true },
|
|
2323
3261
|
);
|
|
2324
3262
|
const data = res?.body ? res.body["response"] : undefined;
|
|
2325
3263
|
if (data && data["numFound"] === 1) {
|
|
@@ -2330,16 +3268,21 @@ export async function extractJarArchive(jarFile, tempDir, jarNSMapping = {}) {
|
|
|
2330
3268
|
technique = "hash-comparison";
|
|
2331
3269
|
}
|
|
2332
3270
|
} catch (err) {
|
|
3271
|
+
// Any failure other than "not found" pauses the search for a
|
|
3272
|
+
// while; a 429 has already done so. Only a rate limit or an
|
|
3273
|
+
// unanswered request is the host's doing and worth a warning; a
|
|
3274
|
+
// dry run or an allowlist block pauses it quietly.
|
|
2333
3275
|
if (err?.message && !err.message.includes("404")) {
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
err.
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
3276
|
+
const rateLimited = err.message.includes("429");
|
|
3277
|
+
const unanswered =
|
|
3278
|
+
err.name === "RequestError" &&
|
|
3279
|
+
!err.options?.context?.activityBlocked;
|
|
3280
|
+
openHostCircuit(MAVEN_SEARCH_HOST, {
|
|
3281
|
+
cause: rateLimited
|
|
3282
|
+
? "answered HTTP 429 (rate limited)"
|
|
3283
|
+
: "did not answer",
|
|
3284
|
+
quiet: !rateLimited && !unanswered,
|
|
3285
|
+
});
|
|
2343
3286
|
}
|
|
2344
3287
|
}
|
|
2345
3288
|
}
|
|
@@ -2442,12 +3385,16 @@ export async function extractJarArchive(jarFile, tempDir, jarNSMapping = {}) {
|
|
|
2442
3385
|
value: jf,
|
|
2443
3386
|
},
|
|
2444
3387
|
];
|
|
3388
|
+
const qualifiers = { type: "jar" };
|
|
3389
|
+
if (classifier) {
|
|
3390
|
+
qualifiers.classifier = classifier;
|
|
3391
|
+
}
|
|
2445
3392
|
const purl = build({
|
|
2446
3393
|
type: "maven",
|
|
2447
3394
|
namespace: group || null,
|
|
2448
3395
|
name: name,
|
|
2449
3396
|
version: version || null,
|
|
2450
|
-
qualifiers
|
|
3397
|
+
qualifiers,
|
|
2451
3398
|
});
|
|
2452
3399
|
let namespaceValues;
|
|
2453
3400
|
let namespaceList;
|
|
@@ -2518,6 +3465,39 @@ export async function extractJarArchive(jarFile, tempDir, jarNSMapping = {}) {
|
|
|
2518
3465
|
},
|
|
2519
3466
|
properties,
|
|
2520
3467
|
};
|
|
3468
|
+
// The jar carries its own Maven descriptor, which holds the
|
|
3469
|
+
// description and licence no remote lookup is needed for, and a
|
|
3470
|
+
// manifest licence for the jars whose POM declares none.
|
|
3471
|
+
const descriptor = readEmbeddedMavenDescriptor(mavenDir, jarname);
|
|
3472
|
+
if (descriptor?.pomXml) {
|
|
3473
|
+
try {
|
|
3474
|
+
applyPomMetadata(
|
|
3475
|
+
apkg,
|
|
3476
|
+
xml2js(descriptor.pomXml, POM_XML_OPTIONS).project,
|
|
3477
|
+
descriptor.pomXml,
|
|
3478
|
+
);
|
|
3479
|
+
} catch (_err) {
|
|
3480
|
+
// An unparseable embedded POM contributes nothing.
|
|
3481
|
+
}
|
|
3482
|
+
}
|
|
3483
|
+
if (!hasMavenLicense(apkg)) {
|
|
3484
|
+
let manifestText;
|
|
3485
|
+
try {
|
|
3486
|
+
manifestText =
|
|
3487
|
+
safeExistsSync(manifestFile) &&
|
|
3488
|
+
statSync(manifestFile).size <= 256 * 1024
|
|
3489
|
+
? readFileSync(manifestFile, { encoding: "utf-8" })
|
|
3490
|
+
: undefined;
|
|
3491
|
+
} catch (_err) {
|
|
3492
|
+
manifestText = undefined;
|
|
3493
|
+
}
|
|
3494
|
+
applyManifestLicenses(
|
|
3495
|
+
apkg,
|
|
3496
|
+
parseManifestLicenseList(
|
|
3497
|
+
parseJarManifest(manifestText)["Bundle-License"],
|
|
3498
|
+
),
|
|
3499
|
+
);
|
|
3500
|
+
}
|
|
2521
3501
|
pkgList.push(apkg);
|
|
2522
3502
|
} else {
|
|
2523
3503
|
unresolvedJars.push(jf);
|
|
@@ -2527,12 +3507,17 @@ export async function extractJarArchive(jarFile, tempDir, jarNSMapping = {}) {
|
|
|
2527
3507
|
}
|
|
2528
3508
|
}
|
|
2529
3509
|
try {
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
3510
|
+
// Only directories this function created are removed. A caller's
|
|
3511
|
+
// tempDir is never the scanned directory, but the check keeps it that
|
|
3512
|
+
// way should a caller ever pass one.
|
|
3513
|
+
if (workDir && workDir !== tempDir && isInsideTmpDir(workDir)) {
|
|
3514
|
+
safeRmSync(workDir, { recursive: true, force: true });
|
|
3515
|
+
} else if (
|
|
3516
|
+
workDir === tempDir &&
|
|
3517
|
+
isInsideTmpDir(tempDir) &&
|
|
3518
|
+
safeExistsSync(manifestDir)
|
|
3519
|
+
) {
|
|
3520
|
+
safeRmSync(manifestDir, { recursive: true, force: true });
|
|
2536
3521
|
}
|
|
2537
3522
|
} catch (_err) {
|
|
2538
3523
|
// ignore cleanup errors
|
|
@@ -2628,14 +3613,31 @@ async function unresolvedJarComponents(unresolvedJars, jarFile, tempDir) {
|
|
|
2628
3613
|
*/
|
|
2629
3614
|
const METADATA_VERSION_PROP = "cdx:nuget:metadata_version";
|
|
2630
3615
|
|
|
3616
|
+
/**
|
|
3617
|
+
* Fetch one JSON document through the batch pool, so it shares the rate gate
|
|
3618
|
+
* and the disk cache with every other registry request. A direct request is
|
|
3619
|
+
* made only when batched fetching is off, which is the cassette replay the
|
|
3620
|
+
* golden corpus runs under.
|
|
3621
|
+
*
|
|
3622
|
+
* @param {string} url Document URL
|
|
3623
|
+
* @returns {Promise<{body: Object}|undefined>} The response, or undefined when
|
|
3624
|
+
* the batch answered definitely that the document is not available
|
|
3625
|
+
*/
|
|
3626
|
+
async function batchedJsonDocument(url) {
|
|
3627
|
+
if (prefetchEnabled()) {
|
|
3628
|
+
const fetched = await prefetchJson([{ url, responseType: "json" }]);
|
|
3629
|
+
const entry = fetched.get(url);
|
|
3630
|
+
return entry?.ok ? { body: entry.body } : undefined;
|
|
3631
|
+
}
|
|
3632
|
+
return await gatedGet(url, { responseType: "json" });
|
|
3633
|
+
}
|
|
3634
|
+
|
|
2631
3635
|
async function getNugetUrl() {
|
|
2632
3636
|
const req = "https://api.nuget.org/v3/index.json";
|
|
2633
3637
|
const fallbackUrl = "https://api.nuget.org/v3/registration3/";
|
|
2634
3638
|
let res;
|
|
2635
3639
|
try {
|
|
2636
|
-
res = await
|
|
2637
|
-
responseType: "json",
|
|
2638
|
-
});
|
|
3640
|
+
res = await batchedJsonDocument(req);
|
|
2639
3641
|
} catch (err) {
|
|
2640
3642
|
// Package lookups already degrade to unenriched components on error. An
|
|
2641
3643
|
// unreachable service index used to reject here instead and abort the
|
|
@@ -2645,6 +3647,10 @@ async function getNugetUrl() {
|
|
|
2645
3647
|
);
|
|
2646
3648
|
return fallbackUrl;
|
|
2647
3649
|
}
|
|
3650
|
+
if (!res) {
|
|
3651
|
+
thoughtLog(`Unable to read the NuGet service index. Using ${fallbackUrl}.`);
|
|
3652
|
+
return fallbackUrl;
|
|
3653
|
+
}
|
|
2648
3654
|
const urls = res?.body?.resources || [];
|
|
2649
3655
|
for (const resource of urls) {
|
|
2650
3656
|
if (resource["@type"] === "RegistrationsBaseUrl/3.6.0") {
|
|
@@ -2708,10 +3714,15 @@ async function queryNuget(p, NUGET_URL) {
|
|
|
2708
3714
|
const body = [];
|
|
2709
3715
|
const newBody = [];
|
|
2710
3716
|
const indexUrl = `${NUGET_URL + np.name.toLowerCase()}/index.json`;
|
|
2711
|
-
let res
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
3717
|
+
let res;
|
|
3718
|
+
try {
|
|
3719
|
+
res =
|
|
3720
|
+
prefetchedResponse(prefetchedNugetIndex, indexUrl) ||
|
|
3721
|
+
(await batchedJsonDocument(indexUrl));
|
|
3722
|
+
} catch {
|
|
3723
|
+
res = undefined;
|
|
3724
|
+
}
|
|
3725
|
+
const items = res?.body?.items;
|
|
2715
3726
|
if (!items?.[0]) {
|
|
2716
3727
|
return [np, newBody, body];
|
|
2717
3728
|
}
|
|
@@ -2745,8 +3756,13 @@ async function queryNuget(p, NUGET_URL) {
|
|
|
2745
3756
|
// at a page that must be fetched.
|
|
2746
3757
|
let leaves = item.items;
|
|
2747
3758
|
if (!leaves) {
|
|
2748
|
-
|
|
2749
|
-
|
|
3759
|
+
// The page URL is only known once the index answered, but it still
|
|
3760
|
+
// goes through the batch pool, its rate gate and its disk cache.
|
|
3761
|
+
const page = await batchedJsonDocument(item["@id"]);
|
|
3762
|
+
if (!page) {
|
|
3763
|
+
return [np, newBody, body];
|
|
3764
|
+
}
|
|
3765
|
+
leaves = page.body.items;
|
|
2750
3766
|
}
|
|
2751
3767
|
for (const i of leaves.reverse()) {
|
|
2752
3768
|
if (
|
|
@@ -2762,6 +3778,200 @@ async function queryNuget(p, NUGET_URL) {
|
|
|
2762
3778
|
return [np, newBody];
|
|
2763
3779
|
}
|
|
2764
3780
|
|
|
3781
|
+
/**
|
|
3782
|
+
* The global NuGet packages folder of this machine.
|
|
3783
|
+
*
|
|
3784
|
+
* @returns {string} `$NUGET_PACKAGES`, or the default `~/.nuget/packages`.
|
|
3785
|
+
*/
|
|
3786
|
+
function nugetPackagesRoot() {
|
|
3787
|
+
return resolve(
|
|
3788
|
+
readEnvironmentVariable("NUGET_PACKAGES") ||
|
|
3789
|
+
join(homedir(), ".nuget", "packages"),
|
|
3790
|
+
);
|
|
3791
|
+
}
|
|
3792
|
+
|
|
3793
|
+
/**
|
|
3794
|
+
* Read the nuspec of a package version installed in the global packages
|
|
3795
|
+
* folder, without asking the NuGet service.
|
|
3796
|
+
*
|
|
3797
|
+
* The folder and the nuspec inside it are named by the lower-cased package id,
|
|
3798
|
+
* under a folder named by the lower-cased version.
|
|
3799
|
+
*
|
|
3800
|
+
* @param {Object} p Component with `name` and an exact `version`
|
|
3801
|
+
* @returns {{license: string|undefined, description: string|undefined,
|
|
3802
|
+
* author: string|undefined, repository: {url: string}|undefined,
|
|
3803
|
+
* source: string}|undefined} The nuspec's fields, or undefined when the
|
|
3804
|
+
* package is not installed here or states no exact version
|
|
3805
|
+
*/
|
|
3806
|
+
function readLocalNugetNuspec(p) {
|
|
3807
|
+
if (
|
|
3808
|
+
!p?.name ||
|
|
3809
|
+
!p.version ||
|
|
3810
|
+
["0.0.0", "latest"].includes(p.version) ||
|
|
3811
|
+
p.version === "workspace"
|
|
3812
|
+
) {
|
|
3813
|
+
return undefined;
|
|
3814
|
+
}
|
|
3815
|
+
// The folder holds the lower-cased id and version. A name or version that is
|
|
3816
|
+
// not a single path segment cannot be a package there.
|
|
3817
|
+
const id = p.name.toLowerCase();
|
|
3818
|
+
const version = p.version.trim().toLowerCase();
|
|
3819
|
+
if (
|
|
3820
|
+
[id, version].some(
|
|
3821
|
+
(segment) =>
|
|
3822
|
+
segment.includes("/") ||
|
|
3823
|
+
segment.includes("\\") ||
|
|
3824
|
+
segment === "." ||
|
|
3825
|
+
segment === "..",
|
|
3826
|
+
)
|
|
3827
|
+
) {
|
|
3828
|
+
return undefined;
|
|
3829
|
+
}
|
|
3830
|
+
const nuspecFile = join(nugetPackagesRoot(), id, version, `${id}.nuspec`);
|
|
3831
|
+
if (!safeExistsSync(nuspecFile)) {
|
|
3832
|
+
return undefined;
|
|
3833
|
+
}
|
|
3834
|
+
let nuspec;
|
|
3835
|
+
try {
|
|
3836
|
+
nuspec = parseNuspecData(nuspecFile, readFileSync(nuspecFile, "utf-8"));
|
|
3837
|
+
} catch {
|
|
3838
|
+
return undefined;
|
|
3839
|
+
}
|
|
3840
|
+
const pkg = nuspec?.pkgList?.[0];
|
|
3841
|
+
if (!pkg?.name) {
|
|
3842
|
+
return undefined;
|
|
3843
|
+
}
|
|
3844
|
+
return {
|
|
3845
|
+
license: pkg.license,
|
|
3846
|
+
description: pkg.description,
|
|
3847
|
+
author: pkg.author,
|
|
3848
|
+
repository: pkg.repository,
|
|
3849
|
+
source: nuspecFile,
|
|
3850
|
+
};
|
|
3851
|
+
}
|
|
3852
|
+
|
|
3853
|
+
/**
|
|
3854
|
+
* Apply the installed nuspec of a package to its component, and say whether it
|
|
3855
|
+
* answered everything the registry round would have been asked for.
|
|
3856
|
+
*
|
|
3857
|
+
* @param {Object} p Component, enriched in place
|
|
3858
|
+
* @returns {boolean} true when the registry can be skipped for this package
|
|
3859
|
+
*/
|
|
3860
|
+
function applyLocalNugetNuspec(p) {
|
|
3861
|
+
const local = readLocalNugetNuspec(p);
|
|
3862
|
+
if (!local) {
|
|
3863
|
+
return false;
|
|
3864
|
+
}
|
|
3865
|
+
if (!p.license && local.license) {
|
|
3866
|
+
p.license = local.license;
|
|
3867
|
+
}
|
|
3868
|
+
if (!p.description && local.description) {
|
|
3869
|
+
p.description = local.description;
|
|
3870
|
+
}
|
|
3871
|
+
if (!p.author && local.author) {
|
|
3872
|
+
p.author = local.author;
|
|
3873
|
+
}
|
|
3874
|
+
if (!p.repository && local.repository) {
|
|
3875
|
+
p.repository = local.repository;
|
|
3876
|
+
}
|
|
3877
|
+
if (!p.license || !p.description) {
|
|
3878
|
+
return false;
|
|
3879
|
+
}
|
|
3880
|
+
p.properties = p.properties || [];
|
|
3881
|
+
if (!p.properties.some((prop) => prop.name === "cdx:nuget:metadataSource")) {
|
|
3882
|
+
p.properties.push({
|
|
3883
|
+
name: "cdx:nuget:metadataSource",
|
|
3884
|
+
value: "local-nuspec",
|
|
3885
|
+
});
|
|
3886
|
+
}
|
|
3887
|
+
return true;
|
|
3888
|
+
}
|
|
3889
|
+
|
|
3890
|
+
/**
|
|
3891
|
+
* Whether a licence is still the URL a package named rather than an id.
|
|
3892
|
+
*
|
|
3893
|
+
* @param {*} license Licence value.
|
|
3894
|
+
* @returns {boolean}
|
|
3895
|
+
*/
|
|
3896
|
+
function isUnresolvedLicenseUrl(license) {
|
|
3897
|
+
return typeof license === "string" && license.includes("://");
|
|
3898
|
+
}
|
|
3899
|
+
|
|
3900
|
+
/**
|
|
3901
|
+
* Fill a component from the NuGet catalog entry of its release, keeping every
|
|
3902
|
+
* value the project already gave it.
|
|
3903
|
+
*
|
|
3904
|
+
* When the entry names no licence id, only a GitHub licence URL or project URL,
|
|
3905
|
+
* the repository's licence is the remaining source. Those lookups are returned
|
|
3906
|
+
* rather than made here, so the caller can batch them for every package.
|
|
3907
|
+
*
|
|
3908
|
+
* @param {Object} p Component, enriched in place
|
|
3909
|
+
* @param {{leaf: Object, metadataVersion?: string}} entry The registration leaf
|
|
3910
|
+
* the service returned, and the release it describes when the component
|
|
3911
|
+
* states no version
|
|
3912
|
+
* @returns {string[]} GitHub URLs to ask for the licence, in order of preference
|
|
3913
|
+
*/
|
|
3914
|
+
function applyNugetCatalogEntry(p, { leaf, metadataVersion }) {
|
|
3915
|
+
const repoLicenseUrls = [];
|
|
3916
|
+
const catalogEntry = leaf.catalogEntry || {};
|
|
3917
|
+
// The component states no version, so the description and licence below
|
|
3918
|
+
// were read from whichever release the lookup fell back to. That release is
|
|
3919
|
+
// recorded rather than adopted as the version: a version cdxgen chose is not
|
|
3920
|
+
// a version the project declared.
|
|
3921
|
+
if (!p.version && metadataVersion) {
|
|
3922
|
+
p.properties = p.properties || [];
|
|
3923
|
+
if (!p.properties.some((prop) => prop.name === METADATA_VERSION_PROP)) {
|
|
3924
|
+
p.properties.push({
|
|
3925
|
+
name: METADATA_VERSION_PROP,
|
|
3926
|
+
value: metadataVersion,
|
|
3927
|
+
});
|
|
3928
|
+
}
|
|
3929
|
+
}
|
|
3930
|
+
if (catalogEntry.description && !p.description) {
|
|
3931
|
+
p.description = catalogEntry.description;
|
|
3932
|
+
}
|
|
3933
|
+
if (catalogEntry.authors && !p.author) {
|
|
3934
|
+
p.author = catalogEntry.authors.trim();
|
|
3935
|
+
}
|
|
3936
|
+
if (
|
|
3937
|
+
!p.license &&
|
|
3938
|
+
catalogEntry.licenseExpression &&
|
|
3939
|
+
catalogEntry.licenseExpression !== ""
|
|
3940
|
+
) {
|
|
3941
|
+
p.license = findLicenseId(catalogEntry.licenseExpression);
|
|
3942
|
+
} else if (!p.license && catalogEntry.licenseUrl) {
|
|
3943
|
+
p.license = findLicenseId(catalogEntry.licenseUrl);
|
|
3944
|
+
if (isUnresolvedLicenseUrl(p.license) && isGitHubUrl(p.license)) {
|
|
3945
|
+
repoLicenseUrls.push(p.license);
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3948
|
+
// Capture the tags
|
|
3949
|
+
if (catalogEntry.tags?.length && Array.isArray(catalogEntry.tags)) {
|
|
3950
|
+
p.tags = catalogEntry.tags.map((t) => t.toLowerCase().replaceAll(" ", "-"));
|
|
3951
|
+
}
|
|
3952
|
+
if (catalogEntry.projectUrl && !p.repository) {
|
|
3953
|
+
p.repository = { url: catalogEntry.projectUrl };
|
|
3954
|
+
// A versionless component gets the package's landing page. Pasting an
|
|
3955
|
+
// undefined version into the path produced a 404 link.
|
|
3956
|
+
p.homepage = {
|
|
3957
|
+
url: p.version
|
|
3958
|
+
? `https://www.nuget.org/packages/${p.name}/${p.version}/`
|
|
3959
|
+
: `https://www.nuget.org/packages/${p.name}/`,
|
|
3960
|
+
};
|
|
3961
|
+
// Only a licence that is missing or still a URL is looked for in the
|
|
3962
|
+
// repository. An id the package declared is its own answer, and asking
|
|
3963
|
+
// GitHub about every package would spend its small anonymous quota.
|
|
3964
|
+
if (
|
|
3965
|
+
(!p.license || isUnresolvedLicenseUrl(p.license)) &&
|
|
3966
|
+
typeof p.repository.url === "string" &&
|
|
3967
|
+
isGitHubUrl(p.repository.url)
|
|
3968
|
+
) {
|
|
3969
|
+
repoLicenseUrls.push(p.repository.url);
|
|
3970
|
+
}
|
|
3971
|
+
}
|
|
3972
|
+
return repoLicenseUrls;
|
|
3973
|
+
}
|
|
3974
|
+
|
|
2765
3975
|
/**
|
|
2766
3976
|
* Method to retrieve metadata for nuget packages
|
|
2767
3977
|
*
|
|
@@ -2776,14 +3986,21 @@ export async function getNugetMetadata(pkgList, dependencies = undefined) {
|
|
|
2776
3986
|
// Batch the per-package registration index requests. queryNuget's first
|
|
2777
3987
|
// request for each package is the index; the follow-up to a specific
|
|
2778
3988
|
// registration page depends on the index response and stays serial. Only
|
|
2779
|
-
// packages that would actually be queried (
|
|
2780
|
-
//
|
|
3989
|
+
// packages that would actually be queried (no remembered answer or error) are
|
|
3990
|
+
// prefetched, matching the loop's skip conditions.
|
|
2781
3991
|
const batchUrls = [];
|
|
2782
3992
|
const seenNugetUrls = new Set();
|
|
3993
|
+
// Packages the installed nuspec answered, so neither the batch nor the loop
|
|
3994
|
+
// asks the service about them.
|
|
3995
|
+
const servedLocally = new Set();
|
|
2783
3996
|
for (const p of pkgList) {
|
|
2784
3997
|
// A cached entry — a body or a recorded error — means the loop will not
|
|
2785
3998
|
// reach the network for this package, so the batch must not either.
|
|
2786
|
-
if (
|
|
3999
|
+
if (nugetEntries.has(`${p.name}|${p.version}`)) {
|
|
4000
|
+
continue;
|
|
4001
|
+
}
|
|
4002
|
+
if (applyLocalNugetNuspec(p)) {
|
|
4003
|
+
servedLocally.add(p);
|
|
2787
4004
|
continue;
|
|
2788
4005
|
}
|
|
2789
4006
|
const indexUrl = `${NUGET_URL + p.name.toLowerCase()}/index.json`;
|
|
@@ -2800,110 +4017,58 @@ export async function getNugetMetadata(pkgList, dependencies = undefined) {
|
|
|
2800
4017
|
ownIndexUrls.push(key);
|
|
2801
4018
|
}
|
|
2802
4019
|
}
|
|
4020
|
+
// Packages whose licence can only come from their GitHub repository, with
|
|
4021
|
+
// the URLs to try, so those lookups are batched once the loop is done.
|
|
4022
|
+
const repoLicenseLookups = [];
|
|
2803
4023
|
try {
|
|
2804
4024
|
for (const p of pkgList) {
|
|
2805
|
-
|
|
4025
|
+
// If there is a version, we can safely use the cache to retrieve the license
|
|
4026
|
+
// See: https://github.com/cdxgen/cdxgen/issues/352
|
|
4027
|
+
const cacheKey = `${p.name}|${p.version}`;
|
|
2806
4028
|
try {
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
cacheKey = `${p.name}|${p.version}`;
|
|
2810
|
-
let body = metadata_cache[cacheKey];
|
|
2811
|
-
|
|
2812
|
-
if (body?.error) {
|
|
4029
|
+
let entry = nugetEntries.get(cacheKey);
|
|
4030
|
+
if (entry?.error) {
|
|
2813
4031
|
cdepList.push(p);
|
|
2814
4032
|
continue;
|
|
2815
4033
|
}
|
|
2816
|
-
if (!
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
if (p.version !== np.version) {
|
|
2821
|
-
const oldRef = p["bom-ref"];
|
|
2822
|
-
// The purl is rebuilt alongside the bom-ref. Leaving it behind let a
|
|
2823
|
-
// component state one version in `version` and another - or none - in
|
|
2824
|
-
// its purl, which is what a scanner range matches on. See #4359.
|
|
2825
|
-
p.version = np.version;
|
|
2826
|
-
applyPurl(p, nugetPurl(np.name, np.version));
|
|
2827
|
-
if (oldRef && p["bom-ref"] !== oldRef) {
|
|
2828
|
-
depRepList[oldRef] = p["bom-ref"];
|
|
2829
|
-
}
|
|
4034
|
+
if (!entry) {
|
|
4035
|
+
if (servedLocally.has(p)) {
|
|
4036
|
+
cdepList.push(p);
|
|
4037
|
+
continue;
|
|
2830
4038
|
}
|
|
2831
|
-
|
|
2832
|
-
|
|
4039
|
+
const [np, leaves] = await queryNuget(p, NUGET_URL);
|
|
4040
|
+
entry = {
|
|
4041
|
+
leaf: leaves?.[0],
|
|
4042
|
+
version: np.version,
|
|
4043
|
+
metadataVersion: np.metadataVersion,
|
|
4044
|
+
};
|
|
4045
|
+
if (entry.leaf) {
|
|
4046
|
+
nugetEntries.set(cacheKey, entry);
|
|
2833
4047
|
}
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
(prop) => prop.name === METADATA_VERSION_PROP,
|
|
2845
|
-
)
|
|
2846
|
-
) {
|
|
2847
|
-
p.properties.push({
|
|
2848
|
-
name: METADATA_VERSION_PROP,
|
|
2849
|
-
value: np.metadataVersion,
|
|
2850
|
-
});
|
|
2851
|
-
}
|
|
2852
|
-
}
|
|
2853
|
-
p.description = body.catalogEntry.description;
|
|
2854
|
-
if (body.catalogEntry.authors) {
|
|
2855
|
-
p.author = body.catalogEntry.authors.trim();
|
|
2856
|
-
}
|
|
2857
|
-
if (
|
|
2858
|
-
body.catalogEntry.licenseExpression &&
|
|
2859
|
-
body.catalogEntry.licenseExpression !== ""
|
|
2860
|
-
) {
|
|
2861
|
-
p.license = findLicenseId(body.catalogEntry.licenseExpression);
|
|
2862
|
-
} else if (body.catalogEntry.licenseUrl) {
|
|
2863
|
-
p.license = findLicenseId(body.catalogEntry.licenseUrl);
|
|
2864
|
-
if (typeof p.license === "string" && isGitHubUrl(p.license)) {
|
|
2865
|
-
p.license =
|
|
2866
|
-
(await getRepoLicense(p.license, undefined)) || p.license;
|
|
2867
|
-
}
|
|
2868
|
-
}
|
|
2869
|
-
// Capture the tags
|
|
2870
|
-
if (
|
|
2871
|
-
body.catalogEntry?.tags?.length &&
|
|
2872
|
-
Array.isArray(body.catalogEntry.tags)
|
|
2873
|
-
) {
|
|
2874
|
-
p.tags = body.catalogEntry.tags.map((t) =>
|
|
2875
|
-
t.toLowerCase().replaceAll(" ", "-"),
|
|
2876
|
-
);
|
|
2877
|
-
}
|
|
2878
|
-
if (body.catalogEntry.projectUrl) {
|
|
2879
|
-
p.repository = { url: body.catalogEntry.projectUrl };
|
|
2880
|
-
// A versionless component gets the package's landing page. Pasting
|
|
2881
|
-
// an undefined version into the path produced a 404 link.
|
|
2882
|
-
p.homepage = {
|
|
2883
|
-
url: p.version
|
|
2884
|
-
? `https://www.nuget.org/packages/${p.name}/${p.version}/`
|
|
2885
|
-
: `https://www.nuget.org/packages/${p.name}/`,
|
|
2886
|
-
};
|
|
2887
|
-
if (
|
|
2888
|
-
(!p.license || typeof p.license === "string") &&
|
|
2889
|
-
typeof p.repository.url === "string" &&
|
|
2890
|
-
isGitHubUrl(p.repository.url)
|
|
2891
|
-
) {
|
|
2892
|
-
// license couldn't be properly identified and is still a url,
|
|
2893
|
-
// therefore trying to resolve license via repository
|
|
2894
|
-
p.license =
|
|
2895
|
-
(await getRepoLicense(p.repository.url, undefined)) ||
|
|
2896
|
-
p.license;
|
|
2897
|
-
}
|
|
2898
|
-
}
|
|
2899
|
-
cdepList.push(p);
|
|
4048
|
+
}
|
|
4049
|
+
if (p.version !== entry.version) {
|
|
4050
|
+
const oldRef = p["bom-ref"];
|
|
4051
|
+
// The purl is rebuilt alongside the bom-ref. Leaving it behind let a
|
|
4052
|
+
// component state one version in `version` and another - or none - in
|
|
4053
|
+
// its purl, which is what a scanner range matches on. See #4359.
|
|
4054
|
+
p.version = entry.version;
|
|
4055
|
+
applyPurl(p, nugetPurl(p.name, entry.version));
|
|
4056
|
+
if (oldRef && p["bom-ref"] !== oldRef) {
|
|
4057
|
+
depRepList[oldRef] = p["bom-ref"];
|
|
2900
4058
|
}
|
|
2901
4059
|
}
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
4060
|
+
// A package the service does not know, such as a private one, stays in
|
|
4061
|
+
// the BOM as the project described it.
|
|
4062
|
+
if (entry.leaf) {
|
|
4063
|
+
const repoLicenseUrls = applyNugetCatalogEntry(p, entry);
|
|
4064
|
+
if (repoLicenseUrls.length) {
|
|
4065
|
+
repoLicenseLookups.push({ p, repoLicenseUrls });
|
|
4066
|
+
}
|
|
2905
4067
|
}
|
|
2906
4068
|
cdepList.push(p);
|
|
4069
|
+
} catch (err) {
|
|
4070
|
+
nugetEntries.set(cacheKey, { error: err.code });
|
|
4071
|
+
cdepList.push(p);
|
|
2907
4072
|
}
|
|
2908
4073
|
}
|
|
2909
4074
|
} finally {
|
|
@@ -2915,6 +4080,20 @@ export async function getNugetMetadata(pkgList, dependencies = undefined) {
|
|
|
2915
4080
|
prefetchedNugetIndex.delete(indexUrl);
|
|
2916
4081
|
}
|
|
2917
4082
|
}
|
|
4083
|
+
// The first URL of each package is batched. A second is asked only when the
|
|
4084
|
+
// first gave no licence, as it was before.
|
|
4085
|
+
await prefetchRepoLicenses(
|
|
4086
|
+
repoLicenseLookups.map(({ repoLicenseUrls }) => repoLicenseUrls[0]),
|
|
4087
|
+
);
|
|
4088
|
+
for (const { p, repoLicenseUrls } of repoLicenseLookups) {
|
|
4089
|
+
for (const repoUrl of repoLicenseUrls) {
|
|
4090
|
+
const license = await getRepoLicense(repoUrl, undefined);
|
|
4091
|
+
if (license) {
|
|
4092
|
+
p.license = license;
|
|
4093
|
+
break;
|
|
4094
|
+
}
|
|
4095
|
+
}
|
|
4096
|
+
}
|
|
2918
4097
|
const newDependencies = [].concat(dependencies);
|
|
2919
4098
|
if (depRepList && newDependencies.length) {
|
|
2920
4099
|
const changed = Object.keys(depRepList);
|