@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/inventory/hbom.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { basename, win32 } from "node:path";
|
|
3
|
+
import process from "node:process";
|
|
2
4
|
|
|
3
5
|
import {
|
|
4
6
|
isDryRun,
|
|
@@ -40,6 +42,18 @@ function getConfiguredHbomPathAllowlist() {
|
|
|
40
42
|
);
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
// A Windows plan can be inspected from any host (`getCommandPlan`), where
|
|
46
|
+
// node:path would not split a path on its backslashes.
|
|
47
|
+
function isWindowsStyleCommand(command) {
|
|
48
|
+
return command.includes("\\") || /^[A-Za-z]:/u.test(command);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function getHbomCommandName(command) {
|
|
52
|
+
return isWindowsStyleCommand(command)
|
|
53
|
+
? win32.basename(command)
|
|
54
|
+
: basename(command);
|
|
55
|
+
}
|
|
56
|
+
|
|
43
57
|
function isAllowedHbomCommand(command, allowlistValue = undefined) {
|
|
44
58
|
const normalizedCommand = `${command ?? ""}`.trim();
|
|
45
59
|
if (!normalizedCommand) {
|
|
@@ -51,7 +65,10 @@ function isAllowedHbomCommand(command, allowlistValue = undefined) {
|
|
|
51
65
|
if (!allowlistEntries.length) {
|
|
52
66
|
return true;
|
|
53
67
|
}
|
|
54
|
-
|
|
68
|
+
// Exact and case-sensitive on purpose: cdx-hbom applies the same allowlist
|
|
69
|
+
// again when it spawns, so a looser preflight would pass here and then run
|
|
70
|
+
// nothing, which silently degrades the BOM.
|
|
71
|
+
const commandName = getHbomCommandName(normalizedCommand);
|
|
55
72
|
return allowlistEntries.some(
|
|
56
73
|
(entry) => entry === normalizedCommand || entry === commandName,
|
|
57
74
|
);
|
|
@@ -61,9 +78,27 @@ function formatHbomCommandTarget(command, args = []) {
|
|
|
61
78
|
return `${command}${args.length ? ` ${args.join(" ")}` : ""}`;
|
|
62
79
|
}
|
|
63
80
|
|
|
81
|
+
// Windows plan entries carry a multi-kilobyte PowerShell script as their last
|
|
82
|
+
// argument. Like the cdx-hbom trace and evidence, the preflight names it by
|
|
83
|
+
// digest so a blocked run does not print every script.
|
|
84
|
+
function summarizeHbomPlanArgs(planEntry) {
|
|
85
|
+
const args = [...(planEntry.args || [])];
|
|
86
|
+
if (planEntry.parser !== "windows-envelope" || !args.length) {
|
|
87
|
+
return args;
|
|
88
|
+
}
|
|
89
|
+
const digest = createHash("sha256")
|
|
90
|
+
.update(String(args.at(-1)))
|
|
91
|
+
.digest("hex")
|
|
92
|
+
.slice(0, 12);
|
|
93
|
+
return ["-Command", planEntry.id, "script", `sha256:${digest}`];
|
|
94
|
+
}
|
|
95
|
+
|
|
64
96
|
function buildHbomPlanActivities(planEntry, normalizedOptions) {
|
|
65
97
|
const baseArgs = [...(planEntry.args || [])];
|
|
66
|
-
const baseTarget = formatHbomCommandTarget(
|
|
98
|
+
const baseTarget = formatHbomCommandTarget(
|
|
99
|
+
planEntry.command,
|
|
100
|
+
summarizeHbomPlanArgs(planEntry),
|
|
101
|
+
);
|
|
67
102
|
const activities = [
|
|
68
103
|
{
|
|
69
104
|
args: baseArgs,
|
|
@@ -115,7 +150,7 @@ function collectDisallowedHbomCommands(
|
|
|
115
150
|
}
|
|
116
151
|
const existingEntry = disallowedCommands.get(requestedCommand) ?? {
|
|
117
152
|
command: requestedCommand,
|
|
118
|
-
commandName:
|
|
153
|
+
commandName: getHbomCommandName(requestedCommand),
|
|
119
154
|
ids: new Set(),
|
|
120
155
|
targets: new Set(),
|
|
121
156
|
};
|
|
@@ -214,6 +249,28 @@ function createHbomAllowlistPreflightError(
|
|
|
214
249
|
return new Error(messageLines.join("\n"));
|
|
215
250
|
}
|
|
216
251
|
|
|
252
|
+
/**
|
|
253
|
+
* Whether cdx-hbom spawns no command for these collector options.
|
|
254
|
+
*
|
|
255
|
+
* Only the Linux collector reads `includeCommandEnrichment: false` as "run no
|
|
256
|
+
* commands", and even there the privileged dmidecode step still runs. The
|
|
257
|
+
* Darwin collector ignores the option, and the Windows collector always runs
|
|
258
|
+
* its `windows-cim-platform` batch, so those plans are checked in full.
|
|
259
|
+
*
|
|
260
|
+
* @param {object} normalizedOptions Collector options from normalizeHbomOptions
|
|
261
|
+
* @returns {boolean} `true` when the allowlist has nothing to check
|
|
262
|
+
*/
|
|
263
|
+
function collectorSpawnsNoCommands(normalizedOptions) {
|
|
264
|
+
const targetPlatform = `${normalizedOptions.platform ?? process.platform}`
|
|
265
|
+
.trim()
|
|
266
|
+
.toLowerCase();
|
|
267
|
+
return (
|
|
268
|
+
normalizedOptions.includeCommandEnrichment === false &&
|
|
269
|
+
normalizedOptions.includePrivilegedEnrichment !== true &&
|
|
270
|
+
targetPlatform === "linux"
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
217
274
|
async function enforceSecureModeHbomAllowlists(hbomModule, normalizedOptions) {
|
|
218
275
|
if (isDryRun || normalizedOptions?.isDryRun || !isSecureMode) {
|
|
219
276
|
return;
|
|
@@ -249,7 +306,7 @@ async function enforceSecureModeHbomAllowlists(hbomModule, normalizedOptions) {
|
|
|
249
306
|
: [];
|
|
250
307
|
} else if (
|
|
251
308
|
hasCommandAllowlist &&
|
|
252
|
-
normalizedOptions
|
|
309
|
+
!collectorSpawnsNoCommands(normalizedOptions)
|
|
253
310
|
) {
|
|
254
311
|
traceActivities = hbomModule
|
|
255
312
|
.getCommandPlan(normalizedOptions)
|
|
@@ -259,7 +316,7 @@ async function enforceSecureModeHbomAllowlists(hbomModule, normalizedOptions) {
|
|
|
259
316
|
}
|
|
260
317
|
|
|
261
318
|
const disallowedCommands =
|
|
262
|
-
hasCommandAllowlist && normalizedOptions
|
|
319
|
+
hasCommandAllowlist && !collectorSpawnsNoCommands(normalizedOptions)
|
|
263
320
|
? collectDisallowedHbomCommands(traceActivities, commandAllowlistValue)
|
|
264
321
|
: [];
|
|
265
322
|
const disallowedPaths = hasPathAllowlist
|
|
@@ -398,8 +455,11 @@ export function normalizeHbomOptions(options = {}) {
|
|
|
398
455
|
timeoutValue === undefined || timeoutValue === null || timeoutValue === ""
|
|
399
456
|
? undefined
|
|
400
457
|
: Number.parseInt(`${timeoutValue}`, 10);
|
|
458
|
+
// yargs negates `--no-command-enrichment` into `commandEnrichment: false`
|
|
459
|
+
// and never sets `noCommandEnrichment`, so the CLIs rely on this spelling.
|
|
401
460
|
const includeCommandEnrichment =
|
|
402
|
-
options.includeCommandEnrichment ??
|
|
461
|
+
options.includeCommandEnrichment ??
|
|
462
|
+
(options.commandEnrichment !== false && !options.noCommandEnrichment);
|
|
403
463
|
const allowPartial = options.allowPartial ?? !options.strict;
|
|
404
464
|
|
|
405
465
|
return {
|
|
@@ -174,6 +174,22 @@ export function isHbomLikeBom(bomJson) {
|
|
|
174
174
|
);
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Explain how the permission-denied enrichments of an HBOM can be recovered.
|
|
179
|
+
*
|
|
180
|
+
* Windows never uses sudo: the elevated-only batch needs an already-elevated
|
|
181
|
+
* session, and a network logon is denied every WMI query.
|
|
182
|
+
*
|
|
183
|
+
* @param {string|undefined} platform `cdx:hbom:platform` of the HBOM
|
|
184
|
+
* @returns {string} Guidance for the platform; the Linux wording for any other platform
|
|
185
|
+
*/
|
|
186
|
+
export function getHbomPrivilegeGuidance(platform) {
|
|
187
|
+
if (`${platform ?? ""}`.toLowerCase() === "windows") {
|
|
188
|
+
return "Some Windows enrichments were denied. Run HBOM from a local session (a non-administrator over SSH or WinRM is denied every WMI query) and, for the TPM, BitLocker, storage-reliability and Secure Boot data, from an elevated (Run as administrator) prompt with --privileged. HBOM never prompts for elevation.";
|
|
189
|
+
}
|
|
190
|
+
return "Some Linux enrichments can likely succeed only with --privileged and a target environment that allows non-interactive sudo.";
|
|
191
|
+
}
|
|
192
|
+
|
|
177
193
|
/**
|
|
178
194
|
* Return a component's `cdx:hbom:hardwareClass` property value.
|
|
179
195
|
*
|
|
@@ -276,6 +276,7 @@ function isFilesystemIdentity(value) {
|
|
|
276
276
|
function createRuntimeIndexes(runtimeComponents) {
|
|
277
277
|
const interfaceAddressesByName = new Map();
|
|
278
278
|
const kernelModulesByName = new Map();
|
|
279
|
+
const logicalDrivesByDeviceId = new Map();
|
|
279
280
|
const mountPathsByDevice = new Map();
|
|
280
281
|
const mountPathsByPath = new Map();
|
|
281
282
|
const runtimeByStorageIdentity = new Map();
|
|
@@ -306,6 +307,13 @@ function createRuntimeIndexes(runtimeComponents) {
|
|
|
306
307
|
component["bom-ref"],
|
|
307
308
|
);
|
|
308
309
|
}
|
|
310
|
+
if (runtimeCategory === "logical_drives") {
|
|
311
|
+
appendMapEntry(
|
|
312
|
+
logicalDrivesByDeviceId,
|
|
313
|
+
toNormalizedIdentity(getPropertyValue(component, "device_id")),
|
|
314
|
+
component["bom-ref"],
|
|
315
|
+
);
|
|
316
|
+
}
|
|
309
317
|
if (runtimeCategory === "mount_hardening") {
|
|
310
318
|
appendMapEntry(
|
|
311
319
|
mountPathsByDevice,
|
|
@@ -339,6 +347,7 @@ function createRuntimeIndexes(runtimeComponents) {
|
|
|
339
347
|
return {
|
|
340
348
|
interfaceAddressesByName,
|
|
341
349
|
kernelModulesByName,
|
|
350
|
+
logicalDrivesByDeviceId,
|
|
342
351
|
mountPathsByDevice,
|
|
343
352
|
mountPathsByPath,
|
|
344
353
|
runtimeByStorageIdentity,
|
|
@@ -532,6 +541,14 @@ function createDependencyEdgeList(
|
|
|
532
541
|
"mount_hardening",
|
|
533
542
|
Array.from(indexes.mountPathsByPath.get(mountPath) || []),
|
|
534
543
|
);
|
|
544
|
+
// The Windows osquery pack has no mount_hardening; a volume's drive
|
|
545
|
+
// letter (C:) is the device_id of its logical drive.
|
|
546
|
+
addCategoryRefs(
|
|
547
|
+
linkedRefs,
|
|
548
|
+
linkedCategoryRefs,
|
|
549
|
+
"logical_drives",
|
|
550
|
+
Array.from(indexes.logicalDrivesByDeviceId.get(mountPath) || []),
|
|
551
|
+
);
|
|
535
552
|
}
|
|
536
553
|
const explicitFilesystemIdentity = isFilesystemIdentity(component.version)
|
|
537
554
|
? toNormalizedIdentity(component.version)
|
|
@@ -788,6 +805,10 @@ export function mergeHostInventoryBoms(hbomJson, obomData) {
|
|
|
788
805
|
if (obomData.parentComponent?.name && obomData.parentComponent?.type) {
|
|
789
806
|
runtimeComponents.unshift(obomData.parentComponent);
|
|
790
807
|
}
|
|
808
|
+
// trimComponents keys a component without a purl or bom-ref on name and
|
|
809
|
+
// version, which would fold hardware that shares a name (every Windows
|
|
810
|
+
// volume without a drive letter is named "Volume") into one component.
|
|
811
|
+
ensureBomRefs(hbomJson);
|
|
791
812
|
const mergedBomJson = {
|
|
792
813
|
...hbomJson,
|
|
793
814
|
components: trimComponents([
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// The one place that decides what purl an operating-system package gets.
|
|
2
|
+
//
|
|
3
|
+
// A package found by `cdxgen -t docker <image>` and by `obom` run inside that
|
|
4
|
+
// image must be the same package, so the OBOM rows, the file-to-package
|
|
5
|
+
// resolver and the container scan all name a deb, an rpm or an apk package with
|
|
6
|
+
// the vendor and the qualifiers set here and in getDistroInfo(): `arch`,
|
|
7
|
+
// `distro`, `distro_name` and, when the package has one, `epoch`. A pacman
|
|
8
|
+
// package has the vendor and only the `arch` qualifier the alpm type defines.
|
|
9
|
+
// Homebrew, MacPorts and snap packages have no distro, so their purl is the
|
|
10
|
+
// name and the version; a flatpak adds the `arch` and `branch` of its ref.
|
|
11
|
+
//
|
|
12
|
+
// None of these purls has a subpath. A subpath names a path inside a package;
|
|
13
|
+
// the dpkg directory or the source rpm of a row is not one.
|
|
14
|
+
|
|
15
|
+
import { getDistroInfo, isCleanDistroQualifier } from "./osinfo.js";
|
|
16
|
+
import { tryBuildPurl } from "./purl.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The purl types an OS scan builds from what a package manager reports.
|
|
20
|
+
* deb, rpm, apk and alpm belong to a distro; brew, macports, snap and flatpak
|
|
21
|
+
* belong to none. brew, macports, snap and flatpak are not purl-spec types;
|
|
22
|
+
* cdx-purl builds them as it builds `pkg:chocolatey`.
|
|
23
|
+
*/
|
|
24
|
+
export const OS_PACKAGE_PURL_TYPES = new Set([
|
|
25
|
+
"deb",
|
|
26
|
+
"rpm",
|
|
27
|
+
"apk",
|
|
28
|
+
"alpm",
|
|
29
|
+
"brew",
|
|
30
|
+
"macports",
|
|
31
|
+
"snap",
|
|
32
|
+
"flatpak",
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Whether a purl type is one of the OS package managers handled here.
|
|
37
|
+
*
|
|
38
|
+
* @param {string|undefined} type Purl type
|
|
39
|
+
* @returns {boolean} true for the types in OS_PACKAGE_PURL_TYPES
|
|
40
|
+
*/
|
|
41
|
+
export function isOsPackagePurlType(type) {
|
|
42
|
+
return OS_PACKAGE_PURL_TYPES.has(type);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Whether a value names an architecture. rpm says `(none)` and trivy says
|
|
47
|
+
* `None` for a package that has none, such as an rpm's `gpg-pubkey` key record;
|
|
48
|
+
* neither is an architecture, and neither belongs in a purl.
|
|
49
|
+
*
|
|
50
|
+
* @param {string|undefined|null} arch Architecture as a package manager reports it
|
|
51
|
+
* @returns {boolean} true for a real architecture
|
|
52
|
+
*/
|
|
53
|
+
export function isRealArchitecture(arch) {
|
|
54
|
+
const value = `${arch ?? ""}`.trim().toLowerCase();
|
|
55
|
+
return value !== "" && value !== "(none)" && value !== "none";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isDigits(text) {
|
|
59
|
+
if (!text) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
for (let i = 0; i < text.length; i++) {
|
|
63
|
+
const code = text.charCodeAt(i);
|
|
64
|
+
if (code < 48 || code > 57) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// An epoch is a non-negative integer, and 0 is the same as none. Anything else
|
|
72
|
+
// ("", "(none)", "null") is no epoch.
|
|
73
|
+
function normaliseEpoch(value) {
|
|
74
|
+
const text = `${value ?? ""}`.trim();
|
|
75
|
+
if (!isDigits(text)) {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
const digits = text.replace(/^0+/, "");
|
|
79
|
+
return digits || undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// dpkg writes a version as [epoch:]upstream[-revision]. A colon is only ever an
|
|
83
|
+
// epoch's separator when what comes before it is all digits.
|
|
84
|
+
function debVersionEpoch(version) {
|
|
85
|
+
const colon = version.indexOf(":");
|
|
86
|
+
if (colon <= 0) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
const prefix = version.slice(0, colon);
|
|
90
|
+
return isDigits(prefix) ? prefix : undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The epoch of an OS package, as the `epoch` qualifier carries it.
|
|
95
|
+
*
|
|
96
|
+
* An rpm keeps its epoch in a field of its own. A deb keeps it in its version
|
|
97
|
+
* (`2:93`), so the version is read first. Epoch 0 and no epoch are the same,
|
|
98
|
+
* and apk and Homebrew packages have none.
|
|
99
|
+
*
|
|
100
|
+
* @param {{type: string, version?: string, epoch?: string|number}} pkg Package
|
|
101
|
+
* @returns {string|undefined} The epoch without leading zeros, or undefined
|
|
102
|
+
*/
|
|
103
|
+
export function osPackageEpoch(pkg) {
|
|
104
|
+
switch (pkg?.type) {
|
|
105
|
+
case "rpm":
|
|
106
|
+
return normaliseEpoch(pkg.epoch);
|
|
107
|
+
case "deb": {
|
|
108
|
+
const fromVersion = debVersionEpoch(`${pkg.version ?? ""}`);
|
|
109
|
+
return fromVersion === undefined
|
|
110
|
+
? normaliseEpoch(pkg.epoch)
|
|
111
|
+
: normaliseEpoch(fromVersion);
|
|
112
|
+
}
|
|
113
|
+
default:
|
|
114
|
+
return undefined;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The version an OS package carries on its component and in its purl.
|
|
120
|
+
*
|
|
121
|
+
* - rpm: `[epoch:]version-release`, the form the container scan writes. The
|
|
122
|
+
* purl spec keeps the epoch out of an rpm version, but the container scan
|
|
123
|
+
* puts it in the version as well as in the `epoch` qualifier, and an OBOM must
|
|
124
|
+
* name the package the same way.
|
|
125
|
+
* - deb: the version exactly as dpkg reports it, epoch included.
|
|
126
|
+
* - macports: `version_revision`, the form `port installed` prints.
|
|
127
|
+
* - apk, alpm, brew, snap and flatpak: the version as it is. A pacman version
|
|
128
|
+
* already holds its epoch and release (`1:1.3.2-4`).
|
|
129
|
+
*
|
|
130
|
+
* @param {{type: string, version?: string, release?: string, epoch?: string|number, revision?: string|number}} pkg Package
|
|
131
|
+
* @returns {string} The version, or an empty string when the package has none
|
|
132
|
+
*/
|
|
133
|
+
export function osPackageVersion(pkg) {
|
|
134
|
+
const version = `${pkg?.version ?? ""}`;
|
|
135
|
+
if (!version) {
|
|
136
|
+
return "";
|
|
137
|
+
}
|
|
138
|
+
switch (pkg.type) {
|
|
139
|
+
case "rpm": {
|
|
140
|
+
const release = `${pkg.release ?? ""}`;
|
|
141
|
+
const evr = release ? `${version}-${release}` : version;
|
|
142
|
+
const epoch = osPackageEpoch(pkg);
|
|
143
|
+
return epoch ? `${epoch}:${evr}` : evr;
|
|
144
|
+
}
|
|
145
|
+
case "deb": {
|
|
146
|
+
if (debVersionEpoch(version) !== undefined) {
|
|
147
|
+
return version;
|
|
148
|
+
}
|
|
149
|
+
const epoch = normaliseEpoch(pkg.epoch);
|
|
150
|
+
return epoch ? `${epoch}:${version}` : version;
|
|
151
|
+
}
|
|
152
|
+
case "macports": {
|
|
153
|
+
// Revision 0 is a real revision: `port installed` prints `@3.6.0_0`.
|
|
154
|
+
const revision = `${pkg.revision ?? ""}`.trim();
|
|
155
|
+
return isDigits(revision) ? `${version}_${revision}` : version;
|
|
156
|
+
}
|
|
157
|
+
default:
|
|
158
|
+
return version;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Build the purl of an OS package.
|
|
164
|
+
*
|
|
165
|
+
* deb, rpm and apk purls take their vendor (`ubuntu`, `almalinux`, `alpine`,
|
|
166
|
+
* `wolfi`) and the `distro` and `distro_name` qualifiers from the distro, plus
|
|
167
|
+
* `arch` and `epoch` from the package. alpm purls take the vendor (`arch`,
|
|
168
|
+
* `manjaro`) and `arch` only: that is all the alpm type defines, and an Arch
|
|
169
|
+
* system is rolling, with no release to name. A qualifier with no value is
|
|
170
|
+
* left out. brew, macports and snap purls carry neither vendor nor qualifiers;
|
|
171
|
+
* a flatpak purl carries the `arch` and `branch` of the flatpak ref.
|
|
172
|
+
*
|
|
173
|
+
* @param {Object} pkg The package
|
|
174
|
+
* @param {string} pkg.type One of OS_PACKAGE_PURL_TYPES
|
|
175
|
+
* @param {string} pkg.name Package name; a flatpak's application ID
|
|
176
|
+
* @param {string} [pkg.version] deb: the dpkg version. rpm: the version tag only. apk: version-rN. macports: the version without its revision.
|
|
177
|
+
* @param {string} [pkg.release] rpm release tag
|
|
178
|
+
* @param {string|number} [pkg.epoch] rpm epoch tag
|
|
179
|
+
* @param {string|number} [pkg.revision] MacPorts revision
|
|
180
|
+
* @param {string} [pkg.arch] Package architecture
|
|
181
|
+
* @param {string} [pkg.branch] flatpak branch (`stable`, `48`)
|
|
182
|
+
* @param {{namespace?: string, distroId?: string, distroName?: string}} [distro]
|
|
183
|
+
* What getDistroInfo() reports; read from the host's os-release when left out
|
|
184
|
+
* @returns {string|null} The purl, or null when the parts do not form a valid one
|
|
185
|
+
*/
|
|
186
|
+
export function buildOsPackagePurl(pkg, distro) {
|
|
187
|
+
const type = pkg?.type;
|
|
188
|
+
const name = `${pkg?.name ?? ""}`;
|
|
189
|
+
if (!isOsPackagePurlType(type) || !name) {
|
|
190
|
+
return null;
|
|
191
|
+
}
|
|
192
|
+
const version = osPackageVersion(pkg) || null;
|
|
193
|
+
switch (type) {
|
|
194
|
+
case "brew":
|
|
195
|
+
case "macports":
|
|
196
|
+
case "snap":
|
|
197
|
+
return tryBuildPurl({ type, name, version });
|
|
198
|
+
case "flatpak": {
|
|
199
|
+
const qualifiers = {};
|
|
200
|
+
if (isRealArchitecture(pkg.arch)) {
|
|
201
|
+
qualifiers.arch = `${pkg.arch}`.trim();
|
|
202
|
+
}
|
|
203
|
+
const branch = `${pkg.branch ?? ""}`.trim();
|
|
204
|
+
if (branch) {
|
|
205
|
+
qualifiers.branch = branch;
|
|
206
|
+
}
|
|
207
|
+
return tryBuildPurl({
|
|
208
|
+
type,
|
|
209
|
+
name,
|
|
210
|
+
version,
|
|
211
|
+
qualifiers: Object.keys(qualifiers).length ? qualifiers : null,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
default:
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
const { namespace, distroId, distroName } = distro || getDistroInfo();
|
|
218
|
+
if (!namespace) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
const qualifiers = {};
|
|
222
|
+
if (isRealArchitecture(pkg.arch)) {
|
|
223
|
+
qualifiers.arch = `${pkg.arch}`.trim();
|
|
224
|
+
}
|
|
225
|
+
if (type !== "alpm") {
|
|
226
|
+
if (isCleanDistroQualifier(distroId)) {
|
|
227
|
+
qualifiers.distro = distroId;
|
|
228
|
+
}
|
|
229
|
+
if (isCleanDistroQualifier(distroName)) {
|
|
230
|
+
qualifiers.distro_name = distroName;
|
|
231
|
+
}
|
|
232
|
+
const epoch = osPackageEpoch(pkg);
|
|
233
|
+
if (epoch) {
|
|
234
|
+
qualifiers.epoch = epoch;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return tryBuildPurl({
|
|
238
|
+
type,
|
|
239
|
+
namespace,
|
|
240
|
+
name,
|
|
241
|
+
version,
|
|
242
|
+
qualifiers: Object.keys(qualifiers).length ? qualifiers : null,
|
|
243
|
+
});
|
|
244
|
+
}
|