@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.
- package/README.md +69 -25
- package/bin/audit.js +21 -7
- package/bin/cdxgen.js +270 -127
- package/bin/convert.js +34 -15
- package/bin/hbom.js +495 -0
- package/bin/repl.js +592 -37
- package/bin/validate.js +31 -4
- package/bin/verify.js +18 -5
- package/data/README.md +298 -25
- package/data/component-tags.json +6 -0
- package/data/crypto-oid.json +16 -0
- package/data/cyclonedx-2.0-bundled.schema.json +7182 -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 +210 -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 +527 -99
- package/lib/cli/index.poku.js +1469 -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/bomUtils.js +155 -1
- package/lib/helpers/bomUtils.poku.js +79 -1
- 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 +350 -0
- package/lib/helpers/plugins.poku.js +57 -0
- package/lib/helpers/protobom.js +209 -45
- package/lib/helpers/protobom.poku.js +183 -5
- package/lib/helpers/protobomLoader.js +43 -0
- package/lib/helpers/protobomLoader.poku.js +31 -0
- 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 +4 -28
- 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 +406 -8
- package/lib/stages/postgen/postgen.poku.js +484 -0
- package/lib/stages/postgen/ruleEngine.js +116 -0
- package/lib/stages/pregen/envAudit.js +14 -3
- package/lib/validator/bomValidator.js +90 -38
- package/lib/validator/bomValidator.poku.js +90 -0
- package/lib/validator/complianceRules.js +4 -2
- package/lib/validator/index.poku.js +14 -0
- 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 +1 -1
- 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/bomUtils.d.ts +10 -0
- package/types/lib/helpers/bomUtils.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 +76 -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 +5 -4
- package/types/lib/helpers/protobom.d.ts.map +1 -1
- package/types/lib/helpers/protobomLoader.d.ts +17 -0
- package/types/lib/helpers/protobomLoader.d.ts.map +1 -0
- 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/types/lib/third-party/arborist/lib/node.d.ts +23 -0
- package/types/lib/third-party/arborist/lib/node.d.ts.map +1 -1
- package/types/lib/validator/bomValidator.d.ts.map +1 -1
- package/types/lib/validator/complianceRules.d.ts.map +1 -1
- package/data/spdx-model-v3.0.1.jsonld +0 -15999
|
@@ -124,6 +124,8 @@ it("parseImageName tests", () => {
|
|
|
124
124
|
async function loadDockerModule({
|
|
125
125
|
clientResponse,
|
|
126
126
|
fsOverrides,
|
|
127
|
+
streamOverrides,
|
|
128
|
+
tarOverrides,
|
|
127
129
|
utilsOverrides,
|
|
128
130
|
} = {}) {
|
|
129
131
|
const dockerClient = sinon.stub().resolves(
|
|
@@ -151,7 +153,13 @@ async function loadDockerModule({
|
|
|
151
153
|
getAllFiles: sinon.stub().returns([]),
|
|
152
154
|
getTmpDir: sinon.stub().returns("/tmp"),
|
|
153
155
|
isDryRun: false,
|
|
156
|
+
readEnvironmentVariable: sinon
|
|
157
|
+
.stub()
|
|
158
|
+
.callsFake((varName) => process.env[varName]),
|
|
154
159
|
recordActivity: sinon.stub(),
|
|
160
|
+
recordDecisionActivity: sinon.stub(),
|
|
161
|
+
recordSensitiveFileRead: sinon.stub(),
|
|
162
|
+
safeExtractArchive: sinon.stub().resolves(true),
|
|
155
163
|
safeExistsSync: sinon.stub().returns(false),
|
|
156
164
|
safeMkdirSync: sinon.stub(),
|
|
157
165
|
safeMkdtempSync: sinon.stub().returns("/tmp/docker-images-test"),
|
|
@@ -162,7 +170,15 @@ async function loadDockerModule({
|
|
|
162
170
|
};
|
|
163
171
|
const dockerModule = await esmock("./docker.js", {
|
|
164
172
|
"node:fs": fsStub,
|
|
173
|
+
"node:stream/promises": {
|
|
174
|
+
pipeline: sinon.stub().resolves(),
|
|
175
|
+
...streamOverrides,
|
|
176
|
+
},
|
|
165
177
|
got: { default: gotStub },
|
|
178
|
+
tar: {
|
|
179
|
+
x: sinon.stub().returns("extractor"),
|
|
180
|
+
...tarOverrides,
|
|
181
|
+
},
|
|
166
182
|
"../helpers/utils.js": utilsStub,
|
|
167
183
|
});
|
|
168
184
|
return { dockerClient, dockerModule, fsStub, gotStub, utilsStub };
|
|
@@ -386,12 +402,265 @@ await it("docker getConnection reports blocked network activity in dry-run mode"
|
|
|
386
402
|
});
|
|
387
403
|
});
|
|
388
404
|
|
|
389
|
-
await it("docker
|
|
405
|
+
await it("docker getConnection skips dry-run tracing on containerd runtimes", async () => {
|
|
406
|
+
const recordActivity = sinon.stub();
|
|
407
|
+
const recordSensitiveFileRead = sinon.stub();
|
|
408
|
+
await withEnv(
|
|
409
|
+
{
|
|
410
|
+
CONTAINERD_ADDRESS: "/run/containerd/containerd.sock",
|
|
411
|
+
},
|
|
412
|
+
async () => {
|
|
413
|
+
const { dockerModule } = await loadDockerModule({
|
|
414
|
+
fsOverrides: {
|
|
415
|
+
readFileSync: sinon.stub().returns(authConfigData("docker.io")),
|
|
416
|
+
},
|
|
417
|
+
utilsOverrides: {
|
|
418
|
+
isDryRun: true,
|
|
419
|
+
recordActivity,
|
|
420
|
+
recordSensitiveFileRead,
|
|
421
|
+
safeExistsSync: dockerConfigExistsStub(),
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
const conn = await dockerModule.getConnection({}, "docker.io");
|
|
425
|
+
assert.strictEqual(conn, undefined);
|
|
426
|
+
},
|
|
427
|
+
);
|
|
428
|
+
sinon.assert.notCalled(recordActivity);
|
|
429
|
+
sinon.assert.notCalled(recordSensitiveFileRead);
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
await it("docker getConnection traces docker credential file reads in dry-run mode", async () => {
|
|
390
433
|
const recordActivity = sinon.stub();
|
|
434
|
+
const recordSensitiveFileRead = sinon.stub();
|
|
435
|
+
await withDockerConfig(async () => {
|
|
436
|
+
const { dockerModule } = await loadDockerModule({
|
|
437
|
+
fsOverrides: {
|
|
438
|
+
readFileSync: sinon.stub().returns(authConfigData("docker.io")),
|
|
439
|
+
},
|
|
440
|
+
utilsOverrides: {
|
|
441
|
+
isDryRun: true,
|
|
442
|
+
recordActivity,
|
|
443
|
+
recordSensitiveFileRead,
|
|
444
|
+
safeExistsSync: dockerConfigExistsStub(),
|
|
445
|
+
},
|
|
446
|
+
});
|
|
447
|
+
await dockerModule.getConnection({}, "docker.io");
|
|
448
|
+
});
|
|
449
|
+
sinon.assert.calledWithMatch(recordSensitiveFileRead, sinon.match.string, {
|
|
450
|
+
label: "Docker credential file",
|
|
451
|
+
});
|
|
452
|
+
sinon.assert.calledWithMatch(recordActivity, {
|
|
453
|
+
kind: "network",
|
|
454
|
+
status: "blocked",
|
|
455
|
+
target: "docker.io",
|
|
456
|
+
});
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
await it("docker makeRequest does not trace docker credential file reads when the read fails", async () => {
|
|
460
|
+
const recordSensitiveFileRead = sinon.stub();
|
|
461
|
+
await withEnv(
|
|
462
|
+
{
|
|
463
|
+
DOCKER_AUTH_CONFIG: undefined,
|
|
464
|
+
DOCKER_EMAIL: undefined,
|
|
465
|
+
DOCKER_PASSWORD: undefined,
|
|
466
|
+
DOCKER_USER: undefined,
|
|
467
|
+
},
|
|
468
|
+
async () => {
|
|
469
|
+
await withDockerConfig(async () => {
|
|
470
|
+
const { dockerModule } = await loadDockerModule({
|
|
471
|
+
fsOverrides: {
|
|
472
|
+
readFileSync: sinon.stub().throws(new Error("read failed")),
|
|
473
|
+
},
|
|
474
|
+
utilsOverrides: {
|
|
475
|
+
recordSensitiveFileRead,
|
|
476
|
+
safeExistsSync: dockerConfigExistsStub(),
|
|
477
|
+
},
|
|
478
|
+
});
|
|
479
|
+
await assert.rejects(() =>
|
|
480
|
+
dockerModule.makeRequest(
|
|
481
|
+
"images/create?fromImage=docker.io/library/alpine:latest",
|
|
482
|
+
"POST",
|
|
483
|
+
"docker.io/library/alpine:latest",
|
|
484
|
+
),
|
|
485
|
+
);
|
|
486
|
+
});
|
|
487
|
+
},
|
|
488
|
+
);
|
|
489
|
+
sinon.assert.notCalled(recordSensitiveFileRead);
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
await it("docker getConnection does not trace TLS client files when reading them fails", async () => {
|
|
493
|
+
const recordSensitiveFileRead = sinon.stub();
|
|
494
|
+
await withEnv(
|
|
495
|
+
{
|
|
496
|
+
DOCKER_AUTH_CONFIG: undefined,
|
|
497
|
+
DOCKER_CERT_PATH: "/tmp/docker-certs",
|
|
498
|
+
DOCKER_EMAIL: undefined,
|
|
499
|
+
DOCKER_HOST: "tcp://docker.example.test:2376",
|
|
500
|
+
DOCKER_PASSWORD: undefined,
|
|
501
|
+
DOCKER_USER: undefined,
|
|
502
|
+
},
|
|
503
|
+
async () => {
|
|
504
|
+
const { dockerModule } = await loadDockerModule({
|
|
505
|
+
fsOverrides: {
|
|
506
|
+
readFileSync: sinon
|
|
507
|
+
.stub()
|
|
508
|
+
.onFirstCall()
|
|
509
|
+
.throws(new Error("cert read failed")),
|
|
510
|
+
},
|
|
511
|
+
utilsOverrides: {
|
|
512
|
+
recordSensitiveFileRead,
|
|
513
|
+
},
|
|
514
|
+
});
|
|
515
|
+
await assert.rejects(() => dockerModule.getConnection({}, "docker.io"));
|
|
516
|
+
},
|
|
517
|
+
);
|
|
518
|
+
sinon.assert.notCalled(recordSensitiveFileRead);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
await it("docker makeRequest does not trace TLS client files when reading them fails", async () => {
|
|
522
|
+
const recordSensitiveFileRead = sinon.stub();
|
|
523
|
+
await withEnv(
|
|
524
|
+
{
|
|
525
|
+
DOCKER_AUTH_CONFIG: undefined,
|
|
526
|
+
DOCKER_CERT_PATH: "/tmp/docker-certs",
|
|
527
|
+
DOCKER_EMAIL: undefined,
|
|
528
|
+
DOCKER_HOST: "tcp://docker.example.test:2376",
|
|
529
|
+
DOCKER_PASSWORD: undefined,
|
|
530
|
+
DOCKER_USER: undefined,
|
|
531
|
+
},
|
|
532
|
+
async () => {
|
|
533
|
+
const { dockerModule } = await loadDockerModule({
|
|
534
|
+
fsOverrides: {
|
|
535
|
+
readFileSync: sinon
|
|
536
|
+
.stub()
|
|
537
|
+
.onFirstCall()
|
|
538
|
+
.throws(new Error("cert read failed")),
|
|
539
|
+
},
|
|
540
|
+
utilsOverrides: {
|
|
541
|
+
recordSensitiveFileRead,
|
|
542
|
+
},
|
|
543
|
+
});
|
|
544
|
+
await assert.rejects(() =>
|
|
545
|
+
dockerModule.makeRequest(
|
|
546
|
+
"images/create?fromImage=docker.io/library/alpine:latest",
|
|
547
|
+
"POST",
|
|
548
|
+
"docker.io/library/alpine:latest",
|
|
549
|
+
),
|
|
550
|
+
);
|
|
551
|
+
},
|
|
552
|
+
);
|
|
553
|
+
sinon.assert.notCalled(recordSensitiveFileRead);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
await it("docker getConnection records which credential source was selected", async () => {
|
|
557
|
+
const recordDecisionActivity = sinon.stub();
|
|
558
|
+
await withDockerConfig(async () => {
|
|
559
|
+
const { dockerModule } = await loadDockerModule({
|
|
560
|
+
fsOverrides: {
|
|
561
|
+
readFileSync: sinon.stub().returns(authConfigData("docker.io")),
|
|
562
|
+
},
|
|
563
|
+
utilsOverrides: {
|
|
564
|
+
isDryRun: true,
|
|
565
|
+
recordDecisionActivity,
|
|
566
|
+
safeExistsSync: dockerConfigExistsStub(),
|
|
567
|
+
},
|
|
568
|
+
});
|
|
569
|
+
await dockerModule.getConnection({}, "docker.io");
|
|
570
|
+
});
|
|
571
|
+
sinon.assert.calledWithMatch(
|
|
572
|
+
recordDecisionActivity,
|
|
573
|
+
"docker-auth:docker.io",
|
|
574
|
+
{
|
|
575
|
+
metadata: sinon.match({
|
|
576
|
+
decisionType: "credential-source-selection",
|
|
577
|
+
selectedSource: "docker-config-auth",
|
|
578
|
+
}),
|
|
579
|
+
},
|
|
580
|
+
);
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
await it("docker getConnection traces credential helper resolution in dry-run mode", async () => {
|
|
584
|
+
const safeSpawnSync = sinon.stub().returns({
|
|
585
|
+
status: 1,
|
|
586
|
+
stdout: "",
|
|
587
|
+
stderr: "",
|
|
588
|
+
});
|
|
589
|
+
await withDockerConfig(async () => {
|
|
590
|
+
const { dockerModule } = await loadDockerModule({
|
|
591
|
+
fsOverrides: {
|
|
592
|
+
readFileSync: sinon.stub().returns(credHelperConfigData("docker.io")),
|
|
593
|
+
},
|
|
594
|
+
utilsOverrides: {
|
|
595
|
+
isDryRun: true,
|
|
596
|
+
safeExistsSync: dockerConfigExistsStub(),
|
|
597
|
+
safeSpawnSync,
|
|
598
|
+
},
|
|
599
|
+
});
|
|
600
|
+
await dockerModule.getConnection({}, "docker.io");
|
|
601
|
+
});
|
|
602
|
+
sinon.assert.calledWithExactly(
|
|
603
|
+
safeSpawnSync,
|
|
604
|
+
credHelperExe("osxkeychain"),
|
|
605
|
+
["get"],
|
|
606
|
+
{
|
|
607
|
+
input: "docker.io",
|
|
608
|
+
},
|
|
609
|
+
);
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
await it("docker extractTar reports a blocked untar activity in dry-run mode", async () => {
|
|
613
|
+
const safeExtractArchive = sinon.stub().resolves(false);
|
|
391
614
|
const { dockerModule } = await loadDockerModule({
|
|
392
615
|
utilsOverrides: {
|
|
393
|
-
|
|
394
|
-
|
|
616
|
+
safeExtractArchive,
|
|
617
|
+
},
|
|
618
|
+
});
|
|
619
|
+
const result = await dockerModule.extractTar(
|
|
620
|
+
"/tmp/image.tar",
|
|
621
|
+
"/tmp/out",
|
|
622
|
+
{},
|
|
623
|
+
);
|
|
624
|
+
assert.strictEqual(result, false);
|
|
625
|
+
sinon.assert.calledWithMatch(
|
|
626
|
+
safeExtractArchive,
|
|
627
|
+
"/tmp/image.tar",
|
|
628
|
+
"/tmp/out",
|
|
629
|
+
sinon.match.func,
|
|
630
|
+
"untar",
|
|
631
|
+
{
|
|
632
|
+
blockedReason:
|
|
633
|
+
"Dry run mode blocks untar and layer extraction operations because they create files on disk.",
|
|
634
|
+
metadata: {
|
|
635
|
+
archiveFormat: "tar",
|
|
636
|
+
},
|
|
637
|
+
},
|
|
638
|
+
);
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
await it("docker extractTar delegates successful untar tracing to safeExtractArchive", async () => {
|
|
642
|
+
const safeExtractArchive = sinon.stub().resolves(true);
|
|
643
|
+
const { dockerModule } = await loadDockerModule({
|
|
644
|
+
utilsOverrides: {
|
|
645
|
+
safeExtractArchive,
|
|
646
|
+
},
|
|
647
|
+
});
|
|
648
|
+
const result = await dockerModule.extractTar(
|
|
649
|
+
"/tmp/image.tar",
|
|
650
|
+
"/tmp/out",
|
|
651
|
+
{},
|
|
652
|
+
);
|
|
653
|
+
assert.strictEqual(result, true);
|
|
654
|
+
sinon.assert.calledOnce(safeExtractArchive);
|
|
655
|
+
});
|
|
656
|
+
|
|
657
|
+
await it("docker extractTar preserves failure handling after safeExtractArchive rejects", async () => {
|
|
658
|
+
const extractionError = new Error("permission denied");
|
|
659
|
+
extractionError.code = "EACCES";
|
|
660
|
+
const safeExtractArchive = sinon.stub().rejects(extractionError);
|
|
661
|
+
const { dockerModule } = await loadDockerModule({
|
|
662
|
+
utilsOverrides: {
|
|
663
|
+
safeExtractArchive,
|
|
395
664
|
},
|
|
396
665
|
});
|
|
397
666
|
const result = await dockerModule.extractTar(
|
|
@@ -400,28 +669,84 @@ await it("docker extractTar reports a blocked untar activity in dry-run mode", a
|
|
|
400
669
|
{},
|
|
401
670
|
);
|
|
402
671
|
assert.strictEqual(result, false);
|
|
672
|
+
sinon.assert.calledOnce(safeExtractArchive);
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
await it("docker exportImage reports a blocked container activity in dry-run mode", async () => {
|
|
676
|
+
const recordActivity = sinon.stub();
|
|
677
|
+
const recordSensitiveFileRead = sinon.stub();
|
|
678
|
+
await withDockerConfig(async () => {
|
|
679
|
+
const { dockerModule } = await loadDockerModule({
|
|
680
|
+
fsOverrides: {
|
|
681
|
+
readFileSync: sinon.stub().returns(authConfigData("docker.io")),
|
|
682
|
+
},
|
|
683
|
+
utilsOverrides: {
|
|
684
|
+
isDryRun: true,
|
|
685
|
+
recordActivity,
|
|
686
|
+
recordSensitiveFileRead,
|
|
687
|
+
safeExistsSync: dockerConfigExistsStub(),
|
|
688
|
+
},
|
|
689
|
+
});
|
|
690
|
+
const result = await dockerModule.exportImage("alpine:3.20", {});
|
|
691
|
+
assert.strictEqual(result, undefined);
|
|
692
|
+
});
|
|
693
|
+
sinon.assert.calledWithMatch(recordSensitiveFileRead, sinon.match.string, {
|
|
694
|
+
label: "Docker credential file",
|
|
695
|
+
});
|
|
403
696
|
sinon.assert.calledWithMatch(recordActivity, {
|
|
404
|
-
kind: "
|
|
697
|
+
kind: "container",
|
|
405
698
|
status: "blocked",
|
|
406
|
-
target: "
|
|
699
|
+
target: "alpine:3.20",
|
|
407
700
|
});
|
|
408
701
|
});
|
|
409
702
|
|
|
410
|
-
await it("docker exportImage
|
|
703
|
+
await it("docker exportImage preserves scoped registry refs for dry-run auth tracing", async () => {
|
|
704
|
+
const recordDecisionActivity = sinon.stub();
|
|
705
|
+
await withDockerConfig(async () => {
|
|
706
|
+
const { dockerModule } = await loadDockerModule({
|
|
707
|
+
fsOverrides: {
|
|
708
|
+
readFileSync: sinon
|
|
709
|
+
.stub()
|
|
710
|
+
.returns(authConfigData("registry.example.com/team")),
|
|
711
|
+
},
|
|
712
|
+
utilsOverrides: {
|
|
713
|
+
isDryRun: true,
|
|
714
|
+
recordDecisionActivity,
|
|
715
|
+
safeExistsSync: dockerConfigExistsStub(),
|
|
716
|
+
},
|
|
717
|
+
});
|
|
718
|
+
const result = await dockerModule.exportImage(
|
|
719
|
+
"registry.example.com/team/app:latest",
|
|
720
|
+
{},
|
|
721
|
+
);
|
|
722
|
+
assert.strictEqual(result, undefined);
|
|
723
|
+
});
|
|
724
|
+
sinon.assert.calledWithMatch(
|
|
725
|
+
recordDecisionActivity,
|
|
726
|
+
"docker-auth:registry.example.com/team/app",
|
|
727
|
+
{
|
|
728
|
+
metadata: sinon.match({
|
|
729
|
+
selectedSource: "docker-config-auth",
|
|
730
|
+
}),
|
|
731
|
+
},
|
|
732
|
+
);
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
await it("docker exportImage skips dry-run tracing for local paths", async () => {
|
|
411
736
|
const recordActivity = sinon.stub();
|
|
737
|
+
const recordSensitiveFileRead = sinon.stub();
|
|
412
738
|
const { dockerModule } = await loadDockerModule({
|
|
413
739
|
utilsOverrides: {
|
|
414
740
|
isDryRun: true,
|
|
415
741
|
recordActivity,
|
|
742
|
+
recordSensitiveFileRead,
|
|
743
|
+
safeExistsSync: sinon.stub().returns(true),
|
|
416
744
|
},
|
|
417
745
|
});
|
|
418
|
-
const result = await dockerModule.exportImage("
|
|
746
|
+
const result = await dockerModule.exportImage("/tmp/image.tar", {});
|
|
419
747
|
assert.strictEqual(result, undefined);
|
|
420
|
-
sinon.assert.
|
|
421
|
-
|
|
422
|
-
status: "blocked",
|
|
423
|
-
target: "alpine:3.20",
|
|
424
|
-
});
|
|
748
|
+
sinon.assert.notCalled(recordActivity);
|
|
749
|
+
sinon.assert.notCalled(recordSensitiveFileRead);
|
|
425
750
|
});
|
|
426
751
|
|
|
427
752
|
await it("docker exportImage ignores local directories", async () => {
|
package/lib/server/server.js
CHANGED
|
@@ -8,6 +8,7 @@ import compression from "compression";
|
|
|
8
8
|
import connect from "connect";
|
|
9
9
|
|
|
10
10
|
import { createBom, submitBom } from "../cli/index.js";
|
|
11
|
+
import { isCycloneDxBom } from "../helpers/bomUtils.js";
|
|
11
12
|
import { normalizeOutputFormats } from "../helpers/exportUtils.js";
|
|
12
13
|
import {
|
|
13
14
|
cleanupSourceDir,
|
|
@@ -27,7 +28,7 @@ import {
|
|
|
27
28
|
} from "../helpers/source.js";
|
|
28
29
|
import {
|
|
29
30
|
CDXGEN_VERSION,
|
|
30
|
-
|
|
31
|
+
isAllowedHttpHost,
|
|
31
32
|
isSecureMode,
|
|
32
33
|
isWin,
|
|
33
34
|
} from "../helpers/utils.js";
|
|
@@ -76,32 +77,7 @@ const ALLOWED_PARAMS = [
|
|
|
76
77
|
|
|
77
78
|
const app = connect();
|
|
78
79
|
|
|
79
|
-
export
|
|
80
|
-
if (!process.env.CDXGEN_ALLOWED_HOSTS) {
|
|
81
|
-
return true;
|
|
82
|
-
}
|
|
83
|
-
if (!hostname || hasDangerousUnicode(hostname)) {
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
-
const normalizedHostname = hostname.toLowerCase();
|
|
87
|
-
const allowHosts = process.env.CDXGEN_ALLOWED_HOSTS.split(",")
|
|
88
|
-
.map((host) => host.trim())
|
|
89
|
-
.filter(Boolean);
|
|
90
|
-
for (const allowedHost of allowHosts) {
|
|
91
|
-
const normalizedAllowedHost = allowedHost.toLowerCase();
|
|
92
|
-
if (normalizedHostname === normalizedAllowedHost) {
|
|
93
|
-
return true;
|
|
94
|
-
}
|
|
95
|
-
if (
|
|
96
|
-
normalizedAllowedHost.startsWith("*.") &&
|
|
97
|
-
normalizedHostname.length > normalizedAllowedHost.length - 1 &&
|
|
98
|
-
normalizedHostname.endsWith(`.${normalizedAllowedHost.slice(2)}`)
|
|
99
|
-
) {
|
|
100
|
-
return true;
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
80
|
+
export { isAllowedHttpHost };
|
|
105
81
|
|
|
106
82
|
app.use(
|
|
107
83
|
bodyParser.json({
|
|
@@ -492,7 +468,7 @@ const start = (options) => {
|
|
|
492
468
|
let responseBomJson = bomNSData.bomJson;
|
|
493
469
|
if (
|
|
494
470
|
requestedFormats.includes("spdx") &&
|
|
495
|
-
bomNSData?.bomJson
|
|
471
|
+
isCycloneDxBom(bomNSData?.bomJson)
|
|
496
472
|
) {
|
|
497
473
|
responseBomJson = convertCycloneDxToSpdx(bomNSData.bomJson, reqOptions);
|
|
498
474
|
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
|
|
4
|
+
import {
|
|
5
|
+
formatHbomHardwareClassSummary,
|
|
6
|
+
getHbomSummary,
|
|
7
|
+
isHbomLikeBom,
|
|
8
|
+
} from "../../helpers/hbomAnalysis.js";
|
|
9
|
+
import { getContainerFileInventoryStats } from "../../helpers/inventoryStats.js";
|
|
4
10
|
import { thoughtLog } from "../../helpers/logger.js";
|
|
5
11
|
import { getTrustedPublishingComponentCounts } from "../../helpers/provenanceUtils.js";
|
|
6
12
|
import { dirNameStr } from "../../helpers/utils.js";
|
|
@@ -247,6 +253,11 @@ export function findBomType(bomJson) {
|
|
|
247
253
|
bomType = "SBOM";
|
|
248
254
|
description = "Software Bill-of-Materials (SBOM) including GitHub Actions";
|
|
249
255
|
}
|
|
256
|
+
// Is this an HBOM?
|
|
257
|
+
else if (isHbomLikeBom(bomJson)) {
|
|
258
|
+
bomType = "HBOM";
|
|
259
|
+
description = "Hardware Bill-of-Materials (HBOM)";
|
|
260
|
+
}
|
|
250
261
|
// Is this an OBOM?
|
|
251
262
|
else if (lifecycles.filter((l) => l.phase === "operations").length > 0) {
|
|
252
263
|
bomType = "OBOM";
|
|
@@ -296,7 +307,10 @@ export function textualMetadata(bomJson) {
|
|
|
296
307
|
const swidCount = bomJson?.components?.filter((c) =>
|
|
297
308
|
c?.purl?.startsWith("pkg:swid"),
|
|
298
309
|
).length;
|
|
310
|
+
const { unpackagedExecutableCount, unpackagedSharedLibraryCount } =
|
|
311
|
+
getContainerFileInventoryStats(bomJson?.components);
|
|
299
312
|
const githubStats = getGitHubWorkflowStats(bomJson?.components);
|
|
313
|
+
const hbomSummary = bomType === "HBOM" ? getHbomSummary(bomJson) : undefined;
|
|
300
314
|
const trustedPublishingCounts = getTrustedPublishingComponentCounts(
|
|
301
315
|
bomJson?.components,
|
|
302
316
|
);
|
|
@@ -511,6 +525,9 @@ export function textualMetadata(bomJson) {
|
|
|
511
525
|
if (bundledSdks.length) {
|
|
512
526
|
text = `${text} Furthermore, the container image bundles the following SDKs: ${bundledSdks.join(", ")}.`;
|
|
513
527
|
}
|
|
528
|
+
if (unpackagedExecutableCount || unpackagedSharedLibraryCount) {
|
|
529
|
+
text = `${text} The container or rootfs inventory includes ${unpackagedExecutableCount} executable file component(s) and ${unpackagedSharedLibraryCount} shared library component(s) that were not traced to OS package ownership.`;
|
|
530
|
+
}
|
|
514
531
|
if (bomPkgTypes.length && bomPkgNamespaces.length) {
|
|
515
532
|
if (bomPkgTypes.length === 1) {
|
|
516
533
|
if (bomPkgNamespaces.length === 1) {
|
|
@@ -537,6 +554,27 @@ export function textualMetadata(bomJson) {
|
|
|
537
554
|
text = `${text} In addition, there are ${swidCount} applications installed on the system.`;
|
|
538
555
|
}
|
|
539
556
|
}
|
|
557
|
+
if (bomType === "HBOM" && hbomSummary) {
|
|
558
|
+
if (hbomSummary.hardwareClassCount > 0) {
|
|
559
|
+
text = `${text} The hardware inventory spans ${hbomSummary.hardwareClassCount} hardware classes.`;
|
|
560
|
+
const hardwareClassSummary = formatHbomHardwareClassSummary(
|
|
561
|
+
hbomSummary.hardwareClassCounts,
|
|
562
|
+
);
|
|
563
|
+
if (hardwareClassSummary) {
|
|
564
|
+
text = `${text} The most represented hardware classes are ${hardwareClassSummary}.`;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (hbomSummary.collectorProfile) {
|
|
568
|
+
text = `${text} Collector profile '${hbomSummary.collectorProfile}' recorded ${hbomSummary.evidenceCommandCount} command evidence entr${hbomSummary.evidenceCommandCount === 1 ? "y" : "ies"}`;
|
|
569
|
+
if (hbomSummary.evidenceFileCount > 0) {
|
|
570
|
+
text = `${text} and ${hbomSummary.evidenceFileCount} observed file entr${hbomSummary.evidenceFileCount === 1 ? "y" : "ies"}`;
|
|
571
|
+
}
|
|
572
|
+
text = `${text}.`;
|
|
573
|
+
}
|
|
574
|
+
if (hbomSummary.identifierPolicy) {
|
|
575
|
+
text = `${text} Identifier policy is '${hbomSummary.identifierPolicy}'.`;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
540
578
|
if (bomType === "SaaSBOM") {
|
|
541
579
|
text = `${text} ${bomJson.services.length} are described in this ${bomType} under services.`;
|
|
542
580
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { assert, it } from "poku";
|
|
2
2
|
|
|
3
|
-
import { extractTags, textualMetadata } from "./annotator.js";
|
|
3
|
+
import { extractTags, findBomType, textualMetadata } from "./annotator.js";
|
|
4
4
|
|
|
5
5
|
it("textualMetadata tests", () => {
|
|
6
6
|
assert.deepStrictEqual(textualMetadata({}), undefined);
|
|
@@ -303,6 +303,36 @@ it("textualMetadata tests", () => {
|
|
|
303
303
|
"Trusted publishing metadata is present for 1 npm component(s) and 1 PyPI component(s).",
|
|
304
304
|
),
|
|
305
305
|
);
|
|
306
|
+
|
|
307
|
+
assert.ok(
|
|
308
|
+
textualMetadata({
|
|
309
|
+
metadata: {
|
|
310
|
+
component: {
|
|
311
|
+
name: "demo-image",
|
|
312
|
+
type: "container",
|
|
313
|
+
version: "1.0.0",
|
|
314
|
+
},
|
|
315
|
+
timestamp: "2026-01-01T00:00:00Z",
|
|
316
|
+
tools: {
|
|
317
|
+
components: [{ name: "cdxgen" }],
|
|
318
|
+
},
|
|
319
|
+
},
|
|
320
|
+
components: [
|
|
321
|
+
{
|
|
322
|
+
type: "file",
|
|
323
|
+
name: "demo",
|
|
324
|
+
properties: [{ name: "internal:is_executable", value: "true" }],
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
type: "file",
|
|
328
|
+
name: "libdemo.so",
|
|
329
|
+
properties: [{ name: "internal:is_shared_library", value: "true" }],
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
}).includes(
|
|
333
|
+
"The container or rootfs inventory includes 1 executable file component(s) and 1 shared library component(s) that were not traced to OS package ownership.",
|
|
334
|
+
),
|
|
335
|
+
);
|
|
306
336
|
});
|
|
307
337
|
|
|
308
338
|
it("extractTags tests", () => {
|
|
@@ -326,3 +356,79 @@ it("textualMetadata includes the CycloneDX 1.7 TLP classification from distribut
|
|
|
326
356
|
/TLP\) classification for this document is 'AMBER_AND_STRICT'/,
|
|
327
357
|
);
|
|
328
358
|
});
|
|
359
|
+
|
|
360
|
+
it("recognizes HBOMs and summarizes hardware-specific metadata", () => {
|
|
361
|
+
const hbom = {
|
|
362
|
+
bomFormat: "CycloneDX",
|
|
363
|
+
specVersion: "1.7",
|
|
364
|
+
metadata: {
|
|
365
|
+
timestamp: "2026-01-01T00:00:00Z",
|
|
366
|
+
tools: {
|
|
367
|
+
components: [{ name: "cdxgen" }],
|
|
368
|
+
},
|
|
369
|
+
component: {
|
|
370
|
+
name: "demo-host",
|
|
371
|
+
type: "device",
|
|
372
|
+
manufacturer: { name: "Example Corp" },
|
|
373
|
+
properties: [
|
|
374
|
+
{ name: "cdx:hbom:platform", value: "linux" },
|
|
375
|
+
{ name: "cdx:hbom:architecture", value: "amd64" },
|
|
376
|
+
{
|
|
377
|
+
name: "cdx:hbom:identifierPolicy",
|
|
378
|
+
value: "redacted-by-default",
|
|
379
|
+
},
|
|
380
|
+
],
|
|
381
|
+
},
|
|
382
|
+
},
|
|
383
|
+
components: [
|
|
384
|
+
{
|
|
385
|
+
name: "eth0",
|
|
386
|
+
type: "device",
|
|
387
|
+
properties: [
|
|
388
|
+
{ name: "cdx:hbom:hardwareClass", value: "network-interface" },
|
|
389
|
+
],
|
|
390
|
+
},
|
|
391
|
+
{
|
|
392
|
+
name: "wlan0",
|
|
393
|
+
type: "device",
|
|
394
|
+
properties: [
|
|
395
|
+
{ name: "cdx:hbom:hardwareClass", value: "network-interface" },
|
|
396
|
+
],
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
name: "nvme0",
|
|
400
|
+
type: "device",
|
|
401
|
+
properties: [{ name: "cdx:hbom:hardwareClass", value: "storage" }],
|
|
402
|
+
},
|
|
403
|
+
],
|
|
404
|
+
properties: [
|
|
405
|
+
{ name: "cdx:hbom:collectorProfile", value: "linux-amd64-v1" },
|
|
406
|
+
{ name: "cdx:hbom:evidence:commandCount", value: "2" },
|
|
407
|
+
{
|
|
408
|
+
name: "cdx:hbom:evidence:command",
|
|
409
|
+
value: "lscpu-json|cpu-memory|/usr/bin/lscpu -J",
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
name: "cdx:hbom:evidence:command",
|
|
413
|
+
value: "ip-link-json|network|/usr/sbin/ip -j link",
|
|
414
|
+
},
|
|
415
|
+
],
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
assert.deepStrictEqual(findBomType(hbom), {
|
|
419
|
+
bomType: "HBOM",
|
|
420
|
+
bomTypeDescription: "Hardware Bill-of-Materials (HBOM)",
|
|
421
|
+
});
|
|
422
|
+
const summary = textualMetadata(hbom);
|
|
423
|
+
assert.match(summary, /Hardware Bill-of-Materials \(HBOM\)/u);
|
|
424
|
+
assert.match(summary, /The hardware inventory spans 2 hardware classes\./u);
|
|
425
|
+
assert.match(
|
|
426
|
+
summary,
|
|
427
|
+
/The most represented hardware classes are network-interface \(2\), storage \(1\)\./u,
|
|
428
|
+
);
|
|
429
|
+
assert.match(
|
|
430
|
+
summary,
|
|
431
|
+
/Collector profile 'linux-amd64-v1' recorded 2 command evidence entries\./u,
|
|
432
|
+
);
|
|
433
|
+
assert.match(summary, /Identifier policy is 'redacted-by-default'\./u);
|
|
434
|
+
});
|