@cyclonedx/cdxgen 12.3.3 → 12.4.1

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.
Files changed (175) hide show
  1. package/README.md +69 -25
  2. package/bin/audit.js +21 -7
  3. package/bin/cdxgen.js +270 -127
  4. package/bin/convert.js +34 -15
  5. package/bin/hbom.js +495 -0
  6. package/bin/repl.js +592 -37
  7. package/bin/validate.js +31 -4
  8. package/bin/verify.js +18 -5
  9. package/data/README.md +298 -25
  10. package/data/component-tags.json +6 -0
  11. package/data/crypto-oid.json +16 -0
  12. package/data/cyclonedx-2.0-bundled.schema.json +7182 -0
  13. package/data/predictive-audit-allowlist.json +11 -0
  14. package/data/queries-darwin.json +12 -1
  15. package/data/queries-win.json +7 -1
  16. package/data/queries.json +39 -2
  17. package/data/rules/ai-agent-governance.yaml +16 -0
  18. package/data/rules/asar-archives.yaml +150 -0
  19. package/data/rules/chrome-extensions.yaml +8 -0
  20. package/data/rules/ci-permissions.yaml +42 -18
  21. package/data/rules/container-risk.yaml +14 -7
  22. package/data/rules/dependency-sources.yaml +11 -0
  23. package/data/rules/hbom-compliance.yaml +325 -0
  24. package/data/rules/hbom-performance.yaml +307 -0
  25. package/data/rules/hbom-security.yaml +248 -0
  26. package/data/rules/host-topology.yaml +165 -0
  27. package/data/rules/mcp-servers.yaml +18 -3
  28. package/data/rules/obom-runtime.yaml +907 -22
  29. package/data/rules/package-integrity.yaml +14 -0
  30. package/data/rules/rootfs-hardening.yaml +179 -0
  31. package/data/rules/vscode-extensions.yaml +9 -0
  32. package/lib/audit/index.js +210 -8
  33. package/lib/audit/index.poku.js +332 -0
  34. package/lib/audit/reporters.js +222 -0
  35. package/lib/audit/targets.js +146 -1
  36. package/lib/audit/targets.poku.js +186 -0
  37. package/lib/cli/asar.poku.js +328 -0
  38. package/lib/cli/index.js +527 -99
  39. package/lib/cli/index.poku.js +1469 -212
  40. package/lib/evinser/evinser.js +14 -9
  41. package/lib/helpers/analyzer.js +1406 -29
  42. package/lib/helpers/analyzer.poku.js +342 -0
  43. package/lib/helpers/analyzerScope.js +712 -0
  44. package/lib/helpers/asarutils.js +1556 -0
  45. package/lib/helpers/asarutils.poku.js +443 -0
  46. package/lib/helpers/auditCategories.js +12 -0
  47. package/lib/helpers/auditCategories.poku.js +32 -0
  48. package/lib/helpers/bomUtils.js +155 -1
  49. package/lib/helpers/bomUtils.poku.js +79 -1
  50. package/lib/helpers/cbomutils.js +271 -1
  51. package/lib/helpers/cbomutils.poku.js +248 -5
  52. package/lib/helpers/display.js +291 -1
  53. package/lib/helpers/display.poku.js +149 -0
  54. package/lib/helpers/evidenceUtils.js +58 -0
  55. package/lib/helpers/evidenceUtils.poku.js +54 -0
  56. package/lib/helpers/exportUtils.js +9 -0
  57. package/lib/helpers/gtfobins.js +142 -8
  58. package/lib/helpers/gtfobins.poku.js +24 -1
  59. package/lib/helpers/hbom.js +710 -0
  60. package/lib/helpers/hbom.poku.js +496 -0
  61. package/lib/helpers/hbomAnalysis.js +268 -0
  62. package/lib/helpers/hbomAnalysis.poku.js +249 -0
  63. package/lib/helpers/hbomLoader.js +35 -0
  64. package/lib/helpers/hostTopology.js +803 -0
  65. package/lib/helpers/hostTopology.poku.js +363 -0
  66. package/lib/helpers/inventoryStats.js +69 -0
  67. package/lib/helpers/inventoryStats.poku.js +86 -0
  68. package/lib/helpers/lolbas.js +19 -1
  69. package/lib/helpers/lolbas.poku.js +23 -0
  70. package/lib/helpers/osqueryTransform.js +47 -0
  71. package/lib/helpers/osqueryTransform.poku.js +47 -0
  72. package/lib/helpers/plugins.js +350 -0
  73. package/lib/helpers/plugins.poku.js +57 -0
  74. package/lib/helpers/protobom.js +209 -45
  75. package/lib/helpers/protobom.poku.js +183 -5
  76. package/lib/helpers/protobomLoader.js +43 -0
  77. package/lib/helpers/protobomLoader.poku.js +31 -0
  78. package/lib/helpers/remote/dependency-track.js +36 -3
  79. package/lib/helpers/remote/dependency-track.poku.js +44 -0
  80. package/lib/helpers/source.js +24 -0
  81. package/lib/helpers/source.poku.js +32 -0
  82. package/lib/helpers/utils.js +1438 -93
  83. package/lib/helpers/utils.poku.js +846 -4
  84. package/lib/managers/binary.e2e.poku.js +367 -0
  85. package/lib/managers/binary.js +2293 -353
  86. package/lib/managers/binary.poku.js +1699 -1
  87. package/lib/managers/docker.js +201 -79
  88. package/lib/managers/docker.poku.js +337 -12
  89. package/lib/server/server.js +4 -28
  90. package/lib/stages/postgen/annotator.js +38 -0
  91. package/lib/stages/postgen/annotator.poku.js +107 -1
  92. package/lib/stages/postgen/auditBom.js +121 -18
  93. package/lib/stages/postgen/auditBom.poku.js +1366 -31
  94. package/lib/stages/postgen/hostTopologyAudit.poku.js +186 -0
  95. package/lib/stages/postgen/postgen.js +406 -8
  96. package/lib/stages/postgen/postgen.poku.js +484 -0
  97. package/lib/stages/postgen/ruleEngine.js +116 -0
  98. package/lib/stages/pregen/envAudit.js +14 -3
  99. package/lib/validator/bomValidator.js +90 -38
  100. package/lib/validator/bomValidator.poku.js +90 -0
  101. package/lib/validator/complianceRules.js +4 -2
  102. package/lib/validator/index.poku.js +14 -0
  103. package/package.json +23 -21
  104. package/types/bin/hbom.d.ts +3 -0
  105. package/types/bin/hbom.d.ts.map +1 -0
  106. package/types/bin/repl.d.ts +1 -1
  107. package/types/bin/repl.d.ts.map +1 -1
  108. package/types/lib/audit/index.d.ts +44 -0
  109. package/types/lib/audit/index.d.ts.map +1 -1
  110. package/types/lib/audit/reporters.d.ts +16 -0
  111. package/types/lib/audit/reporters.d.ts.map +1 -1
  112. package/types/lib/audit/targets.d.ts.map +1 -1
  113. package/types/lib/cli/index.d.ts +16 -0
  114. package/types/lib/cli/index.d.ts.map +1 -1
  115. package/types/lib/evinser/evinser.d.ts +4 -0
  116. package/types/lib/evinser/evinser.d.ts.map +1 -1
  117. package/types/lib/helpers/analyzer.d.ts +33 -0
  118. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  119. package/types/lib/helpers/analyzerScope.d.ts +11 -0
  120. package/types/lib/helpers/analyzerScope.d.ts.map +1 -0
  121. package/types/lib/helpers/asarutils.d.ts +34 -0
  122. package/types/lib/helpers/asarutils.d.ts.map +1 -0
  123. package/types/lib/helpers/auditCategories.d.ts +5 -0
  124. package/types/lib/helpers/auditCategories.d.ts.map +1 -1
  125. package/types/lib/helpers/bomUtils.d.ts +10 -0
  126. package/types/lib/helpers/bomUtils.d.ts.map +1 -1
  127. package/types/lib/helpers/cbomutils.d.ts +3 -2
  128. package/types/lib/helpers/cbomutils.d.ts.map +1 -1
  129. package/types/lib/helpers/display.d.ts.map +1 -1
  130. package/types/lib/helpers/evidenceUtils.d.ts +8 -0
  131. package/types/lib/helpers/evidenceUtils.d.ts.map +1 -0
  132. package/types/lib/helpers/exportUtils.d.ts.map +1 -1
  133. package/types/lib/helpers/gtfobins.d.ts +8 -0
  134. package/types/lib/helpers/gtfobins.d.ts.map +1 -1
  135. package/types/lib/helpers/hbom.d.ts +49 -0
  136. package/types/lib/helpers/hbom.d.ts.map +1 -0
  137. package/types/lib/helpers/hbomAnalysis.d.ts +76 -0
  138. package/types/lib/helpers/hbomAnalysis.d.ts.map +1 -0
  139. package/types/lib/helpers/hbomLoader.d.ts +7 -0
  140. package/types/lib/helpers/hbomLoader.d.ts.map +1 -0
  141. package/types/lib/helpers/hostTopology.d.ts +12 -0
  142. package/types/lib/helpers/hostTopology.d.ts.map +1 -0
  143. package/types/lib/helpers/inventoryStats.d.ts +11 -0
  144. package/types/lib/helpers/inventoryStats.d.ts.map +1 -0
  145. package/types/lib/helpers/lolbas.d.ts.map +1 -1
  146. package/types/lib/helpers/osqueryTransform.d.ts +3 -0
  147. package/types/lib/helpers/osqueryTransform.d.ts.map +1 -1
  148. package/types/lib/helpers/plugins.d.ts +58 -0
  149. package/types/lib/helpers/plugins.d.ts.map +1 -0
  150. package/types/lib/helpers/protobom.d.ts +5 -4
  151. package/types/lib/helpers/protobom.d.ts.map +1 -1
  152. package/types/lib/helpers/protobomLoader.d.ts +17 -0
  153. package/types/lib/helpers/protobomLoader.d.ts.map +1 -0
  154. package/types/lib/helpers/remote/dependency-track.d.ts +10 -3
  155. package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
  156. package/types/lib/helpers/source.d.ts.map +1 -1
  157. package/types/lib/helpers/utils.d.ts +45 -8
  158. package/types/lib/helpers/utils.d.ts.map +1 -1
  159. package/types/lib/managers/binary.d.ts +5 -0
  160. package/types/lib/managers/binary.d.ts.map +1 -1
  161. package/types/lib/managers/docker.d.ts.map +1 -1
  162. package/types/lib/server/server.d.ts +2 -1
  163. package/types/lib/server/server.d.ts.map +1 -1
  164. package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
  165. package/types/lib/stages/postgen/auditBom.d.ts +26 -1
  166. package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
  167. package/types/lib/stages/postgen/postgen.d.ts +2 -1
  168. package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
  169. package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
  170. package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
  171. package/types/lib/third-party/arborist/lib/node.d.ts +23 -0
  172. package/types/lib/third-party/arborist/lib/node.d.ts.map +1 -1
  173. package/types/lib/validator/bomValidator.d.ts.map +1 -1
  174. package/types/lib/validator/complianceRules.d.ts.map +1 -1
  175. package/data/spdx-model-v3.0.1.jsonld +0 -15999
