@cdxgen/cdxgen 13.4.0 → 13.6.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 +9 -9
- package/bin/cdxgen-secure.js +15 -0
- package/bin/cdxgen.js +29 -5
- package/bin/evinse.js +8 -0
- package/bin/hbom.js +15 -7
- package/bin/repl.js +29 -0
- package/bin/validate.js +6 -0
- package/data/cbomosdb-queries.json +13 -1
- package/data/component-tags.json +8 -0
- package/data/license-aliases.json +24 -2
- package/data/queries-darwin.json +25 -1
- package/data/queries-win.json +19 -1
- package/data/queries.json +41 -0
- package/data/remediations.json +407 -1
- package/data/rules/ai-bom.yaml +1 -1
- package/data/rules/brotli-decoder.yaml +107 -0
- package/data/rules/cbom-security.yaml +235 -4
- 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/vscode-extensions.yaml +75 -0
- package/data/spdx-license-list.json +121 -7
- package/data/spdx-licenses.json +69 -0
- package/data/spdx.schema.json +845 -2
- package/lib/audit/index.js +14 -0
- package/lib/cli/bomAssembly.js +120 -6
- package/lib/cli/cliOptions.js +110 -3
- package/lib/cli/index.js +182 -6
- package/lib/cli/jsBom.js +153 -64
- package/lib/cli/jvmBom.js +363 -66
- package/lib/cli/managedBom.js +853 -87
- package/lib/cli/nativeBom.js +267 -33
- package/lib/core/boundedFile.js +344 -0
- package/lib/core/brotliProbe.js +182 -0
- package/lib/core/env.js +38 -8
- package/lib/core/fs.js +40 -19
- package/lib/core/httpClient.js +172 -46
- package/lib/core/lockPaths.js +38 -0
- package/lib/core/paths.js +24 -1
- package/lib/core/processTree.js +5 -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/toolRegistry.js +170 -0
- package/lib/ecosystems/cppEvidence.js +263 -106
- package/lib/ecosystems/cppIncludeResolver.js +16 -5
- package/lib/ecosystems/ecosystems.js +25 -10
- package/lib/ecosystems/gemHostProbe.js +301 -0
- package/lib/ecosystems/golem.js +550 -6
- package/lib/ecosystems/gradleutils.js +858 -244
- package/lib/ecosystems/parsers-bazel.js +107 -6
- package/lib/ecosystems/parsers-dotnet.js +117 -18
- package/lib/ecosystems/parsers-go.js +1090 -367
- package/lib/ecosystems/parsers-js.js +505 -22
- package/lib/ecosystems/parsers-jvm.js +259 -17
- package/lib/ecosystems/parsers-misc.js +438 -64
- package/lib/ecosystems/parsers-python.js +100 -12
- package/lib/ecosystems/pythonutils.js +128 -2
- package/lib/ecosystems/rubyutils.js +544 -34
- package/lib/ecosystems/rusi.js +10 -0
- package/lib/ecosystems/scalacliutils.js +4 -1
- package/lib/ecosystems/scalasem.js +798 -151
- package/lib/ecosystems/vendoredCode.js +237 -3
- package/lib/ecosystems/venvProbe.js +360 -0
- package/lib/evinser/evinser.js +472 -61
- package/lib/evinser/swiftsem.js +68 -5
- package/lib/helpers/agenticToolutils.js +413 -93
- package/lib/helpers/vsixutils.js +1574 -315
- package/lib/inventory/agentSkillSpec.js +17 -0
- package/lib/inventory/apkInstalledDb.js +243 -0
- package/lib/inventory/atomUtils.js +110 -0
- package/lib/inventory/auditCategories.js +3 -3
- package/lib/inventory/brotliDecoder.js +142 -0
- package/lib/inventory/buildToolRateLimit.js +1 -1
- package/lib/inventory/cacheDir.js +52 -9
- package/lib/inventory/cbomutils.js +56 -7
- package/lib/inventory/certificates.js +2371 -0
- package/lib/inventory/communityAiConfigParser.js +4 -9
- package/lib/inventory/deps.js +41 -4
- package/lib/inventory/display.js +200 -3
- package/lib/inventory/dosai.js +639 -63
- package/lib/inventory/dotnetRuntime.js +217 -0
- package/lib/inventory/dynamic.js +45 -3
- package/lib/inventory/envcontext.js +475 -18
- package/lib/inventory/evidenceUtils.js +447 -23
- package/lib/inventory/fetchBatch.js +1 -1
- package/lib/inventory/formulationParsers.js +32 -3
- package/lib/inventory/goBuildInfo.js +140 -0
- package/lib/inventory/gtfobins.js +13 -0
- package/lib/inventory/hbom.js +67 -7
- package/lib/inventory/hbomAnalysis.js +16 -0
- package/lib/inventory/hostRuntimeProbe.js +686 -0
- package/lib/inventory/hostTopology.js +21 -0
- package/lib/inventory/jvmLocalRepos.js +9 -2
- package/lib/inventory/jvmToolEnv.js +29 -0
- package/lib/inventory/licenseEnhancer.js +12 -0
- package/lib/inventory/mavenCentralMirror.js +534 -0
- package/lib/inventory/nodeBuildInfo.js +218 -0
- package/lib/inventory/nodeHostProbe.js +202 -0
- package/lib/inventory/osPackagePurl.js +244 -0
- package/lib/inventory/osPackageResolver.js +271 -87
- package/lib/inventory/osinfo.js +79 -31
- package/lib/inventory/osqueryTransform.js +114 -5
- package/lib/inventory/phpBuildInfo.js +438 -0
- package/lib/inventory/phpHostProbe.js +185 -0
- package/lib/inventory/rootfsCerts.js +426 -0
- package/lib/inventory/rubyBuildInfo.js +972 -0
- package/lib/inventory/rubyHostProbe.js +218 -0
- package/lib/inventory/scalaCoords.js +4 -3
- package/lib/inventory/scalaDedupe.js +440 -0
- package/lib/inventory/source.js +256 -5
- package/lib/inventory/spdx.js +144 -0
- package/lib/inventory/traceRunner.js +5 -1
- package/lib/inventory/tracedCrypto.js +129 -0
- package/lib/managers/binary.js +866 -186
- package/lib/managers/docker.js +245 -12
- package/lib/parsers/email.js +277 -0
- package/lib/parsers/largeJson.js +62 -13
- package/lib/parsers/vscodeExtensionId.js +183 -0
- package/lib/server/server.js +198 -2
- package/lib/stages/postgen/annotator.js +17 -1
- package/lib/stages/postgen/introspection/reflect.js +4 -1
- package/lib/stages/postgen/postgen.js +5 -5
- package/lib/stages/postgen/ruleEngine.js +71 -6
- package/lib/stages/postgen/specVersionCompat.js +30 -0
- package/lib/stages/pregen/envAudit.js +1 -0
- package/lib/stages/pregen/pregen.js +231 -62
- package/lib/validator/bomValidator.js +32 -6
- package/lib/validator/generated/runtime.mjs +13 -0
- package/lib/validator/generated/validate-1.6.mjs +27 -8
- package/lib/validator/generated/validate-1.7.mjs +32 -13
- package/lib/validator/generated/validate-2.0.mjs +31 -12
- package/lib/validator/index.js +1 -1
- package/package.json +19 -18
- 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 +22 -3
- 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 +1 -17
- package/types/lib/cli/jsBom.d.ts.map +1 -1
- package/types/lib/cli/jvmBom.d.ts +20 -0
- package/types/lib/cli/jvmBom.d.ts.map +1 -1
- package/types/lib/cli/managedBom.d.ts.map +1 -1
- package/types/lib/cli/nativeBom.d.ts.map +1 -1
- package/types/lib/core/boundedFile.d.ts +70 -0
- package/types/lib/core/boundedFile.d.ts.map +1 -0
- package/types/lib/core/brotliProbe.d.ts +37 -0
- package/types/lib/core/brotliProbe.d.ts.map +1 -0
- package/types/lib/core/env.d.ts +22 -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 +24 -0
- package/types/lib/core/httpClient.d.ts.map +1 -1
- package/types/lib/core/lockPaths.d.ts +19 -0
- package/types/lib/core/lockPaths.d.ts.map +1 -0
- package/types/lib/core/processTree.d.ts +2 -0
- package/types/lib/core/processTree.d.ts.map +1 -1
- 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/toolRegistry.d.ts +36 -0
- package/types/lib/core/toolRegistry.d.ts.map +1 -0
- package/types/lib/ecosystems/cppEvidence.d.ts +40 -13
- package/types/lib/ecosystems/cppEvidence.d.ts.map +1 -1
- package/types/lib/ecosystems/cppIncludeResolver.d.ts +2 -1
- package/types/lib/ecosystems/cppIncludeResolver.d.ts.map +1 -1
- package/types/lib/ecosystems/ecosystems.d.ts.map +1 -1
- package/types/lib/ecosystems/gemHostProbe.d.ts +28 -0
- package/types/lib/ecosystems/gemHostProbe.d.ts.map +1 -0
- package/types/lib/ecosystems/golem.d.ts +81 -1
- package/types/lib/ecosystems/golem.d.ts.map +1 -1
- package/types/lib/ecosystems/gradleutils.d.ts +57 -21
- package/types/lib/ecosystems/gradleutils.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-bazel.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-dotnet.d.ts +40 -4
- package/types/lib/ecosystems/parsers-dotnet.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-go.d.ts +85 -13
- package/types/lib/ecosystems/parsers-go.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-js.d.ts +17 -20
- package/types/lib/ecosystems/parsers-js.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-jvm.d.ts +28 -2
- package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-misc.d.ts +37 -58
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-python.d.ts +14 -3
- package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
- package/types/lib/ecosystems/pythonutils.d.ts +19 -1
- package/types/lib/ecosystems/pythonutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rubyutils.d.ts +144 -12
- package/types/lib/ecosystems/rubyutils.d.ts.map +1 -1
- package/types/lib/ecosystems/rusi.d.ts.map +1 -1
- package/types/lib/ecosystems/scalacliutils.d.ts.map +1 -1
- package/types/lib/ecosystems/scalasem.d.ts.map +1 -1
- package/types/lib/ecosystems/vendoredCode.d.ts +26 -0
- package/types/lib/ecosystems/vendoredCode.d.ts.map +1 -1
- package/types/lib/ecosystems/venvProbe.d.ts +40 -0
- package/types/lib/ecosystems/venvProbe.d.ts.map +1 -0
- package/types/lib/evinser/evinser.d.ts +88 -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/inventory/agentSkillSpec.d.ts +13 -0
- package/types/lib/inventory/agentSkillSpec.d.ts.map +1 -0
- 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 +33 -0
- package/types/lib/inventory/atomUtils.d.ts.map +1 -1
- package/types/lib/inventory/brotliDecoder.d.ts +58 -0
- package/types/lib/inventory/brotliDecoder.d.ts.map +1 -0
- package/types/lib/inventory/cacheDir.d.ts +12 -4
- package/types/lib/inventory/cacheDir.d.ts.map +1 -1
- package/types/lib/inventory/cbomutils.d.ts +17 -0
- package/types/lib/inventory/cbomutils.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/communityAiConfigParser.d.ts.map +1 -1
- package/types/lib/inventory/deps.d.ts +2 -1
- 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 +162 -7
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- package/types/lib/inventory/dotnetRuntime.d.ts +44 -0
- package/types/lib/inventory/dotnetRuntime.d.ts.map +1 -0
- package/types/lib/inventory/dynamic.d.ts +6 -1
- package/types/lib/inventory/dynamic.d.ts.map +1 -1
- package/types/lib/inventory/envcontext.d.ts +92 -8
- package/types/lib/inventory/envcontext.d.ts.map +1 -1
- package/types/lib/inventory/evidenceUtils.d.ts +29 -10
- package/types/lib/inventory/evidenceUtils.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/gtfobins.d.ts.map +1 -1
- 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/hostRuntimeProbe.d.ts +44 -0
- package/types/lib/inventory/hostRuntimeProbe.d.ts.map +1 -0
- package/types/lib/inventory/hostTopology.d.ts.map +1 -1
- package/types/lib/inventory/jvmToolEnv.d.ts +12 -0
- package/types/lib/inventory/jvmToolEnv.d.ts.map +1 -1
- package/types/lib/inventory/licenseEnhancer.d.ts.map +1 -1
- package/types/lib/inventory/mavenCentralMirror.d.ts +170 -0
- package/types/lib/inventory/mavenCentralMirror.d.ts.map +1 -0
- package/types/lib/inventory/nodeBuildInfo.d.ts +95 -0
- package/types/lib/inventory/nodeBuildInfo.d.ts.map +1 -0
- package/types/lib/inventory/nodeHostProbe.d.ts +15 -0
- package/types/lib/inventory/nodeHostProbe.d.ts.map +1 -0
- package/types/lib/inventory/osPackagePurl.d.ts +99 -0
- package/types/lib/inventory/osPackagePurl.d.ts.map +1 -0
- package/types/lib/inventory/osPackageResolver.d.ts +44 -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/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/rubyBuildInfo.d.ts +271 -0
- package/types/lib/inventory/rubyBuildInfo.d.ts.map +1 -0
- package/types/lib/inventory/rubyHostProbe.d.ts +47 -0
- package/types/lib/inventory/rubyHostProbe.d.ts.map +1 -0
- package/types/lib/inventory/source.d.ts +38 -0
- package/types/lib/inventory/source.d.ts.map +1 -1
- package/types/lib/inventory/spdx.d.ts +15 -0
- package/types/lib/inventory/spdx.d.ts.map +1 -1
- package/types/lib/inventory/traceRunner.d.ts +4 -1
- package/types/lib/inventory/traceRunner.d.ts.map +1 -1
- package/types/lib/inventory/tracedCrypto.d.ts +22 -0
- package/types/lib/inventory/tracedCrypto.d.ts.map +1 -1
- 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 +24 -0
- package/types/lib/managers/docker.d.ts.map +1 -1
- 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 +17 -3
- package/types/lib/parsers/largeJson.d.ts.map +1 -1
- 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/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/envAudit.d.ts.map +1 -1
- package/types/lib/stages/pregen/pregen.d.ts +26 -9
- package/types/lib/stages/pregen/pregen.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
package/lib/cli/managedBom.js
CHANGED
|
@@ -1,11 +1,25 @@
|
|
|
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
|
+
relative,
|
|
10
|
+
resolve,
|
|
11
|
+
} from "node:path";
|
|
4
12
|
import process from "node:process";
|
|
5
13
|
|
|
6
14
|
import { build } from "@cdxgen/cdx-purl";
|
|
7
15
|
|
|
8
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
DEBUG_MODE,
|
|
18
|
+
hasDangerousUnicode,
|
|
19
|
+
isDryRun,
|
|
20
|
+
readEnvironmentVariable,
|
|
21
|
+
recordActivity,
|
|
22
|
+
} from "../core/activity.js";
|
|
9
23
|
import { recordDegradation } from "../core/buildLedger.js";
|
|
10
24
|
import { deferFailOnError } from "../core/deferredExit.js";
|
|
11
25
|
import {
|
|
@@ -26,7 +40,10 @@ import {
|
|
|
26
40
|
safeWriteSync,
|
|
27
41
|
} from "../core/fs.js";
|
|
28
42
|
import { thoughtLog } from "../core/logger.js";
|
|
29
|
-
import {
|
|
43
|
+
import {
|
|
44
|
+
isProjectToolingAllowed,
|
|
45
|
+
reportProjectToolingSkipped,
|
|
46
|
+
} from "../core/projectTooling.js";
|
|
30
47
|
import {
|
|
31
48
|
extractJarArchive,
|
|
32
49
|
getNugetMetadata,
|
|
@@ -35,7 +52,9 @@ import {
|
|
|
35
52
|
} from "../ecosystems/ecosystems.js";
|
|
36
53
|
import {
|
|
37
54
|
agreedNugetVersions,
|
|
55
|
+
applyNugetVersion,
|
|
38
56
|
dotnetManifestProjectDir,
|
|
57
|
+
dotnetRestoredProjectFile,
|
|
39
58
|
getCentralPackageVersions,
|
|
40
59
|
indexPropsProperties,
|
|
41
60
|
parseCsPkgData,
|
|
@@ -83,11 +102,14 @@ import {
|
|
|
83
102
|
addRubyGemsDistributionUrls,
|
|
84
103
|
addRubyRuntimeComponent,
|
|
85
104
|
collectGemModuleNames,
|
|
105
|
+
collectProjectRubyDeclarations,
|
|
86
106
|
describeProjectGem,
|
|
87
107
|
enrichGemsFromLocalCache,
|
|
88
108
|
parseBundleConfig,
|
|
89
109
|
parseGemfileLockData,
|
|
90
110
|
parseGemspecData,
|
|
111
|
+
resolveProjectRuby,
|
|
112
|
+
toGemPurl,
|
|
91
113
|
} from "../ecosystems/rubyutils.js";
|
|
92
114
|
import { getTerraformRegistryMetadata } from "../ecosystems/terraformRegistry.js";
|
|
93
115
|
import {
|
|
@@ -100,6 +122,15 @@ import {
|
|
|
100
122
|
collectDosaiCryptoComponents,
|
|
101
123
|
collectSourceCryptoComponents,
|
|
102
124
|
} from "../inventory/cbomutils.js";
|
|
125
|
+
import {
|
|
126
|
+
androidNetworkSecurityConfigPins,
|
|
127
|
+
CertificateCollector,
|
|
128
|
+
certRecordFromDer,
|
|
129
|
+
classifyCryptoFile,
|
|
130
|
+
containerRelativePath,
|
|
131
|
+
kubernetesTlsSecretRecords,
|
|
132
|
+
linkFallbackIssuers,
|
|
133
|
+
} from "../inventory/certificates.js";
|
|
103
134
|
import { readZipEntry } from "../inventory/deps.js";
|
|
104
135
|
import {
|
|
105
136
|
filterInvalidCryptoComponents,
|
|
@@ -113,14 +144,23 @@ import {
|
|
|
113
144
|
collectDosaiServiceComponents,
|
|
114
145
|
collectDosaiServicesFromMethods,
|
|
115
146
|
createDosaiMethodsSlice,
|
|
116
|
-
|
|
147
|
+
dosaiFailureReported,
|
|
148
|
+
dosaiMethodsSectionsArgument,
|
|
149
|
+
dosaiReportFileOmittedSections,
|
|
150
|
+
dosaiToolComponents,
|
|
117
151
|
isDosaiDotnetLanguage,
|
|
118
152
|
normalizeDosaiServiceMap,
|
|
119
153
|
readDosaiMethodsReport,
|
|
154
|
+
runsEvidenceAnalysis,
|
|
120
155
|
} from "../inventory/dosai.js";
|
|
121
156
|
import { addEvidenceForDotnet } from "../inventory/evidenceUtils.js";
|
|
157
|
+
import { resolvePluginBinary } from "../inventory/plugins.js";
|
|
122
158
|
import { nugetPurl, pypiBomRef } from "../inventory/purl.js";
|
|
123
|
-
import {
|
|
159
|
+
import { isInsideInstalledGem } from "../inventory/rubyBuildInfo.js";
|
|
160
|
+
import {
|
|
161
|
+
classifyProbeResult,
|
|
162
|
+
parseGemfileLockBundlerVersion,
|
|
163
|
+
} from "../inventory/toolRequirements.js";
|
|
124
164
|
import { getPluginToolComponents } from "../managers/binary.js";
|
|
125
165
|
import { getTreeWithPlugin } from "../managers/piptree.js";
|
|
126
166
|
import {
|
|
@@ -225,11 +265,14 @@ export function createPixiBom(path, options) {
|
|
|
225
265
|
generatePixiLockFile(path);
|
|
226
266
|
|
|
227
267
|
const pixiLockFile = join(path, "pixi.lock");
|
|
228
|
-
if (!safeExistsSync(pixiLockFile)
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
268
|
+
if (!safeExistsSync(pixiLockFile)) {
|
|
269
|
+
if (DEBUG_MODE) {
|
|
270
|
+
console.log(
|
|
271
|
+
"Unexpected Error tried to generate pixi.lock file but failed.",
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
// Let createPythonBOM() generate the BOM from the other manifests.
|
|
275
|
+
return null;
|
|
233
276
|
}
|
|
234
277
|
PixiLockData = parsePixiLockFile(pixiLockFile);
|
|
235
278
|
metadataFilename = "pixi.lock";
|
|
@@ -494,11 +537,14 @@ export async function createPythonBom(path, options) {
|
|
|
494
537
|
retMap.dependenciesList,
|
|
495
538
|
parentComponent,
|
|
496
539
|
);
|
|
497
|
-
} else {
|
|
540
|
+
} else if (isProjectToolingAllowed(options)) {
|
|
541
|
+
// poetry export and pdm export load plugins from the project tree
|
|
542
|
+
// (.poetry/plugins, .pdm-plugins), so they run only when the
|
|
543
|
+
// project's own tooling may.
|
|
498
544
|
let exportedReqs = "";
|
|
499
545
|
if (f.endsWith("poetry.lock")) {
|
|
500
546
|
thoughtLog(
|
|
501
|
-
"Using poetry export
|
|
547
|
+
"Using poetry export to collect the resolved closure without installing anything.",
|
|
502
548
|
);
|
|
503
549
|
const expCmd = safeSpawnSync(
|
|
504
550
|
"poetry",
|
|
@@ -509,7 +555,7 @@ export async function createPythonBom(path, options) {
|
|
|
509
555
|
exportedReqs = expCmd.stdout.toString();
|
|
510
556
|
} else if (f.endsWith("pdm.lock")) {
|
|
511
557
|
thoughtLog(
|
|
512
|
-
"Using pdm export
|
|
558
|
+
"Using pdm export to collect the resolved closure without installing anything.",
|
|
513
559
|
);
|
|
514
560
|
const expCmd = safeSpawnSync(
|
|
515
561
|
"pdm",
|
|
@@ -539,6 +585,19 @@ export async function createPythonBom(path, options) {
|
|
|
539
585
|
pkgList = pkgList.concat(dlist);
|
|
540
586
|
}
|
|
541
587
|
}
|
|
588
|
+
} else {
|
|
589
|
+
reportProjectToolingSkipped(
|
|
590
|
+
options,
|
|
591
|
+
"python.poetry-export.tooling-disabled",
|
|
592
|
+
"Skipping 'poetry export' and 'pdm export' since dependency installation is disabled. Only the packages parsed from the lock file will be used.",
|
|
593
|
+
{
|
|
594
|
+
ecosystem: "python",
|
|
595
|
+
tool: f.endsWith("pdm.lock") ? "pdm" : "poetry",
|
|
596
|
+
impact: "transitive-deps",
|
|
597
|
+
detail:
|
|
598
|
+
"Dependency installation is disabled, so poetry or pdm export was not run and only the packages parsed from the lock file are available.",
|
|
599
|
+
},
|
|
600
|
+
);
|
|
542
601
|
}
|
|
543
602
|
}
|
|
544
603
|
}
|
|
@@ -879,9 +938,11 @@ export async function createPythonBom(path, options) {
|
|
|
879
938
|
);
|
|
880
939
|
}
|
|
881
940
|
emitAiInventorySummary(aiInventory, path);
|
|
882
|
-
// Check and complete the dependency tree
|
|
941
|
+
// Check and complete the dependency tree. This installs every package into a
|
|
942
|
+
// virtual environment, so --no-install-deps turns it off too.
|
|
883
943
|
if (
|
|
884
944
|
isFeatureEnabled(options, "safe-pip-install") &&
|
|
945
|
+
options.installDeps !== false &&
|
|
885
946
|
pkgList.length &&
|
|
886
947
|
isPartialTree(dependencies, pkgList.length)
|
|
887
948
|
) {
|
|
@@ -1108,6 +1169,28 @@ export function createHelmBom(path, options) {
|
|
|
1108
1169
|
return {};
|
|
1109
1170
|
}
|
|
1110
1171
|
|
|
1172
|
+
/**
|
|
1173
|
+
* The properties of a list followed by those of another it does not hold yet.
|
|
1174
|
+
* It returns a new array, so a shallow copy of a component keeps its own
|
|
1175
|
+
* properties.
|
|
1176
|
+
*
|
|
1177
|
+
* @param {{name: string, value: string}[]|undefined} existing Properties held so far
|
|
1178
|
+
* @param {{name: string, value: string}[]} additions Properties to add
|
|
1179
|
+
* @returns {{name: string, value: string}[]} The merged properties
|
|
1180
|
+
*/
|
|
1181
|
+
function withMissingProperties(existing, additions) {
|
|
1182
|
+
const held = existing || [];
|
|
1183
|
+
return [
|
|
1184
|
+
...held,
|
|
1185
|
+
...additions.filter(
|
|
1186
|
+
(added) =>
|
|
1187
|
+
!held.some(
|
|
1188
|
+
(known) => known.name === added.name && known.value === added.value,
|
|
1189
|
+
),
|
|
1190
|
+
),
|
|
1191
|
+
];
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1111
1194
|
/**
|
|
1112
1195
|
* Function to create bom string for php projects
|
|
1113
1196
|
*
|
|
@@ -1140,6 +1223,10 @@ export function createPHPBom(path, options) {
|
|
|
1140
1223
|
composerLockSearchOptions,
|
|
1141
1224
|
);
|
|
1142
1225
|
let pkgList = [];
|
|
1226
|
+
// Platform facts of the scanned directory's lock when its composer.json names
|
|
1227
|
+
// no project: there is no module component to carry them, so they go on the
|
|
1228
|
+
// document. A nested lock like that has nowhere to put them.
|
|
1229
|
+
let metadataProperties = [];
|
|
1143
1230
|
const composerJsonMode = composerJsonFiles.length;
|
|
1144
1231
|
const composerLockMode = composerLockFiles.length;
|
|
1145
1232
|
// Create a composer.lock file for each composer.json file if needed.
|
|
@@ -1253,6 +1340,37 @@ export function createPHPBom(path, options) {
|
|
|
1253
1340
|
}
|
|
1254
1341
|
}
|
|
1255
1342
|
const retMap = parseComposerLock(f, rootRequires);
|
|
1343
|
+
if (retMap.platformProperties?.length) {
|
|
1344
|
+
if (moduleParent?.["bom-ref"]) {
|
|
1345
|
+
// The root composer.json is parsed twice, once for the parent
|
|
1346
|
+
// component and once per lock, so the object that ends up as
|
|
1347
|
+
// metadata.component is not always the module parsed here.
|
|
1348
|
+
for (const module of [moduleParent, parentComponent]) {
|
|
1349
|
+
if (module?.["bom-ref"] === moduleParent["bom-ref"]) {
|
|
1350
|
+
module.properties = withMissingProperties(
|
|
1351
|
+
module.properties,
|
|
1352
|
+
retMap.platformProperties,
|
|
1353
|
+
);
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1356
|
+
} else if (resolve(basePath) === resolve(path)) {
|
|
1357
|
+
// The document describes the scanned directory, so only the lock
|
|
1358
|
+
// in it may put its facts on the document.
|
|
1359
|
+
metadataProperties = withMissingProperties(
|
|
1360
|
+
metadataProperties,
|
|
1361
|
+
retMap.platformProperties,
|
|
1362
|
+
);
|
|
1363
|
+
} else {
|
|
1364
|
+
recordDegradation("php.composer.platform-unattributed", {
|
|
1365
|
+
ecosystem: "php",
|
|
1366
|
+
tool: "composer",
|
|
1367
|
+
impact: "versions",
|
|
1368
|
+
path: f,
|
|
1369
|
+
detail:
|
|
1370
|
+
"The composer.json beside this lock names no project, so no component can carry the PHP and extension requirements of the lock.",
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1256
1374
|
if (retMap.pkgList?.length) {
|
|
1257
1375
|
pkgList = pkgList.concat(retMap.pkgList);
|
|
1258
1376
|
pkgList = trimComponents(pkgList);
|
|
@@ -1292,6 +1410,7 @@ export function createPHPBom(path, options) {
|
|
|
1292
1410
|
filename: composerLockFiles.join(", "),
|
|
1293
1411
|
dependencies,
|
|
1294
1412
|
parentComponent,
|
|
1413
|
+
metadataProperties,
|
|
1295
1414
|
});
|
|
1296
1415
|
}
|
|
1297
1416
|
if (composerJsonMode && !composerLockMode) {
|
|
@@ -1318,7 +1437,14 @@ export async function createRubyBom(path, options) {
|
|
|
1318
1437
|
if (typeof options.includeNodeModulesDir === "undefined" || options.deep) {
|
|
1319
1438
|
options.includeNodeModulesDir = true;
|
|
1320
1439
|
}
|
|
1321
|
-
|
|
1440
|
+
// RubyGems caches the specs it fetched from a source under ~/.cache/gem/specs
|
|
1441
|
+
// and ~/.gem/specs, Marshal-dumped into files named <gem>-<version>.gemspec.
|
|
1442
|
+
// Those describe what a source offered, not what is installed.
|
|
1443
|
+
const excludeList = (options.exclude || []).concat([
|
|
1444
|
+
"**/vendor/cache/**",
|
|
1445
|
+
"**/.cache/gem/specs/**",
|
|
1446
|
+
"**/.gem/specs/**",
|
|
1447
|
+
]);
|
|
1322
1448
|
const gemLockExcludeList = (options.exclude || []).concat([
|
|
1323
1449
|
"**/vendor/bundle/ruby/**/Gemfile.lock",
|
|
1324
1450
|
"**/test/data/**/Gemfile*.lock",
|
|
@@ -1344,6 +1470,12 @@ export async function createRubyBom(path, options) {
|
|
|
1344
1470
|
exclude: gemLockExcludeList,
|
|
1345
1471
|
},
|
|
1346
1472
|
);
|
|
1473
|
+
// An installed gem's own directory can hold its development Gemfile.lock,
|
|
1474
|
+
// nested gemspecs and manifests (rbs ships steep/Gemfile.lock, typeprof a
|
|
1475
|
+
// VS Code extension). They describe how the gem is developed, not what is
|
|
1476
|
+
// installed, and would add gems that are not there.
|
|
1477
|
+
const isGemInternalFile = (f) =>
|
|
1478
|
+
isInsideInstalledGem(f, safeExistsSync, path);
|
|
1347
1479
|
let gemspecFiles = getAllFiles(
|
|
1348
1480
|
path,
|
|
1349
1481
|
`${options.multiProject ? "**/" : ""}*.gemspec`,
|
|
@@ -1351,7 +1483,7 @@ export async function createRubyBom(path, options) {
|
|
|
1351
1483
|
...options,
|
|
1352
1484
|
exclude: excludeList,
|
|
1353
1485
|
},
|
|
1354
|
-
);
|
|
1486
|
+
).filter((f) => !isGemInternalFile(f));
|
|
1355
1487
|
let gemHome =
|
|
1356
1488
|
readEnvironmentVariable("CDXGEN_GEM_HOME") ||
|
|
1357
1489
|
readEnvironmentVariable("GEM_HOME");
|
|
@@ -1432,7 +1564,9 @@ export async function createRubyBom(path, options) {
|
|
|
1432
1564
|
...options,
|
|
1433
1565
|
exclude: gemLockExcludeList,
|
|
1434
1566
|
},
|
|
1435
|
-
);
|
|
1567
|
+
).filter((f) => !isGemInternalFile(f));
|
|
1568
|
+
// The lockfiles each Bundler release resolved
|
|
1569
|
+
const bundlerVersions = new Map();
|
|
1436
1570
|
if (gemLockFiles.length) {
|
|
1437
1571
|
for (const f of gemLockFiles) {
|
|
1438
1572
|
if (DEBUG_MODE) {
|
|
@@ -1440,6 +1574,15 @@ export async function createRubyBom(path, options) {
|
|
|
1440
1574
|
}
|
|
1441
1575
|
const gemLockData = readFileSync(f, { encoding: "utf-8" });
|
|
1442
1576
|
const retMap = await parseGemfileLockData(gemLockData, f, { gemHome });
|
|
1577
|
+
// Bundler is not listed among the specs, but runs every bundle: the
|
|
1578
|
+
// lockfile names the release that resolved it under BUNDLED WITH.
|
|
1579
|
+
const bundlerVersion = parseGemfileLockBundlerVersion(gemLockData);
|
|
1580
|
+
if (bundlerVersion) {
|
|
1581
|
+
bundlerVersions.set(bundlerVersion, [
|
|
1582
|
+
...(bundlerVersions.get(bundlerVersion) || []),
|
|
1583
|
+
f,
|
|
1584
|
+
]);
|
|
1585
|
+
}
|
|
1443
1586
|
if (retMap.pkgList?.length) {
|
|
1444
1587
|
pkgList = pkgList.concat(retMap.pkgList);
|
|
1445
1588
|
pkgList = trimComponents(pkgList);
|
|
@@ -1522,6 +1665,42 @@ export async function createRubyBom(path, options) {
|
|
|
1522
1665
|
options,
|
|
1523
1666
|
));
|
|
1524
1667
|
}
|
|
1668
|
+
for (const [bundlerVersion, lockFiles] of [...bundlerVersions.entries()].sort(
|
|
1669
|
+
(a, b) => a[0].localeCompare(b[0]),
|
|
1670
|
+
)) {
|
|
1671
|
+
const purl = toGemPurl("bundler", bundlerVersion);
|
|
1672
|
+
const ref = decodeURIComponent(purl);
|
|
1673
|
+
if (pkgList.some((p) => p["bom-ref"] === ref)) {
|
|
1674
|
+
continue;
|
|
1675
|
+
}
|
|
1676
|
+
pkgList.push({
|
|
1677
|
+
name: "bundler",
|
|
1678
|
+
version: bundlerVersion,
|
|
1679
|
+
purl,
|
|
1680
|
+
"bom-ref": ref,
|
|
1681
|
+
scope: "required",
|
|
1682
|
+
properties: [
|
|
1683
|
+
{ name: "cdx:gem:bundledWith", value: "true" },
|
|
1684
|
+
{ name: "internal:SrcFile", value: lockFiles.join(", ") },
|
|
1685
|
+
],
|
|
1686
|
+
evidence: {
|
|
1687
|
+
identity: [
|
|
1688
|
+
{
|
|
1689
|
+
field: "purl",
|
|
1690
|
+
confidence: 0.8,
|
|
1691
|
+
methods: [
|
|
1692
|
+
{
|
|
1693
|
+
technique: "manifest-analysis",
|
|
1694
|
+
confidence: 0.8,
|
|
1695
|
+
value: lockFiles.join(", "),
|
|
1696
|
+
},
|
|
1697
|
+
],
|
|
1698
|
+
},
|
|
1699
|
+
],
|
|
1700
|
+
},
|
|
1701
|
+
});
|
|
1702
|
+
rootList.push(ref);
|
|
1703
|
+
}
|
|
1525
1704
|
if (rootList.length) {
|
|
1526
1705
|
dependencies = mergeDependencies(
|
|
1527
1706
|
dependencies,
|
|
@@ -1539,7 +1718,14 @@ export async function createRubyBom(path, options) {
|
|
|
1539
1718
|
options.resolveClass ||
|
|
1540
1719
|
(options.deep && !hasAnyProjectType(["oci"], options, false))
|
|
1541
1720
|
) {
|
|
1542
|
-
|
|
1721
|
+
// `bundle exec` evaluates the project's Gemfile, a Ruby program
|
|
1722
|
+
if (gemHome && !isGemHomeEmpty && !isProjectToolingAllowed(options)) {
|
|
1723
|
+
reportProjectToolingSkipped(
|
|
1724
|
+
options,
|
|
1725
|
+
null,
|
|
1726
|
+
"Skipping the module names of gems since dependency installation is disabled. Collecting them runs 'bundle exec', which evaluates the Gemfile.",
|
|
1727
|
+
);
|
|
1728
|
+
} else if (gemHome && !isGemHomeEmpty) {
|
|
1543
1729
|
const rubyCommand =
|
|
1544
1730
|
readEnvironmentVariable("CDXGEN_RUBY_CMD") ||
|
|
1545
1731
|
readEnvironmentVariable("RUBY_CMD") ||
|
|
@@ -1619,18 +1805,76 @@ export async function createRubyBom(path, options) {
|
|
|
1619
1805
|
pkgList = await enrichGemsFromLocalCache(pkgList, { gemHome });
|
|
1620
1806
|
// The download URL of a gem from the public registry needs no network call
|
|
1621
1807
|
pkgList = addRubyGemsDistributionUrls(pkgList);
|
|
1622
|
-
//
|
|
1623
|
-
//
|
|
1624
|
-
//
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
) {
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1808
|
+
// The project and its gems declare the Ruby they run on; surface it as the
|
|
1809
|
+
// runtime they depend on. A pinned Ruby (Gemfile.lock, .ruby-version,
|
|
1810
|
+
// .tool-versions, mise, the Gemfile) is a version with a CPE, which every
|
|
1811
|
+
// spec version can carry. A bare range needs CycloneDX 1.7's versionRange
|
|
1812
|
+
// and isExternal. A container image ships its own Ruby, which the image
|
|
1813
|
+
// scan reads from the interpreter's files.
|
|
1814
|
+
if (!hasAnyProjectType(["oci"], options, false)) {
|
|
1815
|
+
// Each project of a multi-project scan pins its own Ruby
|
|
1816
|
+
const rootDir = resolve(path);
|
|
1817
|
+
const projectDirs = [rootDir];
|
|
1818
|
+
if (options.multiProject) {
|
|
1819
|
+
for (const f of [...gemLockFiles, ...gemFiles]) {
|
|
1820
|
+
const dir = dirname(resolve(f));
|
|
1821
|
+
if (!projectDirs.includes(dir)) {
|
|
1822
|
+
projectDirs.push(dir);
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
const rubies = [];
|
|
1827
|
+
for (const dir of projectDirs) {
|
|
1828
|
+
const ruby = resolveProjectRuby(collectProjectRubyDeclarations(dir));
|
|
1829
|
+
if (!ruby) {
|
|
1830
|
+
continue;
|
|
1831
|
+
}
|
|
1832
|
+
if (dir !== rootDir && ruby.source) {
|
|
1833
|
+
ruby.source = relative(rootDir, join(dir, ruby.source)).replaceAll(
|
|
1834
|
+
"\\",
|
|
1835
|
+
"/",
|
|
1836
|
+
);
|
|
1837
|
+
}
|
|
1838
|
+
if (ruby.conflicts?.length) {
|
|
1839
|
+
console.log(
|
|
1840
|
+
`NOTE: The project in ${dir} declares more than one Ruby. The BOM records ${ruby.engine} ${ruby.version} from ${ruby.source}; also declared: ${ruby.conflicts.join(", ")}.`,
|
|
1841
|
+
);
|
|
1842
|
+
}
|
|
1843
|
+
rubies.push(ruby);
|
|
1844
|
+
}
|
|
1845
|
+
// One runtime per distinct pinned Ruby; the scanned directory's first
|
|
1846
|
+
const pinned = [];
|
|
1847
|
+
for (const ruby of rubies) {
|
|
1848
|
+
if (
|
|
1849
|
+
ruby.version &&
|
|
1850
|
+
!pinned.some(
|
|
1851
|
+
(other) =>
|
|
1852
|
+
other.engine === ruby.engine && other.version === ruby.version,
|
|
1853
|
+
)
|
|
1854
|
+
) {
|
|
1855
|
+
pinned.push(ruby);
|
|
1856
|
+
}
|
|
1857
|
+
}
|
|
1858
|
+
const [primary, ...others] = pinned.length ? pinned : rubies.slice(0, 1);
|
|
1859
|
+
if (options.specVersion >= 1.7 || primary?.version) {
|
|
1860
|
+
({ pkgList, dependencies } = addRubyRuntimeComponent(
|
|
1861
|
+
pkgList,
|
|
1862
|
+
parentComponent,
|
|
1863
|
+
dependencies,
|
|
1864
|
+
primary,
|
|
1865
|
+
));
|
|
1866
|
+
}
|
|
1867
|
+
// The gems were linked to the first; the others hang off the project
|
|
1868
|
+
for (const ruby of others) {
|
|
1869
|
+
let extra;
|
|
1870
|
+
({ pkgList: extra, dependencies } = addRubyRuntimeComponent(
|
|
1871
|
+
[],
|
|
1872
|
+
parentComponent,
|
|
1873
|
+
dependencies,
|
|
1874
|
+
ruby,
|
|
1875
|
+
));
|
|
1876
|
+
pkgList = pkgList.concat(extra);
|
|
1877
|
+
}
|
|
1634
1878
|
}
|
|
1635
1879
|
return buildBomNSData(options, pkgList, "gem", {
|
|
1636
1880
|
src: path,
|
|
@@ -1655,6 +1899,12 @@ export async function createCsharpBom(path, options) {
|
|
|
1655
1899
|
return createBinaryBom(path, options);
|
|
1656
1900
|
}
|
|
1657
1901
|
let parentComponent = createDefaultParentComponent(path, "nuget", options);
|
|
1902
|
+
// The directory a scan starts from is not a package, so it has a version
|
|
1903
|
+
// only when --project-version gives it one; "latest" is not a version.
|
|
1904
|
+
applyNugetVersion(
|
|
1905
|
+
parentComponent,
|
|
1906
|
+
options.projectVersion ? `${options.projectVersion}` : undefined,
|
|
1907
|
+
);
|
|
1658
1908
|
const slnFiles = getAllFiles(
|
|
1659
1909
|
path,
|
|
1660
1910
|
`${options.multiProject ? "**/" : ""}*.sln*`,
|
|
@@ -1939,6 +2189,47 @@ export async function createCsharpBom(path, options) {
|
|
|
1939
2189
|
// one project's manifest are not dropped just because another format exists
|
|
1940
2190
|
// elsewhere in the scan.
|
|
1941
2191
|
const assetsProjectDirs = new Set();
|
|
2192
|
+
// Restore records a project file the way the build was pointed at it, which
|
|
2193
|
+
// on a case-insensitive file system can differ in case from the file found
|
|
2194
|
+
// here. Such a path takes the found file's spelling, unless that is ambiguous.
|
|
2195
|
+
const foundProjectFiles = new Set();
|
|
2196
|
+
const foundProjectFilesByKey = new Map();
|
|
2197
|
+
for (const f of csProjFiles) {
|
|
2198
|
+
const file = resolve(f);
|
|
2199
|
+
const key = file.toLowerCase();
|
|
2200
|
+
foundProjectFiles.add(file);
|
|
2201
|
+
foundProjectFilesByKey.set(
|
|
2202
|
+
key,
|
|
2203
|
+
foundProjectFilesByKey.has(key) ? null : file,
|
|
2204
|
+
);
|
|
2205
|
+
}
|
|
2206
|
+
const foundProjectFile = (file) =>
|
|
2207
|
+
!file || foundProjectFiles.has(file)
|
|
2208
|
+
? file
|
|
2209
|
+
: foundProjectFilesByKey.get(file.toLowerCase()) || file;
|
|
2210
|
+
// The package id and version each project's restore output records, by
|
|
2211
|
+
// project file. A project whose restore outputs disagree, such as a stale
|
|
2212
|
+
// obj/ beside an artifacts directory, takes neither.
|
|
2213
|
+
const restoredIdentities = new Map();
|
|
2214
|
+
const recordRestoredIdentity = (projectFile, identity) => {
|
|
2215
|
+
if (!projectFile || !identity) {
|
|
2216
|
+
return;
|
|
2217
|
+
}
|
|
2218
|
+
const known = restoredIdentities.get(projectFile);
|
|
2219
|
+
if (known === undefined) {
|
|
2220
|
+
restoredIdentities.set(projectFile, identity);
|
|
2221
|
+
} else if (
|
|
2222
|
+
known &&
|
|
2223
|
+
(known.name !== identity.name || known.version !== identity.version)
|
|
2224
|
+
) {
|
|
2225
|
+
if (DEBUG_MODE) {
|
|
2226
|
+
console.log(
|
|
2227
|
+
`Restore outputs disagree on the package id or version of ${projectFile}, so the identity its project file states is used.`,
|
|
2228
|
+
);
|
|
2229
|
+
}
|
|
2230
|
+
restoredIdentities.set(projectFile, null);
|
|
2231
|
+
}
|
|
2232
|
+
};
|
|
1942
2233
|
// project.assets.json parsing
|
|
1943
2234
|
if (projAssetsFiles.length) {
|
|
1944
2235
|
manifestFiles = manifestFiles.concat(projAssetsFiles);
|
|
@@ -1952,9 +2243,13 @@ export async function createCsharpBom(path, options) {
|
|
|
1952
2243
|
const dlist = results["pkgList"];
|
|
1953
2244
|
// Restore output names its project; an artifacts layout keeps it outside
|
|
1954
2245
|
// the project directory.
|
|
1955
|
-
const
|
|
2246
|
+
const projectFile = foundProjectFile(
|
|
2247
|
+
dotnetRestoredProjectFile(af, results.projectPath, results.outputPath),
|
|
2248
|
+
);
|
|
2249
|
+
const projectDir = dotnetManifestProjectDir(af, projectFile);
|
|
1956
2250
|
assetsProjectDirs.add(projectDir);
|
|
1957
2251
|
recordProjectVersions(projectDir, dlist);
|
|
2252
|
+
recordRestoredIdentity(projectFile, results.projectIdentity);
|
|
1958
2253
|
if (dlist?.length) {
|
|
1959
2254
|
pkgList = pkgList.concat(dlist);
|
|
1960
2255
|
}
|
|
@@ -2065,6 +2360,7 @@ export async function createCsharpBom(path, options) {
|
|
|
2065
2360
|
}
|
|
2066
2361
|
const projectRefByDir = {};
|
|
2067
2362
|
let services = [];
|
|
2363
|
+
let dosaiTools = [];
|
|
2068
2364
|
if (csProjFiles.length) {
|
|
2069
2365
|
manifestFiles = manifestFiles.concat(csProjFiles);
|
|
2070
2366
|
// Parsing csproj is quite error-prone. Some project files may not have
|
|
@@ -2073,11 +2369,18 @@ export async function createCsharpBom(path, options) {
|
|
|
2073
2369
|
// whole scan agrees on.
|
|
2074
2370
|
const agreedVersions = agreedNugetVersions(pkgList);
|
|
2075
2371
|
let msbuildInstalled = false;
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2372
|
+
// `dotnet msbuild` evaluates the project (loading Directory.Build.rsp,
|
|
2373
|
+
// Directory.Build.props/targets and build extensions), so it belongs to
|
|
2374
|
+
// the dependency-tooling surface disabled by --no-install-deps and the
|
|
2375
|
+
// pre-build lifecycle. Version labels it would resolve then fall back to
|
|
2376
|
+
// the static props/central-version resolution.
|
|
2377
|
+
if (isProjectToolingAllowed(options)) {
|
|
2378
|
+
const result = safeSpawnSync(DOTNET_CMD, ["msbuild", "--version"], {
|
|
2379
|
+
shell: isWin,
|
|
2380
|
+
});
|
|
2381
|
+
if (result.status === 0 && !result.error && result.stdout?.trim()) {
|
|
2382
|
+
msbuildInstalled = true;
|
|
2383
|
+
}
|
|
2081
2384
|
}
|
|
2082
2385
|
// `$(Name)` version labels resolve from the props files that can apply to
|
|
2083
2386
|
// each project (beside and above it), read once for the whole scan.
|
|
@@ -2103,6 +2406,7 @@ export async function createCsharpBom(path, options) {
|
|
|
2103
2406
|
getCentralPackageVersions(f, centralVersionsCache),
|
|
2104
2407
|
agreedVersions,
|
|
2105
2408
|
projectNames[projectDir] || {},
|
|
2409
|
+
restoredIdentities.get(resolve(f)) || undefined,
|
|
2106
2410
|
);
|
|
2107
2411
|
if (retMap?.parentComponent?.purl) {
|
|
2108
2412
|
projectRefByDir[projectDir] ??= retMap.parentComponent["bom-ref"];
|
|
@@ -2127,6 +2431,24 @@ export async function createCsharpBom(path, options) {
|
|
|
2127
2431
|
parentComponent,
|
|
2128
2432
|
);
|
|
2129
2433
|
}
|
|
2434
|
+
// Only a version that needed msbuild makes its skip a loss
|
|
2435
|
+
if (
|
|
2436
|
+
retMap?.unresolvedVersionLabels?.length &&
|
|
2437
|
+
!isProjectToolingAllowed(options)
|
|
2438
|
+
) {
|
|
2439
|
+
reportProjectToolingSkipped(
|
|
2440
|
+
options,
|
|
2441
|
+
"dotnet.msbuild.tooling-disabled",
|
|
2442
|
+
"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.",
|
|
2443
|
+
{
|
|
2444
|
+
ecosystem: "csharp",
|
|
2445
|
+
tool: "msbuild",
|
|
2446
|
+
impact: "versions",
|
|
2447
|
+
detail:
|
|
2448
|
+
"Dependency installation is disabled, so $(Property) version references that the project and props files do not set were not evaluated with msbuild.",
|
|
2449
|
+
},
|
|
2450
|
+
);
|
|
2451
|
+
}
|
|
2130
2452
|
}
|
|
2131
2453
|
}
|
|
2132
2454
|
// A lock file's or packages.config's direct dependencies belong to the
|
|
@@ -2148,6 +2470,17 @@ export async function createCsharpBom(path, options) {
|
|
|
2148
2470
|
}
|
|
2149
2471
|
}
|
|
2150
2472
|
}
|
|
2473
|
+
// A scan of several projects is made of them, so the directory depends on
|
|
2474
|
+
// each, as a Gradle build does on its subprojects. Restore output also lists
|
|
2475
|
+
// every project it restored as a package; the parent and its projects are
|
|
2476
|
+
// already in the BOM, and a second component with the same bom-ref would
|
|
2477
|
+
// merge the two in every consumer.
|
|
2478
|
+
const projectRefs = new Set([parentComponent?.["bom-ref"]]);
|
|
2479
|
+
for (const project of parentComponent?.components || []) {
|
|
2480
|
+
parentDependsOn.add(project["bom-ref"]);
|
|
2481
|
+
projectRefs.add(project["bom-ref"]);
|
|
2482
|
+
}
|
|
2483
|
+
pkgList = pkgList.filter((p) => !projectRefs.has(p["bom-ref"]));
|
|
2151
2484
|
if (pkgList.length) {
|
|
2152
2485
|
pkgList = trimComponents(pkgList);
|
|
2153
2486
|
// Perform deep analysis using dosai
|
|
@@ -2166,19 +2499,46 @@ export async function createCsharpBom(path, options) {
|
|
|
2166
2499
|
? resolve(path, options.depsSlicesFile)
|
|
2167
2500
|
: join(slicesDir, "dosai.json");
|
|
2168
2501
|
try {
|
|
2502
|
+
// A run that will run evinse (--evidence or --include-crypto, after
|
|
2503
|
+
// profiles are applied) keeps the report full: evinse reuses this
|
|
2504
|
+
// file as its usages slice and persists it as the native report.
|
|
2505
|
+
// Anything else needs only the sections cdxgen reads, so dosai
|
|
2506
|
+
// leaves the rest out (Metadata.OmittedSections names it).
|
|
2507
|
+
const willRunEvinse = runsEvidenceAnalysis(options);
|
|
2508
|
+
// Reuse an existing slice as-is, with one exception: a trimmed file
|
|
2509
|
+
// (an earlier --deep run left it behind) is regenerated full before
|
|
2510
|
+
// an evidence run uses it, so dosai still runs once per scan.
|
|
2511
|
+
let reuseExisting = safeExistsSync(slicesFile);
|
|
2512
|
+
if (reuseExisting && willRunEvinse) {
|
|
2513
|
+
const omitted = dosaiReportFileOmittedSections(slicesFile);
|
|
2514
|
+
if (omitted.length) {
|
|
2515
|
+
if (DEBUG_MODE) {
|
|
2516
|
+
console.log(
|
|
2517
|
+
`The existing dosai methods slice "${slicesFile}" omits sections (${omitted.join(", ")}); regenerating the full report because this run performs evidence analysis.`,
|
|
2518
|
+
);
|
|
2519
|
+
}
|
|
2520
|
+
reuseExisting = false;
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2169
2523
|
// Create the slices file if it doesn't exist
|
|
2170
|
-
if (!
|
|
2524
|
+
if (!reuseExisting) {
|
|
2171
2525
|
thoughtLog(
|
|
2172
2526
|
"Alright, the next step is to invoke the dosai command to identify evidence of occurrences for various components.",
|
|
2173
2527
|
);
|
|
2174
2528
|
const sliceResult = createDosaiMethodsSlice(
|
|
2175
2529
|
resolve(path),
|
|
2176
2530
|
resolve(slicesFile),
|
|
2177
|
-
|
|
2531
|
+
{
|
|
2532
|
+
...options,
|
|
2533
|
+
dosaiMethodsSections: willRunEvinse
|
|
2534
|
+
? undefined
|
|
2535
|
+
: dosaiMethodsSectionsArgument(),
|
|
2536
|
+
},
|
|
2178
2537
|
);
|
|
2179
|
-
// A stopped run
|
|
2180
|
-
//
|
|
2181
|
-
|
|
2538
|
+
// A stopped run, or one the .NET host refused to start, has
|
|
2539
|
+
// already said why and what to change; the debug output would show
|
|
2540
|
+
// no other dosai error for it (issue 4438).
|
|
2541
|
+
if (!sliceResult && !dosaiFailureReported(resolve(slicesFile))) {
|
|
2182
2542
|
// Visible without debug output: otherwise the components silently
|
|
2183
2543
|
// lose the dependency-slice evidence while the scan still exits 0.
|
|
2184
2544
|
// --evidence (which implies --deep) runs dosai again during evinse
|
|
@@ -2205,6 +2565,11 @@ export async function createCsharpBom(path, options) {
|
|
|
2205
2565
|
const servicesMap = collectDosaiServiceComponents(methodsSlice, {});
|
|
2206
2566
|
collectDosaiServicesFromMethods(methodsSlice, servicesMap);
|
|
2207
2567
|
services = normalizeDosaiServiceMap(servicesMap);
|
|
2568
|
+
dosaiTools = dosaiToolComponents(
|
|
2569
|
+
slicesFile,
|
|
2570
|
+
methodsSlice,
|
|
2571
|
+
getPluginToolComponents(["dosai"])[0],
|
|
2572
|
+
);
|
|
2208
2573
|
} finally {
|
|
2209
2574
|
if (slicesDir) {
|
|
2210
2575
|
safeRmSync(slicesDir, { recursive: true, force: true });
|
|
@@ -2244,7 +2609,7 @@ export async function createCsharpBom(path, options) {
|
|
|
2244
2609
|
dependencies,
|
|
2245
2610
|
parentComponent,
|
|
2246
2611
|
services,
|
|
2247
|
-
tools:
|
|
2612
|
+
tools: dosaiTools,
|
|
2248
2613
|
});
|
|
2249
2614
|
}
|
|
2250
2615
|
|
|
@@ -2263,58 +2628,239 @@ export async function createCryptoCertsBom(path, options) {
|
|
|
2263
2628
|
},
|
|
2264
2629
|
};
|
|
2265
2630
|
}
|
|
2631
|
+
const collector = new CertificateCollector();
|
|
2266
2632
|
const pkgList = [];
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2633
|
+
// In rootfs/container mode the scan path sits inside the extracted tree;
|
|
2634
|
+
// locations are recorded relative to that tree so they merge with the
|
|
2635
|
+
// trustinspector rootfs producer (G18).
|
|
2636
|
+
const locationRoot = options.allLayersExplodedDir;
|
|
2637
|
+
const relativeLocation = (file) =>
|
|
2638
|
+
containerRelativePath(file, locationRoot) || file;
|
|
2639
|
+
// Bom-refs never carry host paths: outside rootfs mode, key and keystore
|
|
2640
|
+
// refs are relative to the scanned path.
|
|
2641
|
+
const refLocation = (file) =>
|
|
2642
|
+
containerRelativePath(file, locationRoot || path) || file;
|
|
2643
|
+
const certFiles = [
|
|
2644
|
+
...new Set([
|
|
2645
|
+
...getAllFiles(
|
|
2646
|
+
path,
|
|
2647
|
+
`${
|
|
2648
|
+
options.multiProject ? "**/" : ""
|
|
2649
|
+
}*.{p12,jks,jceks,bks,keystore,key,pem,cer,crt,der,p7b,p7c,pfx,truststore,gpg,pub}`,
|
|
2650
|
+
options,
|
|
2651
|
+
),
|
|
2652
|
+
// Java truststores carry no extension; match them by exact name.
|
|
2653
|
+
...getAllFiles(
|
|
2654
|
+
path,
|
|
2655
|
+
`${options.multiProject ? "**/" : ""}{cacerts,jssecacerts}`,
|
|
2656
|
+
options,
|
|
2657
|
+
),
|
|
2658
|
+
]),
|
|
2659
|
+
];
|
|
2660
|
+
// Newlines would corrupt the plugin's stdin file list, and oversized
|
|
2661
|
+
// files are capped in the fallback; the plugin list gets the same caps.
|
|
2662
|
+
const pluginCandidateFiles = certFiles.filter(
|
|
2663
|
+
(file) =>
|
|
2664
|
+
!/[\r\n]/.test(file) &&
|
|
2665
|
+
statSync(file, { throwIfNoEntry: false })?.size <= MAX_CRYPTO_FILE_BYTES,
|
|
2273
2666
|
);
|
|
2274
|
-
const
|
|
2275
|
-
|
|
2667
|
+
const pluginMaterials = runTrustInspectorCertsMode(
|
|
2668
|
+
pluginCandidateFiles,
|
|
2669
|
+
locationRoot,
|
|
2276
2670
|
);
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
const
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2671
|
+
const fallbackRecords = [];
|
|
2672
|
+
if (pluginMaterials) {
|
|
2673
|
+
collector.addTrustInspectorMaterials(pluginMaterials);
|
|
2674
|
+
for (const material of pluginMaterials) {
|
|
2675
|
+
if (typeof material?.path !== "string" || !material.path) {
|
|
2676
|
+
continue;
|
|
2677
|
+
}
|
|
2678
|
+
if (material.kind === "keystore") {
|
|
2679
|
+
pkgList.push(
|
|
2680
|
+
sourceKeystoreComponent(material, refLocation(material.path)),
|
|
2681
|
+
);
|
|
2682
|
+
} else if (material.kind === "private-key") {
|
|
2683
|
+
pkgList.push(
|
|
2684
|
+
sourcePrivateKeyComponent(material, refLocation(material.path)),
|
|
2685
|
+
);
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
// Files the plugin was not given (names with line breaks, oversized
|
|
2690
|
+
// files) are still read here, so no name can hide a file from the
|
|
2691
|
+
// inventory; without a plugin every file is. The files it was given are
|
|
2692
|
+
// its own, except for the keys it builds nothing from: it reports nothing
|
|
2693
|
+
// for a bare PEM public key, and an OpenPGP key only as public-key
|
|
2694
|
+
// material, so both are built here whether it saw them or not.
|
|
2695
|
+
const pluginFiles = new Set(pluginMaterials ? pluginCandidateFiles : []);
|
|
2696
|
+
for (const f of certFiles) {
|
|
2697
|
+
let bytes;
|
|
2698
|
+
try {
|
|
2699
|
+
const stats = statSync(f);
|
|
2700
|
+
if (stats.size > MAX_CRYPTO_FILE_BYTES) {
|
|
2701
|
+
continue;
|
|
2702
|
+
}
|
|
2703
|
+
bytes = readFileSync(f);
|
|
2704
|
+
} catch {
|
|
2705
|
+
continue;
|
|
2706
|
+
}
|
|
2707
|
+
const location = relativeLocation(f);
|
|
2708
|
+
const classification = classifyCryptoFile(bytes);
|
|
2709
|
+
if (
|
|
2710
|
+
pluginFiles.has(f) &&
|
|
2711
|
+
classification.kind !== "gpg" &&
|
|
2712
|
+
classification.kind !== "public-key"
|
|
2713
|
+
) {
|
|
2714
|
+
continue;
|
|
2715
|
+
}
|
|
2716
|
+
if (classification.kind === "certificates") {
|
|
2717
|
+
for (const der of classification.ders) {
|
|
2718
|
+
const record = certRecordFromDer(der, {
|
|
2719
|
+
location,
|
|
2720
|
+
sourceType: deriveCryptoSourceType(f),
|
|
2721
|
+
});
|
|
2722
|
+
if (record) {
|
|
2723
|
+
record.fileExtension = extname(f).replace(/^\./, "") || undefined;
|
|
2724
|
+
fallbackRecords.push(record);
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
} else if (classification.kind === "private-key") {
|
|
2728
|
+
pkgList.push(
|
|
2729
|
+
sourcePrivateKeyComponent(
|
|
2288
2730
|
{
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
value: f,
|
|
2731
|
+
format: classification.format,
|
|
2732
|
+
sourceType: deriveCryptoSourceType(f),
|
|
2292
2733
|
},
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2734
|
+
refLocation(f),
|
|
2735
|
+
),
|
|
2736
|
+
);
|
|
2737
|
+
} else if (classification.kind === "gpg") {
|
|
2738
|
+
pkgList.push(await sourceGpgComponent(f, location, options));
|
|
2739
|
+
} else if (classification.kind === "public-key") {
|
|
2740
|
+
pkgList.push(await sourcePemPublicKeyComponent(f, location, options));
|
|
2741
|
+
} else if (classification.kind === "keystore") {
|
|
2742
|
+
pkgList.push(
|
|
2743
|
+
sourceKeystoreComponent(
|
|
2744
|
+
{
|
|
2745
|
+
format: classification.format,
|
|
2746
|
+
sourceType: deriveCryptoSourceType(f),
|
|
2747
|
+
},
|
|
2748
|
+
refLocation(f),
|
|
2749
|
+
),
|
|
2750
|
+
);
|
|
2751
|
+
}
|
|
2752
|
+
}
|
|
2753
|
+
// Kubernetes TLS secrets and Android SPKI pins are cdxgen-side producers:
|
|
2754
|
+
// the plugin has no YAML or Android-config parser.
|
|
2755
|
+
const yamlFiles = getAllFiles(
|
|
2756
|
+
path,
|
|
2757
|
+
`${options.multiProject ? "**/" : ""}*.{yaml,yml}`,
|
|
2758
|
+
options,
|
|
2759
|
+
);
|
|
2760
|
+
for (const yamlFile of yamlFiles) {
|
|
2761
|
+
let yamlText;
|
|
2762
|
+
try {
|
|
2763
|
+
const stats = statSync(yamlFile);
|
|
2764
|
+
if (stats.size > MAX_CRYPTO_FILE_BYTES) {
|
|
2765
|
+
continue;
|
|
2766
|
+
}
|
|
2767
|
+
yamlText = readFileSync(yamlFile, "utf-8");
|
|
2768
|
+
} catch {
|
|
2769
|
+
continue;
|
|
2300
2770
|
}
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2771
|
+
// Cheap pre-filter: the Secret type string must be present before the
|
|
2772
|
+
// YAML parser runs.
|
|
2773
|
+
if (!yamlText.includes("kubernetes.io/tls")) {
|
|
2774
|
+
continue;
|
|
2775
|
+
}
|
|
2776
|
+
for (const record of kubernetesTlsSecretRecords(yamlText, {
|
|
2777
|
+
file: relativeLocation(yamlFile),
|
|
2778
|
+
})) {
|
|
2779
|
+
fallbackRecords.push(record);
|
|
2780
|
+
}
|
|
2781
|
+
}
|
|
2782
|
+
const androidXmlFiles = getAllFiles(path, "**/res/xml/*.xml", options);
|
|
2783
|
+
for (const xmlFile of androidXmlFiles) {
|
|
2784
|
+
let xmlText;
|
|
2785
|
+
try {
|
|
2786
|
+
const stats = statSync(xmlFile, { throwIfNoEntry: false });
|
|
2787
|
+
if (!stats || stats.size > MAX_CRYPTO_FILE_BYTES) {
|
|
2788
|
+
continue;
|
|
2789
|
+
}
|
|
2790
|
+
xmlText = readFileSync(xmlFile, "utf-8");
|
|
2791
|
+
} catch {
|
|
2792
|
+
continue;
|
|
2793
|
+
}
|
|
2794
|
+
if (!xmlText.includes("<network-security-config")) {
|
|
2795
|
+
continue;
|
|
2796
|
+
}
|
|
2797
|
+
for (const pin of androidNetworkSecurityConfigPins(xmlText, {
|
|
2798
|
+
file: relativeLocation(xmlFile),
|
|
2799
|
+
})) {
|
|
2800
|
+
collector.addPin(pin);
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
if (fallbackRecords.length) {
|
|
2804
|
+
linkFallbackIssuers(fallbackRecords);
|
|
2805
|
+
collector.addRecords(fallbackRecords);
|
|
2806
|
+
}
|
|
2807
|
+
const {
|
|
2808
|
+
components: certificateComponents,
|
|
2809
|
+
dependencies: certificateDependencies,
|
|
2810
|
+
} = collector.build();
|
|
2811
|
+
if (pluginMaterials) {
|
|
2812
|
+
// The plugin reports the certificates a readable keystore holds, not the
|
|
2813
|
+
// keystore itself; give it the same component the fallback emits and
|
|
2814
|
+
// link it to what it holds.
|
|
2815
|
+
const keystoreMaterials = new Map();
|
|
2816
|
+
for (const material of pluginMaterials) {
|
|
2817
|
+
if (material?.kind === "certificate" && material.path) {
|
|
2818
|
+
keystoreMaterials.set(material.path, material);
|
|
2819
|
+
}
|
|
2820
|
+
}
|
|
2821
|
+
for (const file of pluginCandidateFiles) {
|
|
2822
|
+
const location = relativeLocation(file);
|
|
2823
|
+
const material = keystoreMaterials.get(location);
|
|
2824
|
+
if (!material) {
|
|
2825
|
+
continue;
|
|
2826
|
+
}
|
|
2827
|
+
let classification;
|
|
2828
|
+
try {
|
|
2829
|
+
classification = classifyCryptoFile(readFileSync(file));
|
|
2830
|
+
} catch {
|
|
2831
|
+
continue;
|
|
2832
|
+
}
|
|
2833
|
+
if (classification.kind !== "keystore") {
|
|
2834
|
+
continue;
|
|
2835
|
+
}
|
|
2836
|
+
const keystore = sourceKeystoreComponent(
|
|
2837
|
+
{
|
|
2838
|
+
format: classification.format,
|
|
2839
|
+
sourceType: material.sourceType || deriveCryptoSourceType(file),
|
|
2840
|
+
readable: true,
|
|
2311
2841
|
},
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2842
|
+
refLocation(file),
|
|
2843
|
+
);
|
|
2844
|
+
pkgList.push(keystore);
|
|
2845
|
+
const held = certificateComponents
|
|
2846
|
+
.filter(
|
|
2847
|
+
(component) =>
|
|
2848
|
+
component.cryptoProperties?.assetType === "certificate" &&
|
|
2849
|
+
component.evidence?.occurrences?.some(
|
|
2850
|
+
(occurrence) => occurrence.location === location,
|
|
2851
|
+
),
|
|
2852
|
+
)
|
|
2853
|
+
.map((component) => component["bom-ref"])
|
|
2854
|
+
.sort();
|
|
2855
|
+
if (held.length) {
|
|
2856
|
+
certificateDependencies.push({
|
|
2857
|
+
ref: keystore["bom-ref"],
|
|
2858
|
+
dependsOn: held,
|
|
2859
|
+
});
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2317
2862
|
}
|
|
2863
|
+
pkgList.push(...certificateComponents);
|
|
2318
2864
|
const sourceCryptoComponents = await collectSourceCryptoComponents(
|
|
2319
2865
|
path,
|
|
2320
2866
|
options,
|
|
@@ -2335,9 +2881,229 @@ export async function createCryptoCertsBom(path, options) {
|
|
|
2335
2881
|
bomJson: {
|
|
2336
2882
|
components: filterInvalidCryptoComponents(pkgList),
|
|
2337
2883
|
},
|
|
2884
|
+
dependencies: certificateDependencies,
|
|
2885
|
+
};
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
// Files larger than this are not classified: no real certificate bundle or
|
|
2889
|
+
// keystore comes close, and the cap bounds hostile inputs.
|
|
2890
|
+
const MAX_CRYPTO_FILE_BYTES = 16 * 1024 * 1024;
|
|
2891
|
+
|
|
2892
|
+
function deriveCryptoSourceType(filePath) {
|
|
2893
|
+
const normalized = String(filePath).replace(/\\/g, "/");
|
|
2894
|
+
if (
|
|
2895
|
+
normalized.includes("/etc/ssl/certs") ||
|
|
2896
|
+
normalized.includes("ca-certificates")
|
|
2897
|
+
) {
|
|
2898
|
+
return "ca-store";
|
|
2899
|
+
}
|
|
2900
|
+
if (normalized.includes("certifi") || normalized.includes("botocore")) {
|
|
2901
|
+
return "language-bundle";
|
|
2902
|
+
}
|
|
2903
|
+
if (
|
|
2904
|
+
/\/etc\/(nginx|apache2|httpd|haproxy|letsencrypt|kubernetes|docker|containerd)\//.test(
|
|
2905
|
+
normalized,
|
|
2906
|
+
)
|
|
2907
|
+
) {
|
|
2908
|
+
return "server-config";
|
|
2909
|
+
}
|
|
2910
|
+
if (
|
|
2911
|
+
normalized.includes("cacerts") ||
|
|
2912
|
+
/\.(jks|jceks|p12|pfx|truststore)$/i.test(normalized)
|
|
2913
|
+
) {
|
|
2914
|
+
return "java-truststore";
|
|
2915
|
+
}
|
|
2916
|
+
return "generic";
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2919
|
+
function sourceCryptoFileEvidence(location, options) {
|
|
2920
|
+
if (!options.evidence) {
|
|
2921
|
+
return undefined;
|
|
2922
|
+
}
|
|
2923
|
+
const identityEvidence = {
|
|
2924
|
+
field: "name",
|
|
2925
|
+
confidence: 1,
|
|
2926
|
+
concludedValue: basename(location),
|
|
2927
|
+
methods: [
|
|
2928
|
+
{
|
|
2929
|
+
technique: "filename",
|
|
2930
|
+
confidence: 1,
|
|
2931
|
+
value: location,
|
|
2932
|
+
},
|
|
2933
|
+
],
|
|
2934
|
+
};
|
|
2935
|
+
return {
|
|
2936
|
+
identity:
|
|
2937
|
+
options.specVersion >= 1.6 ? [identityEvidence] : identityEvidence,
|
|
2938
|
+
occurrences: [{ location }],
|
|
2939
|
+
};
|
|
2940
|
+
}
|
|
2941
|
+
|
|
2942
|
+
async function sourceGpgComponent(srcFile, location, options) {
|
|
2943
|
+
const fileHash = await checksumFile("sha256", srcFile);
|
|
2944
|
+
return {
|
|
2945
|
+
name: basename(location),
|
|
2946
|
+
type: "cryptographic-asset",
|
|
2947
|
+
version: fileHash,
|
|
2948
|
+
"bom-ref": `crypto/related-crypto-material/public-key/${encodeURIComponent(basename(location))}@sha256:${fileHash}`,
|
|
2949
|
+
cryptoProperties: {
|
|
2950
|
+
assetType: "related-crypto-material",
|
|
2951
|
+
relatedCryptoMaterialProperties: {
|
|
2952
|
+
type: "public-key",
|
|
2953
|
+
format: "OpenPGP",
|
|
2954
|
+
},
|
|
2955
|
+
},
|
|
2956
|
+
...(sourceCryptoFileEvidence(location, options)
|
|
2957
|
+
? { evidence: sourceCryptoFileEvidence(location, options) }
|
|
2958
|
+
: {}),
|
|
2959
|
+
properties: [
|
|
2960
|
+
{ name: "internal:SrcFile", value: location },
|
|
2961
|
+
{ name: "cdx:crypto:sourceType", value: "generic" },
|
|
2962
|
+
],
|
|
2963
|
+
};
|
|
2964
|
+
}
|
|
2965
|
+
|
|
2966
|
+
async function sourcePemPublicKeyComponent(srcFile, location, options) {
|
|
2967
|
+
const fileHash = await checksumFile("sha256", srcFile);
|
|
2968
|
+
return {
|
|
2969
|
+
name: basename(location),
|
|
2970
|
+
type: "cryptographic-asset",
|
|
2971
|
+
version: fileHash,
|
|
2972
|
+
"bom-ref": `crypto/related-crypto-material/public-key/${encodeURIComponent(basename(location))}@sha256:${fileHash}`,
|
|
2973
|
+
cryptoProperties: {
|
|
2974
|
+
assetType: "related-crypto-material",
|
|
2975
|
+
relatedCryptoMaterialProperties: {
|
|
2976
|
+
type: "public-key",
|
|
2977
|
+
format: "SPKI",
|
|
2978
|
+
},
|
|
2979
|
+
},
|
|
2980
|
+
...(sourceCryptoFileEvidence(location, options)
|
|
2981
|
+
? { evidence: sourceCryptoFileEvidence(location, options) }
|
|
2982
|
+
: {}),
|
|
2983
|
+
properties: [
|
|
2984
|
+
{ name: "internal:SrcFile", value: location },
|
|
2985
|
+
{ name: "cdx:crypto:sourceType", value: "generic" },
|
|
2986
|
+
],
|
|
2338
2987
|
};
|
|
2339
2988
|
}
|
|
2340
2989
|
|
|
2990
|
+
function sourcePrivateKeyComponent(material, location) {
|
|
2991
|
+
return {
|
|
2992
|
+
name: basename(location),
|
|
2993
|
+
type: "cryptographic-asset",
|
|
2994
|
+
"bom-ref": `crypto/related-crypto-material/private-key/${encodeURIComponent(location)}`,
|
|
2995
|
+
cryptoProperties: {
|
|
2996
|
+
assetType: "related-crypto-material",
|
|
2997
|
+
relatedCryptoMaterialProperties: {
|
|
2998
|
+
type: "private-key",
|
|
2999
|
+
...(material?.format ? { format: material.format } : {}),
|
|
3000
|
+
},
|
|
3001
|
+
},
|
|
3002
|
+
evidence: { occurrences: [{ location }] },
|
|
3003
|
+
properties: [
|
|
3004
|
+
{ name: "internal:SrcFile", value: location },
|
|
3005
|
+
...(material?.sourceType
|
|
3006
|
+
? [{ name: "cdx:crypto:sourceType", value: material.sourceType }]
|
|
3007
|
+
: []),
|
|
3008
|
+
],
|
|
3009
|
+
};
|
|
3010
|
+
}
|
|
3011
|
+
|
|
3012
|
+
function sourceKeystoreComponent(material, location) {
|
|
3013
|
+
const readable = (material?.properties || []).find(
|
|
3014
|
+
(property) => property.name === "cdx:crypto:keystoreReadable",
|
|
3015
|
+
)?.value;
|
|
3016
|
+
return {
|
|
3017
|
+
name: basename(location),
|
|
3018
|
+
type: "cryptographic-asset",
|
|
3019
|
+
"bom-ref": `crypto/related-crypto-material/keystore/${encodeURIComponent(location)}`,
|
|
3020
|
+
cryptoProperties: {
|
|
3021
|
+
assetType: "related-crypto-material",
|
|
3022
|
+
relatedCryptoMaterialProperties: {
|
|
3023
|
+
type: "other",
|
|
3024
|
+
...(material?.format ? { format: material.format } : {}),
|
|
3025
|
+
},
|
|
3026
|
+
},
|
|
3027
|
+
evidence: { occurrences: [{ location }] },
|
|
3028
|
+
properties: [
|
|
3029
|
+
{ name: "internal:SrcFile", value: location },
|
|
3030
|
+
...(material?.sourceType
|
|
3031
|
+
? [{ name: "cdx:crypto:sourceType", value: material.sourceType }]
|
|
3032
|
+
: []),
|
|
3033
|
+
// A keystore the plugin read has no keystoreReadable property.
|
|
3034
|
+
...(material?.readable
|
|
3035
|
+
? []
|
|
3036
|
+
: [
|
|
3037
|
+
{ name: "cdx:crypto:keystoreReadable", value: readable || "false" },
|
|
3038
|
+
]),
|
|
3039
|
+
],
|
|
3040
|
+
};
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
/**
|
|
3044
|
+
* Parse the candidate certificate files with the trustinspector `certs`
|
|
3045
|
+
* mode. Only a report-2 envelope is accepted: report-1 output has no
|
|
3046
|
+
* certificate identity to build the shared model from, and the Node
|
|
3047
|
+
* fallback covers that case.
|
|
3048
|
+
*
|
|
3049
|
+
* @param {string[]} files candidate files
|
|
3050
|
+
* @param {string} root paths are reported relative to this directory
|
|
3051
|
+
* @returns {object[]|undefined} materials, or undefined when the plugin is
|
|
3052
|
+
* absent, blocked or not report-2
|
|
3053
|
+
*/
|
|
3054
|
+
function runTrustInspectorCertsMode(files, root) {
|
|
3055
|
+
if (!files?.length) {
|
|
3056
|
+
return undefined;
|
|
3057
|
+
}
|
|
3058
|
+
if (isDryRun) {
|
|
3059
|
+
recordActivity({
|
|
3060
|
+
kind: "trustinspector",
|
|
3061
|
+
reason:
|
|
3062
|
+
"Dry run mode blocks trustinspector execution and reports the requested inspection instead.",
|
|
3063
|
+
status: "blocked",
|
|
3064
|
+
target: `certs --stdin (${files.length} files)`,
|
|
3065
|
+
});
|
|
3066
|
+
return undefined;
|
|
3067
|
+
}
|
|
3068
|
+
const trustInspectorBin = resolvePluginBinary("trustinspector");
|
|
3069
|
+
if (!trustInspectorBin) {
|
|
3070
|
+
return undefined;
|
|
3071
|
+
}
|
|
3072
|
+
// `--root` is passed only in rootfs/container mode, where locations must
|
|
3073
|
+
// come out relative to the extracted tree; a plain source scan keeps the
|
|
3074
|
+
// host paths every other producer records. The value reaches a spawned
|
|
3075
|
+
// command, so it gets the same rejection the rootfs path applies:
|
|
3076
|
+
// dangerous unicode or line breaks disable the plugin call.
|
|
3077
|
+
const args = ["certs", "--stdin"];
|
|
3078
|
+
const safeRoot =
|
|
3079
|
+
typeof root === "string" &&
|
|
3080
|
+
!hasDangerousUnicode(root) &&
|
|
3081
|
+
!/[\r\n]/.test(root)
|
|
3082
|
+
? root
|
|
3083
|
+
: undefined;
|
|
3084
|
+
if (safeRoot) {
|
|
3085
|
+
args.push("--root", safeRoot);
|
|
3086
|
+
}
|
|
3087
|
+
const result = safeSpawnSync(trustInspectorBin, args, {
|
|
3088
|
+
input: `${files.join("\n")}\n`,
|
|
3089
|
+
maxBuffer: 256 * 1024 * 1024,
|
|
3090
|
+
// A hung plugin must not hang cdxgen; the Node fallback takes over.
|
|
3091
|
+
timeout: 10 * 60 * 1000,
|
|
3092
|
+
});
|
|
3093
|
+
if (result?.status !== 0 || result?.error || !result?.stdout) {
|
|
3094
|
+
return undefined;
|
|
3095
|
+
}
|
|
3096
|
+
try {
|
|
3097
|
+
const report = JSON.parse(result.stdout);
|
|
3098
|
+
if (report?.schema !== "trustinspector/report-2") {
|
|
3099
|
+
return undefined;
|
|
3100
|
+
}
|
|
3101
|
+
return report.materials || [];
|
|
3102
|
+
} catch {
|
|
3103
|
+
return undefined;
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
|
|
2341
3107
|
/**
|
|
2342
3108
|
* Function to create bom string for R projects using renv.
|
|
2343
3109
|
*
|