@cdxgen/cdxgen 13.4.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 +7 -7
- package/bin/cdxgen-secure.js +15 -0
- package/bin/cdxgen.js +25 -5
- package/bin/evinse.js +5 -0
- package/bin/hbom.js +15 -7
- package/bin/repl.js +29 -0
- package/data/cbomosdb-queries.json +13 -1
- package/data/component-tags.json +8 -0
- package/data/queries-darwin.json +25 -1
- package/data/queries-win.json +19 -1
- package/data/queries.json +41 -0
- package/data/remediations.json +283 -1
- 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/lib/audit/index.js +14 -0
- package/lib/cli/bomAssembly.js +28 -3
- package/lib/cli/cliOptions.js +110 -3
- package/lib/cli/index.js +73 -4
- package/lib/cli/jvmBom.js +126 -20
- package/lib/cli/managedBom.js +711 -71
- package/lib/cli/nativeBom.js +225 -28
- package/lib/core/env.js +30 -8
- package/lib/core/fs.js +28 -19
- package/lib/core/httpClient.js +74 -45
- package/lib/core/projectConfig.js +1 -0
- package/lib/core/projectTooling.js +97 -0
- package/lib/core/propertySanitizer.js +72 -0
- package/lib/ecosystems/golem.js +550 -6
- package/lib/ecosystems/gradleutils.js +30 -20
- package/lib/ecosystems/parsers-dotnet.js +117 -18
- package/lib/ecosystems/parsers-go.js +1090 -367
- package/lib/ecosystems/parsers-jvm.js +259 -17
- package/lib/ecosystems/parsers-misc.js +229 -44
- package/lib/ecosystems/parsers-python.js +11 -10
- package/lib/ecosystems/rubyutils.js +6 -3
- package/lib/ecosystems/rusi.js +10 -0
- package/lib/ecosystems/scalacliutils.js +2 -1
- package/lib/ecosystems/scalasem.js +3 -1
- package/lib/evinser/evinser.js +126 -18
- package/lib/evinser/swiftsem.js +68 -5
- package/lib/inventory/agentSkillSpec.js +17 -0
- package/lib/inventory/apkInstalledDb.js +243 -0
- package/lib/inventory/atomUtils.js +33 -0
- package/lib/inventory/auditCategories.js +3 -3
- package/lib/inventory/cacheDir.js +52 -9
- package/lib/inventory/certificates.js +2371 -0
- package/lib/inventory/communityAiConfigParser.js +4 -9
- package/lib/inventory/deps.js +6 -4
- package/lib/inventory/display.js +200 -3
- package/lib/inventory/dosai.js +500 -58
- package/lib/inventory/dynamic.js +45 -3
- package/lib/inventory/envcontext.js +168 -5
- package/lib/inventory/evidenceUtils.js +436 -23
- 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/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/phpBuildInfo.js +487 -0
- package/lib/inventory/phpHostProbe.js +641 -0
- package/lib/inventory/rootfsCerts.js +426 -0
- package/lib/inventory/source.js +207 -3
- 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 +529 -183
- package/lib/managers/docker.js +166 -12
- package/lib/parsers/email.js +277 -0
- package/lib/parsers/largeJson.js +34 -2
- package/lib/server/server.js +186 -1
- 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 +58 -1
- package/lib/stages/postgen/specVersionCompat.js +30 -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/package.json +17 -17
- 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/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/env.d.ts +18 -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.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/ecosystems/cppEvidence.d.ts +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 +10 -8
- package/types/lib/ecosystems/gradleutils.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-jvm.d.ts +28 -2
- package/types/lib/ecosystems/parsers-jvm.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-misc.d.ts +18 -58
- package/types/lib/ecosystems/parsers-misc.d.ts.map +1 -1
- package/types/lib/ecosystems/parsers-python.d.ts +4 -3
- package/types/lib/ecosystems/parsers-python.d.ts.map +1 -1
- package/types/lib/ecosystems/pythonutils.d.ts +1 -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/scalacliutils.d.ts.map +1 -1
- package/types/lib/ecosystems/scalasem.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +25 -0
- 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 +14 -0
- package/types/lib/inventory/atomUtils.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/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 +145 -6
- package/types/lib/inventory/dosai.d.ts.map +1 -1
- 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 +40 -2
- 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/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/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/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/source.d.ts +28 -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 +12 -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 +13 -0
- 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/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/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 {
|
|
@@ -26,7 +39,10 @@ import {
|
|
|
26
39
|
safeWriteSync,
|
|
27
40
|
} from "../core/fs.js";
|
|
28
41
|
import { thoughtLog } from "../core/logger.js";
|
|
29
|
-
import {
|
|
42
|
+
import {
|
|
43
|
+
isProjectToolingAllowed,
|
|
44
|
+
reportProjectToolingSkipped,
|
|
45
|
+
} from "../core/projectTooling.js";
|
|
30
46
|
import {
|
|
31
47
|
extractJarArchive,
|
|
32
48
|
getNugetMetadata,
|
|
@@ -35,7 +51,9 @@ import {
|
|
|
35
51
|
} from "../ecosystems/ecosystems.js";
|
|
36
52
|
import {
|
|
37
53
|
agreedNugetVersions,
|
|
54
|
+
applyNugetVersion,
|
|
38
55
|
dotnetManifestProjectDir,
|
|
56
|
+
dotnetRestoredProjectFile,
|
|
39
57
|
getCentralPackageVersions,
|
|
40
58
|
indexPropsProperties,
|
|
41
59
|
parseCsPkgData,
|
|
@@ -100,6 +118,15 @@ import {
|
|
|
100
118
|
collectDosaiCryptoComponents,
|
|
101
119
|
collectSourceCryptoComponents,
|
|
102
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";
|
|
103
130
|
import { readZipEntry } from "../inventory/deps.js";
|
|
104
131
|
import {
|
|
105
132
|
filterInvalidCryptoComponents,
|
|
@@ -113,12 +140,17 @@ import {
|
|
|
113
140
|
collectDosaiServiceComponents,
|
|
114
141
|
collectDosaiServicesFromMethods,
|
|
115
142
|
createDosaiMethodsSlice,
|
|
116
|
-
|
|
143
|
+
dosaiFailureReported,
|
|
144
|
+
dosaiMethodsSectionsArgument,
|
|
145
|
+
dosaiReportFileOmittedSections,
|
|
146
|
+
dosaiToolComponents,
|
|
117
147
|
isDosaiDotnetLanguage,
|
|
118
148
|
normalizeDosaiServiceMap,
|
|
119
149
|
readDosaiMethodsReport,
|
|
150
|
+
runsEvidenceAnalysis,
|
|
120
151
|
} from "../inventory/dosai.js";
|
|
121
152
|
import { addEvidenceForDotnet } from "../inventory/evidenceUtils.js";
|
|
153
|
+
import { resolvePluginBinary } from "../inventory/plugins.js";
|
|
122
154
|
import { nugetPurl, pypiBomRef } from "../inventory/purl.js";
|
|
123
155
|
import { classifyProbeResult } from "../inventory/toolRequirements.js";
|
|
124
156
|
import { getPluginToolComponents } from "../managers/binary.js";
|
|
@@ -225,11 +257,14 @@ export function createPixiBom(path, options) {
|
|
|
225
257
|
generatePixiLockFile(path);
|
|
226
258
|
|
|
227
259
|
const pixiLockFile = join(path, "pixi.lock");
|
|
228
|
-
if (!safeExistsSync(pixiLockFile)
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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;
|
|
233
268
|
}
|
|
234
269
|
PixiLockData = parsePixiLockFile(pixiLockFile);
|
|
235
270
|
metadataFilename = "pixi.lock";
|
|
@@ -494,11 +529,14 @@ export async function createPythonBom(path, options) {
|
|
|
494
529
|
retMap.dependenciesList,
|
|
495
530
|
parentComponent,
|
|
496
531
|
);
|
|
497
|
-
} 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.
|
|
498
536
|
let exportedReqs = "";
|
|
499
537
|
if (f.endsWith("poetry.lock")) {
|
|
500
538
|
thoughtLog(
|
|
501
|
-
"Using poetry export
|
|
539
|
+
"Using poetry export to collect the resolved closure without installing anything.",
|
|
502
540
|
);
|
|
503
541
|
const expCmd = safeSpawnSync(
|
|
504
542
|
"poetry",
|
|
@@ -509,7 +547,7 @@ export async function createPythonBom(path, options) {
|
|
|
509
547
|
exportedReqs = expCmd.stdout.toString();
|
|
510
548
|
} else if (f.endsWith("pdm.lock")) {
|
|
511
549
|
thoughtLog(
|
|
512
|
-
"Using pdm export
|
|
550
|
+
"Using pdm export to collect the resolved closure without installing anything.",
|
|
513
551
|
);
|
|
514
552
|
const expCmd = safeSpawnSync(
|
|
515
553
|
"pdm",
|
|
@@ -539,6 +577,19 @@ export async function createPythonBom(path, options) {
|
|
|
539
577
|
pkgList = pkgList.concat(dlist);
|
|
540
578
|
}
|
|
541
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
|
+
);
|
|
542
593
|
}
|
|
543
594
|
}
|
|
544
595
|
}
|
|
@@ -879,9 +930,11 @@ export async function createPythonBom(path, options) {
|
|
|
879
930
|
);
|
|
880
931
|
}
|
|
881
932
|
emitAiInventorySummary(aiInventory, path);
|
|
882
|
-
// 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.
|
|
883
935
|
if (
|
|
884
936
|
isFeatureEnabled(options, "safe-pip-install") &&
|
|
937
|
+
options.installDeps !== false &&
|
|
885
938
|
pkgList.length &&
|
|
886
939
|
isPartialTree(dependencies, pkgList.length)
|
|
887
940
|
) {
|
|
@@ -1108,6 +1161,28 @@ export function createHelmBom(path, options) {
|
|
|
1108
1161
|
return {};
|
|
1109
1162
|
}
|
|
1110
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
|
+
|
|
1111
1186
|
/**
|
|
1112
1187
|
* Function to create bom string for php projects
|
|
1113
1188
|
*
|
|
@@ -1140,6 +1215,10 @@ export function createPHPBom(path, options) {
|
|
|
1140
1215
|
composerLockSearchOptions,
|
|
1141
1216
|
);
|
|
1142
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 = [];
|
|
1143
1222
|
const composerJsonMode = composerJsonFiles.length;
|
|
1144
1223
|
const composerLockMode = composerLockFiles.length;
|
|
1145
1224
|
// Create a composer.lock file for each composer.json file if needed.
|
|
@@ -1253,6 +1332,37 @@ export function createPHPBom(path, options) {
|
|
|
1253
1332
|
}
|
|
1254
1333
|
}
|
|
1255
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
|
+
}
|
|
1256
1366
|
if (retMap.pkgList?.length) {
|
|
1257
1367
|
pkgList = pkgList.concat(retMap.pkgList);
|
|
1258
1368
|
pkgList = trimComponents(pkgList);
|
|
@@ -1292,6 +1402,7 @@ export function createPHPBom(path, options) {
|
|
|
1292
1402
|
filename: composerLockFiles.join(", "),
|
|
1293
1403
|
dependencies,
|
|
1294
1404
|
parentComponent,
|
|
1405
|
+
metadataProperties,
|
|
1295
1406
|
});
|
|
1296
1407
|
}
|
|
1297
1408
|
if (composerJsonMode && !composerLockMode) {
|
|
@@ -1539,7 +1650,14 @@ export async function createRubyBom(path, options) {
|
|
|
1539
1650
|
options.resolveClass ||
|
|
1540
1651
|
(options.deep && !hasAnyProjectType(["oci"], options, false))
|
|
1541
1652
|
) {
|
|
1542
|
-
|
|
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) {
|
|
1543
1661
|
const rubyCommand =
|
|
1544
1662
|
readEnvironmentVariable("CDXGEN_RUBY_CMD") ||
|
|
1545
1663
|
readEnvironmentVariable("RUBY_CMD") ||
|
|
@@ -1655,6 +1773,12 @@ export async function createCsharpBom(path, options) {
|
|
|
1655
1773
|
return createBinaryBom(path, options);
|
|
1656
1774
|
}
|
|
1657
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
|
+
);
|
|
1658
1782
|
const slnFiles = getAllFiles(
|
|
1659
1783
|
path,
|
|
1660
1784
|
`${options.multiProject ? "**/" : ""}*.sln*`,
|
|
@@ -1939,6 +2063,47 @@ export async function createCsharpBom(path, options) {
|
|
|
1939
2063
|
// one project's manifest are not dropped just because another format exists
|
|
1940
2064
|
// elsewhere in the scan.
|
|
1941
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
|
+
};
|
|
1942
2107
|
// project.assets.json parsing
|
|
1943
2108
|
if (projAssetsFiles.length) {
|
|
1944
2109
|
manifestFiles = manifestFiles.concat(projAssetsFiles);
|
|
@@ -1952,9 +2117,13 @@ export async function createCsharpBom(path, options) {
|
|
|
1952
2117
|
const dlist = results["pkgList"];
|
|
1953
2118
|
// Restore output names its project; an artifacts layout keeps it outside
|
|
1954
2119
|
// the project directory.
|
|
1955
|
-
const
|
|
2120
|
+
const projectFile = foundProjectFile(
|
|
2121
|
+
dotnetRestoredProjectFile(af, results.projectPath, results.outputPath),
|
|
2122
|
+
);
|
|
2123
|
+
const projectDir = dotnetManifestProjectDir(af, projectFile);
|
|
1956
2124
|
assetsProjectDirs.add(projectDir);
|
|
1957
2125
|
recordProjectVersions(projectDir, dlist);
|
|
2126
|
+
recordRestoredIdentity(projectFile, results.projectIdentity);
|
|
1958
2127
|
if (dlist?.length) {
|
|
1959
2128
|
pkgList = pkgList.concat(dlist);
|
|
1960
2129
|
}
|
|
@@ -2065,6 +2234,7 @@ export async function createCsharpBom(path, options) {
|
|
|
2065
2234
|
}
|
|
2066
2235
|
const projectRefByDir = {};
|
|
2067
2236
|
let services = [];
|
|
2237
|
+
let dosaiTools = [];
|
|
2068
2238
|
if (csProjFiles.length) {
|
|
2069
2239
|
manifestFiles = manifestFiles.concat(csProjFiles);
|
|
2070
2240
|
// Parsing csproj is quite error-prone. Some project files may not have
|
|
@@ -2073,11 +2243,18 @@ export async function createCsharpBom(path, options) {
|
|
|
2073
2243
|
// whole scan agrees on.
|
|
2074
2244
|
const agreedVersions = agreedNugetVersions(pkgList);
|
|
2075
2245
|
let msbuildInstalled = false;
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
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
|
+
}
|
|
2081
2258
|
}
|
|
2082
2259
|
// `$(Name)` version labels resolve from the props files that can apply to
|
|
2083
2260
|
// each project (beside and above it), read once for the whole scan.
|
|
@@ -2103,6 +2280,7 @@ export async function createCsharpBom(path, options) {
|
|
|
2103
2280
|
getCentralPackageVersions(f, centralVersionsCache),
|
|
2104
2281
|
agreedVersions,
|
|
2105
2282
|
projectNames[projectDir] || {},
|
|
2283
|
+
restoredIdentities.get(resolve(f)) || undefined,
|
|
2106
2284
|
);
|
|
2107
2285
|
if (retMap?.parentComponent?.purl) {
|
|
2108
2286
|
projectRefByDir[projectDir] ??= retMap.parentComponent["bom-ref"];
|
|
@@ -2127,6 +2305,24 @@ export async function createCsharpBom(path, options) {
|
|
|
2127
2305
|
parentComponent,
|
|
2128
2306
|
);
|
|
2129
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
|
+
}
|
|
2130
2326
|
}
|
|
2131
2327
|
}
|
|
2132
2328
|
// A lock file's or packages.config's direct dependencies belong to the
|
|
@@ -2148,6 +2344,17 @@ export async function createCsharpBom(path, options) {
|
|
|
2148
2344
|
}
|
|
2149
2345
|
}
|
|
2150
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"]));
|
|
2151
2358
|
if (pkgList.length) {
|
|
2152
2359
|
pkgList = trimComponents(pkgList);
|
|
2153
2360
|
// Perform deep analysis using dosai
|
|
@@ -2166,19 +2373,46 @@ export async function createCsharpBom(path, options) {
|
|
|
2166
2373
|
? resolve(path, options.depsSlicesFile)
|
|
2167
2374
|
: join(slicesDir, "dosai.json");
|
|
2168
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
|
+
}
|
|
2169
2397
|
// Create the slices file if it doesn't exist
|
|
2170
|
-
if (!
|
|
2398
|
+
if (!reuseExisting) {
|
|
2171
2399
|
thoughtLog(
|
|
2172
2400
|
"Alright, the next step is to invoke the dosai command to identify evidence of occurrences for various components.",
|
|
2173
2401
|
);
|
|
2174
2402
|
const sliceResult = createDosaiMethodsSlice(
|
|
2175
2403
|
resolve(path),
|
|
2176
2404
|
resolve(slicesFile),
|
|
2177
|
-
|
|
2405
|
+
{
|
|
2406
|
+
...options,
|
|
2407
|
+
dosaiMethodsSections: willRunEvinse
|
|
2408
|
+
? undefined
|
|
2409
|
+
: dosaiMethodsSectionsArgument(),
|
|
2410
|
+
},
|
|
2178
2411
|
);
|
|
2179
|
-
// A stopped run
|
|
2180
|
-
//
|
|
2181
|
-
|
|
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))) {
|
|
2182
2416
|
// Visible without debug output: otherwise the components silently
|
|
2183
2417
|
// lose the dependency-slice evidence while the scan still exits 0.
|
|
2184
2418
|
// --evidence (which implies --deep) runs dosai again during evinse
|
|
@@ -2205,6 +2439,11 @@ export async function createCsharpBom(path, options) {
|
|
|
2205
2439
|
const servicesMap = collectDosaiServiceComponents(methodsSlice, {});
|
|
2206
2440
|
collectDosaiServicesFromMethods(methodsSlice, servicesMap);
|
|
2207
2441
|
services = normalizeDosaiServiceMap(servicesMap);
|
|
2442
|
+
dosaiTools = dosaiToolComponents(
|
|
2443
|
+
slicesFile,
|
|
2444
|
+
methodsSlice,
|
|
2445
|
+
getPluginToolComponents(["dosai"])[0],
|
|
2446
|
+
);
|
|
2208
2447
|
} finally {
|
|
2209
2448
|
if (slicesDir) {
|
|
2210
2449
|
safeRmSync(slicesDir, { recursive: true, force: true });
|
|
@@ -2244,7 +2483,7 @@ export async function createCsharpBom(path, options) {
|
|
|
2244
2483
|
dependencies,
|
|
2245
2484
|
parentComponent,
|
|
2246
2485
|
services,
|
|
2247
|
-
tools:
|
|
2486
|
+
tools: dosaiTools,
|
|
2248
2487
|
});
|
|
2249
2488
|
}
|
|
2250
2489
|
|
|
@@ -2263,58 +2502,239 @@ export async function createCryptoCertsBom(path, options) {
|
|
|
2263
2502
|
},
|
|
2264
2503
|
};
|
|
2265
2504
|
}
|
|
2505
|
+
const collector = new CertificateCollector();
|
|
2266
2506
|
const pkgList = [];
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
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,
|
|
2273
2540
|
);
|
|
2274
|
-
const
|
|
2275
|
-
|
|
2541
|
+
const pluginMaterials = runTrustInspectorCertsMode(
|
|
2542
|
+
pluginCandidateFiles,
|
|
2543
|
+
locationRoot,
|
|
2276
2544
|
);
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
const
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
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(
|
|
2288
2604
|
{
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
value: f,
|
|
2605
|
+
format: classification.format,
|
|
2606
|
+
sourceType: deriveCryptoSourceType(f),
|
|
2292
2607
|
},
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
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
|
+
);
|
|
2300
2625
|
}
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
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,
|
|
2311
2715
|
},
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
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
|
+
}
|
|
2317
2736
|
}
|
|
2737
|
+
pkgList.push(...certificateComponents);
|
|
2318
2738
|
const sourceCryptoComponents = await collectSourceCryptoComponents(
|
|
2319
2739
|
path,
|
|
2320
2740
|
options,
|
|
@@ -2335,9 +2755,229 @@ export async function createCryptoCertsBom(path, options) {
|
|
|
2335
2755
|
bomJson: {
|
|
2336
2756
|
components: filterInvalidCryptoComponents(pkgList),
|
|
2337
2757
|
},
|
|
2758
|
+
dependencies: certificateDependencies,
|
|
2338
2759
|
};
|
|
2339
2760
|
}
|
|
2340
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
|
+
],
|
|
2914
|
+
};
|
|
2915
|
+
}
|
|
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
|
+
|
|
2341
2981
|
/**
|
|
2342
2982
|
* Function to create bom string for R projects using renv.
|
|
2343
2983
|
*
|