@@ -0,0 +1,443 @@
1
+ import { createHash } from "node:crypto";
2
+ import * as nodeFs from "node:fs";
3
+ import {
4
+ existsSync,
5
+ mkdirSync,
6
+ mkdtempSync,
7
+ readFileSync,
8
+ writeFileSync,
9
+ } from "node:fs";
10
+ import { tmpdir } from "node:os";
11
+ import { join } from "node:path";
12
+
13
+ import esmock from "esmock";
14
+ import { assert, describe, it } from "poku";
15
+ import sinon from "sinon";
16
+
17
+ import {
18
+ createAsarFixture,
19
+ writeElectronAsarIntegrityPlist,
20
+ } from "../../test/helpers/asar-fixture-builder.js";
21
+ import {
22
+ cleanupAsarTempDir,
23
+ extractAsarToTempDir,
24
+ listAsarEntries,
25
+ parseAsarArchive,
26
+ readAsarArchiveHeaderSync,
27
+ rewriteExtractedArchivePaths,
28
+ } from "./asarutils.js";
29
+ import { safeRmSync } from "./utils.js";
30
+
31
+ const baseTempDir = mkdtempSync(join(tmpdir(), "cdxgen-asar-poku-"));
32
+
33
+ function align4(value) {
34
+ return value + ((4 - (value % 4)) % 4);
35
+ }
36
+
37
+ function makeStringPickle(value) {
38
+ const valueBuffer = Buffer.from(value, "utf8");
39
+ const alignedStringLength = align4(valueBuffer.length);
40
+ const payloadLength = 4 + alignedStringLength;
41
+ const buffer = Buffer.alloc(4 + payloadLength);
42
+ buffer.writeUInt32LE(payloadLength, 0);
43
+ buffer.writeInt32LE(valueBuffer.length, 4);
44
+ valueBuffer.copy(buffer, 8);
45
+ return buffer;
46
+ }
47
+
48
+ function makeSizePickle(value) {
49
+ const buffer = Buffer.alloc(8);
50
+ buffer.writeUInt32LE(4, 0);
51
+ buffer.writeUInt32LE(value, 4);
52
+ return buffer;
53
+ }
54
+
55
+ function rewriteArchiveHeaderSync(archivePath, transformHeader) {
56
+ const archiveBuffer = readFileSync(archivePath);
57
+ const headerPickleSize = archiveBuffer.readUInt32LE(4);
58
+ const headerBuffer = archiveBuffer.subarray(8, 8 + headerPickleSize);
59
+ const headerStringLength = headerBuffer.readInt32LE(4);
60
+ const headerString = headerBuffer.toString("utf8", 8, 8 + headerStringLength);
61
+ const nextHeader = transformHeader(JSON.parse(headerString));
62
+ const nextHeaderPickle = makeStringPickle(JSON.stringify(nextHeader));
63
+ writeFileSync(
64
+ archivePath,
65
+ Buffer.concat([
66
+ makeSizePickle(nextHeaderPickle.length),
67
+ nextHeaderPickle,
68
+ archiveBuffer.subarray(8 + headerPickleSize),
69
+ ]),
70
+ );
71
+ }
72
+
73
+ process.on("exit", () => {
74
+ safeRmSync(baseTempDir, { force: true, recursive: true });
75
+ });
76
+
77
+ describe("extractAsarToTempDir()", () => {
78
+ it("returns undefined when dry-run blocks ASAR extraction", async () => {
79
+ const safeExtractArchive = sinon.stub().resolves(false);
80
+ const { extractAsarToTempDir: extractAsarToTempDirMocked } = await esmock(
81
+ "./asarutils.js",
82
+ {
83
+ "./utils.js": {
84
+ DEBUG_MODE: false,
85
+ getTmpDir: sinon.stub().returns("/tmp"),
86
+ isDryRun: false,
87
+ recordActivity: sinon.stub(),
88
+ safeCopyFileSync: sinon.stub(),
89
+ safeExtractArchive,
90
+ safeMkdirSync: sinon.stub(),
91
+ safeMkdtempSync: sinon.stub().returns("/tmp/asar-deps-test"),
92
+ safeRmSync: sinon.stub(),
93
+ safeWriteSync: sinon.stub(),
94
+ },
95
+ },
96
+ );
97
+
98
+ const extractedDir = await extractAsarToTempDirMocked("/tmp/sample.asar");
99
+
100
+ assert.strictEqual(extractedDir, undefined);
101
+ sinon.assert.calledOnce(safeExtractArchive);
102
+ });
103
+ });
104
+
105
+ describe("parseAsarArchive()", () => {
106
+ it("catalogs file inventory, hashes, evidence, and security-sensitive properties", async () => {
107
+ const archivePath = join(baseTempDir, "fixture.asar");
108
+ createAsarFixture(archivePath, {
109
+ corruptIntegrityPaths: ["config/settings.json"],
110
+ executablePaths: ["scripts/postinstall.js"],
111
+ symlinks: {
112
+ "config-link": "config/settings.json",
113
+ },
114
+ unpackedPaths: ["native/addon.node"],
115
+ });
116
+
117
+ const analysis = await parseAsarArchive(archivePath, {});
118
+ const entryList = listAsarEntries(archivePath);
119
+
120
+ assert.ok(entryList.entries.some((entry) => entry.path === "config-link"));
121
+ assert.strictEqual(analysis.parentComponent.name, "Sample Electron App");
122
+ assert.strictEqual(
123
+ analysis.parentComponent.purl,
124
+ "pkg:npm/sample-electron-app@1.2.3",
125
+ );
126
+ assert.strictEqual(
127
+ analysis.summary.integrityMismatchCount,
128
+ 1,
129
+ "expected one mismatched declared integrity hash",
130
+ );
131
+ assert.ok(analysis.summary.capabilities.includes("fileAccess"));
132
+ assert.ok(analysis.summary.capabilities.includes("network"));
133
+ assert.ok(analysis.summary.capabilities.includes("hardware"));
134
+ assert.ok(analysis.summary.capabilities.includes("dynamicFetch"));
135
+ assert.ok(analysis.summary.capabilities.includes("dynamicImport"));
136
+ assert.strictEqual(analysis.summary.hasEval, true);
137
+ const archiveProps = analysis.parentComponent.properties;
138
+ assert.strictEqual(
139
+ archiveProps.find((property) => property.name === "cdx:asar:hasEval")
140
+ ?.value,
141
+ "true",
142
+ );
143
+ assert.strictEqual(
144
+ archiveProps.find(
145
+ (property) => property.name === "cdx:asar:hasNativeAddons",
146
+ )?.value,
147
+ "true",
148
+ );
149
+ assert.strictEqual(
150
+ archiveProps.find(
151
+ (property) => property.name === "cdx:asar:hasIntegrityMismatch",
152
+ )?.value,
153
+ "true",
154
+ );
155
+
156
+ const mainFileComponent = analysis.components.find((component) =>
157
+ component.properties?.some(
158
+ (property) =>
159
+ property.name === "cdx:asar:path" && property.value === "src/main.js",
160
+ ),
161
+ );
162
+ assert.ok(mainFileComponent, "expected src/main.js file component");
163
+ assert.ok(mainFileComponent.hashes?.length, "expected SHA-256 hash");
164
+ assert.strictEqual(
165
+ mainFileComponent.evidence?.occurrences?.[0]?.location,
166
+ `${archivePath}#/src/main.js`,
167
+ );
168
+ assert.strictEqual(
169
+ mainFileComponent.properties.find(
170
+ (property) => property.name === "cdx:asar:js:hasDynamicFetch",
171
+ )?.value,
172
+ "true",
173
+ );
174
+ assert.strictEqual(
175
+ mainFileComponent.properties.find(
176
+ (property) => property.name === "cdx:asar:js:capability:hardware",
177
+ )?.value,
178
+ "true",
179
+ );
180
+
181
+ const unpackedComponent = analysis.components.find((component) =>
182
+ component.properties?.some(
183
+ (property) =>
184
+ property.name === "cdx:asar:path" &&
185
+ property.value === "native/addon.node",
186
+ ),
187
+ );
188
+ assert.ok(unpackedComponent, "expected native addon component");
189
+ assert.strictEqual(
190
+ unpackedComponent.properties.find(
191
+ (property) => property.name === "cdx:asar:unpacked",
192
+ )?.value,
193
+ "true",
194
+ );
195
+ });
196
+
197
+ it("extracts ASAR archives and rewrites extracted source paths back to archive paths", async () => {
198
+ const archivePath = join(baseTempDir, "fixture-extract.asar");
199
+ createAsarFixture(archivePath, {
200
+ unpackedPaths: ["native/addon.node"],
201
+ });
202
+
203
+ const extractedDir = await extractAsarToTempDir(archivePath);
204
+ assert.ok(extractedDir, "expected extraction temp dir");
205
+ assert.ok(existsSync(join(extractedDir, "src", "main.js")));
206
+ assert.ok(existsSync(join(extractedDir, "native", "addon.node")));
207
+
208
+ const component = {
209
+ evidence: {
210
+ identity: {
211
+ methods: [
212
+ {
213
+ confidence: 1,
214
+ technique: "manifest-analysis",
215
+ value: join(extractedDir, "package.json"),
216
+ },
217
+ ],
218
+ },
219
+ occurrences: [
220
+ {
221
+ location: join(extractedDir, "src", "main.js"),
222
+ },
223
+ ],
224
+ },
225
+ properties: [
226
+ {
227
+ name: "SrcFile",
228
+ value: join(
229
+ extractedDir,
230
+ "node_modules",
231
+ "sketchy-addon",
232
+ "package.json",
233
+ ),
234
+ },
235
+ ],
236
+ };
237
+ rewriteExtractedArchivePaths(component, extractedDir, archivePath);
238
+ assert.strictEqual(
239
+ component.properties[0].value,
240
+ `${archivePath}#/node_modules/sketchy-addon/package.json`,
241
+ );
242
+ assert.strictEqual(
243
+ component.evidence.identity.methods[0].value,
244
+ `${archivePath}#/package.json`,
245
+ );
246
+ assert.strictEqual(
247
+ component.evidence.occurrences[0].location,
248
+ `${archivePath}#/src/main.js`,
249
+ );
250
+
251
+ cleanupAsarTempDir(extractedDir);
252
+ assert.ok(!existsSync(extractedDir), "expected extracted temp dir cleanup");
253
+ });
254
+
255
+ it("verifies Electron ASAR signing metadata and emits a crypto component", async () => {
256
+ const appDir = join(baseTempDir, "Signed.app");
257
+ const archivePath = join(
258
+ appDir,
259
+ "Contents",
260
+ "Resources",
261
+ "app & signed.asar",
262
+ );
263
+ mkdirSync(join(appDir, "Contents", "Resources"), { recursive: true });
264
+ createAsarFixture(archivePath);
265
+ const { headerString } = readAsarArchiveHeaderSync(archivePath);
266
+ const headerHash = createHash("sha256")
267
+ .update(headerString, "utf8")
268
+ .digest("hex");
269
+ writeElectronAsarIntegrityPlist(join(appDir, "Contents", "Info.plist"), {
270
+ "Resources/app & signed.asar": {
271
+ algorithm: "SHA256",
272
+ hash: headerHash,
273
+ },
274
+ });
275
+
276
+ const analysis = await parseAsarArchive(archivePath, { specVersion: 1.7 });
277
+ const signingComponent = analysis.components.find(
278
+ (component) =>
279
+ component.type === "cryptographic-asset" &&
280
+ component.properties?.some(
281
+ (property) =>
282
+ property.name === "cdx:asar:signingVerified" &&
283
+ property.value === "true",
284
+ ),
285
+ );
286
+ assert.strictEqual(
287
+ analysis.parentComponent.properties.find(
288
+ (property) => property.name === "cdx:asar:hasSigningMetadata",
289
+ )?.value,
290
+ "true",
291
+ );
292
+ assert.strictEqual(
293
+ analysis.parentComponent.properties.find(
294
+ (property) => property.name === "cdx:asar:signingVerified",
295
+ )?.value,
296
+ "true",
297
+ );
298
+ assert.strictEqual(
299
+ analysis.parentComponent.properties.find(
300
+ (property) => property.name === "cdx:asar:signingScope",
301
+ )?.value,
302
+ "header-only",
303
+ );
304
+ assert.ok(signingComponent, "expected ASAR signing crypto component");
305
+ assert.strictEqual(
306
+ signingComponent.properties.find(
307
+ (property) => property.name === "cdx:asar:signingScope",
308
+ )?.value,
309
+ "header-only",
310
+ );
311
+ assert.ok(
312
+ analysis.dependencies.some(
313
+ (dependency) =>
314
+ dependency.ref === analysis.parentComponent["bom-ref"] &&
315
+ dependency.dependsOn.includes(signingComponent["bom-ref"]),
316
+ ),
317
+ "expected parent archive to depend on the signing component",
318
+ );
319
+ });
320
+
321
+ it("rejects ASAR headers with oversized file entries", async () => {
322
+ const archivePath = join(baseTempDir, "fixture-oversized.asar");
323
+ createAsarFixture(archivePath, {
324
+ extraEntries: {
325
+ "huge.bin": {
326
+ content: "x",
327
+ size: 256 * 1024 * 1024 + 1,
328
+ },
329
+ },
330
+ });
331
+
332
+ await assert.rejects(
333
+ () => parseAsarArchive(archivePath, {}),
334
+ /Invalid ASAR file entry/,
335
+ );
336
+ });
337
+
338
+ it("rejects ASAR entries with offsets beyond the safe read limit", async () => {
339
+ const archivePath = join(baseTempDir, "fixture-offset.asar");
340
+ createAsarFixture(archivePath, {
341
+ extraEntries: {
342
+ "too-far.bin": {
343
+ content: "x",
344
+ offset: Number.MAX_SAFE_INTEGER + 10,
345
+ size: 1,
346
+ },
347
+ },
348
+ });
349
+
350
+ await assert.rejects(
351
+ () => parseAsarArchive(archivePath, {}),
352
+ /offset exceeds the safe read limit/,
353
+ );
354
+ });
355
+
356
+ it("rejects ASAR headers with excessive nesting depth", async () => {
357
+ const archivePath = join(baseTempDir, "fixture-deep.asar");
358
+ const deeplyNestedPath = `${Array.from({ length: 260 }, (_, index) => `d${index}`).join("/")}/payload.txt`;
359
+ createAsarFixture(archivePath, {
360
+ extraEntries: {
361
+ [deeplyNestedPath]: {
362
+ content: "payload",
363
+ },
364
+ },
365
+ });
366
+
367
+ await assert.rejects(
368
+ () => parseAsarArchive(archivePath, {}),
369
+ /nesting exceeds 256 levels/,
370
+ );
371
+ });
372
+
373
+ it("rejects ASAR headers with conflicting entry kinds", async () => {
374
+ const archivePath = join(baseTempDir, "fixture-conflicting-kinds.asar");
375
+ createAsarFixture(archivePath);
376
+ rewriteArchiveHeaderSync(archivePath, (header) => {
377
+ header.files["bad-link"] = {
378
+ files: {},
379
+ link: "src/main.js",
380
+ };
381
+ return header;
382
+ });
383
+
384
+ await assert.rejects(
385
+ () => parseAsarArchive(archivePath, {}),
386
+ /Invalid ASAR symlink entry/,
387
+ );
388
+ });
389
+
390
+ it("rejects symlinks that escape the extraction root", async () => {
391
+ const archivePath = join(baseTempDir, "fixture-link-escape.asar");
392
+ createAsarFixture(archivePath, {
393
+ symlinks: {
394
+ "escape-link": "../../outside.txt",
395
+ },
396
+ });
397
+
398
+ const extractedDir = await extractAsarToTempDir(archivePath);
399
+ assert.strictEqual(extractedDir, undefined);
400
+ });
401
+
402
+ it("rejects circular symlink chains during extraction", async () => {
403
+ const archivePath = join(baseTempDir, "fixture-link-cycle.asar");
404
+ createAsarFixture(archivePath, {
405
+ symlinks: {
406
+ a: "b",
407
+ b: "a",
408
+ },
409
+ });
410
+
411
+ const extractedDir = await extractAsarToTempDir(archivePath);
412
+ assert.strictEqual(extractedDir, undefined);
413
+ });
414
+
415
+ it("reuses one packed-entry file descriptor per parse and extraction pass", async () => {
416
+ const archivePath = join(baseTempDir, "fixture-open-reuse.asar");
417
+ createAsarFixture(archivePath, {
418
+ unpackedPaths: ["native/addon.node"],
419
+ });
420
+ const openSync = sinon.spy((...args) => nodeFs.openSync(...args));
421
+ const closeSync = sinon.spy((...args) => nodeFs.closeSync(...args));
422
+ const {
423
+ cleanupAsarTempDir: cleanupAsarTempDirMocked,
424
+ extractAsarToTempDir: extractAsarToTempDirMocked,
425
+ parseAsarArchive: parseAsarArchiveMocked,
426
+ } = await esmock("./asarutils.js", {
427
+ "node:fs": {
428
+ ...nodeFs,
429
+ closeSync,
430
+ openSync,
431
+ },
432
+ });
433
+
434
+ await parseAsarArchiveMocked(archivePath, {});
435
+ const extractedDir = await extractAsarToTempDirMocked(archivePath);
436
+
437
+ assert.ok(extractedDir, "expected extraction temp dir");
438
+ assert.strictEqual(openSync.callCount, 4);
439
+ assert.strictEqual(closeSync.callCount, 4);
440
+
441
+ cleanupAsarTempDirMocked(extractedDir);
442
+ });
443
+ });
@@ -1,5 +1,17 @@
1
+ export const HBOM_AUDIT_CATEGORIES = Object.freeze([
2
+ "hbom-security",
3
+ "hbom-performance",
4
+ "hbom-compliance",
5
+ ]);
6
+
7
+ export const HOST_TOPOLOGY_AUDIT_CATEGORIES = Object.freeze(["host-topology"]);
8
+
9
+ export const DEFAULT_HBOM_AUDIT_CATEGORIES = HBOM_AUDIT_CATEGORIES.join(",");
10
+
1
11
  export const BOM_AUDIT_CATEGORY_ALIASES = Object.freeze({
2
12
  "ai-inventory": ["ai-agent", "mcp-server"],
13
+ hbom: [...HBOM_AUDIT_CATEGORIES],
14
+ host: [...HBOM_AUDIT_CATEGORIES, ...HOST_TOPOLOGY_AUDIT_CATEGORIES],
3
15
  });
