@cyclonedx/cdxgen 12.3.3 → 12.4.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 +64 -22
- package/bin/audit.js +21 -7
- package/bin/cdxgen.js +238 -116
- package/bin/convert.js +28 -13
- package/bin/hbom.js +490 -0
- package/bin/repl.js +580 -29
- package/bin/validate.js +34 -4
- package/bin/verify.js +40 -5
- package/data/README.md +298 -25
- package/data/component-tags.json +6 -0
- package/data/crypto-oid.json +16 -0
- package/data/predictive-audit-allowlist.json +11 -0
- package/data/queries-darwin.json +12 -1
- package/data/queries-win.json +7 -1
- package/data/queries.json +39 -2
- package/data/rules/ai-agent-governance.yaml +16 -0
- package/data/rules/asar-archives.yaml +150 -0
- package/data/rules/chrome-extensions.yaml +8 -0
- package/data/rules/ci-permissions.yaml +42 -18
- package/data/rules/container-risk.yaml +14 -7
- package/data/rules/dependency-sources.yaml +11 -0
- package/data/rules/hbom-compliance.yaml +325 -0
- package/data/rules/hbom-performance.yaml +307 -0
- package/data/rules/hbom-security.yaml +248 -0
- package/data/rules/host-topology.yaml +165 -0
- package/data/rules/mcp-servers.yaml +18 -3
- package/data/rules/obom-runtime.yaml +907 -22
- package/data/rules/package-integrity.yaml +14 -0
- package/data/rules/rootfs-hardening.yaml +179 -0
- package/data/rules/vscode-extensions.yaml +9 -0
- package/lib/audit/index.js +209 -8
- package/lib/audit/index.poku.js +332 -0
- package/lib/audit/reporters.js +222 -0
- package/lib/audit/targets.js +146 -1
- package/lib/audit/targets.poku.js +186 -0
- package/lib/cli/asar.poku.js +328 -0
- package/lib/cli/index.js +506 -88
- package/lib/cli/index.poku.js +1352 -212
- package/lib/evinser/evinser.js +14 -9
- package/lib/helpers/analyzer.js +1406 -29
- package/lib/helpers/analyzer.poku.js +342 -0
- package/lib/helpers/analyzerScope.js +712 -0
- package/lib/helpers/asarutils.js +1556 -0
- package/lib/helpers/asarutils.poku.js +443 -0
- package/lib/helpers/auditCategories.js +12 -0
- package/lib/helpers/auditCategories.poku.js +32 -0
- package/lib/helpers/cbomutils.js +271 -1
- package/lib/helpers/cbomutils.poku.js +248 -5
- package/lib/helpers/display.js +291 -1
- package/lib/helpers/display.poku.js +149 -0
- package/lib/helpers/evidenceUtils.js +58 -0
- package/lib/helpers/evidenceUtils.poku.js +54 -0
- package/lib/helpers/exportUtils.js +9 -0
- package/lib/helpers/gtfobins.js +142 -8
- package/lib/helpers/gtfobins.poku.js +24 -1
- package/lib/helpers/hbom.js +710 -0
- package/lib/helpers/hbom.poku.js +496 -0
- package/lib/helpers/hbomAnalysis.js +268 -0
- package/lib/helpers/hbomAnalysis.poku.js +249 -0
- package/lib/helpers/hbomLoader.js +35 -0
- package/lib/helpers/hostTopology.js +803 -0
- package/lib/helpers/hostTopology.poku.js +363 -0
- package/lib/helpers/inventoryStats.js +69 -0
- package/lib/helpers/inventoryStats.poku.js +86 -0
- package/lib/helpers/lolbas.js +19 -1
- package/lib/helpers/lolbas.poku.js +23 -0
- package/lib/helpers/osqueryTransform.js +47 -0
- package/lib/helpers/osqueryTransform.poku.js +47 -0
- package/lib/helpers/plugins.js +349 -0
- package/lib/helpers/plugins.poku.js +57 -0
- package/lib/helpers/protobom.js +156 -45
- package/lib/helpers/protobom.poku.js +140 -5
- package/lib/helpers/remote/dependency-track.js +36 -3
- package/lib/helpers/remote/dependency-track.poku.js +44 -0
- package/lib/helpers/source.js +24 -0
- package/lib/helpers/source.poku.js +32 -0
- package/lib/helpers/utils.js +1438 -93
- package/lib/helpers/utils.poku.js +846 -4
- package/lib/managers/binary.e2e.poku.js +367 -0
- package/lib/managers/binary.js +2293 -353
- package/lib/managers/binary.poku.js +1699 -1
- package/lib/managers/docker.js +201 -79
- package/lib/managers/docker.poku.js +337 -12
- package/lib/server/server.js +2 -27
- package/lib/stages/postgen/annotator.js +38 -0
- package/lib/stages/postgen/annotator.poku.js +107 -1
- package/lib/stages/postgen/auditBom.js +121 -18
- package/lib/stages/postgen/auditBom.poku.js +1366 -31
- package/lib/stages/postgen/hostTopologyAudit.poku.js +186 -0
- package/lib/stages/postgen/postgen.js +192 -1
- package/lib/stages/postgen/postgen.poku.js +321 -0
- package/lib/stages/postgen/ruleEngine.js +116 -0
- package/lib/stages/pregen/envAudit.js +14 -3
- package/package.json +23 -21
- package/types/bin/hbom.d.ts +3 -0
- package/types/bin/hbom.d.ts.map +1 -0
- package/types/bin/repl.d.ts.map +1 -1
- package/types/lib/audit/index.d.ts +44 -0
- package/types/lib/audit/index.d.ts.map +1 -1
- package/types/lib/audit/reporters.d.ts +16 -0
- package/types/lib/audit/reporters.d.ts.map +1 -1
- package/types/lib/audit/targets.d.ts.map +1 -1
- package/types/lib/cli/index.d.ts +16 -0
- package/types/lib/cli/index.d.ts.map +1 -1
- package/types/lib/evinser/evinser.d.ts +4 -0
- package/types/lib/evinser/evinser.d.ts.map +1 -1
- package/types/lib/helpers/analyzer.d.ts +33 -0
- package/types/lib/helpers/analyzer.d.ts.map +1 -1
- package/types/lib/helpers/analyzerScope.d.ts +11 -0
- package/types/lib/helpers/analyzerScope.d.ts.map +1 -0
- package/types/lib/helpers/asarutils.d.ts +34 -0
- package/types/lib/helpers/asarutils.d.ts.map +1 -0
- package/types/lib/helpers/auditCategories.d.ts +5 -0
- package/types/lib/helpers/auditCategories.d.ts.map +1 -1
- package/types/lib/helpers/cbomutils.d.ts +3 -2
- package/types/lib/helpers/cbomutils.d.ts.map +1 -1
- package/types/lib/helpers/display.d.ts.map +1 -1
- package/types/lib/helpers/evidenceUtils.d.ts +8 -0
- package/types/lib/helpers/evidenceUtils.d.ts.map +1 -0
- package/types/lib/helpers/exportUtils.d.ts.map +1 -1
- package/types/lib/helpers/gtfobins.d.ts +8 -0
- package/types/lib/helpers/gtfobins.d.ts.map +1 -1
- package/types/lib/helpers/hbom.d.ts +49 -0
- package/types/lib/helpers/hbom.d.ts.map +1 -0
- package/types/lib/helpers/hbomAnalysis.d.ts +62 -0
- package/types/lib/helpers/hbomAnalysis.d.ts.map +1 -0
- package/types/lib/helpers/hbomLoader.d.ts +7 -0
- package/types/lib/helpers/hbomLoader.d.ts.map +1 -0
- package/types/lib/helpers/hostTopology.d.ts +12 -0
- package/types/lib/helpers/hostTopology.d.ts.map +1 -0
- package/types/lib/helpers/inventoryStats.d.ts +11 -0
- package/types/lib/helpers/inventoryStats.d.ts.map +1 -0
- package/types/lib/helpers/lolbas.d.ts.map +1 -1
- package/types/lib/helpers/osqueryTransform.d.ts +3 -0
- package/types/lib/helpers/osqueryTransform.d.ts.map +1 -1
- package/types/lib/helpers/plugins.d.ts +58 -0
- package/types/lib/helpers/plugins.d.ts.map +1 -0
- package/types/lib/helpers/protobom.d.ts +3 -4
- package/types/lib/helpers/protobom.d.ts.map +1 -1
- package/types/lib/helpers/remote/dependency-track.d.ts +10 -3
- package/types/lib/helpers/remote/dependency-track.d.ts.map +1 -1
- package/types/lib/helpers/source.d.ts.map +1 -1
- package/types/lib/helpers/utils.d.ts +45 -8
- package/types/lib/helpers/utils.d.ts.map +1 -1
- package/types/lib/managers/binary.d.ts +5 -0
- package/types/lib/managers/binary.d.ts.map +1 -1
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/server/server.d.ts +2 -1
- 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/auditBom.d.ts +26 -1
- package/types/lib/stages/postgen/auditBom.d.ts.map +1 -1
- package/types/lib/stages/postgen/postgen.d.ts +2 -1
- package/types/lib/stages/postgen/postgen.d.ts.map +1 -1
- package/types/lib/stages/postgen/ruleEngine.d.ts.map +1 -1
- package/types/lib/stages/pregen/envAudit.d.ts.map +1 -1
- package/data/spdx-model-v3.0.1.jsonld +0 -15999
package/lib/audit/index.poku.js
CHANGED
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
finalizeAuditReport,
|
|
21
21
|
groupAuditResults,
|
|
22
22
|
loadInputBoms,
|
|
23
|
+
runDirectBomAuditFromBoms,
|
|
23
24
|
} from "./index.js";
|
|
24
25
|
import {
|
|
25
26
|
formatPredictiveAnnotations,
|
|
@@ -179,6 +180,7 @@ describe("runAuditFromBoms()", () => {
|
|
|
179
180
|
|
|
180
181
|
assert.strictEqual(report.summary.totalTargets, 1);
|
|
181
182
|
assert.deepStrictEqual(collectAuditTargetsStub.firstCall.args[1], {
|
|
183
|
+
allowlistFile: undefined,
|
|
182
184
|
maxTargets: 50,
|
|
183
185
|
prioritizeDirectRuntime: true,
|
|
184
186
|
scope: "required",
|
|
@@ -192,6 +194,266 @@ describe("runAuditFromBoms()", () => {
|
|
|
192
194
|
);
|
|
193
195
|
assert.strictEqual(progressEvents[1].type, "run:start");
|
|
194
196
|
});
|
|
197
|
+
|
|
198
|
+
it("supports dry-run predictive audit planning without cloning targets", async () => {
|
|
199
|
+
const collectAuditTargetsStub = sinon.stub().returns({
|
|
200
|
+
skipped: [],
|
|
201
|
+
stats: {
|
|
202
|
+
availableTargets: 1,
|
|
203
|
+
nonRequiredTargets: 0,
|
|
204
|
+
requiredTargets: 1,
|
|
205
|
+
trustedTargets: 0,
|
|
206
|
+
trustedTargetsExcluded: 0,
|
|
207
|
+
truncatedTargets: 0,
|
|
208
|
+
},
|
|
209
|
+
targets: [
|
|
210
|
+
{
|
|
211
|
+
name: "core",
|
|
212
|
+
namespace: "acme",
|
|
213
|
+
purl: "pkg:npm/acme/core@1.0.0",
|
|
214
|
+
required: true,
|
|
215
|
+
type: "npm",
|
|
216
|
+
version: "1.0.0",
|
|
217
|
+
},
|
|
218
|
+
],
|
|
219
|
+
});
|
|
220
|
+
const enrichInputBomsWithRegistryMetadataStub = sinon.stub().resolves();
|
|
221
|
+
const recordActivityStub = sinon.stub();
|
|
222
|
+
const { runAuditFromBoms: mockedRunAuditFromBoms } = await esmock(
|
|
223
|
+
"./index.js",
|
|
224
|
+
{
|
|
225
|
+
"../cli/index.js": {
|
|
226
|
+
createBom: sinon.stub(),
|
|
227
|
+
},
|
|
228
|
+
"../helpers/bomUtils.js": {
|
|
229
|
+
getNonCycloneDxErrorMessage: sinon.stub(),
|
|
230
|
+
isCycloneDxBom: () => true,
|
|
231
|
+
},
|
|
232
|
+
"../helpers/logger.js": { thoughtLog: sinon.stub() },
|
|
233
|
+
"../helpers/provenanceUtils.js": {
|
|
234
|
+
hasRegistryProvenanceEvidenceProperties: () => false,
|
|
235
|
+
hasTrustedPublishingProperties: () => false,
|
|
236
|
+
},
|
|
237
|
+
"../helpers/source.js": {
|
|
238
|
+
cleanupSourceDir: sinon.stub(),
|
|
239
|
+
findGitRefForPurlVersion: sinon.stub().returns(undefined),
|
|
240
|
+
hardenedGitCommand: sinon.stub(),
|
|
241
|
+
resolveGitUrlFromPurl: sinon.stub(),
|
|
242
|
+
resolvePurlSourceDirectory: sinon.stub(),
|
|
243
|
+
sanitizeRemoteUrlForLogs: (value) => value,
|
|
244
|
+
},
|
|
245
|
+
"../helpers/utils.js": {
|
|
246
|
+
dirNameStr: path.resolve("."),
|
|
247
|
+
getTmpDir: () => os.tmpdir(),
|
|
248
|
+
isDryRun: true,
|
|
249
|
+
recordActivity: recordActivityStub,
|
|
250
|
+
safeExistsSync: (filePath) => existsSync(filePath),
|
|
251
|
+
safeMkdirSync: (filePath, options) => mkdirSync(filePath, options),
|
|
252
|
+
safeMkdtempSync: sinon.stub(),
|
|
253
|
+
safeRmSync: sinon.stub(),
|
|
254
|
+
safeWriteSync: sinon.stub(),
|
|
255
|
+
},
|
|
256
|
+
"../stages/postgen/auditBom.js": {
|
|
257
|
+
auditBom: sinon.stub().resolves([]),
|
|
258
|
+
},
|
|
259
|
+
"../stages/postgen/postgen.js": {
|
|
260
|
+
postProcess: sinon.stub().callsFake((bomNSData) => bomNSData),
|
|
261
|
+
},
|
|
262
|
+
"./targets.js": {
|
|
263
|
+
collectAuditTargets: collectAuditTargetsStub,
|
|
264
|
+
enrichInputBomsWithRegistryMetadata:
|
|
265
|
+
enrichInputBomsWithRegistryMetadataStub,
|
|
266
|
+
normalizePackageName: (value) =>
|
|
267
|
+
(value || "").toLowerCase().replace(/[-_.]+/g, "-"),
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
const report = await mockedRunAuditFromBoms(
|
|
273
|
+
[
|
|
274
|
+
{
|
|
275
|
+
bomJson: {
|
|
276
|
+
bomFormat: "CycloneDX",
|
|
277
|
+
components: [],
|
|
278
|
+
specVersion: "1.7",
|
|
279
|
+
version: 1,
|
|
280
|
+
},
|
|
281
|
+
source: "bom.json",
|
|
282
|
+
},
|
|
283
|
+
],
|
|
284
|
+
{},
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
assert.strictEqual(report.dryRun, true);
|
|
288
|
+
assert.strictEqual(report.summary.predictiveDryRun, true);
|
|
289
|
+
assert.strictEqual(report.summary.totalTargets, 1);
|
|
290
|
+
assert.strictEqual(report.summary.scannedTargets, 0);
|
|
291
|
+
assert.strictEqual(report.summary.skippedTargets, 1);
|
|
292
|
+
assert.strictEqual(report.results[0].status, "skipped");
|
|
293
|
+
assert.match(
|
|
294
|
+
report.results[0].assessment.reasons[0],
|
|
295
|
+
/skipped registry metadata fetches/i,
|
|
296
|
+
);
|
|
297
|
+
sinon.assert.notCalled(enrichInputBomsWithRegistryMetadataStub);
|
|
298
|
+
sinon.assert.calledWithMatch(recordActivityStub, {
|
|
299
|
+
kind: "audit",
|
|
300
|
+
reason: sinon.match(/skipped registry metadata fetches/i),
|
|
301
|
+
target: "predictive-dependency-audit",
|
|
302
|
+
});
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
describe("runDirectBomAuditFromBoms()", () => {
|
|
307
|
+
it("throws when no BOM inputs are provided", async () => {
|
|
308
|
+
await assert.rejects(
|
|
309
|
+
runDirectBomAuditFromBoms([], {}),
|
|
310
|
+
/No CycloneDX BOM inputs were found/,
|
|
311
|
+
);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("defaults OBOM-like saved BOMs to the obom-runtime rule category", async () => {
|
|
315
|
+
const auditBomStub = sinon.stub().resolves([
|
|
316
|
+
{
|
|
317
|
+
category: "obom-runtime",
|
|
318
|
+
message: "Mount '/dev/shm' is missing noexec.",
|
|
319
|
+
ruleId: "OBOM-LNX-018",
|
|
320
|
+
severity: "high",
|
|
321
|
+
},
|
|
322
|
+
]);
|
|
323
|
+
const { runDirectBomAuditFromBoms: mockedRunDirectBomAuditFromBoms } =
|
|
324
|
+
await esmock("./index.js", {
|
|
325
|
+
"../stages/postgen/auditBom.js": {
|
|
326
|
+
auditBom: auditBomStub,
|
|
327
|
+
isObomLikeBom: () => true,
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
const report = await mockedRunDirectBomAuditFromBoms(
|
|
332
|
+
[
|
|
333
|
+
{
|
|
334
|
+
bomJson: {
|
|
335
|
+
bomFormat: "CycloneDX",
|
|
336
|
+
metadata: {
|
|
337
|
+
lifecycles: [{ phase: "operations" }],
|
|
338
|
+
},
|
|
339
|
+
specVersion: "1.7",
|
|
340
|
+
version: 1,
|
|
341
|
+
},
|
|
342
|
+
source: "saved-obom.json",
|
|
343
|
+
},
|
|
344
|
+
],
|
|
345
|
+
{
|
|
346
|
+
minSeverity: "low",
|
|
347
|
+
},
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
assert.strictEqual(auditBomStub.callCount, 1);
|
|
351
|
+
assert.deepStrictEqual(auditBomStub.firstCall.args[1], {
|
|
352
|
+
bomAuditCategories: "obom-runtime",
|
|
353
|
+
bomAuditMinSeverity: "low",
|
|
354
|
+
bomAuditRulesDir: undefined,
|
|
355
|
+
});
|
|
356
|
+
assert.strictEqual(report.auditMode, "direct");
|
|
357
|
+
assert.strictEqual(report.summary.totalFindings, 1);
|
|
358
|
+
assert.strictEqual(report.summary.findingsBySeverity.high, 1);
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it("defaults HBOM-like saved BOMs to the HBOM rule categories", async () => {
|
|
362
|
+
const auditBomStub = sinon.stub().resolves([
|
|
363
|
+
{
|
|
364
|
+
category: "hbom-security",
|
|
365
|
+
message: "Storage component 'Main SSD' is reported as unencrypted",
|
|
366
|
+
ruleId: "HBS-001",
|
|
367
|
+
severity: "high",
|
|
368
|
+
},
|
|
369
|
+
]);
|
|
370
|
+
const { runDirectBomAuditFromBoms: mockedRunDirectBomAuditFromBoms } =
|
|
371
|
+
await esmock("./index.js", {
|
|
372
|
+
"../stages/postgen/auditBom.js": {
|
|
373
|
+
auditBom: auditBomStub,
|
|
374
|
+
isHbomLikeBom: () => true,
|
|
375
|
+
isObomLikeBom: () => false,
|
|
376
|
+
},
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
const report = await mockedRunDirectBomAuditFromBoms(
|
|
380
|
+
[
|
|
381
|
+
{
|
|
382
|
+
bomJson: {
|
|
383
|
+
bomFormat: "CycloneDX",
|
|
384
|
+
metadata: {
|
|
385
|
+
component: {
|
|
386
|
+
properties: [
|
|
387
|
+
{
|
|
388
|
+
name: "cdx:hbom:platform",
|
|
389
|
+
value: "darwin",
|
|
390
|
+
},
|
|
391
|
+
],
|
|
392
|
+
type: "device",
|
|
393
|
+
},
|
|
394
|
+
},
|
|
395
|
+
properties: [
|
|
396
|
+
{
|
|
397
|
+
name: "cdx:hbom:collectorProfile",
|
|
398
|
+
value: "darwin-arm64",
|
|
399
|
+
},
|
|
400
|
+
],
|
|
401
|
+
specVersion: "1.7",
|
|
402
|
+
version: 1,
|
|
403
|
+
},
|
|
404
|
+
source: "saved-hbom.json",
|
|
405
|
+
},
|
|
406
|
+
],
|
|
407
|
+
{
|
|
408
|
+
minSeverity: "low",
|
|
409
|
+
},
|
|
410
|
+
);
|
|
411
|
+
|
|
412
|
+
assert.strictEqual(auditBomStub.callCount, 1);
|
|
413
|
+
assert.deepStrictEqual(auditBomStub.firstCall.args[1], {
|
|
414
|
+
bomAuditCategories: "hbom-security,hbom-performance,hbom-compliance",
|
|
415
|
+
bomAuditMinSeverity: "low",
|
|
416
|
+
bomAuditRulesDir: undefined,
|
|
417
|
+
});
|
|
418
|
+
assert.strictEqual(report.auditMode, "direct");
|
|
419
|
+
assert.strictEqual(report.summary.totalFindings, 1);
|
|
420
|
+
assert.strictEqual(report.summary.findingsBySeverity.high, 1);
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
it("honors explicit categories and custom rules for direct BOM audit", async () => {
|
|
424
|
+
const auditBomStub = sinon.stub().resolves([]);
|
|
425
|
+
const { runDirectBomAuditFromBoms: mockedRunDirectBomAuditFromBoms } =
|
|
426
|
+
await esmock("./index.js", {
|
|
427
|
+
"../stages/postgen/auditBom.js": {
|
|
428
|
+
auditBom: auditBomStub,
|
|
429
|
+
isObomLikeBom: () => false,
|
|
430
|
+
},
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
await mockedRunDirectBomAuditFromBoms(
|
|
434
|
+
[
|
|
435
|
+
{
|
|
436
|
+
bomJson: {
|
|
437
|
+
bomFormat: "CycloneDX",
|
|
438
|
+
specVersion: "1.7",
|
|
439
|
+
version: 1,
|
|
440
|
+
},
|
|
441
|
+
source: "saved-bom.json",
|
|
442
|
+
},
|
|
443
|
+
],
|
|
444
|
+
{
|
|
445
|
+
categories: ["obom-runtime", "rootfs-hardening"],
|
|
446
|
+
minSeverity: "medium",
|
|
447
|
+
rulesDir: "/tmp/custom-rules",
|
|
448
|
+
},
|
|
449
|
+
);
|
|
450
|
+
|
|
451
|
+
assert.deepStrictEqual(auditBomStub.firstCall.args[1], {
|
|
452
|
+
bomAuditCategories: "obom-runtime,rootfs-hardening",
|
|
453
|
+
bomAuditMinSeverity: "medium",
|
|
454
|
+
bomAuditRulesDir: "/tmp/custom-rules",
|
|
455
|
+
});
|
|
456
|
+
});
|
|
195
457
|
});
|
|
196
458
|
|
|
197
459
|
describe("finalizeAuditReport()", () => {
|
|
@@ -426,6 +688,49 @@ describe("finalizeAuditReport()", () => {
|
|
|
426
688
|
);
|
|
427
689
|
});
|
|
428
690
|
|
|
691
|
+
it("returns exit code 3 for direct BOM audit findings at the fail threshold", () => {
|
|
692
|
+
const finalized = finalizeAuditReport(
|
|
693
|
+
{
|
|
694
|
+
auditMode: "direct",
|
|
695
|
+
inputs: ["saved-obom.json"],
|
|
696
|
+
results: [
|
|
697
|
+
{
|
|
698
|
+
findings: [
|
|
699
|
+
{
|
|
700
|
+
description: "Temporary mounts should carry noexec.",
|
|
701
|
+
message: "Mount '/dev/shm' is missing noexec.",
|
|
702
|
+
ruleId: "OBOM-LNX-018",
|
|
703
|
+
severity: "high",
|
|
704
|
+
},
|
|
705
|
+
],
|
|
706
|
+
source: "saved-obom.json",
|
|
707
|
+
},
|
|
708
|
+
],
|
|
709
|
+
summary: {
|
|
710
|
+
bomsWithFindings: 1,
|
|
711
|
+
findingsBySeverity: {
|
|
712
|
+
critical: 0,
|
|
713
|
+
high: 1,
|
|
714
|
+
low: 0,
|
|
715
|
+
medium: 0,
|
|
716
|
+
},
|
|
717
|
+
inputBomCount: 1,
|
|
718
|
+
maxSeverity: "high",
|
|
719
|
+
totalFindings: 1,
|
|
720
|
+
},
|
|
721
|
+
},
|
|
722
|
+
{
|
|
723
|
+
failSeverity: "high",
|
|
724
|
+
minSeverity: "low",
|
|
725
|
+
report: "console",
|
|
726
|
+
},
|
|
727
|
+
);
|
|
728
|
+
|
|
729
|
+
assert.strictEqual(finalized.exitCode, 3);
|
|
730
|
+
assert.match(finalized.output, /\/dev\/shm/);
|
|
731
|
+
assert.match(finalized.output, /Mount '\/dev\/shm' is missing noexec/);
|
|
732
|
+
});
|
|
733
|
+
|
|
429
734
|
it("includes synthetic SARIF results when a target fails before findings are produced", () => {
|
|
430
735
|
const rendered = renderAuditReport(
|
|
431
736
|
"sarif",
|
|
@@ -689,6 +994,33 @@ describe("finalizeAuditReport()", () => {
|
|
|
689
994
|
assert.match(rendered, /No dependencies require your attention right now/);
|
|
690
995
|
assert.match(rendered, /configured severity threshold \('low'\)/);
|
|
691
996
|
});
|
|
997
|
+
|
|
998
|
+
it("explains predictive audit planning limits in dry-run mode", () => {
|
|
999
|
+
const rendered = renderConsoleReport(
|
|
1000
|
+
{
|
|
1001
|
+
dryRun: true,
|
|
1002
|
+
results: [],
|
|
1003
|
+
summary: {
|
|
1004
|
+
erroredTargets: 0,
|
|
1005
|
+
groupedResultCount: 0,
|
|
1006
|
+
inputBomCount: 1,
|
|
1007
|
+
predictiveDryRun: true,
|
|
1008
|
+
scannedTargets: 0,
|
|
1009
|
+
skippedTargets: 2,
|
|
1010
|
+
totalTargets: 2,
|
|
1011
|
+
},
|
|
1012
|
+
},
|
|
1013
|
+
{
|
|
1014
|
+
minSeverity: "low",
|
|
1015
|
+
},
|
|
1016
|
+
);
|
|
1017
|
+
|
|
1018
|
+
assert.match(
|
|
1019
|
+
rendered,
|
|
1020
|
+
/Dry-run mode only planned predictive audit targets/i,
|
|
1021
|
+
);
|
|
1022
|
+
assert.match(rendered, /Re-run without --dry-run/i);
|
|
1023
|
+
});
|
|
692
1024
|
});
|
|
693
1025
|
|
|
694
1026
|
describe("groupAuditResults()", () => {
|
package/lib/audit/reporters.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { buildAnnotationText } from "../helpers/annotationFormatter.js";
|
|
2
2
|
import { table } from "../helpers/table.js";
|
|
3
3
|
import { getTimestamp } from "../helpers/utils.js";
|
|
4
|
+
import { renderBomAuditConsoleReport } from "../stages/postgen/auditBom.js";
|
|
4
5
|
import { severityMeetsThreshold } from "./scoring.js";
|
|
5
6
|
|
|
6
7
|
const SARIF_VERSION = "2.1.0";
|
|
@@ -21,6 +22,18 @@ function filterResults(results, minSeverity) {
|
|
|
21
22
|
);
|
|
22
23
|
}
|
|
23
24
|
|
|
25
|
+
function filterDirectFindingEntries(report, minSeverity) {
|
|
26
|
+
const entries = [];
|
|
27
|
+
for (const result of report?.results || []) {
|
|
28
|
+
for (const finding of result?.findings || []) {
|
|
29
|
+
if (severityMeetsThreshold(finding?.severity || "none", minSeverity)) {
|
|
30
|
+
entries.push({ finding, result });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return entries;
|
|
35
|
+
}
|
|
36
|
+
|
|
24
37
|
function effectiveResults(report) {
|
|
25
38
|
return report.groupedResults?.length
|
|
26
39
|
? report.groupedResults
|
|
@@ -50,6 +63,97 @@ function severityToSarifLevel(severity) {
|
|
|
50
63
|
}
|
|
51
64
|
}
|
|
52
65
|
|
|
66
|
+
function directBomFindingLocations(finding, result) {
|
|
67
|
+
const bomRef =
|
|
68
|
+
finding?.location?.bomRef ||
|
|
69
|
+
finding?.location?.purl ||
|
|
70
|
+
result?.serialNumber ||
|
|
71
|
+
result?.source;
|
|
72
|
+
if (finding?.location?.file) {
|
|
73
|
+
return [
|
|
74
|
+
{
|
|
75
|
+
physicalLocation: {
|
|
76
|
+
artifactLocation: {
|
|
77
|
+
uri: finding.location.file,
|
|
78
|
+
},
|
|
79
|
+
},
|
|
80
|
+
logicalLocations: bomRef
|
|
81
|
+
? [{ fullyQualifiedName: bomRef, kind: "package" }]
|
|
82
|
+
: undefined,
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
if (bomRef) {
|
|
87
|
+
return [
|
|
88
|
+
{
|
|
89
|
+
logicalLocations: [{ fullyQualifiedName: bomRef, kind: "package" }],
|
|
90
|
+
},
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
return [
|
|
94
|
+
{
|
|
95
|
+
logicalLocations: [{ fullyQualifiedName: "cdx-audit", kind: "tool" }],
|
|
96
|
+
},
|
|
97
|
+
];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function deriveDirectBomSarifRules(entries) {
|
|
101
|
+
const rulesById = new Map();
|
|
102
|
+
for (const { finding } of entries) {
|
|
103
|
+
if (rulesById.has(finding.ruleId)) {
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
rulesById.set(finding.ruleId, {
|
|
107
|
+
id: finding.ruleId,
|
|
108
|
+
name: finding.name || finding.ruleId,
|
|
109
|
+
shortDescription: {
|
|
110
|
+
text: finding.name || finding.ruleId,
|
|
111
|
+
},
|
|
112
|
+
fullDescription: {
|
|
113
|
+
text: finding.description || finding.name || finding.ruleId,
|
|
114
|
+
},
|
|
115
|
+
defaultConfiguration: {
|
|
116
|
+
level: severityToSarifLevel(finding.severity),
|
|
117
|
+
},
|
|
118
|
+
help: finding.mitigation
|
|
119
|
+
? {
|
|
120
|
+
markdown: `**Remediation:** ${finding.mitigation}`,
|
|
121
|
+
text: finding.mitigation,
|
|
122
|
+
}
|
|
123
|
+
: undefined,
|
|
124
|
+
properties: {
|
|
125
|
+
attackTactics: finding.attackTactics,
|
|
126
|
+
attackTechniques: finding.attackTechniques,
|
|
127
|
+
category: finding.category,
|
|
128
|
+
engine: "cdx-audit-direct-bom",
|
|
129
|
+
tags: attackTags(finding),
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return [...rulesById.values()];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function directBomFindingToSarifResult(finding, result) {
|
|
137
|
+
return {
|
|
138
|
+
level: severityToSarifLevel(finding?.severity),
|
|
139
|
+
locations: directBomFindingLocations(finding, result),
|
|
140
|
+
message: {
|
|
141
|
+
text: finding?.message || finding?.description || finding?.ruleId,
|
|
142
|
+
},
|
|
143
|
+
properties: {
|
|
144
|
+
attackTactics: finding?.attackTactics,
|
|
145
|
+
attackTechniques: finding?.attackTechniques,
|
|
146
|
+
category: finding?.category,
|
|
147
|
+
description: finding?.description,
|
|
148
|
+
evidence: finding?.evidence,
|
|
149
|
+
inputBom: result?.source,
|
|
150
|
+
mitigation: finding?.mitigation,
|
|
151
|
+
severity: finding?.severity,
|
|
152
|
+
},
|
|
153
|
+
ruleId: finding?.ruleId || AUDIT_ERROR_RULE_ID,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
53
157
|
function splitCsv(value) {
|
|
54
158
|
return String(value || "")
|
|
55
159
|
.split(",")
|
|
@@ -481,6 +585,105 @@ export function renderJsonReport(report) {
|
|
|
481
585
|
return `${JSON.stringify(report, null, 2)}\n`;
|
|
482
586
|
}
|
|
483
587
|
|
|
588
|
+
/**
|
|
589
|
+
* Render a direct BOM audit report for terminal output.
|
|
590
|
+
*
|
|
591
|
+
* @param {object} report aggregate direct audit report
|
|
592
|
+
* @param {object} options render options
|
|
593
|
+
* @returns {string} console report text
|
|
594
|
+
*/
|
|
595
|
+
export function renderDirectBomConsoleReport(report, options = {}) {
|
|
596
|
+
const minSeverity = options.minSeverity || "low";
|
|
597
|
+
const visibleResults = (report?.results || [])
|
|
598
|
+
.map((result) => ({
|
|
599
|
+
...result,
|
|
600
|
+
findings: (result.findings || []).filter((finding) =>
|
|
601
|
+
severityMeetsThreshold(finding?.severity || "none", minSeverity),
|
|
602
|
+
),
|
|
603
|
+
}))
|
|
604
|
+
.filter((result) => result.findings.length > 0);
|
|
605
|
+
if (report?.results?.length === 1) {
|
|
606
|
+
if (visibleResults.length > 0) {
|
|
607
|
+
return `${renderBomAuditConsoleReport(visibleResults[0].findings)}\n`;
|
|
608
|
+
}
|
|
609
|
+
return [
|
|
610
|
+
"cdx-audit — direct BOM policy audit",
|
|
611
|
+
"",
|
|
612
|
+
`Input BOMs: ${report?.summary?.inputBomCount || 0}`,
|
|
613
|
+
`Findings: ${report?.summary?.totalFindings || 0}`,
|
|
614
|
+
"",
|
|
615
|
+
`No direct BOM findings met or exceeded the configured severity threshold ('${minSeverity}').`,
|
|
616
|
+
]
|
|
617
|
+
.join("\n")
|
|
618
|
+
.concat("\n");
|
|
619
|
+
}
|
|
620
|
+
const lines = [];
|
|
621
|
+
lines.push("cdx-audit — direct BOM policy audit");
|
|
622
|
+
lines.push("");
|
|
623
|
+
lines.push(`Input BOMs: ${report?.summary?.inputBomCount || 0}`);
|
|
624
|
+
lines.push(`BOMs with findings: ${report?.summary?.bomsWithFindings || 0}`);
|
|
625
|
+
lines.push(`Findings: ${report?.summary?.totalFindings || 0}`);
|
|
626
|
+
lines.push("");
|
|
627
|
+
if (!visibleResults.length) {
|
|
628
|
+
lines.push(
|
|
629
|
+
`No direct BOM findings met or exceeded the configured severity threshold ('${minSeverity}').`,
|
|
630
|
+
);
|
|
631
|
+
return `${lines.join("\n")}\n`;
|
|
632
|
+
}
|
|
633
|
+
for (const result of visibleResults) {
|
|
634
|
+
lines.push(`Input BOM: ${result.source}`);
|
|
635
|
+
lines.push(renderBomAuditConsoleReport(result.findings));
|
|
636
|
+
lines.push("");
|
|
637
|
+
}
|
|
638
|
+
return `${lines.join("\n")}\n`;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Render a direct BOM audit report as SARIF 2.1.0 output.
|
|
643
|
+
*
|
|
644
|
+
* @param {object} report aggregate direct audit report
|
|
645
|
+
* @param {object} [options] render options
|
|
646
|
+
* @returns {string} SARIF output
|
|
647
|
+
*/
|
|
648
|
+
export function renderDirectBomSarifReport(report, options = {}) {
|
|
649
|
+
const minSeverity = options.minSeverity || "low";
|
|
650
|
+
const entries = filterDirectFindingEntries(report, minSeverity);
|
|
651
|
+
const sarifResults = entries.map(({ finding, result }) =>
|
|
652
|
+
directBomFindingToSarifResult(finding, result),
|
|
653
|
+
);
|
|
654
|
+
const toolName = report?.tool?.name || "cdx-audit";
|
|
655
|
+
const toolVersion = report?.tool?.version || "v12";
|
|
656
|
+
const log = {
|
|
657
|
+
$schema: SARIF_SCHEMA,
|
|
658
|
+
version: SARIF_VERSION,
|
|
659
|
+
runs: [
|
|
660
|
+
{
|
|
661
|
+
tool: {
|
|
662
|
+
driver: {
|
|
663
|
+
informationUri: "https://cdxgen.github.io/cdxgen/",
|
|
664
|
+
name: toolName,
|
|
665
|
+
rules: deriveDirectBomSarifRules(entries),
|
|
666
|
+
version: toolVersion,
|
|
667
|
+
},
|
|
668
|
+
},
|
|
669
|
+
invocations: [
|
|
670
|
+
{
|
|
671
|
+
executionSuccessful: true,
|
|
672
|
+
},
|
|
673
|
+
],
|
|
674
|
+
properties: {
|
|
675
|
+
auditMode: report?.auditMode,
|
|
676
|
+
generatedAt: report?.generatedAt,
|
|
677
|
+
inputs: report?.inputs || [],
|
|
678
|
+
summary: report?.summary,
|
|
679
|
+
},
|
|
680
|
+
results: sarifResults,
|
|
681
|
+
},
|
|
682
|
+
],
|
|
683
|
+
};
|
|
684
|
+
return `${JSON.stringify(log, null, 2)}\n`;
|
|
685
|
+
}
|
|
686
|
+
|
|
484
687
|
/**
|
|
485
688
|
* Render an audit report for terminal output.
|
|
486
689
|
*
|
|
@@ -514,6 +717,16 @@ export function renderConsoleReport(report, options = {}) {
|
|
|
514
717
|
lines.push(
|
|
515
718
|
`No predictive findings met or exceeded the configured severity threshold ('${minSeverity}').`,
|
|
516
719
|
);
|
|
720
|
+
if (report.summary?.predictiveDryRun) {
|
|
721
|
+
lines.push(
|
|
722
|
+
"Dry-run mode only planned predictive audit targets. Registry metadata fetches, upstream repository cloning, and child SBOM generation were intentionally skipped.",
|
|
723
|
+
);
|
|
724
|
+
if (report.summary.totalTargets > 0) {
|
|
725
|
+
lines.push(
|
|
726
|
+
"Re-run without --dry-run to analyze the planned targets with the predictive dependency audit.",
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
517
730
|
if (report.summary.erroredTargets > 0) {
|
|
518
731
|
lines.push(
|
|
519
732
|
"Some targets could not be fully analyzed, so review the recorded analysis errors before treating this rollup as complete.",
|
|
@@ -546,6 +759,15 @@ export function renderConsoleReport(report, options = {}) {
|
|
|
546
759
|
* @returns {string} rendered report
|
|
547
760
|
*/
|
|
548
761
|
export function renderAuditReport(reportType, report, options = {}) {
|
|
762
|
+
if (report?.auditMode === "direct") {
|
|
763
|
+
if ((reportType || "console") === "json") {
|
|
764
|
+
return renderJsonReport(report);
|
|
765
|
+
}
|
|
766
|
+
if ((reportType || "console") === "sarif") {
|
|
767
|
+
return renderDirectBomSarifReport(report, options);
|
|
768
|
+
}
|
|
769
|
+
return renderDirectBomConsoleReport(report, options);
|
|
770
|
+
}
|
|
549
771
|
if ((reportType || "console") === "json") {
|
|
550
772
|
return renderJsonReport(report);
|
|
551
773
|
}
|