@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,20 +1,21 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
3
|
-
import {
|
|
2
|
+
import { readdirSync, readFileSync, realpathSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { delimiter, dirname, join, resolve } from "node:path";
|
|
4
5
|
|
|
5
6
|
import { Purl } from "@cdxgen/cdx-purl";
|
|
7
|
+
import { compare as compareSemver } from "semver";
|
|
6
8
|
|
|
7
|
-
import { DEBUG_MODE } from "../core/activity.js";
|
|
9
|
+
import { DEBUG_MODE, readEnvironmentVariable } from "../core/activity.js";
|
|
8
10
|
import { shouldFetchLicense, shouldFetchVCS } from "../core/env.js";
|
|
9
|
-
import { safeExistsSync } from "../core/fs.js";
|
|
10
11
|
import {
|
|
11
12
|
applyPurl,
|
|
12
13
|
canonicalPurlFromLooseString,
|
|
13
14
|
tryBuildPurl,
|
|
14
15
|
tryParsePurl,
|
|
15
16
|
} from "../inventory/purl.js";
|
|
17
|
+
import { guessLicenseIds, isSpdxLicenseExpression } from "../inventory/spdx.js";
|
|
16
18
|
import {
|
|
17
|
-
_clearMetadataCache,
|
|
18
19
|
getGoPkgLicense,
|
|
19
20
|
getGoPkgVCSUrl,
|
|
20
21
|
prefetchGoPkgMetadata,
|
|
@@ -30,6 +31,494 @@ function toBase64(hexString) {
|
|
|
30
31
|
return Buffer.from(hexString, "hex").toString("base64");
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
/**
|
|
35
|
+
* The root of the Go module cache on this machine.
|
|
36
|
+
*
|
|
37
|
+
* @returns {string|undefined} `$GOMODCACHE`, or `$GOPATH/pkg/mod`, or the
|
|
38
|
+
* default `~/go/pkg/mod`.
|
|
39
|
+
*/
|
|
40
|
+
function goModuleCacheDir() {
|
|
41
|
+
const gomodcache = readEnvironmentVariable("GOMODCACHE");
|
|
42
|
+
if (gomodcache) {
|
|
43
|
+
return gomodcache;
|
|
44
|
+
}
|
|
45
|
+
const gopath = readEnvironmentVariable("GOPATH");
|
|
46
|
+
if (gopath) {
|
|
47
|
+
return join(gopath.split(delimiter)[0], "pkg", "mod");
|
|
48
|
+
}
|
|
49
|
+
return join(homedir(), "go", "pkg", "mod");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Escape a module path or version the way Go's module cache directory names
|
|
54
|
+
* do (module.EscapePath and module.EscapeVersion): every upper-case letter
|
|
55
|
+
* becomes `!` followed by the lower-case one, so the cache stays
|
|
56
|
+
* case-preserving on case-insensitive file systems (`github.com/Azure` is
|
|
57
|
+
* stored as `github.com/!azure`, and `v2.10.17-RC.1` as `v2.10.17-!r!c.1`).
|
|
58
|
+
*
|
|
59
|
+
* @param {string} value Module path or version
|
|
60
|
+
* @returns {string} The escaped value
|
|
61
|
+
*/
|
|
62
|
+
function escapeGoCacheName(value) {
|
|
63
|
+
let escaped = "";
|
|
64
|
+
for (const ch of value) {
|
|
65
|
+
if (ch >= "A" && ch <= "Z") {
|
|
66
|
+
escaped += `!${ch.toLowerCase()}`;
|
|
67
|
+
} else {
|
|
68
|
+
escaped += ch;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return escaped;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Path elements Windows reserves, which Go rejects in every module path.
|
|
75
|
+
const GO_WINDOWS_RESERVED_NAMES = new Set([
|
|
76
|
+
"CON",
|
|
77
|
+
"PRN",
|
|
78
|
+
"AUX",
|
|
79
|
+
"NUL",
|
|
80
|
+
..."123456789".split("").flatMap((n) => [`COM${n}`, `LPT${n}`]),
|
|
81
|
+
]);
|
|
82
|
+
// ASCII punctuation a file name element may hold, after module.fileNameOK.
|
|
83
|
+
const GO_FILE_NAME_PUNCTUATION = "!#$%&()+,-.=@[]^_{}~ ";
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Whether one character may appear in a path element of the given kind, as
|
|
87
|
+
* module.modPathOK, module.importPathOK and module.fileNameOK decide.
|
|
88
|
+
*
|
|
89
|
+
* @param {string} ch One character
|
|
90
|
+
* @param {"module"|"import"|"file"} kind What the element belongs to
|
|
91
|
+
* @returns {boolean}
|
|
92
|
+
*/
|
|
93
|
+
function isGoPathCharOK(ch, kind) {
|
|
94
|
+
if (/[A-Za-z0-9]/.test(ch)) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
if (kind === "file") {
|
|
98
|
+
return (
|
|
99
|
+
GO_FILE_NAME_PUNCTUATION.includes(ch) ||
|
|
100
|
+
(ch.charCodeAt(0) >= 0x80 && /\p{L}/u.test(ch))
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
return "-._~".includes(ch) || (kind === "import" && ch === "+");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Whether a path element is one the Go toolchain accepts, as module.checkElem
|
|
108
|
+
* decides. No accepted element is `.` or `..`, or holds a path separator, so a
|
|
109
|
+
* name that passes cannot leave the directory it is joined to.
|
|
110
|
+
*
|
|
111
|
+
* @param {string} elem Path element
|
|
112
|
+
* @param {"module"|"import"|"file"} kind What the element belongs to
|
|
113
|
+
* @returns {boolean}
|
|
114
|
+
*/
|
|
115
|
+
function isGoPathElementOK(elem, kind) {
|
|
116
|
+
if (!elem || /^\.+$/.test(elem) || elem.endsWith(".")) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
if (kind === "module" && elem.startsWith(".")) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
for (const ch of elem) {
|
|
123
|
+
if (!isGoPathCharOK(ch, kind)) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const dot = elem.indexOf(".");
|
|
128
|
+
const short = dot < 0 ? elem : elem.slice(0, dot);
|
|
129
|
+
if (GO_WINDOWS_RESERVED_NAMES.has(short.toUpperCase())) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
if (kind === "file") {
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
// Names that look like Windows short names, such as PROGRA~1.
|
|
136
|
+
const tilde = short.lastIndexOf("~");
|
|
137
|
+
return !(tilde >= 0 && /^\d+$/.test(short.slice(tilde + 1)));
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Whether a slash-separated path is one the Go toolchain accepts, as
|
|
142
|
+
* module.checkPath decides.
|
|
143
|
+
*
|
|
144
|
+
* @param {string} path Module or import path
|
|
145
|
+
* @param {"module"|"import"} kind What the path is
|
|
146
|
+
* @returns {boolean}
|
|
147
|
+
*/
|
|
148
|
+
function isGoPathOK(path, kind) {
|
|
149
|
+
return (
|
|
150
|
+
typeof path === "string" &&
|
|
151
|
+
path.length > 0 &&
|
|
152
|
+
!path.startsWith("-") &&
|
|
153
|
+
path.split("/").every((elem) => isGoPathElementOK(elem, kind))
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Whether a module path passes module.CheckPath: a valid path whose first
|
|
159
|
+
* element is a lower-case host name with a dot in it. Go writes no module
|
|
160
|
+
* cache directory for any other path.
|
|
161
|
+
*
|
|
162
|
+
* @param {string} path Module path
|
|
163
|
+
* @returns {boolean}
|
|
164
|
+
*/
|
|
165
|
+
function isValidGoModulePath(path) {
|
|
166
|
+
if (!isGoPathOK(path, "module")) {
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
const host = path.split("/")[0];
|
|
170
|
+
return host.includes(".") && /^[a-z0-9.-]+$/.test(host);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Whether a version passes module.EscapeVersion: a single valid file name
|
|
175
|
+
* element without `!`.
|
|
176
|
+
*
|
|
177
|
+
* @param {string} version Module version
|
|
178
|
+
* @returns {boolean}
|
|
179
|
+
*/
|
|
180
|
+
function isValidGoVersion(version) {
|
|
181
|
+
return (
|
|
182
|
+
typeof version === "string" &&
|
|
183
|
+
!version.includes("!") &&
|
|
184
|
+
isGoPathElementOK(version, "file")
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// The licence notice file names pkg.go.dev reads, compared without regard to
|
|
189
|
+
// case (golang/pkgsite internal/licenses FileNames).
|
|
190
|
+
const GO_LICENSE_FILE_NAMES = new Set(
|
|
191
|
+
[
|
|
192
|
+
"COPYING",
|
|
193
|
+
"COPYING.md",
|
|
194
|
+
"COPYING.markdown",
|
|
195
|
+
"COPYING.txt",
|
|
196
|
+
"COPYING.LESSER",
|
|
197
|
+
"COPYING.LESSER.md",
|
|
198
|
+
"COPYING.LESSER.markdown",
|
|
199
|
+
"COPYING.LESSER.txt",
|
|
200
|
+
"LICENCE",
|
|
201
|
+
"LICENCE.md",
|
|
202
|
+
"LICENCE.markdown",
|
|
203
|
+
"LICENCE.txt",
|
|
204
|
+
"LICENSE",
|
|
205
|
+
"LICENSE.md",
|
|
206
|
+
"LICENSE.markdown",
|
|
207
|
+
"LICENSE.txt",
|
|
208
|
+
"LICENSE-2.0.txt",
|
|
209
|
+
"LICENCE-2.0.txt",
|
|
210
|
+
"LICENSE-APACHE",
|
|
211
|
+
"LICENCE-APACHE",
|
|
212
|
+
"LICENSE-APACHE-2.0.txt",
|
|
213
|
+
"LICENCE-APACHE-2.0.txt",
|
|
214
|
+
"LICENSE-MIT",
|
|
215
|
+
"LICENCE-MIT",
|
|
216
|
+
"LICENSE.MIT",
|
|
217
|
+
"LICENCE.MIT",
|
|
218
|
+
"LICENSE.code",
|
|
219
|
+
"LICENCE.code",
|
|
220
|
+
"LICENSE.docs",
|
|
221
|
+
"LICENCE.docs",
|
|
222
|
+
"LICENSE.rst",
|
|
223
|
+
"LICENCE.rst",
|
|
224
|
+
"MIT-LICENSE",
|
|
225
|
+
"MIT-LICENCE",
|
|
226
|
+
"MIT-LICENSE.md",
|
|
227
|
+
"MIT-LICENCE.md",
|
|
228
|
+
"MIT-LICENSE.markdown",
|
|
229
|
+
"MIT-LICENCE.markdown",
|
|
230
|
+
"MIT-LICENSE.txt",
|
|
231
|
+
"MIT-LICENCE.txt",
|
|
232
|
+
"MIT_LICENSE",
|
|
233
|
+
"MIT_LICENCE",
|
|
234
|
+
"UNLICENSE",
|
|
235
|
+
"UNLICENCE",
|
|
236
|
+
].map((f) => f.toLowerCase()),
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Whether a file name is one of the licence notices pkg.go.dev reads, whose
|
|
241
|
+
* text describes the module's licence without a registry round.
|
|
242
|
+
*
|
|
243
|
+
* @param {string} fileName Candidate file name
|
|
244
|
+
* @returns {boolean}
|
|
245
|
+
*/
|
|
246
|
+
function isGoLicenseFileName(fileName) {
|
|
247
|
+
return GO_LICENSE_FILE_NAMES.has(fileName.toLowerCase());
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* The licence notices of a module that is already on this machine, from the
|
|
252
|
+
* directory `go list` reported, from the project's `vendor/` tree, or from the
|
|
253
|
+
* module cache. Reading them costs no network round trip, so they answer
|
|
254
|
+
* before pkg.go.dev is asked. Every notice of the first directory holding
|
|
255
|
+
* one is returned, as pkg.go.dev reads every notice of a module.
|
|
256
|
+
*
|
|
257
|
+
* The directory `go list` reported is always read. The vendor tree and the
|
|
258
|
+
* module cache stand in for pkg.go.dev, so they are read only when licences
|
|
259
|
+
* are being fetched. Their directories are built from a module path and
|
|
260
|
+
* version read from modules.txt or go.sum, so a path or version the Go
|
|
261
|
+
* toolchain would reject, such as one with a `..` element, is not looked up:
|
|
262
|
+
* Go never writes such a directory, and joining it could leave the tree.
|
|
263
|
+
*
|
|
264
|
+
* @param {string} name Full module path
|
|
265
|
+
* @param {string} [version] Module version, for the module cache layout
|
|
266
|
+
* @param {Object} [options] Options
|
|
267
|
+
* @param {string} [options.dir] The module's directory, as `go list` reports it
|
|
268
|
+
* @param {string} [options.projectRoot] Project root holding a `vendor/` tree
|
|
269
|
+
* @param {string} [options.vendorPath] The path the module is vendored under,
|
|
270
|
+
* when it differs from `name`: Go vendors a replaced module under the path
|
|
271
|
+
* it replaces
|
|
272
|
+
* @returns {{fileName: string, text: string}[]} The notices, in file name
|
|
273
|
+
* order; empty when none is held locally
|
|
274
|
+
*/
|
|
275
|
+
export function readLocalGoLicenses(name, version, options = {}) {
|
|
276
|
+
if (!name) {
|
|
277
|
+
return [];
|
|
278
|
+
}
|
|
279
|
+
const candidateDirs = [];
|
|
280
|
+
if (options.dir) {
|
|
281
|
+
candidateDirs.push(options.dir);
|
|
282
|
+
}
|
|
283
|
+
const fetchingLicenses = shouldFetchLicense();
|
|
284
|
+
const vendorPath = options.vendorPath || name;
|
|
285
|
+
if (
|
|
286
|
+
fetchingLicenses &&
|
|
287
|
+
options.projectRoot &&
|
|
288
|
+
isGoPathOK(vendorPath, "import")
|
|
289
|
+
) {
|
|
290
|
+
candidateDirs.push(
|
|
291
|
+
join(options.projectRoot, "vendor", ...vendorPath.split("/")),
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
if (
|
|
295
|
+
fetchingLicenses &&
|
|
296
|
+
version &&
|
|
297
|
+
isValidGoModulePath(name) &&
|
|
298
|
+
isValidGoVersion(version)
|
|
299
|
+
) {
|
|
300
|
+
const cacheRoot = goModuleCacheDir();
|
|
301
|
+
if (cacheRoot) {
|
|
302
|
+
candidateDirs.push(
|
|
303
|
+
join(
|
|
304
|
+
cacheRoot,
|
|
305
|
+
`${escapeGoCacheName(name)}@${escapeGoCacheName(version)}`,
|
|
306
|
+
),
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
for (const dir of candidateDirs) {
|
|
311
|
+
let entries;
|
|
312
|
+
try {
|
|
313
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
314
|
+
} catch {
|
|
315
|
+
continue;
|
|
316
|
+
}
|
|
317
|
+
// Only regular files: a module zip holds no links, so a link named like
|
|
318
|
+
// a notice did not come from the module.
|
|
319
|
+
const fileNames = entries
|
|
320
|
+
.filter((e) => e.isFile() && isGoLicenseFileName(e.name))
|
|
321
|
+
.map((e) => e.name)
|
|
322
|
+
.sort((a, b) => a.localeCompare(b));
|
|
323
|
+
const notices = [];
|
|
324
|
+
for (const fileName of fileNames) {
|
|
325
|
+
try {
|
|
326
|
+
const text = readFileSync(join(dir, fileName), {
|
|
327
|
+
encoding: "utf-8",
|
|
328
|
+
});
|
|
329
|
+
if (text?.length) {
|
|
330
|
+
notices.push({ fileName, text });
|
|
331
|
+
}
|
|
332
|
+
} catch {
|
|
333
|
+
// An unreadable notice is the same as none.
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
if (notices.length) {
|
|
337
|
+
return notices;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
return [];
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* The text of the first licence notice {@link readLocalGoLicenses} finds.
|
|
345
|
+
*
|
|
346
|
+
* @param {string} name Full module path
|
|
347
|
+
* @param {string} [version] Module version, for the module cache layout
|
|
348
|
+
* @param {Object} [options] The options {@link readLocalGoLicenses} takes
|
|
349
|
+
* @returns {string|undefined} The notice text, or undefined when none is held
|
|
350
|
+
* locally
|
|
351
|
+
*/
|
|
352
|
+
export function readLocalGoLicense(name, version, options = {}) {
|
|
353
|
+
return readLocalGoLicenses(name, version, options)[0]?.text;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Whether a registry can hold this module: a module path and version the Go
|
|
358
|
+
* toolchain accepts. Any other module was never published, and its path in a
|
|
359
|
+
* pkg.go.dev URL could resolve to a different module (`../x` would ask for
|
|
360
|
+
* `x`).
|
|
361
|
+
*
|
|
362
|
+
* @param {string} name Full module path
|
|
363
|
+
* @param {string} [version] Module version
|
|
364
|
+
* @returns {boolean}
|
|
365
|
+
*/
|
|
366
|
+
function isGoRegistryModule(name, version) {
|
|
367
|
+
return isValidGoModulePath(name) && (!version || isValidGoVersion(version));
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* The modules the pkg.go.dev batch asks about, each marked when its licence
|
|
372
|
+
* notice is held on this machine, so the batch skips its licence tab but
|
|
373
|
+
* still fetches its module page for the repository URL. A module no registry
|
|
374
|
+
* holds (the main module, local code, or a path Go rejects) is left out, as
|
|
375
|
+
* getGoPkgComponent never looks it up.
|
|
376
|
+
*
|
|
377
|
+
* @param {Object[]} modules Modules with `name`, `version`,
|
|
378
|
+
* `skipRegistryLookups` and the options {@link readLocalGoLicenses} takes
|
|
379
|
+
* @returns {Object[]} Copies of the modules to ask about, with
|
|
380
|
+
* `licenseKnown` set
|
|
381
|
+
*/
|
|
382
|
+
function goModulesToPrefetch(modules) {
|
|
383
|
+
return modules
|
|
384
|
+
.filter(
|
|
385
|
+
(m) => !m.skipRegistryLookups && isGoRegistryModule(m.name, m.version),
|
|
386
|
+
)
|
|
387
|
+
.map((m) => ({
|
|
388
|
+
...m,
|
|
389
|
+
licenseKnown: readLocalGoLicenses(m.name, m.version, m).length > 0,
|
|
390
|
+
}));
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* The CycloneDX licences of notices read from disk. Every licence each notice
|
|
395
|
+
* grants is listed with the notice as its text, and a notice that names none
|
|
396
|
+
* is a CUSTOM licence. A licence with an exception is an SPDX expression,
|
|
397
|
+
* which CycloneDX only carries as the sole entry, so then every licence found
|
|
398
|
+
* is joined into one expression.
|
|
399
|
+
*
|
|
400
|
+
* @param {{text: string}[]} notices Notices read by {@link readLocalGoLicenses}
|
|
401
|
+
* @returns {Object[]} CycloneDX licence choices
|
|
402
|
+
*/
|
|
403
|
+
function localGoLicenses(notices) {
|
|
404
|
+
const licenses = [];
|
|
405
|
+
const ids = [];
|
|
406
|
+
for (const { text } of notices) {
|
|
407
|
+
const textObject = { contentType: "text/plain", content: text };
|
|
408
|
+
const noticeIds = guessLicenseIds(text);
|
|
409
|
+
if (!noticeIds.length) {
|
|
410
|
+
licenses.push({ license: { name: "CUSTOM", text: textObject } });
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
for (const id of noticeIds) {
|
|
414
|
+
if (ids.includes(id)) {
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
ids.push(id);
|
|
418
|
+
licenses.push({ license: { id, text: textObject } });
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (ids.some((id) => isSpdxLicenseExpression(id))) {
|
|
422
|
+
return [
|
|
423
|
+
{
|
|
424
|
+
expression: ids
|
|
425
|
+
.map((id) => (ids.length > 1 && id.includes(" ") ? `(${id})` : id))
|
|
426
|
+
.join(" AND "),
|
|
427
|
+
},
|
|
428
|
+
];
|
|
429
|
+
}
|
|
430
|
+
return licenses;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* What a replace directive makes of one module, in the shape every Go parser
|
|
435
|
+
* reports it. The Go toolchain builds the replacement, so a module replacement
|
|
436
|
+
* is reported under the replacement's path and version, as govulncheck and
|
|
437
|
+
* syft report it. A directory replacement is local code that no registry
|
|
438
|
+
* holds: it keeps the module path it replaces and has no version, so its purl
|
|
439
|
+
* names no registry release.
|
|
440
|
+
*
|
|
441
|
+
* @param {string} path The module path being replaced
|
|
442
|
+
* @param {string} [version] The version being replaced, absent for a
|
|
443
|
+
* replacement of every version
|
|
444
|
+
* @param {string} newPath The replacement module path or directory
|
|
445
|
+
* @param {string} [newVersion] The replacement version, absent (or `(devel)`
|
|
446
|
+
* in build info) for a directory
|
|
447
|
+
* @returns {Object|undefined} `name` and `version` to report, `local` for a
|
|
448
|
+
* directory, `localDir` the directory as written, and `replaces` the
|
|
449
|
+
* `path@version` it stands in for
|
|
450
|
+
*/
|
|
451
|
+
function goReplacement(path, version, newPath, newVersion) {
|
|
452
|
+
if (!path || !newPath) {
|
|
453
|
+
return undefined;
|
|
454
|
+
}
|
|
455
|
+
const replaces = version ? `${path}@${version}` : path;
|
|
456
|
+
if (!newVersion || newVersion === "(devel)") {
|
|
457
|
+
return {
|
|
458
|
+
name: path,
|
|
459
|
+
version: undefined,
|
|
460
|
+
local: true,
|
|
461
|
+
localDir: newPath,
|
|
462
|
+
replaces,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
return { name: newPath, version: newVersion, local: false, replaces };
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Record on a component the module a replace directive put it in place of.
|
|
470
|
+
*
|
|
471
|
+
* @param {Object} component Component built for the replacement
|
|
472
|
+
* @param {Object} [replacement] Result of {@link goReplacement}
|
|
473
|
+
* @returns {Object} The same component
|
|
474
|
+
*/
|
|
475
|
+
function addGoReplacementProperties(component, replacement) {
|
|
476
|
+
if (!replacement) {
|
|
477
|
+
return component;
|
|
478
|
+
}
|
|
479
|
+
component.properties = component.properties || [];
|
|
480
|
+
component.properties.push({
|
|
481
|
+
name: "cdx:go:replaces",
|
|
482
|
+
value: replacement.replaces,
|
|
483
|
+
});
|
|
484
|
+
if (replacement.localDir) {
|
|
485
|
+
component.properties.push({
|
|
486
|
+
name: "cdx:go:local_dir",
|
|
487
|
+
value: replacement.localDir,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
return component;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Order two Go module versions by semantic version precedence, as Go's semver
|
|
495
|
+
* package does: a pre-release, pseudo-versions included, sorts before its
|
|
496
|
+
* release, and build metadata such as `+incompatible` does not count.
|
|
497
|
+
*
|
|
498
|
+
* @param {string} a A module version
|
|
499
|
+
* @param {string} b Another module version
|
|
500
|
+
* @returns {number} Negative, zero or positive as `a` sorts before, with or
|
|
501
|
+
* after `b`
|
|
502
|
+
*/
|
|
503
|
+
export function compareGoVersions(a, b) {
|
|
504
|
+
try {
|
|
505
|
+
return compareSemver(a, b);
|
|
506
|
+
} catch {
|
|
507
|
+
return (a || "").localeCompare(b || "");
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Convert a Go checksum (`h1:` followed by the base64 SHA-256) to the
|
|
513
|
+
* integrity string components carry.
|
|
514
|
+
*
|
|
515
|
+
* @param {string} [sum] Go checksum
|
|
516
|
+
* @returns {string|undefined} `sha256-…`, or undefined without a checksum
|
|
517
|
+
*/
|
|
518
|
+
function goSumIntegrity(sum) {
|
|
519
|
+
return sum?.startsWith("h1:") ? `sha256-${sum.slice(3)}` : undefined;
|
|
520
|
+
}
|
|
521
|
+
|
|
33
522
|
/**
|
|
34
523
|
* Builds a Go package component object containing purl, bom-ref, integrity hash,
|
|
35
524
|
* and optionally license and VCS external reference information.
|
|
@@ -38,11 +527,36 @@ function toBase64(hexString) {
|
|
|
38
527
|
* @param {string} name Package name (full module path when group is empty)
|
|
39
528
|
* @param {string} version Package version string
|
|
40
529
|
* @param {string} hash Integrity hash (e.g. "sha256-…"), used as _integrity
|
|
530
|
+
* @param {Object} [options] Options
|
|
531
|
+
* @param {boolean} [options.skipRegistryLookups] Leave the licence and VCS
|
|
532
|
+
* URL unset instead of asking pkg.go.dev. Used for the main module, which
|
|
533
|
+
* no registry holds.
|
|
534
|
+
* @param {string} [options.dir] The module's directory, as `go list` reports
|
|
535
|
+
* it, where a local licence notice is read first
|
|
536
|
+
* @param {string} [options.projectRoot] Project root holding a `vendor/` tree
|
|
537
|
+
* the module may be vendored in
|
|
538
|
+
* @param {string} [options.vendorPath] The path the module is vendored under,
|
|
539
|
+
* when it differs from `name`
|
|
41
540
|
* @returns {Promise<Object>} Component object ready for inclusion in a BOM package list
|
|
42
541
|
*/
|
|
43
|
-
export async function getGoPkgComponent(
|
|
542
|
+
export async function getGoPkgComponent(
|
|
543
|
+
group,
|
|
544
|
+
name,
|
|
545
|
+
version,
|
|
546
|
+
hash,
|
|
547
|
+
options = {},
|
|
548
|
+
) {
|
|
44
549
|
let license;
|
|
45
|
-
|
|
550
|
+
let licenses;
|
|
551
|
+
const registryLookups =
|
|
552
|
+
!options.skipRegistryLookups &&
|
|
553
|
+
isGoRegistryModule(group ? `${group}/${name}` : name, version);
|
|
554
|
+
// Notices held on this machine answer before pkg.go.dev is asked, and the
|
|
555
|
+
// registry is consulted only when there is none.
|
|
556
|
+
const notices = readLocalGoLicenses(name, version, options);
|
|
557
|
+
if (notices.length) {
|
|
558
|
+
licenses = localGoLicenses(notices);
|
|
559
|
+
} else if (shouldFetchLicense() && registryLookups) {
|
|
46
560
|
if (DEBUG_MODE) {
|
|
47
561
|
console.log(
|
|
48
562
|
`About to fetch go package license information for ${group}:${name}`,
|
|
@@ -51,6 +565,7 @@ export async function getGoPkgComponent(group, name, version, hash) {
|
|
|
51
565
|
license = await getGoPkgLicense({
|
|
52
566
|
group: group,
|
|
53
567
|
name: name,
|
|
568
|
+
version: version,
|
|
54
569
|
});
|
|
55
570
|
}
|
|
56
571
|
// Split the full module path into namespace and name, since the caller
|
|
@@ -76,8 +591,8 @@ export async function getGoPkgComponent(group, name, version, hash) {
|
|
|
76
591
|
version: version || null,
|
|
77
592
|
})?.replace(/%2F/g, "/") || null;
|
|
78
593
|
let vcs;
|
|
79
|
-
if (shouldFetchVCS()) {
|
|
80
|
-
vcs = await getGoPkgVCSUrl(group, name);
|
|
594
|
+
if (shouldFetchVCS() && registryLookups) {
|
|
595
|
+
vcs = await getGoPkgVCSUrl(group, name, version);
|
|
81
596
|
}
|
|
82
597
|
const packageInfo = {
|
|
83
598
|
group: group,
|
|
@@ -86,6 +601,9 @@ export async function getGoPkgComponent(group, name, version, hash) {
|
|
|
86
601
|
_integrity: hash,
|
|
87
602
|
license: license,
|
|
88
603
|
};
|
|
604
|
+
if (licenses) {
|
|
605
|
+
packageInfo.licenses = licenses;
|
|
606
|
+
}
|
|
89
607
|
applyPurl(packageInfo, purlString);
|
|
90
608
|
if (vcs) {
|
|
91
609
|
packageInfo.externalReferences = [{ type: "vcs", url: vcs }];
|
|
@@ -99,7 +617,8 @@ export async function getGoPkgComponent(group, name, version, hash) {
|
|
|
99
617
|
* @param {String} goModData Contents of go.mod file
|
|
100
618
|
* @param {Object} gosumMap Data from go.sum files
|
|
101
619
|
*
|
|
102
|
-
* @returns {Object} Object containing parent component, rootList
|
|
620
|
+
* @returns {Object} Object containing parent component, rootList, packages
|
|
621
|
+
* list, and the replace directives as `replacements`
|
|
103
622
|
*/
|
|
104
623
|
export async function parseGoModData(goModData, gosumMap) {
|
|
105
624
|
const pkgComponentsList = [];
|
|
@@ -108,8 +627,13 @@ export async function parseGoModData(goModData, gosumMap) {
|
|
|
108
627
|
if (!goModData) {
|
|
109
628
|
return {};
|
|
110
629
|
}
|
|
111
|
-
const { parentComponent, entries } = decodeGoModData(
|
|
112
|
-
|
|
630
|
+
const { parentComponent, entries, replacements } = decodeGoModData(
|
|
631
|
+
goModData,
|
|
632
|
+
gosumMap,
|
|
633
|
+
);
|
|
634
|
+
// A module whose licence notice is read locally is not asked for its
|
|
635
|
+
// licence; getGoPkgComponent reads the same notice for each entry below.
|
|
636
|
+
await prefetchGoPkgMetadata(goModulesToPrefetch(entries));
|
|
113
637
|
for (const entry of entries) {
|
|
114
638
|
const component = await getGoPkgComponent(
|
|
115
639
|
"",
|
|
@@ -129,8 +653,6 @@ export async function parseGoModData(goModData, gosumMap) {
|
|
|
129
653
|
}
|
|
130
654
|
pkgComponentsList.push(component);
|
|
131
655
|
}
|
|
132
|
-
// Clear the cache
|
|
133
|
-
_clearMetadataCache();
|
|
134
656
|
return {
|
|
135
657
|
parentComponent,
|
|
136
658
|
pkgList: pkgComponentsList.sort((a, b) =>
|
|
@@ -141,6 +663,7 @@ export async function parseGoModData(goModData, gosumMap) {
|
|
|
141
663
|
(a["bom-ref"] || "").localeCompare(b["bom-ref"] || ""),
|
|
142
664
|
),
|
|
143
665
|
rootList,
|
|
666
|
+
replacements,
|
|
144
667
|
};
|
|
145
668
|
}
|
|
146
669
|
|
|
@@ -153,107 +676,251 @@ export async function parseGoModData(goModData, gosumMap) {
|
|
|
153
676
|
*
|
|
154
677
|
* @param {String} goModData Contents of go.mod file
|
|
155
678
|
* @param {Object} gosumMap Data from go.sum files
|
|
156
|
-
* @returns {{parentComponent: Object, entries: Array<Object>}}
|
|
157
|
-
*
|
|
679
|
+
* @returns {{parentComponent: Object, entries: Array<Object>, replacements: Array<Object>}}
|
|
680
|
+
* Parent component, module entries in file order, and every replace
|
|
681
|
+
* directive as `path`, `version`, `newPath` and `newVersion`
|
|
158
682
|
*/
|
|
159
683
|
function decodeGoModData(goModData, gosumMap) {
|
|
160
684
|
const parentComponent = {};
|
|
161
685
|
const entries = [];
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
for (let l of pkgs) {
|
|
167
|
-
// Windows of course
|
|
168
|
-
l = l.replaceAll("\r", "").replace(/[\t ]+/g, " ");
|
|
169
|
-
// Capture the parent component name from the module
|
|
170
|
-
if (l.startsWith("module ")) {
|
|
171
|
-
parentComponent.name = l.split(" ").pop().trim();
|
|
172
|
-
parentComponent.type = "application";
|
|
173
|
-
// Single-segment module paths are valid golang purls as of cdx-purl 0.0.4.
|
|
174
|
-
applyPurl(
|
|
175
|
-
parentComponent,
|
|
176
|
-
tryParsePurl(`pkg:golang/${parentComponent.name}`),
|
|
177
|
-
);
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
686
|
+
const replacements = [];
|
|
687
|
+
// The verb of the parenthesised block being read: undefined outside a block,
|
|
688
|
+
// and "" inside one whose header is not a single verb, which Go rejects.
|
|
689
|
+
let blockVerb;
|
|
180
690
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
continue;
|
|
185
|
-
}
|
|
186
|
-
if (l.includes(")")) {
|
|
187
|
-
isTool = false;
|
|
691
|
+
for (const line of goModData.split("\n")) {
|
|
692
|
+
const { tokens, comment } = tokenizeGoModLine(line);
|
|
693
|
+
if (!tokens.length) {
|
|
188
694
|
continue;
|
|
189
695
|
}
|
|
190
|
-
|
|
696
|
+
let verb;
|
|
697
|
+
let args;
|
|
698
|
+
if (blockVerb !== undefined) {
|
|
699
|
+
if (tokens.length === 1 && tokens[0] === ")") {
|
|
700
|
+
blockVerb = undefined;
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
verb = blockVerb;
|
|
704
|
+
args = tokens;
|
|
705
|
+
} else if (tokens.at(-1) === "(") {
|
|
706
|
+
// `require (` opens a block whose lines all carry the verb.
|
|
707
|
+
blockVerb = tokens.length === 2 ? tokens[0] : "";
|
|
191
708
|
continue;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const toolchainVer = l.split(" ").pop().trim();
|
|
195
|
-
parentComponent.properties = [
|
|
196
|
-
{ name: "cdx:go:toolchain", value: toolchainVer },
|
|
197
|
-
];
|
|
709
|
+
} else if (tokens.at(-2) === "(" && tokens.at(-1) === ")") {
|
|
710
|
+
// `require ( )` is an empty block.
|
|
198
711
|
continue;
|
|
712
|
+
} else {
|
|
713
|
+
verb = tokens[0];
|
|
714
|
+
args = tokens.slice(1);
|
|
199
715
|
}
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
716
|
+
switch (verb) {
|
|
717
|
+
case "module":
|
|
718
|
+
if (args.length) {
|
|
719
|
+
parentComponent.name = unquoteGoModToken(args[0]);
|
|
720
|
+
parentComponent.type = "application";
|
|
721
|
+
// Single-segment module paths are valid golang purls as of cdx-purl 0.0.4.
|
|
722
|
+
applyPurl(
|
|
723
|
+
parentComponent,
|
|
724
|
+
tryParsePurl(`pkg:golang/${parentComponent.name}`),
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
break;
|
|
728
|
+
case "toolchain":
|
|
729
|
+
if (args.length) {
|
|
730
|
+
parentComponent.properties = [
|
|
731
|
+
{ name: "cdx:go:toolchain", value: args[0] },
|
|
732
|
+
];
|
|
733
|
+
}
|
|
734
|
+
break;
|
|
735
|
+
case "require": {
|
|
736
|
+
// module/path v1.2.3
|
|
737
|
+
const name = unquoteGoModToken(args[0]);
|
|
738
|
+
const version = args[1];
|
|
739
|
+
if (!name || !version) {
|
|
740
|
+
break;
|
|
741
|
+
}
|
|
742
|
+
entries.push({
|
|
743
|
+
group: "",
|
|
744
|
+
name,
|
|
745
|
+
version,
|
|
746
|
+
hash: gosumMap[`${name}@${version}`],
|
|
747
|
+
indirect: isGoModIndirectComment(comment),
|
|
748
|
+
replacement: false,
|
|
749
|
+
});
|
|
750
|
+
break;
|
|
751
|
+
}
|
|
752
|
+
case "replace": {
|
|
753
|
+
// module/path [v1.2.3] => other/module v1.4.5
|
|
754
|
+
// module/path [v1.2.3] => ../local/directory
|
|
755
|
+
// Only the first form names a module version; a directory
|
|
756
|
+
// replacement leaves the required module as the component.
|
|
757
|
+
const arrow = args[1] === "=>" ? 1 : 2;
|
|
758
|
+
if (
|
|
759
|
+
args[arrow] !== "=>" ||
|
|
760
|
+
(args.length !== arrow + 2 && args.length !== arrow + 3)
|
|
761
|
+
) {
|
|
762
|
+
break;
|
|
763
|
+
}
|
|
764
|
+
const oldPath = unquoteGoModToken(args[0]);
|
|
765
|
+
const name = unquoteGoModToken(args[arrow + 1]);
|
|
766
|
+
const version = args[arrow + 2];
|
|
767
|
+
if (oldPath && name) {
|
|
768
|
+
replacements.push({
|
|
769
|
+
path: oldPath,
|
|
770
|
+
version: arrow === 2 ? args[1] : undefined,
|
|
771
|
+
newPath: name,
|
|
772
|
+
newVersion: version,
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
if (!name || args.length !== arrow + 3) {
|
|
776
|
+
break;
|
|
777
|
+
}
|
|
778
|
+
entries.push({
|
|
779
|
+
group: "",
|
|
780
|
+
name,
|
|
781
|
+
version,
|
|
782
|
+
hash: gosumMap[`${name}@${version}`],
|
|
783
|
+
indirect: false,
|
|
784
|
+
replacement: true,
|
|
785
|
+
});
|
|
786
|
+
break;
|
|
787
|
+
}
|
|
788
|
+
// `go`, `godebug`, `exclude`, `retract`, `ignore` and `tool` name no
|
|
789
|
+
// dependency: an excluded or retracted version is kept out of the build,
|
|
790
|
+
// and a tool's module is listed under `require` once the module is tidy.
|
|
791
|
+
default:
|
|
792
|
+
break;
|
|
209
793
|
}
|
|
794
|
+
}
|
|
795
|
+
return { parentComponent, entries, replacements };
|
|
796
|
+
}
|
|
210
797
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
798
|
+
// Characters the go.mod lexer always reads as a token of their own.
|
|
799
|
+
const GO_MOD_PUNCTUATION = "()[]{},";
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Split one go.mod line into tokens the way the Go toolchain's modfile lexer
|
|
803
|
+
* does: whitespace separates tokens, `(`, `)`, `[`, `]`, `{`, `}` and `,`
|
|
804
|
+
* stand alone, a quoted string is one token, and `//` starts a comment that
|
|
805
|
+
* runs to the end of the line.
|
|
806
|
+
*
|
|
807
|
+
* @param {string} line A single line of a go.mod file
|
|
808
|
+
* @returns {{tokens: string[], comment: string}} The tokens, quoted strings
|
|
809
|
+
* still quoted, and the trailing comment text without its `//`
|
|
810
|
+
*/
|
|
811
|
+
function tokenizeGoModLine(line) {
|
|
812
|
+
const tokens = [];
|
|
813
|
+
let i = 0;
|
|
814
|
+
while (i < line.length) {
|
|
815
|
+
const c = line[i];
|
|
816
|
+
if (/\s/.test(c)) {
|
|
817
|
+
i++;
|
|
818
|
+
} else if (line.startsWith("//", i)) {
|
|
819
|
+
return { tokens, comment: line.slice(i + 2).trim() };
|
|
820
|
+
} else if (GO_MOD_PUNCTUATION.includes(c)) {
|
|
821
|
+
tokens.push(c);
|
|
822
|
+
i++;
|
|
823
|
+
} else if (c === '"' || c === "`") {
|
|
824
|
+
let end = i + 1;
|
|
825
|
+
while (end < line.length && line[end] !== c) {
|
|
826
|
+
// A double-quoted string escapes its quote with a backslash.
|
|
827
|
+
end += c === '"' && line[end] === "\\" ? 2 : 1;
|
|
828
|
+
}
|
|
829
|
+
tokens.push(line.slice(i, end + 1));
|
|
830
|
+
i = end + 1;
|
|
243
831
|
} else {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
832
|
+
let end = i;
|
|
833
|
+
while (
|
|
834
|
+
end < line.length &&
|
|
835
|
+
!/\s/.test(line[end]) &&
|
|
836
|
+
!GO_MOD_PUNCTUATION.includes(line[end]) &&
|
|
837
|
+
!line.startsWith("//", end)
|
|
838
|
+
) {
|
|
839
|
+
end++;
|
|
840
|
+
}
|
|
841
|
+
tokens.push(line.slice(i, end));
|
|
842
|
+
i = end;
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return { tokens, comment: "" };
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Read a module path or directory token, which go.mod allows in double
|
|
850
|
+
* quotes. Like the Go toolchain, any other quote character makes it invalid.
|
|
851
|
+
*
|
|
852
|
+
* @param {string|undefined} token A go.mod token
|
|
853
|
+
* @returns {string|undefined} The unquoted value, or undefined when invalid
|
|
854
|
+
*/
|
|
855
|
+
function unquoteGoModToken(token) {
|
|
856
|
+
if (token?.startsWith('"')) {
|
|
857
|
+
return token.length >= 2 && token.endsWith('"')
|
|
858
|
+
? token.slice(1, -1)
|
|
859
|
+
: undefined;
|
|
860
|
+
}
|
|
861
|
+
if (/["'`]/.test(token || "")) {
|
|
862
|
+
return undefined;
|
|
863
|
+
}
|
|
864
|
+
return token;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* Whether a require line's comment marks it indirect, as the Go toolchain
|
|
869
|
+
* reads it: `// indirect`, or `// indirect; ` followed by a reason.
|
|
870
|
+
*
|
|
871
|
+
* @param {string} comment Comment text without its `//`
|
|
872
|
+
* @returns {boolean} True for an indirect requirement
|
|
873
|
+
*/
|
|
874
|
+
function isGoModIndirectComment(comment) {
|
|
875
|
+
const fields = comment.split(/\s+/).filter(Boolean);
|
|
876
|
+
return (
|
|
877
|
+
(fields.length === 1 && fields[0] === "indirect") ||
|
|
878
|
+
(fields.length > 1 && fields[0] === "indirect;")
|
|
879
|
+
);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// A semantic version as golang.org/x/mod/semver accepts it, used to tell a
|
|
883
|
+
// module line's version from the `=>` of a replacement.
|
|
884
|
+
const GO_SEMVER_PATTERN =
|
|
885
|
+
/^v(0|[1-9]\d*)(\.(0|[1-9]\d*)){0,2}(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$/;
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* Read one `# ` module line of a vendor/modules.txt file the way the Go
|
|
889
|
+
* toolchain does (modload.readVendorList):
|
|
890
|
+
*
|
|
891
|
+
* # module/path v1.2.3
|
|
892
|
+
* # module/path v1.2.3 => other/module v1.4.5
|
|
893
|
+
* # module/path v1.2.3 => ../local/directory
|
|
894
|
+
* # module/path => other/module v1.4.5 (a replacement of every version)
|
|
895
|
+
*
|
|
896
|
+
* @param {string} line A line starting with `# `
|
|
897
|
+
* @returns {Object|undefined} `path`, `version` and, for a replaced module,
|
|
898
|
+
* `newPath` and `newVersion`; undefined for a line Go ignores
|
|
899
|
+
*/
|
|
900
|
+
function parseModulesTxtModuleLine(line) {
|
|
901
|
+
const fields = line.split(/\s+/).filter(Boolean);
|
|
902
|
+
if (fields.length < 3) {
|
|
903
|
+
return undefined;
|
|
904
|
+
}
|
|
905
|
+
let rest;
|
|
906
|
+
const module = { path: fields[1] };
|
|
907
|
+
if (GO_SEMVER_PATTERN.test(fields[2])) {
|
|
908
|
+
module.version = fields[2];
|
|
909
|
+
rest = fields.slice(3);
|
|
910
|
+
} else if (fields[2] === "=>") {
|
|
911
|
+
rest = fields.slice(2);
|
|
912
|
+
} else {
|
|
913
|
+
return undefined;
|
|
914
|
+
}
|
|
915
|
+
if (rest[0] === "=>") {
|
|
916
|
+
if (rest.length === 2) {
|
|
917
|
+
module.newPath = rest[1];
|
|
918
|
+
} else if (rest.length === 3 && GO_SEMVER_PATTERN.test(rest[2])) {
|
|
919
|
+
module.newPath = rest[1];
|
|
920
|
+
module.newVersion = rest[2];
|
|
254
921
|
}
|
|
255
922
|
}
|
|
256
|
-
return
|
|
923
|
+
return module;
|
|
257
924
|
}
|
|
258
925
|
|
|
259
926
|
/**
|
|
@@ -261,6 +928,12 @@ function decodeGoModData(goModData, gosumMap) {
|
|
|
261
928
|
* Go package components. Cross-references the go.sum map for integrity hashes and
|
|
262
929
|
* sets scope and confidence based on hash availability.
|
|
263
930
|
*
|
|
931
|
+
* Only the modules the vendor tree holds become components: a module line
|
|
932
|
+
* followed by its `##` annotations or its packages. `go mod vendor` also
|
|
933
|
+
* records every replace directive of go.mod at the end of the file, as a
|
|
934
|
+
* module line with nothing after it, so the consumer can tell that the
|
|
935
|
+
* replacement had no effect; those lines name no vendored module.
|
|
936
|
+
*
|
|
264
937
|
* @param {string} txtFile Path to the modules.txt file
|
|
265
938
|
* @param {Object} gosumMap Map of "module@version" keys to sha256 hash values from go.sum
|
|
266
939
|
* @returns {Promise<Object[]>} List of Go package component objects with evidence
|
|
@@ -268,21 +941,66 @@ function decodeGoModData(goModData, gosumMap) {
|
|
|
268
941
|
export async function parseGoModulesTxt(txtFile, gosumMap) {
|
|
269
942
|
const pkgList = [];
|
|
270
943
|
const txtData = readFileSync(txtFile, { encoding: "utf-8" });
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
const
|
|
280
|
-
if (
|
|
944
|
+
// A modules.txt file lives in the vendor/ tree, so the modules it lists are
|
|
945
|
+
// vendored one directory up from it.
|
|
946
|
+
const projectRoot = dirname(dirname(txtFile));
|
|
947
|
+
const records = [];
|
|
948
|
+
let current;
|
|
949
|
+
for (const rawLine of txtData.split("\n")) {
|
|
950
|
+
// A checkout with CRLF line endings must read the same as the LF file
|
|
951
|
+
// go mod vendor wrote.
|
|
952
|
+
const line = rawLine.replaceAll("\r", "");
|
|
953
|
+
if (line.startsWith("# ")) {
|
|
954
|
+
current = parseModulesTxtModuleLine(line);
|
|
955
|
+
if (current) {
|
|
956
|
+
records.push(current);
|
|
957
|
+
}
|
|
281
958
|
continue;
|
|
282
959
|
}
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
960
|
+
if (current && line.trim()) {
|
|
961
|
+
current.vendored = true;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
const modules = [];
|
|
965
|
+
for (const record of records) {
|
|
966
|
+
// A replacement record of every version names no module version, and a
|
|
967
|
+
// record with nothing after it was not vendored.
|
|
968
|
+
if (!record.version || !record.vendored) {
|
|
969
|
+
continue;
|
|
970
|
+
}
|
|
971
|
+
const replacement = goReplacement(
|
|
972
|
+
record.path,
|
|
973
|
+
record.version,
|
|
974
|
+
record.newPath,
|
|
975
|
+
record.newVersion,
|
|
976
|
+
);
|
|
977
|
+
modules.push({
|
|
978
|
+
group: "",
|
|
979
|
+
name: replacement?.name ?? record.path,
|
|
980
|
+
version: replacement ? replacement.version : record.version,
|
|
981
|
+
replacement,
|
|
982
|
+
projectRoot,
|
|
983
|
+
// Go vendors a replaced module under the path it replaces.
|
|
984
|
+
vendorPath: record.path,
|
|
985
|
+
skipRegistryLookups: !!replacement?.local,
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
// A module whose vendored licence notice is read is not asked for its
|
|
989
|
+
// licence; getGoPkgComponent reads the same file. Local code is never
|
|
990
|
+
// asked about.
|
|
991
|
+
await prefetchGoPkgMetadata(goModulesToPrefetch(modules));
|
|
992
|
+
for (const m of modules) {
|
|
993
|
+
const gosumHash = m.version
|
|
994
|
+
? gosumMap[`${m.name}@${m.version}`]
|
|
995
|
+
: undefined;
|
|
996
|
+
const component = await getGoPkgComponent(
|
|
997
|
+
"",
|
|
998
|
+
m.name,
|
|
999
|
+
m.version,
|
|
1000
|
+
gosumHash,
|
|
1001
|
+
m,
|
|
1002
|
+
);
|
|
1003
|
+
addGoReplacementProperties(component, m.replacement);
|
|
286
1004
|
let confidence = 0.7;
|
|
287
1005
|
if (gosumHash) {
|
|
288
1006
|
component.scope = "required";
|
|
@@ -294,119 +1012,160 @@ export async function parseGoModulesTxt(txtFile, gosumMap) {
|
|
|
294
1012
|
return pkgList;
|
|
295
1013
|
}
|
|
296
1014
|
|
|
1015
|
+
/**
|
|
1016
|
+
* Whether two paths name the same directory. `go list` prints the real path
|
|
1017
|
+
* of a directory, which differs from the one cdxgen was given when that runs
|
|
1018
|
+
* through a symbolic link (macOS /tmp is /private/tmp).
|
|
1019
|
+
*
|
|
1020
|
+
* @param {string} [a] A directory
|
|
1021
|
+
* @param {string} [b] Another directory
|
|
1022
|
+
* @returns {boolean}
|
|
1023
|
+
*/
|
|
1024
|
+
function isSameGoDir(a, b) {
|
|
1025
|
+
if (!a || !b) {
|
|
1026
|
+
return false;
|
|
1027
|
+
}
|
|
1028
|
+
if (resolve(a) === resolve(b)) {
|
|
1029
|
+
return true;
|
|
1030
|
+
}
|
|
1031
|
+
try {
|
|
1032
|
+
return realpathSync(a) === realpathSync(b);
|
|
1033
|
+
} catch {
|
|
1034
|
+
return false;
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
|
|
297
1038
|
/**
|
|
298
1039
|
* Parse go list output
|
|
299
1040
|
*
|
|
1041
|
+
* Each row is printed by the template in createGoBom:
|
|
1042
|
+
* `Path|Version|Indirect|GoMod|GoVersion|Main|Time|Deprecated|GoModSum|Dir|Sum|
|
|
1043
|
+
* Replace.Path|Replace.Version|Replace.Sum`. Rows from older templates end
|
|
1044
|
+
* after `Dir`.
|
|
1045
|
+
*
|
|
1046
|
+
* A replaced module is reported as the module Go builds (see
|
|
1047
|
+
* {@link goReplacement}), with the checksum of the replacement. A go.work
|
|
1048
|
+
* workspace makes every member a main module, so the parent is the main
|
|
1049
|
+
* module in `options.mainModuleDir`, and the other members are returned as
|
|
1050
|
+
* `workspaceModules`.
|
|
1051
|
+
*
|
|
300
1052
|
* @param {string} rawOutput Output from go list invocation
|
|
301
1053
|
* @param {Object} gosumMap go.sum data
|
|
302
|
-
* @
|
|
1054
|
+
* @param {Object} [options] Options
|
|
1055
|
+
* @param {string} [options.mainModuleDir] Directory `go list` ran in, whose
|
|
1056
|
+
* main module is the parent component
|
|
1057
|
+
* @returns Object with parent component, list of packages, and the other main
|
|
1058
|
+
* modules of a workspace
|
|
303
1059
|
*/
|
|
304
|
-
export async function parseGoListDep(rawOutput, gosumMap) {
|
|
1060
|
+
export async function parseGoListDep(rawOutput, gosumMap, options = {}) {
|
|
305
1061
|
let parentComponent = {};
|
|
306
1062
|
const deps = [];
|
|
1063
|
+
const workspaceModules = [];
|
|
307
1064
|
if (typeof rawOutput === "string") {
|
|
308
|
-
const
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
.filter((p) => p.trim().replace(/["']/g, "").length);
|
|
312
|
-
// Same shape and same duplicate filter as the loop below, so the batch
|
|
313
|
-
// covers every module the loop resolves and nothing else.
|
|
314
|
-
const prefetchSeen = new Set();
|
|
315
|
-
const prefetchModules = [];
|
|
316
|
-
for (const l of pkgs) {
|
|
1065
|
+
const rows = [];
|
|
1066
|
+
const seen = new Set();
|
|
1067
|
+
for (const l of rawOutput.split("\n")) {
|
|
317
1068
|
const verArr = l.trim().replace(/["']/g, "").split("|");
|
|
318
|
-
if (
|
|
1069
|
+
if (verArr.length < 5) {
|
|
319
1070
|
continue;
|
|
320
1071
|
}
|
|
1072
|
+
// Every package of a module repeats the module's row.
|
|
321
1073
|
const key = `${verArr[0]}-${verArr[1]}`;
|
|
322
|
-
if (
|
|
1074
|
+
if (seen.has(key)) {
|
|
323
1075
|
continue;
|
|
324
1076
|
}
|
|
325
|
-
|
|
326
|
-
|
|
1077
|
+
seen.add(key);
|
|
1078
|
+
rows.push(verArr);
|
|
327
1079
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
1080
|
+
// The main module is the project itself, which no registry holds. In a
|
|
1081
|
+
// go.work workspace every member is a main module, and the one go list
|
|
1082
|
+
// ran in is the parent.
|
|
1083
|
+
const mainRows = rows.filter((verArr) => verArr[5] === "true");
|
|
1084
|
+
const mainRow =
|
|
1085
|
+
mainRows.find((verArr) =>
|
|
1086
|
+
isSameGoDir(verArr[9], options.mainModuleDir),
|
|
1087
|
+
) || mainRows[0];
|
|
1088
|
+
// The directory of the main module, which is the project root, under
|
|
1089
|
+
// which the vendored copies of the dependencies live.
|
|
1090
|
+
const mainModuleDir = mainRow?.[9] || undefined;
|
|
1091
|
+
const modules = rows.map((verArr) => {
|
|
1092
|
+
const isMainModule = verArr[5] === "true";
|
|
1093
|
+
const replacement = isMainModule
|
|
1094
|
+
? undefined
|
|
1095
|
+
: goReplacement(verArr[0], verArr[1], verArr[11], verArr[12]);
|
|
1096
|
+
const name = replacement?.name ?? verArr[0];
|
|
1097
|
+
const version = replacement ? replacement.version : verArr[1];
|
|
1098
|
+
return {
|
|
1099
|
+
verArr,
|
|
1100
|
+
isMainModule,
|
|
1101
|
+
replacement,
|
|
1102
|
+
group: "",
|
|
1103
|
+
name,
|
|
1104
|
+
version,
|
|
1105
|
+
// Module.Sum is the checksum of the module content, the hash of what
|
|
1106
|
+
// is built. GoModSum hashes only its go.mod file.
|
|
1107
|
+
hash:
|
|
1108
|
+
(version && gosumMap[`${name}@${version}`]) ||
|
|
1109
|
+
goSumIntegrity(replacement ? verArr[13] : verArr[10]),
|
|
1110
|
+
dir: verArr[9]?.length ? verArr[9] : undefined,
|
|
1111
|
+
projectRoot: mainModuleDir,
|
|
1112
|
+
// Go vendors a replaced module under the path it replaces.
|
|
1113
|
+
vendorPath: verArr[0],
|
|
1114
|
+
skipRegistryLookups: isMainModule || !!replacement?.local,
|
|
1115
|
+
};
|
|
1116
|
+
});
|
|
1117
|
+
// A module whose licence notice is held on this machine is not asked for
|
|
1118
|
+
// its licence: getGoPkgComponent reads the same notices, so the batch
|
|
1119
|
+
// covers exactly what the loop will look up.
|
|
1120
|
+
await prefetchGoPkgMetadata(goModulesToPrefetch(modules));
|
|
1121
|
+
for (const m of modules) {
|
|
1122
|
+
const { verArr } = m;
|
|
1123
|
+
const component = await getGoPkgComponent(
|
|
1124
|
+
"",
|
|
1125
|
+
m.name,
|
|
1126
|
+
m.version,
|
|
1127
|
+
m.hash,
|
|
1128
|
+
m,
|
|
1129
|
+
);
|
|
1130
|
+
// This is misusing the scope attribute to represent direct vs indirect
|
|
1131
|
+
if (verArr[2] === "false") {
|
|
1132
|
+
component.scope = "required";
|
|
1133
|
+
} else if (verArr[2] === "true") {
|
|
1134
|
+
component.scope = "optional";
|
|
1135
|
+
}
|
|
1136
|
+
component.properties = [
|
|
1137
|
+
{
|
|
1138
|
+
name: "internal:SrcGoMod",
|
|
1139
|
+
value: verArr[3] || "",
|
|
1140
|
+
},
|
|
1141
|
+
{
|
|
1142
|
+
name: "internal:ModuleGoVersion",
|
|
1143
|
+
value: verArr[4] || "",
|
|
1144
|
+
},
|
|
1145
|
+
{
|
|
1146
|
+
name: "cdx:go:indirect",
|
|
1147
|
+
value: verArr[2],
|
|
1148
|
+
},
|
|
1149
|
+
];
|
|
1150
|
+
if (verArr.length >= 6 && verArr[6]?.length && verArr[6] !== "<nil>") {
|
|
1151
|
+
component.properties.push({
|
|
1152
|
+
name: "cdx:go:creation_time",
|
|
1153
|
+
value: verArr[6],
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
1156
|
+
if (verArr.length >= 7 && verArr[7]?.length) {
|
|
1157
|
+
component.properties.push({
|
|
1158
|
+
name: "cdx:go:deprecated",
|
|
1159
|
+
value: verArr[7],
|
|
1160
|
+
});
|
|
1161
|
+
}
|
|
1162
|
+
addGoReplacementProperties(component, m.replacement);
|
|
1163
|
+
if (verArr === mainRow) {
|
|
1164
|
+
parentComponent = component;
|
|
1165
|
+
} else if (m.isMainModule) {
|
|
1166
|
+
workspaceModules.push(component);
|
|
1167
|
+
} else {
|
|
1168
|
+
deps.push(component);
|
|
410
1169
|
}
|
|
411
1170
|
}
|
|
412
1171
|
}
|
|
@@ -419,6 +1178,7 @@ export async function parseGoListDep(rawOutput, gosumMap) {
|
|
|
419
1178
|
// be built silently reorders the output.
|
|
420
1179
|
(a["bom-ref"] || "").localeCompare(b["bom-ref"] || ""),
|
|
421
1180
|
),
|
|
1181
|
+
workspaceModules,
|
|
422
1182
|
};
|
|
423
1183
|
}
|
|
424
1184
|
|
|
@@ -465,9 +1225,39 @@ function goModGraphTokenPurl(token) {
|
|
|
465
1225
|
return canonical ? Purl.parse(canonical) : null;
|
|
466
1226
|
}
|
|
467
1227
|
|
|
1228
|
+
/**
|
|
1229
|
+
* Split a `go mod graph` token into its module path and version, exactly as
|
|
1230
|
+
* Go printed them. A main module is printed without a version.
|
|
1231
|
+
*
|
|
1232
|
+
* @param {string} token `module/path@version` or `module/path`
|
|
1233
|
+
* @returns {{path: string, version: string|undefined}}
|
|
1234
|
+
*/
|
|
1235
|
+
function splitGoModGraphToken(token) {
|
|
1236
|
+
const at = token.indexOf("@");
|
|
1237
|
+
return at < 0
|
|
1238
|
+
? { path: token, version: undefined }
|
|
1239
|
+
: { path: token.slice(0, at), version: token.slice(at + 1) };
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
/**
|
|
1243
|
+
* Value of the first property with the given name.
|
|
1244
|
+
*
|
|
1245
|
+
* @param {Object} component Component
|
|
1246
|
+
* @param {string} name Property name
|
|
1247
|
+
* @returns {string|undefined}
|
|
1248
|
+
*/
|
|
1249
|
+
function goComponentProperty(component, name) {
|
|
1250
|
+
return component?.properties?.find((p) => p.name === name)?.value;
|
|
1251
|
+
}
|
|
1252
|
+
|
|
468
1253
|
/**
|
|
469
1254
|
* Parse go mod graph
|
|
470
1255
|
*
|
|
1256
|
+
* `go mod graph` names every module by the path and version its requirement
|
|
1257
|
+
* names, whatever replaces it. A replaced module is mapped to the module Go
|
|
1258
|
+
* builds: from the components `go list` reported, which record what they
|
|
1259
|
+
* replace, or else from the replace directives of go.mod.
|
|
1260
|
+
*
|
|
471
1261
|
* @param {string} rawOutput Output from go mod graph invocation
|
|
472
1262
|
* @param {string} goModFile go.mod file
|
|
473
1263
|
* @param {Object} gosumMap Hashes from gosum for lookups
|
|
@@ -493,9 +1283,41 @@ export async function parseGoModGraph(
|
|
|
493
1283
|
let goModPkgMap = {};
|
|
494
1284
|
// Direct dependencies by manually parsing the go.mod data
|
|
495
1285
|
const goModDirectDepsMap = {};
|
|
1286
|
+
// The same direct dependencies by module path, which stays correct when
|
|
1287
|
+
// the version the go.mod text names has drifted from the selected one.
|
|
1288
|
+
const goModDirectDepNames = new Set();
|
|
496
1289
|
// Indirect dependencies by manually parsing the go.mod data
|
|
497
1290
|
const goModOptionalDepsMap = {};
|
|
498
1291
|
const excludedRefs = [];
|
|
1292
|
+
// The module versions `go list` reported are the ones MVS selected, and the
|
|
1293
|
+
// only ones the BOM carries components for. `go mod graph` instead prints
|
|
1294
|
+
// the requirement graph, where each edge names the version the source's
|
|
1295
|
+
// go.mod asks for, so the two routinely disagree for the same module. They
|
|
1296
|
+
// are keyed by the module path exactly as Go prints it, which keeps its
|
|
1297
|
+
// upper-case letters; a replaced module is keyed by the path it replaces,
|
|
1298
|
+
// which is the one the graph prints.
|
|
1299
|
+
const selectedVersions = {};
|
|
1300
|
+
// `path@version` as the graph prints it, for every module a replace
|
|
1301
|
+
// directive put another module in place of.
|
|
1302
|
+
const replacedModules = new Map();
|
|
1303
|
+
for (const epkg of epkgList) {
|
|
1304
|
+
const replaces = goComponentProperty(epkg, "cdx:go:replaces");
|
|
1305
|
+
if (replaces) {
|
|
1306
|
+
const { path, version } = splitGoModGraphToken(replaces);
|
|
1307
|
+
replacedModules.set(replaces, {
|
|
1308
|
+
name: epkg.name,
|
|
1309
|
+
version: epkg.version,
|
|
1310
|
+
local: !epkg.version,
|
|
1311
|
+
localDir: goComponentProperty(epkg, "cdx:go:local_dir"),
|
|
1312
|
+
replaces,
|
|
1313
|
+
});
|
|
1314
|
+
if (version) {
|
|
1315
|
+
selectedVersions[path] = version;
|
|
1316
|
+
}
|
|
1317
|
+
} else if (epkg?.name && epkg?.version) {
|
|
1318
|
+
selectedVersions[epkg.name] = epkg.version;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
499
1321
|
if (goModFile) {
|
|
500
1322
|
goModPkgMap = await parseGoModData(
|
|
501
1323
|
readFileSync(goModFile, { encoding: "utf-8" }),
|
|
@@ -504,6 +1326,11 @@ export async function parseGoModGraph(
|
|
|
504
1326
|
if (goModPkgMap?.rootList) {
|
|
505
1327
|
for (const epkg of goModPkgMap.rootList) {
|
|
506
1328
|
goModDirectDepsMap[epkg["bom-ref"]] = true;
|
|
1329
|
+
// The bom-ref above names the version the go.mod text requires,
|
|
1330
|
+
// which an untidy module can leave older than the version MVS
|
|
1331
|
+
// selected. Root-edge validation below also asks by module path,
|
|
1332
|
+
// so a drifted direct require still counts as direct (#4497).
|
|
1333
|
+
goModDirectDepNames.add(epkg.name);
|
|
507
1334
|
}
|
|
508
1335
|
}
|
|
509
1336
|
if (goModPkgMap?.pkgList) {
|
|
@@ -514,59 +1341,117 @@ export async function parseGoModGraph(
|
|
|
514
1341
|
}
|
|
515
1342
|
}
|
|
516
1343
|
}
|
|
1344
|
+
// What the replace directives of go.mod make of a module the graph names. A
|
|
1345
|
+
// directive for one version wins over one for every version, as in Go.
|
|
1346
|
+
const goModReplacement = (path, version) => {
|
|
1347
|
+
let wildcard;
|
|
1348
|
+
for (const r of goModPkgMap?.replacements || []) {
|
|
1349
|
+
if (r.path !== path) {
|
|
1350
|
+
continue;
|
|
1351
|
+
}
|
|
1352
|
+
if (r.version === undefined) {
|
|
1353
|
+
wildcard = r;
|
|
1354
|
+
} else if (r.version === version) {
|
|
1355
|
+
return goReplacement(path, version, r.newPath, r.newVersion);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
return wildcard
|
|
1359
|
+
? goReplacement(path, version, wildcard.newPath, wildcard.newVersion)
|
|
1360
|
+
: undefined;
|
|
1361
|
+
};
|
|
1362
|
+
// The module Go builds for one graph token, with the reference of the
|
|
1363
|
+
// component that stands for it.
|
|
1364
|
+
const graphModule = (path, version) => {
|
|
1365
|
+
const replacement = version
|
|
1366
|
+
? replacedModules.get(`${path}@${version}`) ||
|
|
1367
|
+
goModReplacement(path, version)
|
|
1368
|
+
: undefined;
|
|
1369
|
+
const name = replacement ? replacement.name : path;
|
|
1370
|
+
const moduleVersion = replacement ? replacement.version : version;
|
|
1371
|
+
const token = moduleVersion ? `${name}@${moduleVersion}` : name;
|
|
1372
|
+
// Some golang modules (e.g. go4.org) have no path separator and
|
|
1373
|
+
// cdx-purl requires a namespace. Use the raw ref when parsing fails.
|
|
1374
|
+
const purl = goModGraphTokenPurl(token);
|
|
1375
|
+
return {
|
|
1376
|
+
path,
|
|
1377
|
+
name,
|
|
1378
|
+
version: moduleVersion,
|
|
1379
|
+
token,
|
|
1380
|
+
replacement,
|
|
1381
|
+
ref: purl ? decodeURIComponent(purl.toString()) : `pkg:golang/${token}`,
|
|
1382
|
+
};
|
|
1383
|
+
};
|
|
517
1384
|
for (const epkg of epkgList) {
|
|
518
1385
|
existingPkgMap[epkg["bom-ref"]] = true;
|
|
519
1386
|
}
|
|
520
1387
|
if (parentComponent && Object.keys(parentComponent).length) {
|
|
521
1388
|
existingPkgMap[parentComponent["bom-ref"]] = true;
|
|
522
1389
|
}
|
|
1390
|
+
const filterByExisting = Object.keys(existingPkgMap).length > 0;
|
|
523
1391
|
if (typeof rawOutput === "string") {
|
|
524
1392
|
const lines = rawOutput.split("\n");
|
|
525
1393
|
// Each line is of the form ref dependsOn
|
|
526
1394
|
// github.com/spf13/afero@v1.2.2 golang.org/x/text@v0.3.0
|
|
527
1395
|
for (const l of lines) {
|
|
528
|
-
// To keep the parsing logic simple we prefix pkg:golang/
|
|
529
|
-
// and let packageurl work out the rest
|
|
530
1396
|
const tmpA = l.replaceAll("\r", "").split(" ");
|
|
531
1397
|
if (tmpA && tmpA.length === 2) {
|
|
532
1398
|
try {
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
let
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
1399
|
+
const sourceToken = splitGoModGraphToken(tmpA[0]);
|
|
1400
|
+
const dependsToken = splitGoModGraphToken(tmpA[1]);
|
|
1401
|
+
const source = graphModule(sourceToken.path, sourceToken.version);
|
|
1402
|
+
let depends = graphModule(dependsToken.path, dependsToken.version);
|
|
1403
|
+
// An edge whose target names a version the build did not select is
|
|
1404
|
+
// rewritten to the selected one rather than dropped: the selected
|
|
1405
|
+
// module is the one actually linked, and dropping the edge silently
|
|
1406
|
+
// disconnects it from every dependent that asked for an older
|
|
1407
|
+
// version (#4497). Only the target is rewritten; an edge from a
|
|
1408
|
+
// version that was not selected describes the requirements of a
|
|
1409
|
+
// module the build never used, so it stays filtered out.
|
|
1410
|
+
if (filterByExisting && !existingPkgMap[depends.ref]) {
|
|
1411
|
+
const selectedVersion = selectedVersions[dependsToken.path];
|
|
1412
|
+
if (selectedVersion && selectedVersion !== dependsToken.version) {
|
|
1413
|
+
const rewritten = graphModule(dependsToken.path, selectedVersion);
|
|
1414
|
+
if (existingPkgMap[rewritten.ref]) {
|
|
1415
|
+
depends = rewritten;
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
552
1418
|
}
|
|
1419
|
+
const sourceRefString = source.ref;
|
|
1420
|
+
const dependsRefString = depends.ref;
|
|
553
1421
|
// Since go mod graph over-reports direct dependencies we use the existing list
|
|
554
1422
|
// from go deps to filter the result
|
|
555
1423
|
if (
|
|
556
|
-
|
|
557
|
-
Object.keys(existingPkgMap).length &&
|
|
1424
|
+
filterByExisting &&
|
|
558
1425
|
(!existingPkgMap[sourceRefString] ||
|
|
559
1426
|
!existingPkgMap[dependsRefString])
|
|
560
1427
|
) {
|
|
561
1428
|
continue;
|
|
562
1429
|
}
|
|
563
1430
|
// Add the source and depends to the pkgList
|
|
564
|
-
if (
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
1431
|
+
if (
|
|
1432
|
+
!addedPkgs[sourceRefString] &&
|
|
1433
|
+
!excludedRefs.includes(sourceRefString)
|
|
1434
|
+
) {
|
|
1435
|
+
// go mod graph prints a main module without a version. A main
|
|
1436
|
+
// module is the project itself, or a go.work member, which no
|
|
1437
|
+
// registry holds, so it is never looked up. It still becomes a
|
|
1438
|
+
// component, because the graph's edges hang off it.
|
|
1439
|
+
const isParentSource =
|
|
1440
|
+
!sourceToken.version ||
|
|
1441
|
+
source.name === goModPkgMap?.parentComponent?.name ||
|
|
1442
|
+
sourceRefString === parentComponent?.["bom-ref"];
|
|
1443
|
+
const component = addGoReplacementProperties(
|
|
1444
|
+
await getGoPkgComponent(
|
|
1445
|
+
"",
|
|
1446
|
+
source.name,
|
|
1447
|
+
source.version,
|
|
1448
|
+
gosumMap[source.token],
|
|
1449
|
+
{
|
|
1450
|
+
skipRegistryLookups:
|
|
1451
|
+
isParentSource || !!source.replacement?.local,
|
|
1452
|
+
},
|
|
1453
|
+
),
|
|
1454
|
+
source.replacement,
|
|
570
1455
|
);
|
|
571
1456
|
let confidence = 0.7;
|
|
572
1457
|
if (goModOptionalDepsMap[component["bom-ref"]]) {
|
|
@@ -578,26 +1463,35 @@ export async function parseGoModGraph(
|
|
|
578
1463
|
// These are likely false positives
|
|
579
1464
|
if (
|
|
580
1465
|
goModFile &&
|
|
581
|
-
!
|
|
1466
|
+
!filterByExisting &&
|
|
582
1467
|
goModPkgMap?.parentComponent?.["bom-ref"] !== sourceRefString &&
|
|
583
1468
|
!component.scope
|
|
584
1469
|
) {
|
|
585
1470
|
continue;
|
|
586
1471
|
}
|
|
587
|
-
// Don't add the parent component to the package list
|
|
588
|
-
|
|
1472
|
+
// Don't add the parent component to the package list: neither
|
|
1473
|
+
// this go.mod's module nor the caller's parent, which in a go.work
|
|
1474
|
+
// workspace is another member that the graph also prints.
|
|
1475
|
+
if (
|
|
1476
|
+
goModPkgMap?.parentComponent?.["bom-ref"] !== sourceRefString &&
|
|
1477
|
+
parentComponent?.["bom-ref"] !== sourceRefString
|
|
1478
|
+
) {
|
|
589
1479
|
pkgList.push(
|
|
590
1480
|
_addGoComponentEvidence(component, goModFile, confidence),
|
|
591
1481
|
);
|
|
592
1482
|
}
|
|
593
|
-
addedPkgs[
|
|
1483
|
+
addedPkgs[sourceRefString] = true;
|
|
594
1484
|
}
|
|
595
|
-
if (!addedPkgs[
|
|
596
|
-
const component =
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
1485
|
+
if (!addedPkgs[dependsRefString]) {
|
|
1486
|
+
const component = addGoReplacementProperties(
|
|
1487
|
+
await getGoPkgComponent(
|
|
1488
|
+
"",
|
|
1489
|
+
depends.name,
|
|
1490
|
+
depends.version,
|
|
1491
|
+
gosumMap[depends.token],
|
|
1492
|
+
{ skipRegistryLookups: !!depends.replacement?.local },
|
|
1493
|
+
),
|
|
1494
|
+
depends.replacement,
|
|
601
1495
|
);
|
|
602
1496
|
let confidence = 0.7;
|
|
603
1497
|
if (goModDirectDepsMap[component["bom-ref"]]) {
|
|
@@ -627,7 +1521,7 @@ export async function parseGoModGraph(
|
|
|
627
1521
|
if (
|
|
628
1522
|
goModFile &&
|
|
629
1523
|
goModPkgMap?.parentComponent?.["bom-ref"] !== sourceRefString &&
|
|
630
|
-
!
|
|
1524
|
+
!filterByExisting &&
|
|
631
1525
|
!component.scope
|
|
632
1526
|
) {
|
|
633
1527
|
excludedRefs.push(dependsRefString);
|
|
@@ -639,7 +1533,7 @@ export async function parseGoModGraph(
|
|
|
639
1533
|
pkgList.push(
|
|
640
1534
|
_addGoComponentEvidence(component, goModFile, confidence),
|
|
641
1535
|
);
|
|
642
|
-
addedPkgs[
|
|
1536
|
+
addedPkgs[dependsRefString] = true;
|
|
643
1537
|
}
|
|
644
1538
|
if (!depsMap[sourceRefString]) {
|
|
645
1539
|
depsMap[sourceRefString] = new Set();
|
|
@@ -647,11 +1541,18 @@ export async function parseGoModGraph(
|
|
|
647
1541
|
if (!depsMap[dependsRefString]) {
|
|
648
1542
|
depsMap[dependsRefString] = new Set();
|
|
649
1543
|
}
|
|
650
|
-
// Check if the root is really dependent on this component
|
|
1544
|
+
// Check if the root is really dependent on this component. The
|
|
1545
|
+
// by-path check accepts a direct require whose go.mod version
|
|
1546
|
+
// drifted from the selected one: the edge was already rewritten
|
|
1547
|
+
// to the selected version above, and the by-ref map only knows
|
|
1548
|
+
// the stale bom-ref (#4497). It also accepts a directly required
|
|
1549
|
+
// module that a directory replaces, whose component has no
|
|
1550
|
+
// version.
|
|
651
1551
|
if (
|
|
652
1552
|
goModPkgMap?.parentComponent?.["bom-ref"] === sourceRefString &&
|
|
653
1553
|
Object.keys(goModDirectDepsMap).length &&
|
|
654
|
-
!goModDirectDepsMap[dependsRefString]
|
|
1554
|
+
!goModDirectDepsMap[dependsRefString] &&
|
|
1555
|
+
!goModDirectDepNames.has(dependsToken.path)
|
|
655
1556
|
) {
|
|
656
1557
|
// ignore
|
|
657
1558
|
} else if (!excludedRefs.includes(dependsRefString)) {
|
|
@@ -722,33 +1623,73 @@ export function isGoModWhyNotNeeded(rawOutput) {
|
|
|
722
1623
|
}
|
|
723
1624
|
|
|
724
1625
|
/**
|
|
725
|
-
*
|
|
1626
|
+
* Read the `module@version` to hash map out of go.sum contents, without
|
|
1627
|
+
* building components or touching the network.
|
|
1628
|
+
*
|
|
726
1629
|
* @param {string} gosumData Content of go.sum
|
|
727
|
-
* @returns
|
|
1630
|
+
* @returns {Object} Map of `module@version` keys to the `sha256-…` checksum of
|
|
1631
|
+
* the module content, for every version go.sum holds one for
|
|
728
1632
|
*/
|
|
729
|
-
export
|
|
730
|
-
const
|
|
1633
|
+
export function parseGosumHashes(gosumData) {
|
|
1634
|
+
const gosumMap = {};
|
|
1635
|
+
for (const entry of decodeGosumData(gosumData)) {
|
|
1636
|
+
if (entry.hash) {
|
|
1637
|
+
gosumMap[`${entry.name}@${entry.version}`] = entry.hash;
|
|
1638
|
+
}
|
|
1639
|
+
}
|
|
1640
|
+
return gosumMap;
|
|
1641
|
+
}
|
|
1642
|
+
|
|
1643
|
+
/**
|
|
1644
|
+
* Decode a go.sum file into one entry per module version it names.
|
|
1645
|
+
*
|
|
1646
|
+
* go.sum holds up to two lines per module version: `path version h1:…`, the
|
|
1647
|
+
* checksum of the module's content, and `path version/go.mod h1:…`, the
|
|
1648
|
+
* checksum of its go.mod file alone. The content checksum is the hash of the
|
|
1649
|
+
* component. A version with only a go.mod line was needed to resolve the
|
|
1650
|
+
* module graph but never downloaded, so it has no content hash.
|
|
1651
|
+
*
|
|
1652
|
+
* @param {string} gosumData Content of go.sum
|
|
1653
|
+
* @returns {Object[]} Entries with `name`, `version` and `hash`, in the order
|
|
1654
|
+
* go.sum first names them
|
|
1655
|
+
*/
|
|
1656
|
+
function decodeGosumData(gosumData) {
|
|
1657
|
+
const entries = new Map();
|
|
731
1658
|
if (!gosumData) {
|
|
732
|
-
return
|
|
1659
|
+
return [];
|
|
733
1660
|
}
|
|
734
|
-
// Decoding every line first lets the whole module set be fetched in one
|
|
735
|
-
// batched round before any component is built. One decode function feeds both,
|
|
736
|
-
// so the batch cannot ask for a module the loop does not resolve.
|
|
737
|
-
const entries = [];
|
|
738
1661
|
for (const l of gosumData.split("\n")) {
|
|
739
|
-
const
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
1662
|
+
const fields = l.replaceAll("\r", "").trim().split(/\s+/);
|
|
1663
|
+
if (fields.length !== 3 || !fields[2].startsWith("h1:")) {
|
|
1664
|
+
continue;
|
|
1665
|
+
}
|
|
1666
|
+
const isGoModLine = fields[1].endsWith("/go.mod");
|
|
1667
|
+
const name = fields[0];
|
|
1668
|
+
const version = isGoModLine
|
|
1669
|
+
? fields[1].slice(0, -"/go.mod".length)
|
|
1670
|
+
: fields[1];
|
|
1671
|
+
const key = `${name}@${version}`;
|
|
1672
|
+
if (!entries.has(key)) {
|
|
1673
|
+
entries.set(key, { group: "", name, version, hash: undefined });
|
|
1674
|
+
}
|
|
1675
|
+
if (!isGoModLine) {
|
|
1676
|
+
entries.get(key).hash = goSumIntegrity(fields[2]);
|
|
749
1677
|
}
|
|
750
1678
|
}
|
|
751
|
-
|
|
1679
|
+
return [...entries.values()];
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
/**
|
|
1683
|
+
* Parse go sum data
|
|
1684
|
+
* @param {string} gosumData Content of go.sum
|
|
1685
|
+
* @returns package list
|
|
1686
|
+
*/
|
|
1687
|
+
export async function parseGosumData(gosumData) {
|
|
1688
|
+
const pkgList = [];
|
|
1689
|
+
const entries = decodeGosumData(gosumData);
|
|
1690
|
+
// A module whose licence notice is read locally is not asked for its
|
|
1691
|
+
// licence; getGoPkgComponent reads the same notice for each entry below.
|
|
1692
|
+
await prefetchGoPkgMetadata(goModulesToPrefetch(entries));
|
|
752
1693
|
for (const entry of entries) {
|
|
753
1694
|
pkgList.push(
|
|
754
1695
|
await getGoPkgComponent("", entry.name, entry.version, entry.hash),
|
|
@@ -775,13 +1716,17 @@ export async function parseGopkgData(gopkgData) {
|
|
|
775
1716
|
for (const l of pkgs) {
|
|
776
1717
|
let key = null;
|
|
777
1718
|
let value = null;
|
|
778
|
-
|
|
1719
|
+
// Every table header ends the project read so far, and only a
|
|
1720
|
+
// [[projects]] table starts another. The keys of [solve-meta] belong to
|
|
1721
|
+
// no project.
|
|
1722
|
+
if (l.trim().startsWith("[")) {
|
|
779
1723
|
if (pkg) {
|
|
780
|
-
pkgList.push(pkg);
|
|
1724
|
+
pkgList.push(await finalizeGopkgProject(pkg));
|
|
781
1725
|
}
|
|
782
|
-
pkg = {};
|
|
1726
|
+
pkg = l.indexOf("[[projects]]") > -1 ? {} : null;
|
|
1727
|
+
continue;
|
|
783
1728
|
}
|
|
784
|
-
if (l.indexOf("=") > -1) {
|
|
1729
|
+
if (pkg && l.indexOf("=") > -1) {
|
|
785
1730
|
const tmpA = l.split("=");
|
|
786
1731
|
key = tmpA[0].trim();
|
|
787
1732
|
value = tmpA[1].trim().replace(/"/g, "");
|
|
@@ -794,12 +1739,6 @@ export async function parseGopkgData(gopkgData) {
|
|
|
794
1739
|
case "name":
|
|
795
1740
|
pkg.group = "";
|
|
796
1741
|
pkg.name = value;
|
|
797
|
-
if (shouldFetchLicense()) {
|
|
798
|
-
pkg.license = await getGoPkgLicense({
|
|
799
|
-
group: pkg.group,
|
|
800
|
-
name: pkg.name,
|
|
801
|
-
});
|
|
802
|
-
}
|
|
803
1742
|
break;
|
|
804
1743
|
case "version":
|
|
805
1744
|
pkg.version = value;
|
|
@@ -811,13 +1750,41 @@ export async function parseGopkgData(gopkgData) {
|
|
|
811
1750
|
}
|
|
812
1751
|
}
|
|
813
1752
|
}
|
|
1753
|
+
// The last project has no header after it.
|
|
1754
|
+
if (pkg) {
|
|
1755
|
+
pkgList.push(await finalizeGopkgProject(pkg));
|
|
1756
|
+
}
|
|
814
1757
|
return pkgList;
|
|
815
1758
|
}
|
|
816
1759
|
|
|
1760
|
+
/**
|
|
1761
|
+
* Attach the licence of one Gopkg project once its name and version are both
|
|
1762
|
+
* known, so the pkg.go.dev URL names the pinned release.
|
|
1763
|
+
*
|
|
1764
|
+
* @param {Object} pkg The project entry read so far
|
|
1765
|
+
* @returns {Promise<Object>} The same entry, with a licence when one was found
|
|
1766
|
+
*/
|
|
1767
|
+
async function finalizeGopkgProject(pkg) {
|
|
1768
|
+
if (shouldFetchLicense() && pkg.name) {
|
|
1769
|
+
pkg.license = await getGoPkgLicense({
|
|
1770
|
+
group: pkg.group,
|
|
1771
|
+
name: pkg.name,
|
|
1772
|
+
version: pkg.version,
|
|
1773
|
+
});
|
|
1774
|
+
}
|
|
1775
|
+
return pkg;
|
|
1776
|
+
}
|
|
1777
|
+
|
|
817
1778
|
/**
|
|
818
1779
|
* Parses the output of `go version -m` (build info) and returns a list of Go
|
|
819
1780
|
* package components for each "dep" line, including name, version, and integrity hash.
|
|
820
1781
|
*
|
|
1782
|
+
* A dependency that a replace directive swapped out is printed as its `dep`
|
|
1783
|
+
* line followed by a `=>` line naming the replacement (runtime/debug
|
|
1784
|
+
* BuildInfo.String). The replacement is what the binary holds, so it is
|
|
1785
|
+
* reported as {@link goReplacement} describes, with the replacement's
|
|
1786
|
+
* checksum.
|
|
1787
|
+
*
|
|
821
1788
|
* @param {string} buildInfoData Raw string output from `go version -m`
|
|
822
1789
|
* @returns {Promise<Object[]>} List of Go package component objects
|
|
823
1790
|
*/
|
|
@@ -827,29 +1794,48 @@ export async function parseGoVersionData(buildInfoData) {
|
|
|
827
1794
|
return pkgList;
|
|
828
1795
|
}
|
|
829
1796
|
const entries = [];
|
|
1797
|
+
let lastDep;
|
|
830
1798
|
for (const line of buildInfoData.split("\n")) {
|
|
831
|
-
const
|
|
832
|
-
if (
|
|
1799
|
+
const fields = line.trim().split(/\s+/);
|
|
1800
|
+
if (fields[0] === "=>" && lastDep && fields.length >= 2) {
|
|
1801
|
+
lastDep.replacement = goReplacement(
|
|
1802
|
+
lastDep.name,
|
|
1803
|
+
lastDep.version,
|
|
1804
|
+
fields[1],
|
|
1805
|
+
fields[2],
|
|
1806
|
+
);
|
|
1807
|
+
lastDep.name = lastDep.replacement.name;
|
|
1808
|
+
lastDep.version = lastDep.replacement.version;
|
|
1809
|
+
lastDep.skipRegistryLookups = lastDep.replacement.local;
|
|
1810
|
+
lastDep.hash = goSumIntegrity(fields[3]) || "";
|
|
1811
|
+
lastDep = undefined;
|
|
833
1812
|
continue;
|
|
834
1813
|
}
|
|
835
|
-
|
|
836
|
-
if (
|
|
1814
|
+
lastDep = undefined;
|
|
1815
|
+
if (fields[0] !== "dep" || fields.length < 3) {
|
|
837
1816
|
continue;
|
|
838
1817
|
}
|
|
839
|
-
|
|
1818
|
+
lastDep = {
|
|
840
1819
|
group: "",
|
|
841
|
-
name:
|
|
842
|
-
version:
|
|
843
|
-
hash:
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
: "",
|
|
847
|
-
});
|
|
1820
|
+
name: fields[1],
|
|
1821
|
+
version: fields[2],
|
|
1822
|
+
hash: goSumIntegrity(fields[3]) || "",
|
|
1823
|
+
};
|
|
1824
|
+
entries.push(lastDep);
|
|
848
1825
|
}
|
|
849
|
-
await prefetchGoPkgMetadata(entries);
|
|
1826
|
+
await prefetchGoPkgMetadata(goModulesToPrefetch(entries));
|
|
850
1827
|
for (const entry of entries) {
|
|
851
1828
|
pkgList.push(
|
|
852
|
-
|
|
1829
|
+
addGoReplacementProperties(
|
|
1830
|
+
await getGoPkgComponent(
|
|
1831
|
+
"",
|
|
1832
|
+
entry.name,
|
|
1833
|
+
entry.version,
|
|
1834
|
+
entry.hash,
|
|
1835
|
+
entry,
|
|
1836
|
+
),
|
|
1837
|
+
entry.replacement,
|
|
1838
|
+
),
|
|
853
1839
|
);
|
|
854
1840
|
}
|
|
855
1841
|
return pkgList;
|