4
16
 
5
17
  function uniqueNonEmptyCategories(categories) {
@@ -0,0 +1,32 @@
1
+ import { assert, describe, it } from "poku";
2
+
3
+ import {
4
+ expandBomAuditCategories,
5
+ validateBomAuditCategories,
6
+ } from "./auditCategories.js";
7
+
8
+ describe("auditCategories", () => {
9
+ it("keeps host-topology as a direct category", () => {
10
+ assert.deepStrictEqual(expandBomAuditCategories("host-topology"), [
11
+ "host-topology",
12
+ ]);
13
+ });
14
+
15
+ it("expands the host alias to the HBOM packs plus host-topology", () => {
16
+ assert.deepStrictEqual(expandBomAuditCategories("host"), [
17
+ "hbom-security",
18
+ "hbom-performance",
19
+ "hbom-compliance",
20
+ "host-topology",
21
+ ]);
22
+ });
23
+
24
+ it("accepts host-topology during validation", () => {
25
+ const validation = validateBomAuditCategories("host-topology", [
26
+ { category: "host-topology" },
27
+ { category: "hbom-security" },
28
+ ]);
29
+ assert.deepStrictEqual(validation.categories, ["host-topology"]);
30
+ assert.deepStrictEqual(validation.expandedCategories, ["host-topology"]);
31
+ });
32
+ });
@@ -1,8 +1,16 @@
1
1
  const SPDX_CONTEXT_PREFIX = "https://spdx.org/rdf/";
2
2
  const CYCLONEDX_FORMAT = "CycloneDX";
3
+ const LEGACY_CYCLONEDX_ROOT_KEY = "bomFormat";
4
+ const MODERN_CYCLONEDX_ROOT_KEY = "specFormat";
3
5
  const BOM_FORMAT_CYCLONEDX = "cyclonedx";
4
6
  const BOM_FORMAT_SPDX = "spdx";
5
7
  const BOM_FORMAT_UNKNOWN = "unknown";
8
+ const CYCLONEDX_SPEC_VERSION_PATTERN = /^(\d+)(?:\.(\d+))?$/u;
9
+ const CYCLONEDX_FORMAT_KEYS = new Set([
10
+ LEGACY_CYCLONEDX_ROOT_KEY,
11
+ MODERN_CYCLONEDX_ROOT_KEY,
12
+ "specVersion",
13
+ ]);
6
14
 
7
15
  export const isSpdxJsonLd = (bomJson) =>
8
16
  Boolean(
@@ -11,8 +19,154 @@ export const isSpdxJsonLd = (bomJson) =>
11
19
  bomJson["@graph"].some((element) => element?.type === "SpdxDocument"),
12
20
  );
13
21
 
22
+ const parseCycloneDxSpecVersion = (specVersion) => {
23
+ const match = `${specVersion ?? ""}`
24
+ .trim()
25
+ .match(CYCLONEDX_SPEC_VERSION_PATTERN);
26
+ if (!match) {
27
+ return undefined;
28
+ }
29
+ return {
30
+ major: Number.parseInt(match[1], 10),
31
+ minor: Number.parseInt(match[2] || "0", 10),
32
+ minorText: match[2] || "0",
33
+ };
34
+ };
35
+
36
+ export const normalizeCycloneDxSpecVersion = (specVersion) => {
37
+ const parsed = parseCycloneDxSpecVersion(specVersion);
38
+ if (!parsed) {
39
+ return undefined;
40
+ }
41
+ return Number(`${parsed.major}.${parsed.minor}`);
42
+ };
43
+
44
+ export const toCycloneDxSpecVersionString = (specVersion) => {
45
+ const parsed = parseCycloneDxSpecVersion(specVersion);
46
+ if (!parsed) {
47
+ return undefined;
48
+ }
49
+ if (typeof specVersion === "string" && parsed.minorText !== "0") {
50
+ return `${parsed.major}.${parsed.minorText}`;
51
+ }
52
+ return `${parsed.major}.${parsed.minor}`;
53
+ };
54
+
55
+ export const isCycloneDxSpecVersionAtLeast = (specVersion, minimumVersion) => {
56
+ const parsedSpecVersion = parseCycloneDxSpecVersion(specVersion);
57
+ const parsedMinimumVersion = parseCycloneDxSpecVersion(minimumVersion);
58
+ if (!parsedSpecVersion || !parsedMinimumVersion) {
59
+ return false;
60
+ }
61
+ if (parsedSpecVersion.major !== parsedMinimumVersion.major) {
62
+ return parsedSpecVersion.major > parsedMinimumVersion.major;
63
+ }
64
+ return parsedSpecVersion.minor >= parsedMinimumVersion.minor;
65
+ };
66
+
67
+ export const isCycloneDx20SpecVersion = (specVersion) =>
68
+ isCycloneDxSpecVersionAtLeast(specVersion, 2);
69
+
70
+ export const getCycloneDxRootFormatKey = (specVersionOrBom) => {
71
+ const specVersion =
72
+ specVersionOrBom && typeof specVersionOrBom === "object"
73
+ ? specVersionOrBom.specVersion
74
+ : specVersionOrBom;
75
+ return isCycloneDx20SpecVersion(specVersion)
76
+ ? MODERN_CYCLONEDX_ROOT_KEY
77
+ : LEGACY_CYCLONEDX_ROOT_KEY;
78
+ };
79
+
80
+ export const getCycloneDxFormat = (bomJson) =>
81
+ bomJson?.specFormat || bomJson?.bomFormat;
82
+
83
+ export const hasCycloneDxFormat = (bomJson) =>
84
+ getCycloneDxFormat(bomJson) === CYCLONEDX_FORMAT;
85
+
14
86
  export const isCycloneDxBom = (bomJson) =>
15
- bomJson?.bomFormat === CYCLONEDX_FORMAT && Boolean(bomJson?.specVersion);
87
+ hasCycloneDxFormat(bomJson) &&
88
+ normalizeCycloneDxSpecVersion(bomJson?.specVersion) !== undefined;
89
+
90
+ const rewriteCycloneDxRootFields = (
91
+ bomJson,
92
+ rootKey,
93
+ specVersion,
94
+ preserveLegacyBomFormat,
95
+ ) => {
96
+ const remainingEntries = Object.entries(bomJson).filter(
97
+ ([key]) => !CYCLONEDX_FORMAT_KEYS.has(key),
98
+ );
99
+ for (const key of Object.keys(bomJson)) {
100
+ delete bomJson[key];
101
+ }
102
+ if (rootKey === LEGACY_CYCLONEDX_ROOT_KEY) {
103
+ bomJson.bomFormat = CYCLONEDX_FORMAT;
104
+ if (specVersion !== undefined) {
105
+ bomJson.specVersion = specVersion;
106
+ }
107
+ } else if (preserveLegacyBomFormat) {
108
+ bomJson.bomFormat = CYCLONEDX_FORMAT;
109
+ if (specVersion !== undefined) {
110
+ bomJson.specVersion = specVersion;
111
+ }
112
+ bomJson.specFormat = CYCLONEDX_FORMAT;
113
+ } else {
114
+ bomJson.specFormat = CYCLONEDX_FORMAT;
115
+ if (specVersion !== undefined) {
116
+ bomJson.specVersion = specVersion;
117
+ }
118
+ }
119
+ for (const [key, value] of remainingEntries) {
120
+ bomJson[key] = value;
121
+ }
122
+ };
123
+
124
+ /**
125
+ * Mutates a CycloneDX BOM object so the appropriate root format key is present
126
+ * for the requested spec version, while preserving conventional serialized
127
+ * root-key ordering (`bomFormat`/`specFormat` and `specVersion` first). Only the currently
128
+ * supported CycloneDX major.minor version shape is accepted; multi-component
129
+ * future versions such as `2.0.1` intentionally return `undefined` from the
130
+ * normalizer rather than being silently truncated.
131
+ *
132
+ * @param {object} bomJson BOM JSON object to mutate.
133
+ * @param {string|number} specVersion Desired CycloneDX spec version.
134
+ * @param {object} options Root-key compatibility options.
135
+ * @returns {object} The same `bomJson` object, after in-place mutation.
136
+ */
137
+ export const setCycloneDxFormat = (
138
+ bomJson,
139
+ specVersion,
140
+ { preserveLegacyBomFormat = false } = {},
141
+ ) => {
142
+ if (!bomJson || typeof bomJson !== "object" || Array.isArray(bomJson)) {
143
+ return bomJson;
144
+ }
145
+ const resolvedSpecVersion =
146
+ toCycloneDxSpecVersionString(specVersion ?? bomJson.specVersion) ||
147
+ bomJson.specVersion;
148
+ if (resolvedSpecVersion !== undefined) {
149
+ bomJson.specVersion = resolvedSpecVersion;
150
+ }
151
+ if (
152
+ getCycloneDxRootFormatKey(resolvedSpecVersion) === MODERN_CYCLONEDX_ROOT_KEY
153
+ ) {
154
+ rewriteCycloneDxRootFields(
155
+ bomJson,
156
+ MODERN_CYCLONEDX_ROOT_KEY,
157
+ resolvedSpecVersion,
158
+ preserveLegacyBomFormat,
159
+ );
160
+ return bomJson;
161
+ }
162
+ rewriteCycloneDxRootFields(
163
+ bomJson,
164
+ LEGACY_CYCLONEDX_ROOT_KEY,
165
+ resolvedSpecVersion,
166
+ false,
167
+ );
168
+ return bomJson;
169
+ };
16
170
 
17
171
  export const detectBomFormat = (bomJson) => {
18
172
  if (isCycloneDxBom(bomJson)) {