@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
package/lib/cli/managedBom.js
CHANGED
|
@@ -1,11 +1,24 @@
|
|
|
1
|
-
import { readFileSync } from "node:fs";
|
|
1
|
+
import { readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { platform as _platform } from "node:os";
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
basename,
|
|
5
|
+
dirname,
|
|
6
|
+
extname,
|
|
7
|
+
isAbsolute,
|
|
8
|
+
join,
|
|
9
|
+
resolve,
|
|
10
|
+
} from "node:path";
|
|
4
11
|
import process from "node:process";
|
|
5
12
|
|
|
6
13
|
import { build } from "@cdxgen/cdx-purl";
|
|
7
14
|
|
|
8
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
DEBUG_MODE,
|
|
17
|
+
hasDangerousUnicode,
|
|
18
|
+
isDryRun,
|
|
19
|
+
readEnvironmentVariable,
|
|
20
|
+
recordActivity,
|
|
21
|
+
} from "../core/activity.js";
|
|
9
22
|
import { recordDegradation } from "../core/buildLedger.js";
|
|
10
23
|
import { deferFailOnError } from "../core/deferredExit.js";
|
|
11
24
|
import {
|
|
@@ -13,6 +26,7 @@ import {
|
|
|
13
26
|
hasAnyProjectType,
|
|
14
27
|
isFeatureEnabled,
|
|
15
28
|
shouldFetchLicense,
|
|
29
|
+
shouldFetchPackageMetadata,
|
|
16
30
|
} from "../core/env.js";
|
|
17
31
|
import {
|
|
18
32
|
checksumFile,
|
|
@@ -25,7 +39,10 @@ import {
|
|
|
25
39
|
safeWriteSync,
|
|
26
40
|
} from "../core/fs.js";
|
|
27
41
|
import { thoughtLog } from "../core/logger.js";
|
|
28
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
isProjectToolingAllowed,
|
|
44
|
+
reportProjectToolingSkipped,
|
|
45
|
+
} from "../core/projectTooling.js";
|
|
29
46
|
import {
|
|
30
47
|
extractJarArchive,
|
|
31
48
|
getNugetMetadata,
|
|
@@ -33,14 +50,20 @@ import {
|
|
|
33
50
|
parseBdistMetadata,
|
|
34
51
|
} from "../ecosystems/ecosystems.js";
|
|
35
52
|
import {
|
|
53
|
+
agreedNugetVersions,
|
|
54
|
+
applyNugetVersion,
|
|
55
|
+
dotnetManifestProjectDir,
|
|
56
|
+
dotnetRestoredProjectFile,
|
|
36
57
|
getCentralPackageVersions,
|
|
37
|
-
|
|
58
|
+
indexPropsProperties,
|
|
38
59
|
parseCsPkgData,
|
|
39
60
|
parseCsPkgLockData,
|
|
40
61
|
parseCsProjAssetsData,
|
|
41
62
|
parseCsProjData,
|
|
42
63
|
parseNupkg,
|
|
43
64
|
parsePaketLockData,
|
|
65
|
+
projectPropertyCandidates,
|
|
66
|
+
resolveInstalledNugetVersion,
|
|
44
67
|
} from "../ecosystems/parsers-dotnet.js";
|
|
45
68
|
import { parseMinJs } from "../ecosystems/parsers-js.js";
|
|
46
69
|
import {
|
|
@@ -52,7 +75,9 @@ import {
|
|
|
52
75
|
} from "../ecosystems/parsers-misc.js";
|
|
53
76
|
import { parseCpanSnapshot } from "../ecosystems/parsers-perl.js";
|
|
54
77
|
import {
|
|
78
|
+
enrichFromInstalledMetadata,
|
|
55
79
|
generatePixiLockFile,
|
|
80
|
+
installedMetadataKey,
|
|
56
81
|
parseMojoProject,
|
|
57
82
|
parsePiplockData,
|
|
58
83
|
parsePixiLockFile,
|
|
@@ -63,7 +88,7 @@ import {
|
|
|
63
88
|
parseSetupPyFile,
|
|
64
89
|
} from "../ecosystems/parsers-python.js";
|
|
65
90
|
import { parseRenvLock } from "../ecosystems/parsers-r.js";
|
|
66
|
-
import {
|
|
91
|
+
import { parseTerraformWorkspace } from "../ecosystems/parsers-terraform.js";
|
|
67
92
|
import { collectEmbeddedSboms } from "../ecosystems/pep770.js";
|
|
68
93
|
import { isPyLockFile } from "../ecosystems/pylockutils.js";
|
|
69
94
|
import {
|
|
@@ -82,6 +107,7 @@ import {
|
|
|
82
107
|
parseGemfileLockData,
|
|
83
108
|
parseGemspecData,
|
|
84
109
|
} from "../ecosystems/rubyutils.js";
|
|
110
|
+
import { getTerraformRegistryMetadata } from "../ecosystems/terraformRegistry.js";
|
|
85
111
|
import {
|
|
86
112
|
collectAiInventory,
|
|
87
113
|
summarizeAiInventory,
|
|
@@ -92,6 +118,15 @@ import {
|
|
|
92
118
|
collectDosaiCryptoComponents,
|
|
93
119
|
collectSourceCryptoComponents,
|
|
94
120
|
} from "../inventory/cbomutils.js";
|
|
121
|
+
import {
|
|
122
|
+
androidNetworkSecurityConfigPins,
|
|
123
|
+
CertificateCollector,
|
|
124
|
+
certRecordFromDer,
|
|
125
|
+
classifyCryptoFile,
|
|
126
|
+
containerRelativePath,
|
|
127
|
+
kubernetesTlsSecretRecords,
|
|
128
|
+
linkFallbackIssuers,
|
|
129
|
+
} from "../inventory/certificates.js";
|
|
95
130
|
import { readZipEntry } from "../inventory/deps.js";
|
|
96
131
|
import {
|
|
97
132
|
filterInvalidCryptoComponents,
|
|
@@ -105,11 +140,17 @@ import {
|
|
|
105
140
|
collectDosaiServiceComponents,
|
|
106
141
|
collectDosaiServicesFromMethods,
|
|
107
142
|
createDosaiMethodsSlice,
|
|
143
|
+
dosaiFailureReported,
|
|
144
|
+
dosaiMethodsSectionsArgument,
|
|
145
|
+
dosaiReportFileOmittedSections,
|
|
146
|
+
dosaiToolComponents,
|
|
108
147
|
isDosaiDotnetLanguage,
|
|
109
148
|
normalizeDosaiServiceMap,
|
|
110
149
|
readDosaiMethodsReport,
|
|
150
|
+
runsEvidenceAnalysis,
|
|
111
151
|
} from "../inventory/dosai.js";
|
|
112
152
|
import { addEvidenceForDotnet } from "../inventory/evidenceUtils.js";
|
|
153
|
+
import { resolvePluginBinary } from "../inventory/plugins.js";
|
|
113
154
|
import { nugetPurl, pypiBomRef } from "../inventory/purl.js";
|
|
114
155
|
import { classifyProbeResult } from "../inventory/toolRequirements.js";
|
|
115
156
|
import { getPluginToolComponents } from "../managers/binary.js";
|
|
@@ -216,11 +257,14 @@ export function createPixiBom(path, options) {
|
|
|
216
257
|
generatePixiLockFile(path);
|
|
217
258
|
|
|
218
259
|
const pixiLockFile = join(path, "pixi.lock");
|
|
219
|
-
if (!safeExistsSync(pixiLockFile)
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
260
|
+
if (!safeExistsSync(pixiLockFile)) {
|
|
261
|
+
if (DEBUG_MODE) {
|
|
262
|
+
console.log(
|
|
263
|
+
"Unexpected Error tried to generate pixi.lock file but failed.",
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
// Let createPythonBOM() generate the BOM from the other manifests.
|
|
267
|
+
return null;
|
|
224
268
|
}
|
|
225
269
|
PixiLockData = parsePixiLockFile(pixiLockFile);
|
|
226
270
|
metadataFilename = "pixi.lock";
|
|
@@ -404,6 +448,17 @@ export async function createPythonBom(path, options) {
|
|
|
404
448
|
!pyLockFiles?.length;
|
|
405
449
|
const poetryMode = poetryFiles?.length;
|
|
406
450
|
|
|
451
|
+
// The installed dist-info METADATA files describe the distributions this
|
|
452
|
+
// machine holds. They are read once, before any lockfile is parsed, so the
|
|
453
|
+
// lockfile entries they match are enriched before PyPI is asked and the
|
|
454
|
+
// registry is left only what the installations do not answer.
|
|
455
|
+
const installedLists = (metadataFiles || []).map((mf) =>
|
|
456
|
+
parseBdistMetadata(mf),
|
|
457
|
+
);
|
|
458
|
+
const installedMetadata = installedLists
|
|
459
|
+
.map((dlist) => dlist?.[0])
|
|
460
|
+
.filter((md) => md?.name);
|
|
461
|
+
|
|
407
462
|
// TODO: Support for nested directories
|
|
408
463
|
const setupPy = join(path, "setup.py");
|
|
409
464
|
const setupPyMode = safeExistsSync(setupPy);
|
|
@@ -416,7 +471,9 @@ export async function createPythonBom(path, options) {
|
|
|
416
471
|
if (DEBUG_MODE) {
|
|
417
472
|
console.log(`Parsing ${f}`);
|
|
418
473
|
}
|
|
419
|
-
let retMap = await parsePyLockData(lockData, f
|
|
474
|
+
let retMap = await parsePyLockData(lockData, f, undefined, {
|
|
475
|
+
installedMetadata,
|
|
476
|
+
});
|
|
420
477
|
if (retMap?.workspaceWarningShown) {
|
|
421
478
|
deferFailOnError(options, {
|
|
422
479
|
ecosystem: "python",
|
|
@@ -472,11 +529,14 @@ export async function createPythonBom(path, options) {
|
|
|
472
529
|
retMap.dependenciesList,
|
|
473
530
|
parentComponent,
|
|
474
531
|
);
|
|
475
|
-
} else {
|
|
532
|
+
} else if (isProjectToolingAllowed(options)) {
|
|
533
|
+
// poetry export and pdm export load plugins from the project tree
|
|
534
|
+
// (.poetry/plugins, .pdm-plugins), so they run only when the
|
|
535
|
+
// project's own tooling may.
|
|
476
536
|
let exportedReqs = "";
|
|
477
537
|
if (f.endsWith("poetry.lock")) {
|
|
478
538
|
thoughtLog(
|
|
479
|
-
"Using poetry export
|
|
539
|
+
"Using poetry export to collect the resolved closure without installing anything.",
|
|
480
540
|
);
|
|
481
541
|
const expCmd = safeSpawnSync(
|
|
482
542
|
"poetry",
|
|
@@ -487,7 +547,7 @@ export async function createPythonBom(path, options) {
|
|
|
487
547
|
exportedReqs = expCmd.stdout.toString();
|
|
488
548
|
} else if (f.endsWith("pdm.lock")) {
|
|
489
549
|
thoughtLog(
|
|
490
|
-
"Using pdm export
|
|
550
|
+
"Using pdm export to collect the resolved closure without installing anything.",
|
|
491
551
|
);
|
|
492
552
|
const expCmd = safeSpawnSync(
|
|
493
553
|
"pdm",
|
|
@@ -508,25 +568,33 @@ export async function createPythonBom(path, options) {
|
|
|
508
568
|
`exported-${basename(basePath)}-reqs.txt`,
|
|
509
569
|
);
|
|
510
570
|
safeWriteSync(tmpReqFile, exportedReqs);
|
|
511
|
-
const dlist = await parseReqFile(
|
|
571
|
+
const dlist = await parseReqFile(
|
|
572
|
+
tmpReqFile,
|
|
573
|
+
false,
|
|
574
|
+
installedMetadata,
|
|
575
|
+
);
|
|
512
576
|
if (dlist?.length) {
|
|
513
577
|
pkgList = pkgList.concat(dlist);
|
|
514
578
|
}
|
|
515
579
|
}
|
|
580
|
+
} else {
|
|
581
|
+
reportProjectToolingSkipped(
|
|
582
|
+
options,
|
|
583
|
+
"python.poetry-export.tooling-disabled",
|
|
584
|
+
"Skipping 'poetry export' and 'pdm export' since dependency installation is disabled. Only the packages parsed from the lock file will be used.",
|
|
585
|
+
{
|
|
586
|
+
ecosystem: "python",
|
|
587
|
+
tool: f.endsWith("pdm.lock") ? "pdm" : "poetry",
|
|
588
|
+
impact: "transitive-deps",
|
|
589
|
+
detail:
|
|
590
|
+
"Dependency installation is disabled, so poetry or pdm export was not run and only the packages parsed from the lock file are available.",
|
|
591
|
+
},
|
|
592
|
+
);
|
|
516
593
|
}
|
|
517
594
|
}
|
|
518
595
|
}
|
|
519
596
|
options.parentComponent = parentComponent;
|
|
520
597
|
} // poetryMode
|
|
521
|
-
if (metadataFiles?.length) {
|
|
522
|
-
// dist-info directories
|
|
523
|
-
for (const mf of metadataFiles) {
|
|
524
|
-
const dlist = parseBdistMetadata(mf);
|
|
525
|
-
if (dlist?.length) {
|
|
526
|
-
pkgList = pkgList.concat(dlist);
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
598
|
// .whl files. Zip file containing dist-info directory
|
|
531
599
|
if (whlFiles?.length) {
|
|
532
600
|
for (const wf of whlFiles) {
|
|
@@ -592,7 +660,7 @@ export async function createPythonBom(path, options) {
|
|
|
592
660
|
let dlist;
|
|
593
661
|
try {
|
|
594
662
|
const lockData = JSON.parse(readFileSync(piplockFile));
|
|
595
|
-
dlist = await parsePiplockData(lockData);
|
|
663
|
+
dlist = await parsePiplockData(lockData, installedMetadata);
|
|
596
664
|
} catch (err) {
|
|
597
665
|
recordDegradation("python.lockfile-unparseable.pipenv", {
|
|
598
666
|
ecosystem: "python",
|
|
@@ -617,7 +685,7 @@ export async function createPythonBom(path, options) {
|
|
|
617
685
|
} else if (reqDirFiles?.length) {
|
|
618
686
|
for (const j in reqDirFiles) {
|
|
619
687
|
const f = reqDirFiles[j];
|
|
620
|
-
const dlist = await parseReqFile(f, false);
|
|
688
|
+
const dlist = await parseReqFile(f, false, installedMetadata);
|
|
621
689
|
if (dlist?.length) {
|
|
622
690
|
pkgList = pkgList.concat(dlist);
|
|
623
691
|
}
|
|
@@ -625,13 +693,30 @@ export async function createPythonBom(path, options) {
|
|
|
625
693
|
metadataFilename = reqDirFiles.join(", ");
|
|
626
694
|
} else if (reqFiles?.length) {
|
|
627
695
|
for (const f of reqFiles) {
|
|
628
|
-
const dlist = await parseReqFile(f, false);
|
|
696
|
+
const dlist = await parseReqFile(f, false, installedMetadata);
|
|
629
697
|
if (dlist?.length) {
|
|
630
698
|
pkgList = pkgList.concat(dlist);
|
|
631
699
|
}
|
|
632
700
|
}
|
|
633
701
|
}
|
|
634
702
|
}
|
|
703
|
+
if (metadataFiles?.length) {
|
|
704
|
+
// dist-info directories, after every manifest block has added its
|
|
705
|
+
// entries. An installation whose package a lockfile, Pipfile.lock or
|
|
706
|
+
// requirements entry already lists enriched that entry, so it is not
|
|
707
|
+
// also emitted as a component of its own; an installation nothing else
|
|
708
|
+
// covers still is one. This pass is the emission decision only: each
|
|
709
|
+
// parser enriched its own entries before it asked PyPI.
|
|
710
|
+
const enriched = enrichFromInstalledMetadata(pkgList, installedMetadata);
|
|
711
|
+
for (const dlist of installedLists) {
|
|
712
|
+
if (dlist?.length) {
|
|
713
|
+
if (enriched.has(installedMetadataKey(dlist[0]))) {
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
pkgList = pkgList.concat(dlist);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
}
|
|
635
720
|
|
|
636
721
|
const parentDependsOn = new Set();
|
|
637
722
|
|
|
@@ -742,79 +827,81 @@ export async function createPythonBom(path, options) {
|
|
|
742
827
|
);
|
|
743
828
|
}
|
|
744
829
|
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
830
|
+
}
|
|
831
|
+
// ATOM parsedeps block
|
|
832
|
+
// Atom parsedeps slices can be used to identify packages that are not declared in manifests
|
|
833
|
+
// Since it is a slow operation, we only use it as a fallback or in deep mode
|
|
834
|
+
// This change was made in 10.9.2 release onwards. It reads the sources and
|
|
835
|
+
// installs nothing, so it runs whether or not dependencies are installed.
|
|
836
|
+
if (options.deep || !pkgList.length) {
|
|
837
|
+
if (!pkgList.length) {
|
|
838
|
+
thoughtLog(
|
|
839
|
+
"I couldn't find any components yet. Let's try static analysis with atom parsedeps command.",
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
const retMap = await getPyModules(path, pkgList, options);
|
|
843
|
+
// We need to patch the existing package list to add ImportedModules for evinse to work
|
|
844
|
+
if (retMap.modList?.length) {
|
|
845
|
+
const iSymbolsMap = {};
|
|
846
|
+
retMap.modList.forEach((v) => {
|
|
847
|
+
iSymbolsMap[v.name] = v.importedSymbols;
|
|
848
|
+
iSymbolsMap[v.name.replace(/_/g, "-")] = v.importedSymbols;
|
|
849
|
+
});
|
|
850
|
+
for (const apkg of pkgList) {
|
|
851
|
+
if (iSymbolsMap[apkg.name]) {
|
|
852
|
+
apkg.scope = "required";
|
|
853
|
+
apkg.properties = apkg.properties || [];
|
|
854
|
+
apkg.properties.push({
|
|
855
|
+
name: "internal:ImportedModules",
|
|
856
|
+
value: iSymbolsMap[apkg.name],
|
|
857
|
+
});
|
|
772
858
|
}
|
|
773
859
|
}
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
}
|
|
786
|
-
parentDependsOn.add(pypiBomRef(p.name, p.version));
|
|
860
|
+
}
|
|
861
|
+
if (retMap.pkgList?.length) {
|
|
862
|
+
pkgList = pkgList.concat(retMap.pkgList);
|
|
863
|
+
for (const p of retMap.pkgList) {
|
|
864
|
+
if (
|
|
865
|
+
!p.version ||
|
|
866
|
+
(parentComponent &&
|
|
867
|
+
p.name === parentComponent.name &&
|
|
868
|
+
(p.version === parentComponent.version || p.version === "latest"))
|
|
869
|
+
) {
|
|
870
|
+
continue;
|
|
787
871
|
}
|
|
788
|
-
|
|
789
|
-
if (retMap.dependenciesList) {
|
|
790
|
-
dependencies = mergeDependencies(
|
|
791
|
-
dependencies,
|
|
792
|
-
retMap.dependenciesList,
|
|
793
|
-
parentComponent,
|
|
794
|
-
);
|
|
795
|
-
}
|
|
796
|
-
if (retMap.allImports) {
|
|
797
|
-
allImports = { ...allImports, ...retMap.allImports };
|
|
872
|
+
parentDependsOn.add(pypiBomRef(p.name, p.version));
|
|
798
873
|
}
|
|
799
874
|
}
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
}
|
|
875
|
+
if (retMap.dependenciesList) {
|
|
876
|
+
dependencies = mergeDependencies(
|
|
877
|
+
dependencies,
|
|
878
|
+
retMap.dependenciesList,
|
|
879
|
+
parentComponent,
|
|
880
|
+
);
|
|
807
881
|
}
|
|
808
|
-
if (
|
|
809
|
-
|
|
810
|
-
ref: parentComponent["bom-ref"],
|
|
811
|
-
dependsOn: Array.from(parentDependsOn).filter(
|
|
812
|
-
(r) => parentComponent && r !== parentComponent["bom-ref"],
|
|
813
|
-
),
|
|
814
|
-
};
|
|
815
|
-
dependencies.splice(0, 0, pdependencies);
|
|
882
|
+
if (retMap.allImports) {
|
|
883
|
+
allImports = { ...allImports, ...retMap.allImports };
|
|
816
884
|
}
|
|
817
885
|
}
|
|
886
|
+
// ATOM parsedeps block
|
|
887
|
+
let parentPresent = false;
|
|
888
|
+
for (const d of dependencies) {
|
|
889
|
+
if (d.ref === parentComponent["bom-ref"]) {
|
|
890
|
+
parentPresent = true;
|
|
891
|
+
break;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
// Without an install, the parent's dependencies are known only when the
|
|
895
|
+
// import scan found some: an empty list would claim it has none.
|
|
896
|
+
if (!parentPresent && (options.installDeps || parentDependsOn.size)) {
|
|
897
|
+
const pdependencies = {
|
|
898
|
+
ref: parentComponent["bom-ref"],
|
|
899
|
+
dependsOn: Array.from(parentDependsOn).filter(
|
|
900
|
+
(r) => parentComponent && r !== parentComponent["bom-ref"],
|
|
901
|
+
),
|
|
902
|
+
};
|
|
903
|
+
dependencies.splice(0, 0, pdependencies);
|
|
904
|
+
}
|
|
818
905
|
}
|
|
819
906
|
// Final fallback is to manually parse setup.py if we still
|
|
820
907
|
// have an empty list
|
|
@@ -843,9 +930,11 @@ export async function createPythonBom(path, options) {
|
|
|
843
930
|
);
|
|
844
931
|
}
|
|
845
932
|
emitAiInventorySummary(aiInventory, path);
|
|
846
|
-
// Check and complete the dependency tree
|
|
933
|
+
// Check and complete the dependency tree. This installs every package into a
|
|
934
|
+
// virtual environment, so --no-install-deps turns it off too.
|
|
847
935
|
if (
|
|
848
936
|
isFeatureEnabled(options, "safe-pip-install") &&
|
|
937
|
+
options.installDeps !== false &&
|
|
849
938
|
pkgList.length &&
|
|
850
939
|
isPartialTree(dependencies, pkgList.length)
|
|
851
940
|
) {
|
|
@@ -1072,6 +1161,28 @@ export function createHelmBom(path, options) {
|
|
|
1072
1161
|
return {};
|
|
1073
1162
|
}
|
|
1074
1163
|
|
|
1164
|
+
/**
|
|
1165
|
+
* The properties of a list followed by those of another it does not hold yet.
|
|
1166
|
+
* It returns a new array, so a shallow copy of a component keeps its own
|
|
1167
|
+
* properties.
|
|
1168
|
+
*
|
|
1169
|
+
* @param {{name: string, value: string}[]|undefined} existing Properties held so far
|
|
1170
|
+
* @param {{name: string, value: string}[]} additions Properties to add
|
|
1171
|
+
* @returns {{name: string, value: string}[]} The merged properties
|
|
1172
|
+
*/
|
|
1173
|
+
function withMissingProperties(existing, additions) {
|
|
1174
|
+
const held = existing || [];
|
|
1175
|
+
return [
|
|
1176
|
+
...held,
|
|
1177
|
+
...additions.filter(
|
|
1178
|
+
(added) =>
|
|
1179
|
+
!held.some(
|
|
1180
|
+
(known) => known.name === added.name && known.value === added.value,
|
|
1181
|
+
),
|
|
1182
|
+
),
|
|
1183
|
+
];
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1075
1186
|
/**
|
|
1076
1187
|
* Function to create bom string for php projects
|
|
1077
1188
|
*
|
|
@@ -1104,6 +1215,10 @@ export function createPHPBom(path, options) {
|
|
|
1104
1215
|
composerLockSearchOptions,
|
|
1105
1216
|
);
|
|
1106
1217
|
let pkgList = [];
|
|
1218
|
+
// Platform facts of the scanned directory's lock when its composer.json names
|
|
1219
|
+
// no project: there is no module component to carry them, so they go on the
|
|
1220
|
+
// document. A nested lock like that has nowhere to put them.
|
|
1221
|
+
let metadataProperties = [];
|
|
1107
1222
|
const composerJsonMode = composerJsonFiles.length;
|
|
1108
1223
|
const composerLockMode = composerLockFiles.length;
|
|
1109
1224
|
// Create a composer.lock file for each composer.json file if needed.
|
|
@@ -1217,6 +1332,37 @@ export function createPHPBom(path, options) {
|
|
|
1217
1332
|
}
|
|
1218
1333
|
}
|
|
1219
1334
|
const retMap = parseComposerLock(f, rootRequires);
|
|
1335
|
+
if (retMap.platformProperties?.length) {
|
|
1336
|
+
if (moduleParent?.["bom-ref"]) {
|
|
1337
|
+
// The root composer.json is parsed twice, once for the parent
|
|
1338
|
+
// component and once per lock, so the object that ends up as
|
|
1339
|
+
// metadata.component is not always the module parsed here.
|
|
1340
|
+
for (const module of [moduleParent, parentComponent]) {
|
|
1341
|
+
if (module?.["bom-ref"] === moduleParent["bom-ref"]) {
|
|
1342
|
+
module.properties = withMissingProperties(
|
|
1343
|
+
module.properties,
|
|
1344
|
+
retMap.platformProperties,
|
|
1345
|
+
);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
} else if (resolve(basePath) === resolve(path)) {
|
|
1349
|
+
// The document describes the scanned directory, so only the lock
|
|
1350
|
+
// in it may put its facts on the document.
|
|
1351
|
+
metadataProperties = withMissingProperties(
|
|
1352
|
+
metadataProperties,
|
|
1353
|
+
retMap.platformProperties,
|
|
1354
|
+
);
|
|
1355
|
+
} else {
|
|
1356
|
+
recordDegradation("php.composer.platform-unattributed", {
|
|
1357
|
+
ecosystem: "php",
|
|
1358
|
+
tool: "composer",
|
|
1359
|
+
impact: "versions",
|
|
1360
|
+
path: f,
|
|
1361
|
+
detail:
|
|
1362
|
+
"The composer.json beside this lock names no project, so no component can carry the PHP and extension requirements of the lock.",
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1220
1366
|
if (retMap.pkgList?.length) {
|
|
1221
1367
|
pkgList = pkgList.concat(retMap.pkgList);
|
|
1222
1368
|
pkgList = trimComponents(pkgList);
|
|
@@ -1256,6 +1402,7 @@ export function createPHPBom(path, options) {
|
|
|
1256
1402
|
filename: composerLockFiles.join(", "),
|
|
1257
1403
|
dependencies,
|
|
1258
1404
|
parentComponent,
|
|
1405
|
+
metadataProperties,
|
|
1259
1406
|
});
|
|
1260
1407
|
}
|
|
1261
1408
|
if (composerJsonMode && !composerLockMode) {
|
|
@@ -1403,7 +1550,7 @@ export async function createRubyBom(path, options) {
|
|
|
1403
1550
|
console.log(`Parsing ${f}`);
|
|
1404
1551
|
}
|
|
1405
1552
|
const gemLockData = readFileSync(f, { encoding: "utf-8" });
|
|
1406
|
-
const retMap = await parseGemfileLockData(gemLockData, f);
|
|
1553
|
+
const retMap = await parseGemfileLockData(gemLockData, f, { gemHome });
|
|
1407
1554
|
if (retMap.pkgList?.length) {
|
|
1408
1555
|
pkgList = pkgList.concat(retMap.pkgList);
|
|
1409
1556
|
pkgList = trimComponents(pkgList);
|
|
@@ -1457,7 +1604,7 @@ export async function createRubyBom(path, options) {
|
|
|
1457
1604
|
const gpkgList = await parseGemspecData(
|
|
1458
1605
|
gemspecData,
|
|
1459
1606
|
f,
|
|
1460
|
-
isRootGemspec ? { lockedVersions } : {},
|
|
1607
|
+
isRootGemspec ? { lockedVersions, projectGemspec: true } : {},
|
|
1461
1608
|
);
|
|
1462
1609
|
if (!gpkgList.length) {
|
|
1463
1610
|
continue;
|
|
@@ -1503,7 +1650,14 @@ export async function createRubyBom(path, options) {
|
|
|
1503
1650
|
options.resolveClass ||
|
|
1504
1651
|
(options.deep && !hasAnyProjectType(["oci"], options, false))
|
|
1505
1652
|
) {
|
|
1506
|
-
|
|
1653
|
+
// `bundle exec` evaluates the project's Gemfile, a Ruby program
|
|
1654
|
+
if (gemHome && !isGemHomeEmpty && !isProjectToolingAllowed(options)) {
|
|
1655
|
+
reportProjectToolingSkipped(
|
|
1656
|
+
options,
|
|
1657
|
+
null,
|
|
1658
|
+
"Skipping the module names of gems since dependency installation is disabled. Collecting them runs 'bundle exec', which evaluates the Gemfile.",
|
|
1659
|
+
);
|
|
1660
|
+
} else if (gemHome && !isGemHomeEmpty) {
|
|
1507
1661
|
const rubyCommand =
|
|
1508
1662
|
readEnvironmentVariable("CDXGEN_RUBY_CMD") ||
|
|
1509
1663
|
readEnvironmentVariable("RUBY_CMD") ||
|
|
@@ -1619,6 +1773,12 @@ export async function createCsharpBom(path, options) {
|
|
|
1619
1773
|
return createBinaryBom(path, options);
|
|
1620
1774
|
}
|
|
1621
1775
|
let parentComponent = createDefaultParentComponent(path, "nuget", options);
|
|
1776
|
+
// The directory a scan starts from is not a package, so it has a version
|
|
1777
|
+
// only when --project-version gives it one; "latest" is not a version.
|
|
1778
|
+
applyNugetVersion(
|
|
1779
|
+
parentComponent,
|
|
1780
|
+
options.projectVersion ? `${options.projectVersion}` : undefined,
|
|
1781
|
+
);
|
|
1622
1782
|
const slnFiles = getAllFiles(
|
|
1623
1783
|
path,
|
|
1624
1784
|
`${options.multiProject ? "**/" : ""}*.sln*`,
|
|
@@ -1821,49 +1981,129 @@ export async function createCsharpBom(path, options) {
|
|
|
1821
1981
|
const parentDependsOn = new Set();
|
|
1822
1982
|
if (nupkgFiles.length && projAssetsFiles.length === 0) {
|
|
1823
1983
|
manifestFiles = manifestFiles.concat(nupkgFiles);
|
|
1824
|
-
//
|
|
1825
|
-
//
|
|
1826
|
-
|
|
1827
|
-
|
|
1984
|
+
// A nuspec states the range each dependency accepts, not the version
|
|
1985
|
+
// installed. The installed packages are the components, and a dependency
|
|
1986
|
+
// edge points at the installed version its range resolves to; a minimum
|
|
1987
|
+
// version nobody installed is not a component.
|
|
1988
|
+
const installedVersions = {};
|
|
1989
|
+
const installedNames = {};
|
|
1990
|
+
let dependencyRanges = {};
|
|
1828
1991
|
for (const nf of nupkgFiles) {
|
|
1829
1992
|
if (DEBUG_MODE) {
|
|
1830
1993
|
console.log(`Parsing ${nf}`);
|
|
1831
1994
|
}
|
|
1832
1995
|
const retMap = await parseNupkg(nf);
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1996
|
+
const installed = retMap?.pkgList?.[0];
|
|
1997
|
+
if (installed) {
|
|
1998
|
+
pkgList.push(installed);
|
|
1999
|
+
parentDependsOn.add(installed["bom-ref"]);
|
|
2000
|
+
const key = installed.name.toLowerCase();
|
|
2001
|
+
installedNames[key] ??= installed.name;
|
|
2002
|
+
installedVersions[key] ??= [];
|
|
2003
|
+
if (installed.version) {
|
|
2004
|
+
installedVersions[key].push(installed.version);
|
|
1838
2005
|
}
|
|
1839
2006
|
}
|
|
1840
|
-
if (retMap?.
|
|
1841
|
-
|
|
2007
|
+
if (retMap?.dependencyRanges) {
|
|
2008
|
+
dependencyRanges = { ...dependencyRanges, ...retMap.dependencyRanges };
|
|
1842
2009
|
}
|
|
1843
2010
|
} // end for
|
|
1844
|
-
for (const
|
|
1845
|
-
const dependsOn =
|
|
1846
|
-
|
|
1847
|
-
const
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
2011
|
+
for (const [ref, ranges] of Object.entries(dependencyRanges)) {
|
|
2012
|
+
const dependsOn = new Set();
|
|
2013
|
+
for (const [name, range] of Object.entries(ranges)) {
|
|
2014
|
+
const key = name.toLowerCase();
|
|
2015
|
+
const version = resolveInstalledNugetVersion(
|
|
2016
|
+
range,
|
|
2017
|
+
installedVersions[key],
|
|
2018
|
+
);
|
|
2019
|
+
const purl = version ? nugetPurl(installedNames[key], version) : null;
|
|
2020
|
+
if (purl) {
|
|
2021
|
+
dependsOn.add(decodeURIComponent(purl));
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
dependencies.push({ ref, dependsOn: [...dependsOn].sort() });
|
|
1851
2025
|
}
|
|
1852
2026
|
}
|
|
2027
|
+
// Versions each project restored, by project directory and lowercased
|
|
2028
|
+
// package id. Two projects of one tree routinely restore different versions
|
|
2029
|
+
// of a package, so a version missing from one project's manifest is taken
|
|
2030
|
+
// from that project's restore output, never from another project's.
|
|
2031
|
+
// The restore output also spells each package id the way its nuspec does.
|
|
2032
|
+
const projectVersions = {};
|
|
2033
|
+
const projectNames = {};
|
|
2034
|
+
const recordProjectVersions = (projectDir, pkgs) => {
|
|
2035
|
+
projectVersions[projectDir] ??= {};
|
|
2036
|
+
projectNames[projectDir] ??= {};
|
|
2037
|
+
for (const p of pkgs || []) {
|
|
2038
|
+
if (
|
|
2039
|
+
p?.name &&
|
|
2040
|
+
p.version &&
|
|
2041
|
+
!["application", "project"].includes(p.type)
|
|
2042
|
+
) {
|
|
2043
|
+
projectVersions[projectDir][p.name.toLowerCase()] ??= p.version;
|
|
2044
|
+
projectNames[projectDir][p.name.toLowerCase()] ??= p.name;
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
};
|
|
2048
|
+
// Direct dependencies of each project directory from its lock file or
|
|
2049
|
+
// packages.config, attached to that project once the project files are read.
|
|
2050
|
+
const rootsByProjectDir = {};
|
|
2051
|
+
const recordRoots = (projectDir, pkgs) => {
|
|
2052
|
+
rootsByProjectDir[projectDir] ??= new Set();
|
|
2053
|
+
for (const p of pkgs || []) {
|
|
2054
|
+
if (p?.["bom-ref"]) {
|
|
2055
|
+
rootsByProjectDir[projectDir].add(p["bom-ref"]);
|
|
2056
|
+
}
|
|
2057
|
+
}
|
|
2058
|
+
};
|
|
1853
2059
|
// In a multi-project scan, different projects may use different manifest
|
|
1854
2060
|
// formats: modern SDK-style (project.assets.json / packages.lock.json) or
|
|
1855
2061
|
// legacy (packages.config). Apply the assets > lock > config precedence per
|
|
1856
2062
|
// project directory instead of across the whole scan, so dependencies from
|
|
1857
2063
|
// one project's manifest are not dropped just because another format exists
|
|
1858
2064
|
// elsewhere in the scan.
|
|
1859
|
-
const assetsProjectDirs = new Set(
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
)
|
|
1866
|
-
|
|
2065
|
+
const assetsProjectDirs = new Set();
|
|
2066
|
+
// Restore records a project file the way the build was pointed at it, which
|
|
2067
|
+
// on a case-insensitive file system can differ in case from the file found
|
|
2068
|
+
// here. Such a path takes the found file's spelling, unless that is ambiguous.
|
|
2069
|
+
const foundProjectFiles = new Set();
|
|
2070
|
+
const foundProjectFilesByKey = new Map();
|
|
2071
|
+
for (const f of csProjFiles) {
|
|
2072
|
+
const file = resolve(f);
|
|
2073
|
+
const key = file.toLowerCase();
|
|
2074
|
+
foundProjectFiles.add(file);
|
|
2075
|
+
foundProjectFilesByKey.set(
|
|
2076
|
+
key,
|
|
2077
|
+
foundProjectFilesByKey.has(key) ? null : file,
|
|
2078
|
+
);
|
|
2079
|
+
}
|
|
2080
|
+
const foundProjectFile = (file) =>
|
|
2081
|
+
!file || foundProjectFiles.has(file)
|
|
2082
|
+
? file
|
|
2083
|
+
: foundProjectFilesByKey.get(file.toLowerCase()) || file;
|
|
2084
|
+
// The package id and version each project's restore output records, by
|
|
2085
|
+
// project file. A project whose restore outputs disagree, such as a stale
|
|
2086
|
+
// obj/ beside an artifacts directory, takes neither.
|
|
2087
|
+
const restoredIdentities = new Map();
|
|
2088
|
+
const recordRestoredIdentity = (projectFile, identity) => {
|
|
2089
|
+
if (!projectFile || !identity) {
|
|
2090
|
+
return;
|
|
2091
|
+
}
|
|
2092
|
+
const known = restoredIdentities.get(projectFile);
|
|
2093
|
+
if (known === undefined) {
|
|
2094
|
+
restoredIdentities.set(projectFile, identity);
|
|
2095
|
+
} else if (
|
|
2096
|
+
known &&
|
|
2097
|
+
(known.name !== identity.name || known.version !== identity.version)
|
|
2098
|
+
) {
|
|
2099
|
+
if (DEBUG_MODE) {
|
|
2100
|
+
console.log(
|
|
2101
|
+
`Restore outputs disagree on the package id or version of ${projectFile}, so the identity its project file states is used.`,
|
|
2102
|
+
);
|
|
2103
|
+
}
|
|
2104
|
+
restoredIdentities.set(projectFile, null);
|
|
2105
|
+
}
|
|
2106
|
+
};
|
|
1867
2107
|
// project.assets.json parsing
|
|
1868
2108
|
if (projAssetsFiles.length) {
|
|
1869
2109
|
manifestFiles = manifestFiles.concat(projAssetsFiles);
|
|
@@ -1875,6 +2115,15 @@ export async function createCsharpBom(path, options) {
|
|
|
1875
2115
|
const results = parseCsProjAssetsData(pkgData, af);
|
|
1876
2116
|
const deps = results["dependenciesList"];
|
|
1877
2117
|
const dlist = results["pkgList"];
|
|
2118
|
+
// Restore output names its project; an artifacts layout keeps it outside
|
|
2119
|
+
// the project directory.
|
|
2120
|
+
const projectFile = foundProjectFile(
|
|
2121
|
+
dotnetRestoredProjectFile(af, results.projectPath, results.outputPath),
|
|
2122
|
+
);
|
|
2123
|
+
const projectDir = dotnetManifestProjectDir(af, projectFile);
|
|
2124
|
+
assetsProjectDirs.add(projectDir);
|
|
2125
|
+
recordProjectVersions(projectDir, dlist);
|
|
2126
|
+
recordRestoredIdentity(projectFile, results.projectIdentity);
|
|
1878
2127
|
if (dlist?.length) {
|
|
1879
2128
|
pkgList = pkgList.concat(dlist);
|
|
1880
2129
|
}
|
|
@@ -1907,9 +2156,12 @@ export async function createCsharpBom(path, options) {
|
|
|
1907
2156
|
});
|
|
1908
2157
|
}
|
|
1909
2158
|
}
|
|
2159
|
+
const lockProjectDirs = new Set(
|
|
2160
|
+
pkgLockFiles.map((af) => dotnetManifestProjectDir(af)),
|
|
2161
|
+
);
|
|
1910
2162
|
// packages.lock.json parsing for any project without a project.assets.json
|
|
1911
2163
|
const pkgLockToParse = pkgLockFiles.filter(
|
|
1912
|
-
(af) => !assetsProjectDirs.has(
|
|
2164
|
+
(af) => !assetsProjectDirs.has(dotnetManifestProjectDir(af)),
|
|
1913
2165
|
);
|
|
1914
2166
|
if (pkgLockToParse.length) {
|
|
1915
2167
|
manifestFiles = manifestFiles.concat(pkgLockToParse);
|
|
@@ -1923,37 +2175,29 @@ export async function createCsharpBom(path, options) {
|
|
|
1923
2175
|
const deps = results["dependenciesList"];
|
|
1924
2176
|
const dlist = results["pkgList"];
|
|
1925
2177
|
const rootList = results["rootList"];
|
|
2178
|
+
const projectDir = dotnetManifestProjectDir(af);
|
|
2179
|
+
recordProjectVersions(projectDir, dlist);
|
|
1926
2180
|
if (dlist?.length) {
|
|
1927
2181
|
pkgList = pkgList.concat(dlist);
|
|
1928
2182
|
}
|
|
1929
2183
|
if (deps?.length) {
|
|
1930
2184
|
dependencies = mergeDependencies(dependencies, deps, parentComponent);
|
|
1931
2185
|
}
|
|
1932
|
-
|
|
1933
|
-
// list after processing all lock files
|
|
1934
|
-
if (rootList?.length) {
|
|
1935
|
-
for (const p of rootList) {
|
|
1936
|
-
parentDependsOn.add(p["bom-ref"]);
|
|
1937
|
-
}
|
|
1938
|
-
}
|
|
2186
|
+
recordRoots(projectDir, rootList);
|
|
1939
2187
|
}
|
|
1940
2188
|
}
|
|
1941
2189
|
// packages.config parsing for any project without a modern manifest
|
|
1942
2190
|
// (project.assets.json / packages.lock.json) covering the same directory
|
|
1943
2191
|
const pkgConfigToParse = pkgConfigFiles.filter((f) => {
|
|
1944
|
-
const projDir =
|
|
2192
|
+
const projDir = dotnetManifestProjectDir(f);
|
|
1945
2193
|
return !assetsProjectDirs.has(projDir) && !lockProjectDirs.has(projDir);
|
|
1946
2194
|
});
|
|
1947
2195
|
if (pkgConfigToParse.length) {
|
|
1948
2196
|
manifestFiles = manifestFiles.concat(pkgConfigToParse);
|
|
1949
|
-
//
|
|
1950
|
-
//
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
if (p.name && p.version) {
|
|
1954
|
-
resolvedVersions[p.name] = p.version;
|
|
1955
|
-
}
|
|
1956
|
-
}
|
|
2197
|
+
// A templated or missing version is backfilled only from a version every
|
|
2198
|
+
// other manifest of the scan agrees on: these projects restored nothing of
|
|
2199
|
+
// their own, and another project's version may not be theirs.
|
|
2200
|
+
const resolvedVersions = agreedNugetVersions(pkgList);
|
|
1957
2201
|
// packages.config parsing
|
|
1958
2202
|
for (const f of pkgConfigToParse) {
|
|
1959
2203
|
if (DEBUG_MODE) {
|
|
@@ -1961,11 +2205,11 @@ export async function createCsharpBom(path, options) {
|
|
|
1961
2205
|
}
|
|
1962
2206
|
pkgData = readFileSync(f, { encoding: "utf-8" });
|
|
1963
2207
|
const dlist = parseCsPkgData(pkgData, f, resolvedVersions);
|
|
2208
|
+
const projectDir = dotnetManifestProjectDir(f);
|
|
2209
|
+
recordProjectVersions(projectDir, dlist);
|
|
1964
2210
|
if (dlist?.length) {
|
|
1965
2211
|
pkgList = pkgList.concat(dlist);
|
|
1966
|
-
|
|
1967
|
-
parentDependsOn.add(d["bom-ref"]);
|
|
1968
|
-
}
|
|
2212
|
+
recordRoots(projectDir, dlist);
|
|
1969
2213
|
}
|
|
1970
2214
|
}
|
|
1971
2215
|
}
|
|
@@ -1988,27 +2232,34 @@ export async function createCsharpBom(path, options) {
|
|
|
1988
2232
|
}
|
|
1989
2233
|
}
|
|
1990
2234
|
}
|
|
1991
|
-
const
|
|
2235
|
+
const projectRefByDir = {};
|
|
1992
2236
|
let services = [];
|
|
2237
|
+
let dosaiTools = [];
|
|
1993
2238
|
if (csProjFiles.length) {
|
|
1994
2239
|
manifestFiles = manifestFiles.concat(csProjFiles);
|
|
1995
|
-
// Parsing csproj is quite error-prone. Some project files may not have
|
|
1996
|
-
//
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
}
|
|
2001
|
-
}
|
|
2240
|
+
// Parsing csproj is quite error-prone. Some project files may not have
|
|
2241
|
+
// versions specified. Such a version comes from what this project restored,
|
|
2242
|
+
// then from central package management, and only then from a version the
|
|
2243
|
+
// whole scan agrees on.
|
|
2244
|
+
const agreedVersions = agreedNugetVersions(pkgList);
|
|
2002
2245
|
let msbuildInstalled = false;
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2246
|
+
// `dotnet msbuild` evaluates the project (loading Directory.Build.rsp,
|
|
2247
|
+
// Directory.Build.props/targets and build extensions), so it belongs to
|
|
2248
|
+
// the dependency-tooling surface disabled by --no-install-deps and the
|
|
2249
|
+
// pre-build lifecycle. Version labels it would resolve then fall back to
|
|
2250
|
+
// the static props/central-version resolution.
|
|
2251
|
+
if (isProjectToolingAllowed(options)) {
|
|
2252
|
+
const result = safeSpawnSync(DOTNET_CMD, ["msbuild", "--version"], {
|
|
2253
|
+
shell: isWin,
|
|
2254
|
+
});
|
|
2255
|
+
if (result.status === 0 && !result.error && result.stdout?.trim()) {
|
|
2256
|
+
msbuildInstalled = true;
|
|
2257
|
+
}
|
|
2011
2258
|
}
|
|
2259
|
+
// `$(Name)` version labels resolve from the props files that can apply to
|
|
2260
|
+
// each project (beside and above it), read once for the whole scan.
|
|
2261
|
+
// msbuild evaluates only what they leave unresolved.
|
|
2262
|
+
const propsIndex = indexPropsProperties(propsFiles);
|
|
2012
2263
|
// Directory.Packages.props can sit above the directory being scanned, so it is
|
|
2013
2264
|
// resolved per project file by walking up rather than from the props glob below.
|
|
2014
2265
|
// The cache keeps a repository with many projects from re-parsing the same file.
|
|
@@ -2019,15 +2270,20 @@ export async function createCsharpBom(path, options) {
|
|
|
2019
2270
|
console.log(`Parsing ${f}`);
|
|
2020
2271
|
}
|
|
2021
2272
|
const csProjData = readFileSync(f, { encoding: "utf-8" });
|
|
2273
|
+
const projectDir = dotnetManifestProjectDir(f);
|
|
2022
2274
|
const retMap = parseCsProjData(
|
|
2023
2275
|
csProjData,
|
|
2024
2276
|
f,
|
|
2025
|
-
|
|
2277
|
+
projectVersions[projectDir] || {},
|
|
2026
2278
|
msbuildInstalled,
|
|
2027
|
-
|
|
2279
|
+
projectPropertyCandidates(f, propsIndex),
|
|
2028
2280
|
getCentralPackageVersions(f, centralVersionsCache),
|
|
2281
|
+
agreedVersions,
|
|
2282
|
+
projectNames[projectDir] || {},
|
|
2283
|
+
restoredIdentities.get(resolve(f)) || undefined,
|
|
2029
2284
|
);
|
|
2030
2285
|
if (retMap?.parentComponent?.purl) {
|
|
2286
|
+
projectRefByDir[projectDir] ??= retMap.parentComponent["bom-ref"];
|
|
2031
2287
|
// If there are multiple project files, track the parent components using nested components
|
|
2032
2288
|
if (csProjFiles.length > 1) {
|
|
2033
2289
|
if (!parentComponent.components) {
|
|
@@ -2049,8 +2305,56 @@ export async function createCsharpBom(path, options) {
|
|
|
2049
2305
|
parentComponent,
|
|
2050
2306
|
);
|
|
2051
2307
|
}
|
|
2308
|
+
// Only a version that needed msbuild makes its skip a loss
|
|
2309
|
+
if (
|
|
2310
|
+
retMap?.unresolvedVersionLabels?.length &&
|
|
2311
|
+
!isProjectToolingAllowed(options)
|
|
2312
|
+
) {
|
|
2313
|
+
reportProjectToolingSkipped(
|
|
2314
|
+
options,
|
|
2315
|
+
"dotnet.msbuild.tooling-disabled",
|
|
2316
|
+
"Skipping 'dotnet msbuild' property evaluation since dependency installation is disabled. Package versions that come from MSBuild properties no project or props file sets stay empty.",
|
|
2317
|
+
{
|
|
2318
|
+
ecosystem: "csharp",
|
|
2319
|
+
tool: "msbuild",
|
|
2320
|
+
impact: "versions",
|
|
2321
|
+
detail:
|
|
2322
|
+
"Dependency installation is disabled, so $(Property) version references that the project and props files do not set were not evaluated with msbuild.",
|
|
2323
|
+
},
|
|
2324
|
+
);
|
|
2325
|
+
}
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
// A lock file's or packages.config's direct dependencies belong to the
|
|
2329
|
+
// project in that directory; without a project file there, to the parent.
|
|
2330
|
+
for (const [projectDir, roots] of Object.entries(rootsByProjectDir)) {
|
|
2331
|
+
if (!roots.size) {
|
|
2332
|
+
continue;
|
|
2333
|
+
}
|
|
2334
|
+
const projectRef = projectRefByDir[projectDir];
|
|
2335
|
+
if (projectRef && projectRef !== parentComponent?.["bom-ref"]) {
|
|
2336
|
+
dependencies = mergeDependencies(
|
|
2337
|
+
dependencies,
|
|
2338
|
+
[{ ref: projectRef, dependsOn: [...roots].sort() }],
|
|
2339
|
+
parentComponent,
|
|
2340
|
+
);
|
|
2341
|
+
} else {
|
|
2342
|
+
for (const ref of roots) {
|
|
2343
|
+
parentDependsOn.add(ref);
|
|
2344
|
+
}
|
|
2052
2345
|
}
|
|
2053
2346
|
}
|
|
2347
|
+
// A scan of several projects is made of them, so the directory depends on
|
|
2348
|
+
// each, as a Gradle build does on its subprojects. Restore output also lists
|
|
2349
|
+
// every project it restored as a package; the parent and its projects are
|
|
2350
|
+
// already in the BOM, and a second component with the same bom-ref would
|
|
2351
|
+
// merge the two in every consumer.
|
|
2352
|
+
const projectRefs = new Set([parentComponent?.["bom-ref"]]);
|
|
2353
|
+
for (const project of parentComponent?.components || []) {
|
|
2354
|
+
parentDependsOn.add(project["bom-ref"]);
|
|
2355
|
+
projectRefs.add(project["bom-ref"]);
|
|
2356
|
+
}
|
|
2357
|
+
pkgList = pkgList.filter((p) => !projectRefs.has(p["bom-ref"]));
|
|
2054
2358
|
if (pkgList.length) {
|
|
2055
2359
|
pkgList = trimComponents(pkgList);
|
|
2056
2360
|
// Perform deep analysis using dosai
|
|
@@ -2059,44 +2363,92 @@ export async function createCsharpBom(path, options) {
|
|
|
2059
2363
|
// as-is: join would append it to the scan path, which is never a valid
|
|
2060
2364
|
// path on Windows and silently loses the .NET evidence elsewhere
|
|
2061
2365
|
// (issue 4394). A relative value still resolves against the scan path.
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
)
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2366
|
+
// Without --deps-slices-file the slice goes to a directory of this run:
|
|
2367
|
+
// a fixed name in the shared temp directory was reused by every later
|
|
2368
|
+
// scan, of any project, as if it were that project's evidence.
|
|
2369
|
+
const slicesDir = options.depsSlicesFile
|
|
2370
|
+
? undefined
|
|
2371
|
+
: safeMkdtempSync(join(getTmpDir(), "cdxgen-dosai-"));
|
|
2372
|
+
const slicesFile = options.depsSlicesFile
|
|
2373
|
+
? resolve(path, options.depsSlicesFile)
|
|
2374
|
+
: join(slicesDir, "dosai.json");
|
|
2375
|
+
try {
|
|
2376
|
+
// A run that will run evinse (--evidence or --include-crypto, after
|
|
2377
|
+
// profiles are applied) keeps the report full: evinse reuses this
|
|
2378
|
+
// file as its usages slice and persists it as the native report.
|
|
2379
|
+
// Anything else needs only the sections cdxgen reads, so dosai
|
|
2380
|
+
// leaves the rest out (Metadata.OmittedSections names it).
|
|
2381
|
+
const willRunEvinse = runsEvidenceAnalysis(options);
|
|
2382
|
+
// Reuse an existing slice as-is, with one exception: a trimmed file
|
|
2383
|
+
// (an earlier --deep run left it behind) is regenerated full before
|
|
2384
|
+
// an evidence run uses it, so dosai still runs once per scan.
|
|
2385
|
+
let reuseExisting = safeExistsSync(slicesFile);
|
|
2386
|
+
if (reuseExisting && willRunEvinse) {
|
|
2387
|
+
const omitted = dosaiReportFileOmittedSections(slicesFile);
|
|
2388
|
+
if (omitted.length) {
|
|
2389
|
+
if (DEBUG_MODE) {
|
|
2390
|
+
console.log(
|
|
2391
|
+
`The existing dosai methods slice "${slicesFile}" omits sections (${omitted.join(", ")}); regenerating the full report because this run performs evidence analysis.`,
|
|
2392
|
+
);
|
|
2393
|
+
}
|
|
2394
|
+
reuseExisting = false;
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
// Create the slices file if it doesn't exist
|
|
2398
|
+
if (!reuseExisting) {
|
|
2399
|
+
thoughtLog(
|
|
2400
|
+
"Alright, the next step is to invoke the dosai command to identify evidence of occurrences for various components.",
|
|
2401
|
+
);
|
|
2402
|
+
const sliceResult = createDosaiMethodsSlice(
|
|
2403
|
+
resolve(path),
|
|
2404
|
+
resolve(slicesFile),
|
|
2405
|
+
{
|
|
2406
|
+
...options,
|
|
2407
|
+
dosaiMethodsSections: willRunEvinse
|
|
2408
|
+
? undefined
|
|
2409
|
+
: dosaiMethodsSectionsArgument(),
|
|
2410
|
+
},
|
|
2411
|
+
);
|
|
2412
|
+
// A stopped run, or one the .NET host refused to start, has
|
|
2413
|
+
// already said why and what to change; the debug output would show
|
|
2414
|
+
// no other dosai error for it (issue 4438).
|
|
2415
|
+
if (!sliceResult && !dosaiFailureReported(resolve(slicesFile))) {
|
|
2416
|
+
// Visible without debug output: otherwise the components silently
|
|
2417
|
+
// lose the dependency-slice evidence while the scan still exits 0.
|
|
2418
|
+
// --evidence (which implies --deep) runs dosai again during evinse
|
|
2419
|
+
// and can still add occurrences, so this names only what is lost.
|
|
2420
|
+
// Debug output comes from the environment, not a CLI flag (issue
|
|
2421
|
+
// 4438).
|
|
2422
|
+
console.warn(
|
|
2423
|
+
"Slicing with dosai was unsuccessful, so the .NET components get no occurrence, reachability, or services evidence from the dependency slice. Re-run with CDXGEN_DEBUG_MODE=debug for the dosai errors.",
|
|
2424
|
+
);
|
|
2425
|
+
}
|
|
2426
|
+
} else if (DEBUG_MODE) {
|
|
2427
|
+
console.log(
|
|
2428
|
+
`Reusing the existing dosai methods slice "${slicesFile}" without checking whether it honours the current --exclude patterns. Delete the file to force a fresh run.`,
|
|
2084
2429
|
);
|
|
2085
2430
|
}
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2431
|
+
pkgList = addEvidenceForDotnet(pkgList, slicesFile, {
|
|
2432
|
+
srcPath: resolve(path),
|
|
2433
|
+
});
|
|
2434
|
+
const methodsSlice = readDosaiMethodsReport(slicesFile, {
|
|
2435
|
+
sections: ["Metadata", "Services", "ApiEndpoints"],
|
|
2436
|
+
});
|
|
2437
|
+
// Provider-backed Services[] first so bom-refs, trust zones, and data
|
|
2438
|
+
// classifications survive; ApiEndpoints derivation fills in the rest.
|
|
2439
|
+
const servicesMap = collectDosaiServiceComponents(methodsSlice, {});
|
|
2440
|
+
collectDosaiServicesFromMethods(methodsSlice, servicesMap);
|
|
2441
|
+
services = normalizeDosaiServiceMap(servicesMap);
|
|
2442
|
+
dosaiTools = dosaiToolComponents(
|
|
2443
|
+
slicesFile,
|
|
2444
|
+
methodsSlice,
|
|
2445
|
+
getPluginToolComponents(["dosai"])[0],
|
|
2089
2446
|
);
|
|
2447
|
+
} finally {
|
|
2448
|
+
if (slicesDir) {
|
|
2449
|
+
safeRmSync(slicesDir, { recursive: true, force: true });
|
|
2450
|
+
}
|
|
2090
2451
|
}
|
|
2091
|
-
pkgList = addEvidenceForDotnet(pkgList, slicesFile);
|
|
2092
|
-
const methodsSlice = readDosaiMethodsReport(slicesFile, {
|
|
2093
|
-
sections: ["Metadata", "Services", "ApiEndpoints"],
|
|
2094
|
-
});
|
|
2095
|
-
// Provider-backed Services[] first so bom-refs, trust zones, and data
|
|
2096
|
-
// classifications survive; ApiEndpoints derivation fills in the rest.
|
|
2097
|
-
const servicesMap = collectDosaiServiceComponents(methodsSlice, {});
|
|
2098
|
-
collectDosaiServicesFromMethods(methodsSlice, servicesMap);
|
|
2099
|
-
services = normalizeDosaiServiceMap(servicesMap);
|
|
2100
2452
|
}
|
|
2101
2453
|
}
|
|
2102
2454
|
// Parent dependency tree. A single project file already contributed an
|
|
@@ -2131,7 +2483,7 @@ export async function createCsharpBom(path, options) {
|
|
|
2131
2483
|
dependencies,
|
|
2132
2484
|
parentComponent,
|
|
2133
2485
|
services,
|
|
2134
|
-
tools:
|
|
2486
|
+
tools: dosaiTools,
|
|
2135
2487
|
});
|
|
2136
2488
|
}
|
|
2137
2489
|
|
|
@@ -2150,58 +2502,239 @@ export async function createCryptoCertsBom(path, options) {
|
|
|
2150
2502
|
},
|
|
2151
2503
|
};
|
|
2152
2504
|
}
|
|
2505
|
+
const collector = new CertificateCollector();
|
|
2153
2506
|
const pkgList = [];
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2507
|
+
// In rootfs/container mode the scan path sits inside the extracted tree;
|
|
2508
|
+
// locations are recorded relative to that tree so they merge with the
|
|
2509
|
+
// trustinspector rootfs producer (G18).
|
|
2510
|
+
const locationRoot = options.allLayersExplodedDir;
|
|
2511
|
+
const relativeLocation = (file) =>
|
|
2512
|
+
containerRelativePath(file, locationRoot) || file;
|
|
2513
|
+
// Bom-refs never carry host paths: outside rootfs mode, key and keystore
|
|
2514
|
+
// refs are relative to the scanned path.
|
|
2515
|
+
const refLocation = (file) =>
|
|
2516
|
+
containerRelativePath(file, locationRoot || path) || file;
|
|
2517
|
+
const certFiles = [
|
|
2518
|
+
...new Set([
|
|
2519
|
+
...getAllFiles(
|
|
2520
|
+
path,
|
|
2521
|
+
`${
|
|
2522
|
+
options.multiProject ? "**/" : ""
|
|
2523
|
+
}*.{p12,jks,jceks,bks,keystore,key,pem,cer,crt,der,p7b,p7c,pfx,truststore,gpg,pub}`,
|
|
2524
|
+
options,
|
|
2525
|
+
),
|
|
2526
|
+
// Java truststores carry no extension; match them by exact name.
|
|
2527
|
+
...getAllFiles(
|
|
2528
|
+
path,
|
|
2529
|
+
`${options.multiProject ? "**/" : ""}{cacerts,jssecacerts}`,
|
|
2530
|
+
options,
|
|
2531
|
+
),
|
|
2532
|
+
]),
|
|
2533
|
+
];
|
|
2534
|
+
// Newlines would corrupt the plugin's stdin file list, and oversized
|
|
2535
|
+
// files are capped in the fallback; the plugin list gets the same caps.
|
|
2536
|
+
const pluginCandidateFiles = certFiles.filter(
|
|
2537
|
+
(file) =>
|
|
2538
|
+
!/[\r\n]/.test(file) &&
|
|
2539
|
+
statSync(file, { throwIfNoEntry: false })?.size <= MAX_CRYPTO_FILE_BYTES,
|
|
2160
2540
|
);
|
|
2161
|
-
const
|
|
2162
|
-
|
|
2541
|
+
const pluginMaterials = runTrustInspectorCertsMode(
|
|
2542
|
+
pluginCandidateFiles,
|
|
2543
|
+
locationRoot,
|
|
2163
2544
|
);
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
const
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2545
|
+
const fallbackRecords = [];
|
|
2546
|
+
if (pluginMaterials) {
|
|
2547
|
+
collector.addTrustInspectorMaterials(pluginMaterials);
|
|
2548
|
+
for (const material of pluginMaterials) {
|
|
2549
|
+
if (typeof material?.path !== "string" || !material.path) {
|
|
2550
|
+
continue;
|
|
2551
|
+
}
|
|
2552
|
+
if (material.kind === "keystore") {
|
|
2553
|
+
pkgList.push(
|
|
2554
|
+
sourceKeystoreComponent(material, refLocation(material.path)),
|
|
2555
|
+
);
|
|
2556
|
+
} else if (material.kind === "private-key") {
|
|
2557
|
+
pkgList.push(
|
|
2558
|
+
sourcePrivateKeyComponent(material, refLocation(material.path)),
|
|
2559
|
+
);
|
|
2560
|
+
}
|
|
2561
|
+
}
|
|
2562
|
+
}
|
|
2563
|
+
// Files the plugin was not given (names with line breaks, oversized
|
|
2564
|
+
// files) are still read here, so no name can hide a file from the
|
|
2565
|
+
// inventory; without a plugin every file is. The files it was given are
|
|
2566
|
+
// its own, except for the keys it builds nothing from: it reports nothing
|
|
2567
|
+
// for a bare PEM public key, and an OpenPGP key only as public-key
|
|
2568
|
+
// material, so both are built here whether it saw them or not.
|
|
2569
|
+
const pluginFiles = new Set(pluginMaterials ? pluginCandidateFiles : []);
|
|
2570
|
+
for (const f of certFiles) {
|
|
2571
|
+
let bytes;
|
|
2572
|
+
try {
|
|
2573
|
+
const stats = statSync(f);
|
|
2574
|
+
if (stats.size > MAX_CRYPTO_FILE_BYTES) {
|
|
2575
|
+
continue;
|
|
2576
|
+
}
|
|
2577
|
+
bytes = readFileSync(f);
|
|
2578
|
+
} catch {
|
|
2579
|
+
continue;
|
|
2580
|
+
}
|
|
2581
|
+
const location = relativeLocation(f);
|
|
2582
|
+
const classification = classifyCryptoFile(bytes);
|
|
2583
|
+
if (
|
|
2584
|
+
pluginFiles.has(f) &&
|
|
2585
|
+
classification.kind !== "gpg" &&
|
|
2586
|
+
classification.kind !== "public-key"
|
|
2587
|
+
) {
|
|
2588
|
+
continue;
|
|
2589
|
+
}
|
|
2590
|
+
if (classification.kind === "certificates") {
|
|
2591
|
+
for (const der of classification.ders) {
|
|
2592
|
+
const record = certRecordFromDer(der, {
|
|
2593
|
+
location,
|
|
2594
|
+
sourceType: deriveCryptoSourceType(f),
|
|
2595
|
+
});
|
|
2596
|
+
if (record) {
|
|
2597
|
+
record.fileExtension = extname(f).replace(/^\./, "") || undefined;
|
|
2598
|
+
fallbackRecords.push(record);
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
} else if (classification.kind === "private-key") {
|
|
2602
|
+
pkgList.push(
|
|
2603
|
+
sourcePrivateKeyComponent(
|
|
2175
2604
|
{
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
value: f,
|
|
2605
|
+
format: classification.format,
|
|
2606
|
+
sourceType: deriveCryptoSourceType(f),
|
|
2179
2607
|
},
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2608
|
+
refLocation(f),
|
|
2609
|
+
),
|
|
2610
|
+
);
|
|
2611
|
+
} else if (classification.kind === "gpg") {
|
|
2612
|
+
pkgList.push(await sourceGpgComponent(f, location, options));
|
|
2613
|
+
} else if (classification.kind === "public-key") {
|
|
2614
|
+
pkgList.push(await sourcePemPublicKeyComponent(f, location, options));
|
|
2615
|
+
} else if (classification.kind === "keystore") {
|
|
2616
|
+
pkgList.push(
|
|
2617
|
+
sourceKeystoreComponent(
|
|
2618
|
+
{
|
|
2619
|
+
format: classification.format,
|
|
2620
|
+
sourceType: deriveCryptoSourceType(f),
|
|
2621
|
+
},
|
|
2622
|
+
refLocation(f),
|
|
2623
|
+
),
|
|
2624
|
+
);
|
|
2187
2625
|
}
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2626
|
+
}
|
|
2627
|
+
// Kubernetes TLS secrets and Android SPKI pins are cdxgen-side producers:
|
|
2628
|
+
// the plugin has no YAML or Android-config parser.
|
|
2629
|
+
const yamlFiles = getAllFiles(
|
|
2630
|
+
path,
|
|
2631
|
+
`${options.multiProject ? "**/" : ""}*.{yaml,yml}`,
|
|
2632
|
+
options,
|
|
2633
|
+
);
|
|
2634
|
+
for (const yamlFile of yamlFiles) {
|
|
2635
|
+
let yamlText;
|
|
2636
|
+
try {
|
|
2637
|
+
const stats = statSync(yamlFile);
|
|
2638
|
+
if (stats.size > MAX_CRYPTO_FILE_BYTES) {
|
|
2639
|
+
continue;
|
|
2640
|
+
}
|
|
2641
|
+
yamlText = readFileSync(yamlFile, "utf-8");
|
|
2642
|
+
} catch {
|
|
2643
|
+
continue;
|
|
2644
|
+
}
|
|
2645
|
+
// Cheap pre-filter: the Secret type string must be present before the
|
|
2646
|
+
// YAML parser runs.
|
|
2647
|
+
if (!yamlText.includes("kubernetes.io/tls")) {
|
|
2648
|
+
continue;
|
|
2649
|
+
}
|
|
2650
|
+
for (const record of kubernetesTlsSecretRecords(yamlText, {
|
|
2651
|
+
file: relativeLocation(yamlFile),
|
|
2652
|
+
})) {
|
|
2653
|
+
fallbackRecords.push(record);
|
|
2654
|
+
}
|
|
2655
|
+
}
|
|
2656
|
+
const androidXmlFiles = getAllFiles(path, "**/res/xml/*.xml", options);
|
|
2657
|
+
for (const xmlFile of androidXmlFiles) {
|
|
2658
|
+
let xmlText;
|
|
2659
|
+
try {
|
|
2660
|
+
const stats = statSync(xmlFile, { throwIfNoEntry: false });
|
|
2661
|
+
if (!stats || stats.size > MAX_CRYPTO_FILE_BYTES) {
|
|
2662
|
+
continue;
|
|
2663
|
+
}
|
|
2664
|
+
xmlText = readFileSync(xmlFile, "utf-8");
|
|
2665
|
+
} catch {
|
|
2666
|
+
continue;
|
|
2667
|
+
}
|
|
2668
|
+
if (!xmlText.includes("<network-security-config")) {
|
|
2669
|
+
continue;
|
|
2670
|
+
}
|
|
2671
|
+
for (const pin of androidNetworkSecurityConfigPins(xmlText, {
|
|
2672
|
+
file: relativeLocation(xmlFile),
|
|
2673
|
+
})) {
|
|
2674
|
+
collector.addPin(pin);
|
|
2675
|
+
}
|
|
2676
|
+
}
|
|
2677
|
+
if (fallbackRecords.length) {
|
|
2678
|
+
linkFallbackIssuers(fallbackRecords);
|
|
2679
|
+
collector.addRecords(fallbackRecords);
|
|
2680
|
+
}
|
|
2681
|
+
const {
|
|
2682
|
+
components: certificateComponents,
|
|
2683
|
+
dependencies: certificateDependencies,
|
|
2684
|
+
} = collector.build();
|
|
2685
|
+
if (pluginMaterials) {
|
|
2686
|
+
// The plugin reports the certificates a readable keystore holds, not the
|
|
2687
|
+
// keystore itself; give it the same component the fallback emits and
|
|
2688
|
+
// link it to what it holds.
|
|
2689
|
+
const keystoreMaterials = new Map();
|
|
2690
|
+
for (const material of pluginMaterials) {
|
|
2691
|
+
if (material?.kind === "certificate" && material.path) {
|
|
2692
|
+
keystoreMaterials.set(material.path, material);
|
|
2693
|
+
}
|
|
2694
|
+
}
|
|
2695
|
+
for (const file of pluginCandidateFiles) {
|
|
2696
|
+
const location = relativeLocation(file);
|
|
2697
|
+
const material = keystoreMaterials.get(location);
|
|
2698
|
+
if (!material) {
|
|
2699
|
+
continue;
|
|
2700
|
+
}
|
|
2701
|
+
let classification;
|
|
2702
|
+
try {
|
|
2703
|
+
classification = classifyCryptoFile(readFileSync(file));
|
|
2704
|
+
} catch {
|
|
2705
|
+
continue;
|
|
2706
|
+
}
|
|
2707
|
+
if (classification.kind !== "keystore") {
|
|
2708
|
+
continue;
|
|
2709
|
+
}
|
|
2710
|
+
const keystore = sourceKeystoreComponent(
|
|
2711
|
+
{
|
|
2712
|
+
format: classification.format,
|
|
2713
|
+
sourceType: material.sourceType || deriveCryptoSourceType(file),
|
|
2714
|
+
readable: true,
|
|
2198
2715
|
},
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2716
|
+
refLocation(file),
|
|
2717
|
+
);
|
|
2718
|
+
pkgList.push(keystore);
|
|
2719
|
+
const held = certificateComponents
|
|
2720
|
+
.filter(
|
|
2721
|
+
(component) =>
|
|
2722
|
+
component.cryptoProperties?.assetType === "certificate" &&
|
|
2723
|
+
component.evidence?.occurrences?.some(
|
|
2724
|
+
(occurrence) => occurrence.location === location,
|
|
2725
|
+
),
|
|
2726
|
+
)
|
|
2727
|
+
.map((component) => component["bom-ref"])
|
|
2728
|
+
.sort();
|
|
2729
|
+
if (held.length) {
|
|
2730
|
+
certificateDependencies.push({
|
|
2731
|
+
ref: keystore["bom-ref"],
|
|
2732
|
+
dependsOn: held,
|
|
2733
|
+
});
|
|
2734
|
+
}
|
|
2735
|
+
}
|
|
2204
2736
|
}
|
|
2737
|
+
pkgList.push(...certificateComponents);
|
|
2205
2738
|
const sourceCryptoComponents = await collectSourceCryptoComponents(
|
|
2206
2739
|
path,
|
|
2207
2740
|
options,
|
|
@@ -2222,9 +2755,229 @@ export async function createCryptoCertsBom(path, options) {
|
|
|
2222
2755
|
bomJson: {
|
|
2223
2756
|
components: filterInvalidCryptoComponents(pkgList),
|
|
2224
2757
|
},
|
|
2758
|
+
dependencies: certificateDependencies,
|
|
2759
|
+
};
|
|
2760
|
+
}
|
|
2761
|
+
|
|
2762
|
+
// Files larger than this are not classified: no real certificate bundle or
|
|
2763
|
+
// keystore comes close, and the cap bounds hostile inputs.
|
|
2764
|
+
const MAX_CRYPTO_FILE_BYTES = 16 * 1024 * 1024;
|
|
2765
|
+
|
|
2766
|
+
function deriveCryptoSourceType(filePath) {
|
|
2767
|
+
const normalized = String(filePath).replace(/\\/g, "/");
|
|
2768
|
+
if (
|
|
2769
|
+
normalized.includes("/etc/ssl/certs") ||
|
|
2770
|
+
normalized.includes("ca-certificates")
|
|
2771
|
+
) {
|
|
2772
|
+
return "ca-store";
|
|
2773
|
+
}
|
|
2774
|
+
if (normalized.includes("certifi") || normalized.includes("botocore")) {
|
|
2775
|
+
return "language-bundle";
|
|
2776
|
+
}
|
|
2777
|
+
if (
|
|
2778
|
+
/\/etc\/(nginx|apache2|httpd|haproxy|letsencrypt|kubernetes|docker|containerd)\//.test(
|
|
2779
|
+
normalized,
|
|
2780
|
+
)
|
|
2781
|
+
) {
|
|
2782
|
+
return "server-config";
|
|
2783
|
+
}
|
|
2784
|
+
if (
|
|
2785
|
+
normalized.includes("cacerts") ||
|
|
2786
|
+
/\.(jks|jceks|p12|pfx|truststore)$/i.test(normalized)
|
|
2787
|
+
) {
|
|
2788
|
+
return "java-truststore";
|
|
2789
|
+
}
|
|
2790
|
+
return "generic";
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2793
|
+
function sourceCryptoFileEvidence(location, options) {
|
|
2794
|
+
if (!options.evidence) {
|
|
2795
|
+
return undefined;
|
|
2796
|
+
}
|
|
2797
|
+
const identityEvidence = {
|
|
2798
|
+
field: "name",
|
|
2799
|
+
confidence: 1,
|
|
2800
|
+
concludedValue: basename(location),
|
|
2801
|
+
methods: [
|
|
2802
|
+
{
|
|
2803
|
+
technique: "filename",
|
|
2804
|
+
confidence: 1,
|
|
2805
|
+
value: location,
|
|
2806
|
+
},
|
|
2807
|
+
],
|
|
2808
|
+
};
|
|
2809
|
+
return {
|
|
2810
|
+
identity:
|
|
2811
|
+
options.specVersion >= 1.6 ? [identityEvidence] : identityEvidence,
|
|
2812
|
+
occurrences: [{ location }],
|
|
2813
|
+
};
|
|
2814
|
+
}
|
|
2815
|
+
|
|
2816
|
+
async function sourceGpgComponent(srcFile, location, options) {
|
|
2817
|
+
const fileHash = await checksumFile("sha256", srcFile);
|
|
2818
|
+
return {
|
|
2819
|
+
name: basename(location),
|
|
2820
|
+
type: "cryptographic-asset",
|
|
2821
|
+
version: fileHash,
|
|
2822
|
+
"bom-ref": `crypto/related-crypto-material/public-key/${encodeURIComponent(basename(location))}@sha256:${fileHash}`,
|
|
2823
|
+
cryptoProperties: {
|
|
2824
|
+
assetType: "related-crypto-material",
|
|
2825
|
+
relatedCryptoMaterialProperties: {
|
|
2826
|
+
type: "public-key",
|
|
2827
|
+
format: "OpenPGP",
|
|
2828
|
+
},
|
|
2829
|
+
},
|
|
2830
|
+
...(sourceCryptoFileEvidence(location, options)
|
|
2831
|
+
? { evidence: sourceCryptoFileEvidence(location, options) }
|
|
2832
|
+
: {}),
|
|
2833
|
+
properties: [
|
|
2834
|
+
{ name: "internal:SrcFile", value: location },
|
|
2835
|
+
{ name: "cdx:crypto:sourceType", value: "generic" },
|
|
2836
|
+
],
|
|
2837
|
+
};
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
async function sourcePemPublicKeyComponent(srcFile, location, options) {
|
|
2841
|
+
const fileHash = await checksumFile("sha256", srcFile);
|
|
2842
|
+
return {
|
|
2843
|
+
name: basename(location),
|
|
2844
|
+
type: "cryptographic-asset",
|
|
2845
|
+
version: fileHash,
|
|
2846
|
+
"bom-ref": `crypto/related-crypto-material/public-key/${encodeURIComponent(basename(location))}@sha256:${fileHash}`,
|
|
2847
|
+
cryptoProperties: {
|
|
2848
|
+
assetType: "related-crypto-material",
|
|
2849
|
+
relatedCryptoMaterialProperties: {
|
|
2850
|
+
type: "public-key",
|
|
2851
|
+
format: "SPKI",
|
|
2852
|
+
},
|
|
2853
|
+
},
|
|
2854
|
+
...(sourceCryptoFileEvidence(location, options)
|
|
2855
|
+
? { evidence: sourceCryptoFileEvidence(location, options) }
|
|
2856
|
+
: {}),
|
|
2857
|
+
properties: [
|
|
2858
|
+
{ name: "internal:SrcFile", value: location },
|
|
2859
|
+
{ name: "cdx:crypto:sourceType", value: "generic" },
|
|
2860
|
+
],
|
|
2861
|
+
};
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2864
|
+
function sourcePrivateKeyComponent(material, location) {
|
|
2865
|
+
return {
|
|
2866
|
+
name: basename(location),
|
|
2867
|
+
type: "cryptographic-asset",
|
|
2868
|
+
"bom-ref": `crypto/related-crypto-material/private-key/${encodeURIComponent(location)}`,
|
|
2869
|
+
cryptoProperties: {
|
|
2870
|
+
assetType: "related-crypto-material",
|
|
2871
|
+
relatedCryptoMaterialProperties: {
|
|
2872
|
+
type: "private-key",
|
|
2873
|
+
...(material?.format ? { format: material.format } : {}),
|
|
2874
|
+
},
|
|
2875
|
+
},
|
|
2876
|
+
evidence: { occurrences: [{ location }] },
|
|
2877
|
+
properties: [
|
|
2878
|
+
{ name: "internal:SrcFile", value: location },
|
|
2879
|
+
...(material?.sourceType
|
|
2880
|
+
? [{ name: "cdx:crypto:sourceType", value: material.sourceType }]
|
|
2881
|
+
: []),
|
|
2882
|
+
],
|
|
2883
|
+
};
|
|
2884
|
+
}
|
|
2885
|
+
|
|
2886
|
+
function sourceKeystoreComponent(material, location) {
|
|
2887
|
+
const readable = (material?.properties || []).find(
|
|
2888
|
+
(property) => property.name === "cdx:crypto:keystoreReadable",
|
|
2889
|
+
)?.value;
|
|
2890
|
+
return {
|
|
2891
|
+
name: basename(location),
|
|
2892
|
+
type: "cryptographic-asset",
|
|
2893
|
+
"bom-ref": `crypto/related-crypto-material/keystore/${encodeURIComponent(location)}`,
|
|
2894
|
+
cryptoProperties: {
|
|
2895
|
+
assetType: "related-crypto-material",
|
|
2896
|
+
relatedCryptoMaterialProperties: {
|
|
2897
|
+
type: "other",
|
|
2898
|
+
...(material?.format ? { format: material.format } : {}),
|
|
2899
|
+
},
|
|
2900
|
+
},
|
|
2901
|
+
evidence: { occurrences: [{ location }] },
|
|
2902
|
+
properties: [
|
|
2903
|
+
{ name: "internal:SrcFile", value: location },
|
|
2904
|
+
...(material?.sourceType
|
|
2905
|
+
? [{ name: "cdx:crypto:sourceType", value: material.sourceType }]
|
|
2906
|
+
: []),
|
|
2907
|
+
// A keystore the plugin read has no keystoreReadable property.
|
|
2908
|
+
...(material?.readable
|
|
2909
|
+
? []
|
|
2910
|
+
: [
|
|
2911
|
+
{ name: "cdx:crypto:keystoreReadable", value: readable || "false" },
|
|
2912
|
+
]),
|
|
2913
|
+
],
|
|
2225
2914
|
};
|
|
2226
2915
|
}
|
|
2227
2916
|
|
|
2917
|
+
/**
|
|
2918
|
+
* Parse the candidate certificate files with the trustinspector `certs`
|
|
2919
|
+
* mode. Only a report-2 envelope is accepted: report-1 output has no
|
|
2920
|
+
* certificate identity to build the shared model from, and the Node
|
|
2921
|
+
* fallback covers that case.
|
|
2922
|
+
*
|
|
2923
|
+
* @param {string[]} files candidate files
|
|
2924
|
+
* @param {string} root paths are reported relative to this directory
|
|
2925
|
+
* @returns {object[]|undefined} materials, or undefined when the plugin is
|
|
2926
|
+
* absent, blocked or not report-2
|
|
2927
|
+
*/
|
|
2928
|
+
function runTrustInspectorCertsMode(files, root) {
|
|
2929
|
+
if (!files?.length) {
|
|
2930
|
+
return undefined;
|
|
2931
|
+
}
|
|
2932
|
+
if (isDryRun) {
|
|
2933
|
+
recordActivity({
|
|
2934
|
+
kind: "trustinspector",
|
|
2935
|
+
reason:
|
|
2936
|
+
"Dry run mode blocks trustinspector execution and reports the requested inspection instead.",
|
|
2937
|
+
status: "blocked",
|
|
2938
|
+
target: `certs --stdin (${files.length} files)`,
|
|
2939
|
+
});
|
|
2940
|
+
return undefined;
|
|
2941
|
+
}
|
|
2942
|
+
const trustInspectorBin = resolvePluginBinary("trustinspector");
|
|
2943
|
+
if (!trustInspectorBin) {
|
|
2944
|
+
return undefined;
|
|
2945
|
+
}
|
|
2946
|
+
// `--root` is passed only in rootfs/container mode, where locations must
|
|
2947
|
+
// come out relative to the extracted tree; a plain source scan keeps the
|
|
2948
|
+
// host paths every other producer records. The value reaches a spawned
|
|
2949
|
+
// command, so it gets the same rejection the rootfs path applies:
|
|
2950
|
+
// dangerous unicode or line breaks disable the plugin call.
|
|
2951
|
+
const args = ["certs", "--stdin"];
|
|
2952
|
+
const safeRoot =
|
|
2953
|
+
typeof root === "string" &&
|
|
2954
|
+
!hasDangerousUnicode(root) &&
|
|
2955
|
+
!/[\r\n]/.test(root)
|
|
2956
|
+
? root
|
|
2957
|
+
: undefined;
|
|
2958
|
+
if (safeRoot) {
|
|
2959
|
+
args.push("--root", safeRoot);
|
|
2960
|
+
}
|
|
2961
|
+
const result = safeSpawnSync(trustInspectorBin, args, {
|
|
2962
|
+
input: `${files.join("\n")}\n`,
|
|
2963
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
2964
|
+
// A hung plugin must not hang cdxgen; the Node fallback takes over.
|
|
2965
|
+
timeout: 10 * 60 * 1000,
|
|
2966
|
+
});
|
|
2967
|
+
if (result?.status !== 0 || result?.error || !result?.stdout) {
|
|
2968
|
+
return undefined;
|
|
2969
|
+
}
|
|
2970
|
+
try {
|
|
2971
|
+
const report = JSON.parse(result.stdout);
|
|
2972
|
+
if (report?.schema !== "trustinspector/report-2") {
|
|
2973
|
+
return undefined;
|
|
2974
|
+
}
|
|
2975
|
+
return report.materials || [];
|
|
2976
|
+
} catch {
|
|
2977
|
+
return undefined;
|
|
2978
|
+
}
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2228
2981
|
/**
|
|
2229
2982
|
* Function to create bom string for R projects using renv.
|
|
2230
2983
|
*
|
|
@@ -2342,34 +3095,41 @@ export function createPerlBom(path, options) {
|
|
|
2342
3095
|
/**
|
|
2343
3096
|
* Function to create bom string for Terraform and OpenTofu projects.
|
|
2344
3097
|
*
|
|
2345
|
-
* The
|
|
2346
|
-
*
|
|
3098
|
+
* The workspace assembly turns the provider lock file, the module manifest
|
|
3099
|
+
* `init` writes and the configuration files into module and provider
|
|
3100
|
+
* components with a dependency graph and offline licenses. When metadata
|
|
3101
|
+
* fetching is enabled, the public registries add licenses, repository URLs
|
|
3102
|
+
* and registry flags to components the offline pass could not resolve.
|
|
2347
3103
|
*
|
|
2348
3104
|
* @param {string} path to the project
|
|
2349
3105
|
* @param {Object} options Parse options from the cli
|
|
2350
|
-
* @returns {Object} BOM object
|
|
3106
|
+
* @returns {Promise<Object>} BOM object
|
|
2351
3107
|
*/
|
|
2352
|
-
export function createTerraformBom(path, options) {
|
|
2353
|
-
|
|
2354
|
-
const lockFiles = getAllFiles(
|
|
3108
|
+
export async function createTerraformBom(path, options) {
|
|
3109
|
+
const parentComponent = createDefaultParentComponent(
|
|
2355
3110
|
path,
|
|
2356
|
-
|
|
3111
|
+
"generic",
|
|
2357
3112
|
options,
|
|
2358
3113
|
);
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
}
|
|
3114
|
+
const { pkgList, dependencies, parentProperties, srcFiles } =
|
|
3115
|
+
parseTerraformWorkspace(path, options, parentComponent["bom-ref"]);
|
|
3116
|
+
// No module or provider found (for example, resources only, or a stray
|
|
3117
|
+
// .tf in a JS repo): emit nothing, including no root applications, so
|
|
3118
|
+
// multi-type scans do not gain an empty Terraform parent.
|
|
3119
|
+
if (!pkgList.some((p) => p.type === "library")) {
|
|
3120
|
+
return {};
|
|
2367
3121
|
}
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
3122
|
+
parentComponent.properties = [
|
|
3123
|
+
...(parentComponent.properties || []),
|
|
3124
|
+
...parentProperties,
|
|
3125
|
+
];
|
|
3126
|
+
if (pkgList.length && shouldFetchPackageMetadata()) {
|
|
3127
|
+
await getTerraformRegistryMetadata(pkgList);
|
|
2373
3128
|
}
|
|
2374
|
-
return {
|
|
3129
|
+
return buildBomNSData(options, pkgList, "terraform", {
|
|
3130
|
+
src: path,
|
|
3131
|
+
filename: srcFiles.join(", "),
|
|
3132
|
+
dependencies,
|
|
3133
|
+
parentComponent,
|
|
3134
|
+
});
|
|
2375
3135
|
}
|