@cyclonedx/cdxgen 12.3.2 → 12.3.3

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 (53) hide show
  1. package/README.md +6 -0
  2. package/data/rules/ci-permissions.yaml +132 -0
  3. package/data/rules/dependency-sources.yaml +65 -5
  4. package/data/rules/package-integrity.yaml +22 -0
  5. package/lib/cli/index.js +141 -39
  6. package/lib/cli/index.poku.js +579 -1
  7. package/lib/helpers/agentFormulationParser.js +6 -2
  8. package/lib/helpers/agentFormulationParser.poku.js +42 -0
  9. package/lib/helpers/analyzer.js +38 -9
  10. package/lib/helpers/analyzer.poku.js +67 -0
  11. package/lib/helpers/chromextutils.js +25 -3
  12. package/lib/helpers/chromextutils.poku.js +68 -0
  13. package/lib/helpers/ciParsers/githubActions.js +79 -0
  14. package/lib/helpers/ciParsers/githubActions.poku.js +103 -0
  15. package/lib/helpers/communityAiConfigParser.js +15 -5
  16. package/lib/helpers/communityAiConfigParser.poku.js +71 -0
  17. package/lib/helpers/depsUtils.js +5 -0
  18. package/lib/helpers/depsUtils.poku.js +55 -0
  19. package/lib/helpers/display.js +45 -22
  20. package/lib/helpers/display.poku.js +47 -60
  21. package/lib/helpers/mcpConfigParser.js +21 -5
  22. package/lib/helpers/mcpConfigParser.poku.js +39 -2
  23. package/lib/helpers/propertySanitizer.js +121 -0
  24. package/lib/helpers/utils.js +951 -40
  25. package/lib/helpers/utils.poku.js +882 -0
  26. package/lib/managers/binary.js +16 -0
  27. package/lib/managers/binary.poku.js +1 -0
  28. package/lib/managers/docker.js +240 -16
  29. package/lib/managers/docker.poku.js +1142 -2
  30. package/lib/server/server.js +7 -4
  31. package/lib/server/server.poku.js +36 -1
  32. package/lib/stages/postgen/auditBom.poku.js +644 -2
  33. package/package.json +2 -1
  34. package/types/lib/cli/index.d.ts.map +1 -1
  35. package/types/lib/helpers/agentFormulationParser.d.ts.map +1 -1
  36. package/types/lib/helpers/analyzer.d.ts.map +1 -1
  37. package/types/lib/helpers/chromextutils.d.ts.map +1 -1
  38. package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
  39. package/types/lib/helpers/communityAiConfigParser.d.ts.map +1 -1
  40. package/types/lib/helpers/depsUtils.d.ts.map +1 -1
  41. package/types/lib/helpers/display.d.ts +1 -0
  42. package/types/lib/helpers/display.d.ts.map +1 -1
  43. package/types/lib/helpers/mcpConfigParser.d.ts +1 -1
  44. package/types/lib/helpers/mcpConfigParser.d.ts.map +1 -1
  45. package/types/lib/helpers/propertySanitizer.d.ts +3 -0
  46. package/types/lib/helpers/propertySanitizer.d.ts.map +1 -0
  47. package/types/lib/helpers/utils.d.ts +29 -0
  48. package/types/lib/helpers/utils.d.ts.map +1 -1
  49. package/types/lib/managers/binary.d.ts.map +1 -1
  50. package/types/lib/managers/docker.d.ts +3 -0
  51. package/types/lib/managers/docker.d.ts.map +1 -1
  52. package/types/lib/server/server.d.ts +1 -0
  53. package/types/lib/server/server.d.ts.map +1 -1
@@ -121,7 +121,11 @@ it("parseImageName tests", () => {
121
121
  );
122
122
  });
123
123
 
124
- async function loadDockerModule({ clientResponse, utilsOverrides } = {}) {
124
+ async function loadDockerModule({
125
+ clientResponse,
126
+ fsOverrides,
127
+ utilsOverrides,
128
+ } = {}) {
125
129
  const dockerClient = sinon.stub().resolves(
126
130
  clientResponse || {
127
131
  Id: "sha256:hello-world",
@@ -129,6 +133,13 @@ async function loadDockerModule({ clientResponse, utilsOverrides } = {}) {
129
133
  },
130
134
  );
131
135
  dockerClient.stream = sinon.stub();
136
+ const fsStub = {
137
+ createReadStream: sinon.stub(),
138
+ lstatSync: sinon.stub(),
139
+ readdirSync: sinon.stub().returns([]),
140
+ readFileSync: sinon.stub(),
141
+ ...fsOverrides,
142
+ };
132
143
  const gotStub = {
133
144
  extend: sinon.stub().returns(dockerClient),
134
145
  get: sinon.stub().resolves({ body: "OK" }),
@@ -150,12 +161,107 @@ async function loadDockerModule({ clientResponse, utilsOverrides } = {}) {
150
161
  ...utilsOverrides,
151
162
  };
152
163
  const dockerModule = await esmock("./docker.js", {
164
+ "node:fs": fsStub,
153
165
  got: { default: gotStub },
154
166
  "../helpers/utils.js": utilsStub,
155
167
  });
156
- return { dockerClient, dockerModule, gotStub, utilsStub };
168
+ return { dockerClient, dockerModule, fsStub, gotStub, utilsStub };
169
+ }
170
+
171
+ const decodeRegistryAuthHeader = (header) =>
172
+ JSON.parse(Buffer.from(header, "base64url").toString("utf-8"));
173
+
174
+ const dockerConfigExistsStub = () =>
175
+ sinon.stub().callsFake((filePath) => filePath.endsWith("config.json"));
176
+
177
+ const encodedAuth = Buffer.from("trusted-user:trusted-pass").toString("base64");
178
+
179
+ const authConfigData = (configuredRegistry) =>
180
+ JSON.stringify({
181
+ auths: {
182
+ [configuredRegistry]: {
183
+ auth: encodedAuth,
184
+ },
185
+ },
186
+ });
187
+
188
+ const credHelperConfigData = (configuredRegistry) =>
189
+ JSON.stringify({
190
+ credHelpers: {
191
+ [configuredRegistry]: "osxkeychain",
192
+ },
193
+ });
194
+
195
+ const credHelperExe = (helperSuffix) =>
196
+ isWin
197
+ ? `docker-credential-${helperSuffix}.exe`
198
+ : `docker-credential-${helperSuffix}`;
199
+
200
+ async function loadDockerModuleWithAuths(configuredRegistry) {
201
+ return await loadDockerModule({
202
+ fsOverrides: {
203
+ readFileSync: sinon.stub().returns(authConfigData(configuredRegistry)),
204
+ },
205
+ utilsOverrides: {
206
+ safeExistsSync: dockerConfigExistsStub(),
207
+ },
208
+ });
209
+ }
210
+
211
+ async function loadDockerModuleWithCredHelpers(
212
+ configuredRegistry,
213
+ safeSpawnSync,
214
+ ) {
215
+ return await loadDockerModule({
216
+ fsOverrides: {
217
+ readFileSync: sinon
218
+ .stub()
219
+ .returns(credHelperConfigData(configuredRegistry)),
220
+ },
221
+ utilsOverrides: {
222
+ safeExistsSync: dockerConfigExistsStub(),
223
+ safeSpawnSync,
224
+ },
225
+ });
157
226
  }
158
227
 
228
+ const withDockerConfig = async (callback) => {
229
+ const originalDockerConfig = process.env.DOCKER_CONFIG;
230
+ process.env.DOCKER_CONFIG = "/tmp/cdxgen-docker-config";
231
+ try {
232
+ await callback();
233
+ } finally {
234
+ if (originalDockerConfig === undefined) {
235
+ delete process.env.DOCKER_CONFIG;
236
+ } else {
237
+ process.env.DOCKER_CONFIG = originalDockerConfig;
238
+ }
239
+ }
240
+ };
241
+
242
+ const withEnv = async (updates, callback) => {
243
+ const originalEnv = {};
244
+ for (const envKey of Object.keys(updates)) {
245
+ originalEnv[envKey] = process.env[envKey];
246
+ if (updates[envKey] === undefined) {
247
+ delete process.env[envKey];
248
+ } else {
249
+ process.env[envKey] = updates[envKey];
250
+ }
251
+ }
252
+ try {
253
+ await callback();
254
+ } finally {
255
+ for (const envKey of Object.keys(updates)) {
256
+ if (originalEnv[envKey] === undefined) {
257
+ delete process.env[envKey];
258
+ } else {
259
+ process.env[envKey] = originalEnv[envKey];
260
+ }
261
+ }
262
+ }
263
+ };
264
+
159
265
  await it("docker connection uses the detected daemon client", async () => {
160
266
  const { dockerModule, gotStub, dockerClient } = await loadDockerModule();
161
267
  const dockerConn = await dockerModule.getConnection();
@@ -323,6 +429,1040 @@ await it("docker exportImage ignores local directories", async () => {
323
429
  assert.strictEqual(imageData, undefined);
324
430
  });
325
431
 
432
+ await it("docker makeRequest prefers DOCKER_AUTH_CONFIG over config.json entries for all registries", async () => {
433
+ await withDockerConfig(async () => {
434
+ await withEnv(
435
+ {
436
+ DOCKER_AUTH_CONFIG: "opaque-global-auth-token",
437
+ },
438
+ async () => {
439
+ const safeSpawnSync = sinon.stub().returns({
440
+ status: 0,
441
+ stdout: JSON.stringify({
442
+ username: "helper-user",
443
+ Secret: "helper-pass",
444
+ }),
445
+ stderr: "",
446
+ });
447
+ const { dockerClient, dockerModule } = await loadDockerModule({
448
+ fsOverrides: {
449
+ readFileSync: sinon.stub().returns(
450
+ JSON.stringify({
451
+ auths: {
452
+ "registry.example.com": {
453
+ auth: Buffer.from("trusted-user:trusted-pass").toString(
454
+ "base64",
455
+ ),
456
+ },
457
+ },
458
+ credHelpers: {
459
+ "registry.example.com": "osxkeychain",
460
+ },
461
+ }),
462
+ ),
463
+ },
464
+ utilsOverrides: {
465
+ safeExistsSync: dockerConfigExistsStub(),
466
+ safeSpawnSync,
467
+ },
468
+ });
469
+
470
+ await dockerModule.makeRequest(
471
+ "images/create?fromImage=registry.example.com/team/app:latest",
472
+ "POST",
473
+ "registry.example.com/team/app",
474
+ );
475
+
476
+ const requestOptions = dockerClient.firstCall.args[1];
477
+ assert.strictEqual(
478
+ requestOptions.headers["X-Registry-Auth"],
479
+ "opaque-global-auth-token",
480
+ );
481
+ sinon.assert.notCalled(safeSpawnSync);
482
+ },
483
+ );
484
+ });
485
+ });
486
+
487
+ await it("docker makeRequest prefers DOCKER_USER credentials over matching config.json entries", async () => {
488
+ await withDockerConfig(async () => {
489
+ await withEnv(
490
+ {
491
+ DOCKER_USER: "env-user",
492
+ DOCKER_PASSWORD: "env-pass",
493
+ DOCKER_EMAIL: "env@example.com",
494
+ },
495
+ async () => {
496
+ const { dockerClient, dockerModule } = await loadDockerModule({
497
+ fsOverrides: {
498
+ readFileSync: sinon.stub().returns(
499
+ JSON.stringify({
500
+ auths: {
501
+ "registry.example.com": {
502
+ auth: Buffer.from("trusted-user:trusted-pass").toString(
503
+ "base64",
504
+ ),
505
+ },
506
+ },
507
+ }),
508
+ ),
509
+ },
510
+ utilsOverrides: {
511
+ safeExistsSync: dockerConfigExistsStub(),
512
+ },
513
+ });
514
+
515
+ await dockerModule.makeRequest(
516
+ "images/create?fromImage=registry.example.com/team/app:latest",
517
+ "POST",
518
+ "registry.example.com/team/app",
519
+ );
520
+
521
+ const registryAuthHeader =
522
+ dockerClient.firstCall.args[1].headers["X-Registry-Auth"];
523
+ assert.deepStrictEqual(decodeRegistryAuthHeader(registryAuthHeader), {
524
+ username: "env-user",
525
+ password: "env-pass",
526
+ email: "env@example.com",
527
+ serveraddress: "registry.example.com",
528
+ });
529
+ },
530
+ );
531
+ });
532
+ });
533
+
534
+ await it("docker makeRequest applies DOCKER_USER credentials regardless of configured registry entries", async () => {
535
+ await withDockerConfig(async () => {
536
+ await withEnv(
537
+ {
538
+ DOCKER_USER: "env-user",
539
+ DOCKER_PASSWORD: "env-pass",
540
+ DOCKER_EMAIL: "env@example.com",
541
+ },
542
+ async () => {
543
+ const safeSpawnSync = sinon.stub().returns({
544
+ status: 0,
545
+ stdout: JSON.stringify({
546
+ username: "helper-user",
547
+ Secret: "helper-pass",
548
+ }),
549
+ stderr: "",
550
+ });
551
+ const { dockerClient, dockerModule } = await loadDockerModule({
552
+ fsOverrides: {
553
+ readFileSync: sinon.stub().returns(
554
+ JSON.stringify({
555
+ auths: {
556
+ "other-registry.example.com": {
557
+ auth: Buffer.from("trusted-user:trusted-pass").toString(
558
+ "base64",
559
+ ),
560
+ },
561
+ },
562
+ credHelpers: {
563
+ "other-registry.example.com": "osxkeychain",
564
+ },
565
+ }),
566
+ ),
567
+ },
568
+ utilsOverrides: {
569
+ safeExistsSync: dockerConfigExistsStub(),
570
+ safeSpawnSync,
571
+ },
572
+ });
573
+
574
+ await dockerModule.makeRequest(
575
+ "images/create?fromImage=registry.example.com/team/app:latest",
576
+ "POST",
577
+ "registry.example.com/team/app",
578
+ );
579
+
580
+ const registryAuthHeader =
581
+ dockerClient.firstCall.args[1].headers["X-Registry-Auth"];
582
+ assert.deepStrictEqual(decodeRegistryAuthHeader(registryAuthHeader), {
583
+ username: "env-user",
584
+ password: "env-pass",
585
+ email: "env@example.com",
586
+ serveraddress: "registry.example.com",
587
+ });
588
+ sinon.assert.notCalled(safeSpawnSync);
589
+ },
590
+ );
591
+ });
592
+ });
593
+
594
+ await it("docker makeRequest does not forward auth for substring-matched registries", async () => {
595
+ const originalDockerConfig = process.env.DOCKER_CONFIG;
596
+ process.env.DOCKER_CONFIG = "/tmp/cdxgen-docker-config";
597
+ try {
598
+ const { dockerClient, dockerModule } = await loadDockerModule({
599
+ fsOverrides: {
600
+ readFileSync: sinon.stub().returns(
601
+ JSON.stringify({
602
+ auths: {
603
+ "private-registry.example.com": {
604
+ auth: Buffer.from("trusted-user:trusted-pass").toString(
605
+ "base64",
606
+ ),
607
+ },
608
+ },
609
+ }),
610
+ ),
611
+ },
612
+ utilsOverrides: {
613
+ safeExistsSync: sinon
614
+ .stub()
615
+ .callsFake((filePath) => filePath.endsWith("config.json")),
616
+ },
617
+ });
618
+
619
+ await dockerModule.makeRequest(
620
+ "images/create?fromImage=registry.example.com/team/app:latest",
621
+ "POST",
622
+ "registry.example.com",
623
+ );
624
+
625
+ const requestOptions = dockerClient.firstCall.args[1];
626
+ assert.strictEqual(requestOptions.headers, undefined);
627
+ } finally {
628
+ if (originalDockerConfig === undefined) {
629
+ delete process.env.DOCKER_CONFIG;
630
+ } else {
631
+ process.env.DOCKER_CONFIG = originalDockerConfig;
632
+ }
633
+ }
634
+ });
635
+
636
+ await it("docker makeRequest accepts exact normalized registry matches from config auths", async () => {
637
+ const originalDockerConfig = process.env.DOCKER_CONFIG;
638
+ process.env.DOCKER_CONFIG = "/tmp/cdxgen-docker-config";
639
+ try {
640
+ const { dockerClient, dockerModule } = await loadDockerModule({
641
+ fsOverrides: {
642
+ readFileSync: sinon.stub().returns(
643
+ JSON.stringify({
644
+ auths: {
645
+ "https://registry.example.com/v2/": {
646
+ auth: Buffer.from("trusted-user:trusted-pass").toString(
647
+ "base64",
648
+ ),
649
+ },
650
+ },
651
+ }),
652
+ ),
653
+ },
654
+ utilsOverrides: {
655
+ safeExistsSync: sinon
656
+ .stub()
657
+ .callsFake((filePath) => filePath.endsWith("config.json")),
658
+ },
659
+ });
660
+
661
+ await dockerModule.makeRequest(
662
+ "images/create?fromImage=registry.example.com/team/app:latest",
663
+ "POST",
664
+ "registry.example.com/team/app",
665
+ );
666
+
667
+ const registryAuthHeader =
668
+ dockerClient.firstCall.args[1].headers["X-Registry-Auth"];
669
+ assert.deepStrictEqual(decodeRegistryAuthHeader(registryAuthHeader), {
670
+ username: "trusted-user",
671
+ password: "trusted-pass",
672
+ serveraddress: "https://registry.example.com/v2/",
673
+ });
674
+ } finally {
675
+ if (originalDockerConfig === undefined) {
676
+ delete process.env.DOCKER_CONFIG;
677
+ } else {
678
+ process.env.DOCKER_CONFIG = originalDockerConfig;
679
+ }
680
+ }
681
+ });
682
+
683
+ await it("docker makeRequest accepts normalized exact matches across ipv4 ipv6 explicit ports and scoped subpaths from config auths", async () => {
684
+ const cases = [
685
+ {
686
+ configuredRegistry: "127.0.0.1:5000",
687
+ requestedRegistry: "127.0.0.1:5000/team/app",
688
+ expectedServerAddress: "127.0.0.1:5000",
689
+ },
690
+ {
691
+ configuredRegistry: "[::1]:5000",
692
+ requestedRegistry: "[::1]:5000/team/app",
693
+ expectedServerAddress: "[::1]:5000",
694
+ },
695
+ {
696
+ configuredRegistry: "https://[2001:db8::1]:5000/v2/",
697
+ requestedRegistry: "[2001:db8::1]:5000/team/app",
698
+ expectedServerAddress: "https://[2001:db8::1]:5000/v2/",
699
+ },
700
+ {
701
+ configuredRegistry: "HTTPS://REGISTRY.EXAMPLE.COM/V2/",
702
+ requestedRegistry: "registry.example.com/team/app",
703
+ expectedServerAddress: "HTTPS://REGISTRY.EXAMPLE.COM/V2/",
704
+ },
705
+ {
706
+ configuredRegistry: "https://registry.example.com:443/v2/",
707
+ requestedRegistry: "registry.example.com:443/team/app",
708
+ expectedServerAddress: "https://registry.example.com:443/v2/",
709
+ },
710
+ {
711
+ configuredRegistry: "http://registry.example.com:80/v2/",
712
+ requestedRegistry: "registry.example.com:80/team/app",
713
+ expectedServerAddress: "http://registry.example.com:80/v2/",
714
+ },
715
+ {
716
+ configuredRegistry: "https://registry.example.com/custom/subpath",
717
+ requestedRegistry: "registry.example.com/custom/subpath/team/app",
718
+ expectedServerAddress: "https://registry.example.com/custom/subpath",
719
+ },
720
+ {
721
+ configuredRegistry: "https://registry.example.com/custom/subpath/v2/",
722
+ requestedRegistry: "registry.example.com/custom/subpath/team/app",
723
+ expectedServerAddress: "https://registry.example.com/custom/subpath/v2/",
724
+ },
725
+ ];
726
+
727
+ await withDockerConfig(async () => {
728
+ for (const testCase of cases) {
729
+ const { dockerClient, dockerModule } = await loadDockerModuleWithAuths(
730
+ testCase.configuredRegistry,
731
+ );
732
+
733
+ await dockerModule.makeRequest(
734
+ `images/create?fromImage=${testCase.requestedRegistry}:latest`,
735
+ "POST",
736
+ testCase.requestedRegistry,
737
+ );
738
+
739
+ const registryAuthHeader =
740
+ dockerClient.firstCall.args[1].headers["X-Registry-Auth"];
741
+ assert.deepStrictEqual(decodeRegistryAuthHeader(registryAuthHeader), {
742
+ username: "trusted-user",
743
+ password: "trusted-pass",
744
+ serveraddress: testCase.expectedServerAddress,
745
+ });
746
+ }
747
+ });
748
+ });
749
+
750
+ await it("docker makeRequest rejects wildcard unicode bidi explicit-default-port port-boundary and unrelated scoped-path mismatches from config auths", async () => {
751
+ const bidiRegistry = "reg\u202eistry.example.com";
752
+ const unicodeConfusableRegistry = "reg\u0456stry.example.com";
753
+ const cases = [
754
+ {
755
+ configuredRegistry: "*.example.com",
756
+ requestedRegistry: "team.example.com/app",
757
+ },
758
+ {
759
+ configuredRegistry: "registry.example.com",
760
+ requestedRegistry: "registry.example.com:80/team/app",
761
+ },
762
+ {
763
+ configuredRegistry: "registry.example.com:443",
764
+ requestedRegistry: "registry.example.com/team/app",
765
+ },
766
+ {
767
+ configuredRegistry: "127.0.0.1:5001",
768
+ requestedRegistry: "127.0.0.1:5000/team/app",
769
+ },
770
+ {
771
+ configuredRegistry: "[::1]:5001",
772
+ requestedRegistry: "[::1]:5000/team/app",
773
+ },
774
+ {
775
+ configuredRegistry: "https://registry.example.com.evil.invalid/v2/",
776
+ requestedRegistry: "registry.example.com/team/app",
777
+ },
778
+ {
779
+ configuredRegistry: "https://registry.example.com/custom/subpath",
780
+ requestedRegistry: "registry.example.com/team/app",
781
+ },
782
+ {
783
+ configuredRegistry: "https://registry.example.com/custom/subpath",
784
+ requestedRegistry: "registry.example.com/custom/subpathology/team/app",
785
+ },
786
+ {
787
+ configuredRegistry: "https://registry.example.com:443/v2/",
788
+ requestedRegistry: "registry.example.com:444/team/app",
789
+ },
790
+ {
791
+ configuredRegistry: unicodeConfusableRegistry,
792
+ requestedRegistry: "registry.example.com/team/app",
793
+ },
794
+ {
795
+ configuredRegistry: bidiRegistry,
796
+ requestedRegistry: "registry.example.com/team/app",
797
+ },
798
+ ];
799
+
800
+ await withDockerConfig(async () => {
801
+ for (const testCase of cases) {
802
+ const { dockerClient, dockerModule } = await loadDockerModuleWithAuths(
803
+ testCase.configuredRegistry,
804
+ );
805
+
806
+ await dockerModule.makeRequest(
807
+ `images/create?fromImage=${testCase.requestedRegistry}:latest`,
808
+ "POST",
809
+ testCase.requestedRegistry,
810
+ );
811
+
812
+ const requestOptions = dockerClient.firstCall.args[1];
813
+ assert.strictEqual(requestOptions.headers, undefined);
814
+ }
815
+ });
816
+ });
817
+
818
+ await it("docker makeRequest accepts raw host:port registry matches from config auths", async () => {
819
+ await withDockerConfig(async () => {
820
+ const { dockerClient, dockerModule } = await loadDockerModule({
821
+ fsOverrides: {
822
+ readFileSync: sinon.stub().returns(
823
+ JSON.stringify({
824
+ auths: {
825
+ "localhost:5000": {
826
+ auth: Buffer.from("trusted-user:trusted-pass").toString(
827
+ "base64",
828
+ ),
829
+ },
830
+ },
831
+ }),
832
+ ),
833
+ },
834
+ utilsOverrides: {
835
+ safeExistsSync: sinon
836
+ .stub()
837
+ .callsFake((filePath) => filePath.endsWith("config.json")),
838
+ },
839
+ });
840
+
841
+ await dockerModule.makeRequest(
842
+ "images/create?fromImage=localhost:5000/team/app:latest",
843
+ "POST",
844
+ "localhost:5000/team/app",
845
+ );
846
+
847
+ const registryAuthHeader =
848
+ dockerClient.firstCall.args[1].headers["X-Registry-Auth"];
849
+ assert.deepStrictEqual(decodeRegistryAuthHeader(registryAuthHeader), {
850
+ username: "trusted-user",
851
+ password: "trusted-pass",
852
+ serveraddress: "localhost:5000",
853
+ });
854
+ });
855
+ });
856
+
857
+ await it("docker makeRequest keeps raw host:port registries separated by port", async () => {
858
+ await withDockerConfig(async () => {
859
+ const { dockerClient, dockerModule } = await loadDockerModule({
860
+ fsOverrides: {
861
+ readFileSync: sinon.stub().returns(
862
+ JSON.stringify({
863
+ auths: {
864
+ "localhost:5001": {
865
+ auth: Buffer.from("trusted-user:trusted-pass").toString(
866
+ "base64",
867
+ ),
868
+ },
869
+ },
870
+ }),
871
+ ),
872
+ },
873
+ utilsOverrides: {
874
+ safeExistsSync: sinon
875
+ .stub()
876
+ .callsFake((filePath) => filePath.endsWith("config.json")),
877
+ },
878
+ });
879
+
880
+ await dockerModule.makeRequest(
881
+ "images/create?fromImage=localhost:5000/team/app:latest",
882
+ "POST",
883
+ "localhost:5000/team/app",
884
+ );
885
+
886
+ const requestOptions = dockerClient.firstCall.args[1];
887
+ assert.strictEqual(requestOptions.headers, undefined);
888
+ });
889
+ });
890
+
891
+ await it("docker makeRequest preserves Docker Hub auth aliases without substring matching", async () => {
892
+ const originalDockerConfig = process.env.DOCKER_CONFIG;
893
+ process.env.DOCKER_CONFIG = "/tmp/cdxgen-docker-config";
894
+ try {
895
+ const { dockerClient, dockerModule } = await loadDockerModule({
896
+ fsOverrides: {
897
+ readFileSync: sinon.stub().returns(
898
+ JSON.stringify({
899
+ auths: {
900
+ "https://index.docker.io/v1/": {
901
+ auth: Buffer.from("hub-user:hub-pass").toString("base64"),
902
+ },
903
+ },
904
+ }),
905
+ ),
906
+ },
907
+ utilsOverrides: {
908
+ safeExistsSync: sinon
909
+ .stub()
910
+ .callsFake((filePath) => filePath.endsWith("config.json")),
911
+ },
912
+ });
913
+
914
+ await dockerModule.makeRequest(
915
+ "images/create?fromImage=docker.io/library/alpine:latest",
916
+ "POST",
917
+ "docker.io",
918
+ );
919
+
920
+ const registryAuthHeader =
921
+ dockerClient.firstCall.args[1].headers["X-Registry-Auth"];
922
+ assert.deepStrictEqual(decodeRegistryAuthHeader(registryAuthHeader), {
923
+ username: "hub-user",
924
+ password: "hub-pass",
925
+ serveraddress: "https://index.docker.io/v1/",
926
+ });
927
+ } finally {
928
+ if (originalDockerConfig === undefined) {
929
+ delete process.env.DOCKER_CONFIG;
930
+ } else {
931
+ process.env.DOCKER_CONFIG = originalDockerConfig;
932
+ }
933
+ }
934
+ });
935
+
936
+ await it("docker makeRequest resolves unqualified image pulls to Docker Hub auth entries", async () => {
937
+ const requestedImages = ["myorg/app:latest", "alpine:latest"];
938
+
939
+ await withDockerConfig(async () => {
940
+ for (const requestedImage of requestedImages) {
941
+ const { dockerClient, dockerModule } = await loadDockerModule({
942
+ fsOverrides: {
943
+ readFileSync: sinon.stub().returns(
944
+ JSON.stringify({
945
+ auths: {
946
+ "https://index.docker.io/v1/": {
947
+ auth: Buffer.from("hub-user:hub-pass").toString("base64"),
948
+ },
949
+ },
950
+ }),
951
+ ),
952
+ },
953
+ utilsOverrides: {
954
+ safeExistsSync: dockerConfigExistsStub(),
955
+ },
956
+ });
957
+
958
+ await dockerModule.makeRequest(
959
+ `images/create?fromImage=${requestedImage}`,
960
+ "POST",
961
+ "",
962
+ );
963
+
964
+ const registryAuthHeader =
965
+ dockerClient.firstCall.args[1].headers["X-Registry-Auth"];
966
+ assert.deepStrictEqual(decodeRegistryAuthHeader(registryAuthHeader), {
967
+ username: "hub-user",
968
+ password: "hub-pass",
969
+ serveraddress: "https://index.docker.io/v1/",
970
+ });
971
+ }
972
+ });
973
+ });
974
+
975
+ await it("docker makeRequest skips credHelpers for substring-matched registries", async () => {
976
+ const originalDockerConfig = process.env.DOCKER_CONFIG;
977
+ process.env.DOCKER_CONFIG = "/tmp/cdxgen-docker-config";
978
+ try {
979
+ const safeSpawnSync = sinon.stub().returns({
980
+ status: 0,
981
+ stdout: JSON.stringify({
982
+ Username: "trusted-user",
983
+ Secret: "trusted-pass",
984
+ }),
985
+ stderr: "",
986
+ });
987
+ const { dockerClient, dockerModule } = await loadDockerModule({
988
+ fsOverrides: {
989
+ readFileSync: sinon.stub().returns(
990
+ JSON.stringify({
991
+ credHelpers: {
992
+ "private-registry.example.com": "osxkeychain",
993
+ },
994
+ }),
995
+ ),
996
+ },
997
+ utilsOverrides: {
998
+ safeExistsSync: sinon
999
+ .stub()
1000
+ .callsFake((filePath) => filePath.endsWith("config.json")),
1001
+ safeSpawnSync,
1002
+ },
1003
+ });
1004
+
1005
+ await dockerModule.makeRequest(
1006
+ "images/create?fromImage=registry.example.com/team/app:latest",
1007
+ "POST",
1008
+ "registry.example.com",
1009
+ );
1010
+
1011
+ const requestOptions = dockerClient.firstCall.args[1];
1012
+ assert.strictEqual(requestOptions.headers, undefined);
1013
+ sinon.assert.notCalled(safeSpawnSync);
1014
+ } finally {
1015
+ if (originalDockerConfig === undefined) {
1016
+ delete process.env.DOCKER_CONFIG;
1017
+ } else {
1018
+ process.env.DOCKER_CONFIG = originalDockerConfig;
1019
+ }
1020
+ }
1021
+ });
1022
+
1023
+ await it("docker makeRequest accepts raw host:port registry matches from credHelpers", async () => {
1024
+ await withDockerConfig(async () => {
1025
+ const safeSpawnSync = sinon.stub().returns({
1026
+ status: 0,
1027
+ stdout: JSON.stringify({
1028
+ username: "trusted-user",
1029
+ Secret: "trusted-pass",
1030
+ }),
1031
+ stderr: "",
1032
+ });
1033
+ const { dockerClient, dockerModule } = await loadDockerModule({
1034
+ fsOverrides: {
1035
+ readFileSync: sinon.stub().returns(
1036
+ JSON.stringify({
1037
+ credHelpers: {
1038
+ "localhost:5000": "osxkeychain",
1039
+ },
1040
+ }),
1041
+ ),
1042
+ },
1043
+ utilsOverrides: {
1044
+ safeExistsSync: sinon
1045
+ .stub()
1046
+ .callsFake((filePath) => filePath.endsWith("config.json")),
1047
+ safeSpawnSync,
1048
+ },
1049
+ });
1050
+
1051
+ await dockerModule.makeRequest(
1052
+ "images/create?fromImage=localhost:5000/team/app:latest",
1053
+ "POST",
1054
+ "localhost:5000/team/app",
1055
+ );
1056
+
1057
+ sinon.assert.calledOnceWithExactly(
1058
+ safeSpawnSync,
1059
+ credHelperExe("osxkeychain"),
1060
+ ["get"],
1061
+ {
1062
+ input: "localhost:5000",
1063
+ },
1064
+ );
1065
+ const registryAuthHeader =
1066
+ dockerClient.firstCall.args[1].headers["X-Registry-Auth"];
1067
+ assert.deepStrictEqual(decodeRegistryAuthHeader(registryAuthHeader), {
1068
+ username: "trusted-user",
1069
+ password: "trusted-pass",
1070
+ email: "trusted-user",
1071
+ serveraddress: "localhost:5000",
1072
+ });
1073
+ });
1074
+ });
1075
+
1076
+ await it("docker getCredsFromHelper normalizes cache keys for equivalent registry hosts", async () => {
1077
+ const safeSpawnSync = sinon.stub().returns({
1078
+ status: 0,
1079
+ stdout: JSON.stringify({
1080
+ username: "trusted-user",
1081
+ Secret: "trusted-pass",
1082
+ }),
1083
+ stderr: "",
1084
+ });
1085
+ const { dockerModule } = await loadDockerModule({
1086
+ utilsOverrides: {
1087
+ safeSpawnSync,
1088
+ },
1089
+ });
1090
+
1091
+ const firstToken = dockerModule.getCredsFromHelper(
1092
+ "osxkeychain",
1093
+ "registry.example.com",
1094
+ );
1095
+ const secondToken = dockerModule.getCredsFromHelper(
1096
+ "osxkeychain",
1097
+ "https://registry.example.com/v2/",
1098
+ );
1099
+
1100
+ assert.strictEqual(firstToken, secondToken);
1101
+ sinon.assert.calledOnceWithExactly(
1102
+ safeSpawnSync,
1103
+ credHelperExe("osxkeychain"),
1104
+ ["get"],
1105
+ {
1106
+ input: "registry.example.com",
1107
+ },
1108
+ );
1109
+ });
1110
+
1111
+ await it("docker getCredsFromHelper keeps scoped path cache keys isolated", async () => {
1112
+ const safeSpawnSync = sinon.stub().returns({
1113
+ status: 0,
1114
+ stdout: JSON.stringify({
1115
+ username: "trusted-user",
1116
+ Secret: "trusted-pass",
1117
+ }),
1118
+ stderr: "",
1119
+ });
1120
+ const { dockerModule } = await loadDockerModule({
1121
+ utilsOverrides: {
1122
+ safeSpawnSync,
1123
+ },
1124
+ });
1125
+
1126
+ const firstToken = dockerModule.getCredsFromHelper(
1127
+ "osxkeychain",
1128
+ "https://registry.example.com/custom/subpath/v2/",
1129
+ );
1130
+ const secondToken = dockerModule.getCredsFromHelper(
1131
+ "osxkeychain",
1132
+ "https://registry.example.com/custom/subpath/v2/",
1133
+ );
1134
+ const thirdToken = dockerModule.getCredsFromHelper(
1135
+ "osxkeychain",
1136
+ "https://registry.example.com/other/subpath/v2/",
1137
+ );
1138
+
1139
+ assert.strictEqual(firstToken, secondToken);
1140
+ assert.notStrictEqual(firstToken, thirdToken);
1141
+ assert.deepStrictEqual(decodeRegistryAuthHeader(firstToken), {
1142
+ username: "trusted-user",
1143
+ password: "trusted-pass",
1144
+ email: "trusted-user",
1145
+ serveraddress: "https://registry.example.com/custom/subpath/v2/",
1146
+ });
1147
+ sinon.assert.calledTwice(safeSpawnSync);
1148
+ });
1149
+
1150
+ await it("docker makeRequest accepts ipv4 ipv6 explicit-port and scoped-subpath registry matches from credHelpers", async () => {
1151
+ const cases = [
1152
+ {
1153
+ configuredRegistry: "127.0.0.1:5000",
1154
+ requestedRegistry: "127.0.0.1:5000/team/app",
1155
+ },
1156
+ {
1157
+ configuredRegistry: "[::1]:5000",
1158
+ requestedRegistry: "[::1]:5000/team/app",
1159
+ },
1160
+ {
1161
+ configuredRegistry: "https://registry.example.com:443/v2/",
1162
+ requestedRegistry: "registry.example.com:443/team/app",
1163
+ },
1164
+ {
1165
+ configuredRegistry: "http://registry.example.com:80/v2/",
1166
+ requestedRegistry: "registry.example.com:80/team/app",
1167
+ },
1168
+ {
1169
+ configuredRegistry: "https://registry.example.com/custom/subpath/v2/",
1170
+ requestedRegistry: "registry.example.com/custom/subpath/team/app",
1171
+ },
1172
+ ];
1173
+
1174
+ await withDockerConfig(async () => {
1175
+ for (const testCase of cases) {
1176
+ const safeSpawnSync = sinon.stub().returns({
1177
+ status: 0,
1178
+ stdout: JSON.stringify({
1179
+ username: "trusted-user",
1180
+ Secret: "trusted-pass",
1181
+ }),
1182
+ stderr: "",
1183
+ });
1184
+ const { dockerClient, dockerModule } =
1185
+ await loadDockerModuleWithCredHelpers(
1186
+ testCase.configuredRegistry,
1187
+ safeSpawnSync,
1188
+ );
1189
+
1190
+ await dockerModule.makeRequest(
1191
+ `images/create?fromImage=${testCase.requestedRegistry}:latest`,
1192
+ "POST",
1193
+ testCase.requestedRegistry,
1194
+ );
1195
+
1196
+ sinon.assert.calledOnceWithExactly(
1197
+ safeSpawnSync,
1198
+ credHelperExe("osxkeychain"),
1199
+ ["get"],
1200
+ {
1201
+ input: testCase.configuredRegistry,
1202
+ },
1203
+ );
1204
+ const registryAuthHeader =
1205
+ dockerClient.firstCall.args[1].headers["X-Registry-Auth"];
1206
+ assert.deepStrictEqual(decodeRegistryAuthHeader(registryAuthHeader), {
1207
+ username: "trusted-user",
1208
+ password: "trusted-pass",
1209
+ email: "trusted-user",
1210
+ serveraddress: testCase.configuredRegistry,
1211
+ });
1212
+ }
1213
+ });
1214
+ });
1215
+
1216
+ await it("docker makeRequest does not invoke credHelpers for wildcard unicode bidi explicit-default-port or port-boundary mismatches", async () => {
1217
+ const bidiRegistry = "reg\u202eistry.example.com";
1218
+ const unicodeConfusableRegistry = "reg\u0456stry.example.com";
1219
+ const cases = [
1220
+ {
1221
+ configuredRegistry: "*.example.com",
1222
+ requestedRegistry: "team.example.com/app",
1223
+ },
1224
+ {
1225
+ configuredRegistry: "registry.example.com",
1226
+ requestedRegistry: "registry.example.com:80/team/app",
1227
+ },
1228
+ {
1229
+ configuredRegistry: "registry.example.com:443",
1230
+ requestedRegistry: "registry.example.com/team/app",
1231
+ },
1232
+ {
1233
+ configuredRegistry: "127.0.0.1:5001",
1234
+ requestedRegistry: "127.0.0.1:5000/team/app",
1235
+ },
1236
+ {
1237
+ configuredRegistry: "[::1]:5001",
1238
+ requestedRegistry: "[::1]:5000/team/app",
1239
+ },
1240
+ {
1241
+ configuredRegistry: "https://registry.example.com/custom/subpath/v2/",
1242
+ requestedRegistry: "registry.example.com/team/app",
1243
+ },
1244
+ {
1245
+ configuredRegistry: "https://registry.example.com/custom/subpath/v2/",
1246
+ requestedRegistry: "registry.example.com/custom/subpathology/team/app",
1247
+ },
1248
+ {
1249
+ configuredRegistry: "https://registry.example.com:443/v2/",
1250
+ requestedRegistry: "registry.example.com:444/team/app",
1251
+ },
1252
+ {
1253
+ configuredRegistry: unicodeConfusableRegistry,
1254
+ requestedRegistry: "registry.example.com/team/app",
1255
+ },
1256
+ {
1257
+ configuredRegistry: bidiRegistry,
1258
+ requestedRegistry: "registry.example.com/team/app",
1259
+ },
1260
+ ];
1261
+
1262
+ await withDockerConfig(async () => {
1263
+ for (const testCase of cases) {
1264
+ const safeSpawnSync = sinon.stub().returns({
1265
+ status: 0,
1266
+ stdout: JSON.stringify({
1267
+ username: "trusted-user",
1268
+ Secret: "trusted-pass",
1269
+ }),
1270
+ stderr: "",
1271
+ });
1272
+ const { dockerClient, dockerModule } =
1273
+ await loadDockerModuleWithCredHelpers(
1274
+ testCase.configuredRegistry,
1275
+ safeSpawnSync,
1276
+ );
1277
+
1278
+ await dockerModule.makeRequest(
1279
+ `images/create?fromImage=${testCase.requestedRegistry}:latest`,
1280
+ "POST",
1281
+ testCase.requestedRegistry,
1282
+ );
1283
+
1284
+ const requestOptions = dockerClient.firstCall.args[1];
1285
+ assert.strictEqual(requestOptions.headers, undefined);
1286
+ sinon.assert.notCalled(safeSpawnSync);
1287
+ }
1288
+ });
1289
+ });
1290
+
1291
+ await it("docker makeRequest resolves unqualified image pulls to Docker Hub credHelpers", async () => {
1292
+ const requestedImages = ["myorg/app:latest", "alpine:latest"];
1293
+
1294
+ await withDockerConfig(async () => {
1295
+ for (const requestedImage of requestedImages) {
1296
+ const safeSpawnSync = sinon.stub().returns({
1297
+ status: 0,
1298
+ stdout: JSON.stringify({
1299
+ username: "hub-user",
1300
+ Secret: "hub-pass",
1301
+ }),
1302
+ stderr: "",
1303
+ });
1304
+ const { dockerClient, dockerModule } = await loadDockerModule({
1305
+ fsOverrides: {
1306
+ readFileSync: sinon.stub().returns(
1307
+ JSON.stringify({
1308
+ credHelpers: {
1309
+ "docker.io": "osxkeychain",
1310
+ },
1311
+ }),
1312
+ ),
1313
+ },
1314
+ utilsOverrides: {
1315
+ safeExistsSync: dockerConfigExistsStub(),
1316
+ safeSpawnSync,
1317
+ },
1318
+ });
1319
+
1320
+ await dockerModule.makeRequest(
1321
+ `images/create?fromImage=${requestedImage}`,
1322
+ "POST",
1323
+ "",
1324
+ );
1325
+
1326
+ sinon.assert.calledOnceWithExactly(
1327
+ safeSpawnSync,
1328
+ credHelperExe("osxkeychain"),
1329
+ ["get"],
1330
+ {
1331
+ input: "docker.io",
1332
+ },
1333
+ );
1334
+ const registryAuthHeader =
1335
+ dockerClient.firstCall.args[1].headers["X-Registry-Auth"];
1336
+ assert.deepStrictEqual(decodeRegistryAuthHeader(registryAuthHeader), {
1337
+ username: "hub-user",
1338
+ password: "hub-pass",
1339
+ email: "hub-user",
1340
+ serveraddress: "docker.io",
1341
+ });
1342
+ }
1343
+ });
1344
+ });
1345
+
1346
+ await it("docker makeRequest accepts normalized exact matches for common public registries without aliasing hosts", async () => {
1347
+ const cases = [
1348
+ {
1349
+ configuredRegistry: "https://ghcr.io/v2/",
1350
+ requestedRegistry: "ghcr.io/org/image",
1351
+ },
1352
+ {
1353
+ configuredRegistry: "https://quay.io/v2/",
1354
+ requestedRegistry: "quay.io/org/image",
1355
+ },
1356
+ {
1357
+ configuredRegistry: "https://public.ecr.aws/v2/",
1358
+ requestedRegistry: "public.ecr.aws/alias/image",
1359
+ },
1360
+ {
1361
+ configuredRegistry: "https://gcr.io/v2/",
1362
+ requestedRegistry: "gcr.io/project/image",
1363
+ },
1364
+ ];
1365
+
1366
+ await withDockerConfig(async () => {
1367
+ for (const { configuredRegistry, requestedRegistry } of cases) {
1368
+ const { dockerClient, dockerModule } = await loadDockerModule({
1369
+ fsOverrides: {
1370
+ readFileSync: sinon.stub().returns(
1371
+ JSON.stringify({
1372
+ auths: {
1373
+ [configuredRegistry]: {
1374
+ auth: Buffer.from("trusted-user:trusted-pass").toString(
1375
+ "base64",
1376
+ ),
1377
+ },
1378
+ },
1379
+ }),
1380
+ ),
1381
+ },
1382
+ utilsOverrides: {
1383
+ safeExistsSync: sinon
1384
+ .stub()
1385
+ .callsFake((filePath) => filePath.endsWith("config.json")),
1386
+ },
1387
+ });
1388
+
1389
+ await dockerModule.makeRequest(
1390
+ `images/create?fromImage=${requestedRegistry}:latest`,
1391
+ "POST",
1392
+ requestedRegistry,
1393
+ );
1394
+
1395
+ const registryAuthHeader =
1396
+ dockerClient.firstCall.args[1].headers["X-Registry-Auth"];
1397
+ assert.deepStrictEqual(decodeRegistryAuthHeader(registryAuthHeader), {
1398
+ username: "trusted-user",
1399
+ password: "trusted-pass",
1400
+ serveraddress: configuredRegistry,
1401
+ });
1402
+ }
1403
+ });
1404
+ });
1405
+
1406
+ await it("docker makeRequest keeps ghcr quay aws and gcp registries on separate trust boundaries", async () => {
1407
+ const cases = [
1408
+ {
1409
+ configuredRegistry: "https://tenant.ghcr.io/v2/",
1410
+ requestedRegistry: "ghcr.io",
1411
+ },
1412
+ {
1413
+ configuredRegistry: "https://quay.io.evil.example/v2/",
1414
+ requestedRegistry: "quay.io",
1415
+ },
1416
+ {
1417
+ configuredRegistry:
1418
+ "https://123456789012.dkr.ecr.us-east-1.amazonaws.com/v2/",
1419
+ requestedRegistry: "public.ecr.aws",
1420
+ },
1421
+ {
1422
+ configuredRegistry: "https://mirror.gcr.io/v2/",
1423
+ requestedRegistry: "gcr.io",
1424
+ },
1425
+ {
1426
+ configuredRegistry: "https://us-docker.pkg.dev/v2/",
1427
+ requestedRegistry: "gcr.io",
1428
+ },
1429
+ ];
1430
+
1431
+ await withDockerConfig(async () => {
1432
+ for (const { configuredRegistry, requestedRegistry } of cases) {
1433
+ const { dockerClient, dockerModule } = await loadDockerModule({
1434
+ fsOverrides: {
1435
+ readFileSync: sinon.stub().returns(
1436
+ JSON.stringify({
1437
+ auths: {
1438
+ [configuredRegistry]: {
1439
+ auth: Buffer.from("trusted-user:trusted-pass").toString(
1440
+ "base64",
1441
+ ),
1442
+ },
1443
+ },
1444
+ }),
1445
+ ),
1446
+ },
1447
+ utilsOverrides: {
1448
+ safeExistsSync: sinon
1449
+ .stub()
1450
+ .callsFake((filePath) => filePath.endsWith("config.json")),
1451
+ },
1452
+ });
1453
+
1454
+ await dockerModule.makeRequest(
1455
+ `images/create?fromImage=${requestedRegistry}/team/app:latest`,
1456
+ "POST",
1457
+ requestedRegistry,
1458
+ );
1459
+
1460
+ const requestOptions = dockerClient.firstCall.args[1];
1461
+ assert.strictEqual(requestOptions.headers, undefined);
1462
+ }
1463
+ });
1464
+ });
1465
+
326
1466
  await it("extractFromManifest derives PATH metadata from archive config", async () => {
327
1467
  const tempDir = mkdtempSync(join(tmpdir(), "cdxgen-docker-"));
328
1468
  try {