@cyclonedx/cdxgen 12.3.2 → 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 +70 -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 +171 -15
- package/data/rules/container-risk.yaml +14 -7
- package/data/rules/dependency-sources.yaml +76 -5
- 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 +36 -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 +647 -127
- package/lib/cli/index.poku.js +1905 -187
- package/lib/evinser/evinser.js +14 -9
- package/lib/helpers/agentFormulationParser.js +6 -2
- package/lib/helpers/agentFormulationParser.poku.js +42 -0
- package/lib/helpers/analyzer.js +1444 -38
- package/lib/helpers/analyzer.poku.js +409 -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/chromextutils.js +25 -3
- package/lib/helpers/chromextutils.poku.js +68 -0
- package/lib/helpers/ciParsers/githubActions.js +79 -0
- package/lib/helpers/ciParsers/githubActions.poku.js +103 -0
- package/lib/helpers/communityAiConfigParser.js +15 -5
- package/lib/helpers/communityAiConfigParser.poku.js +71 -0
- package/lib/helpers/depsUtils.js +5 -0
- package/lib/helpers/depsUtils.poku.js +55 -0
- package/lib/helpers/display.js +336 -23
- package/lib/helpers/display.poku.js +179 -43
- 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/mcpConfigParser.js +21 -5
- package/lib/helpers/mcpConfigParser.poku.js +39 -2
- 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/propertySanitizer.js +121 -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 +2454 -198
- package/lib/helpers/utils.poku.js +1798 -74
- package/lib/managers/binary.e2e.poku.js +367 -0
- package/lib/managers/binary.js +2306 -350
- package/lib/managers/binary.poku.js +1700 -1
- package/lib/managers/docker.js +441 -95
- package/lib/managers/docker.poku.js +1479 -14
- package/lib/server/server.js +2 -24
- package/lib/server/server.poku.js +36 -1
- 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 +2967 -990
- 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 +24 -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/agentFormulationParser.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/chromextutils.d.ts.map +1 -1
- package/types/lib/helpers/ciParsers/githubActions.d.ts.map +1 -1
- package/types/lib/helpers/communityAiConfigParser.d.ts.map +1 -1
- package/types/lib/helpers/depsUtils.d.ts.map +1 -1
- package/types/lib/helpers/display.d.ts +1 -0
- 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/mcpConfigParser.d.ts +1 -1
- package/types/lib/helpers/mcpConfigParser.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/propertySanitizer.d.ts +3 -0
- package/types/lib/helpers/propertySanitizer.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 +74 -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 +3 -0
- package/types/lib/managers/docker.d.ts.map +1 -1
- package/types/lib/server/server.d.ts +2 -0
- package/types/lib/server/server.d.ts.map +1 -1
- package/types/lib/stages/postgen/annotator.d.ts.map +1 -1
- package/types/lib/stages/postgen/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
|
@@ -121,7 +121,13 @@ it("parseImageName tests", () => {
|
|
|
121
121
|
);
|
|
122
122
|
});
|
|
123
123
|
|
|
124
|
-
async function loadDockerModule({
|
|
124
|
+
async function loadDockerModule({
|
|
125
|
+
clientResponse,
|
|
126
|
+
fsOverrides,
|
|
127
|
+
streamOverrides,
|
|
128
|
+
tarOverrides,
|
|
129
|
+
utilsOverrides,
|
|
130
|
+
} = {}) {
|
|
125
131
|
const dockerClient = sinon.stub().resolves(
|
|
126
132
|
clientResponse || {
|
|
127
133
|
Id: "sha256:hello-world",
|
|
@@ -129,6 +135,13 @@ async function loadDockerModule({ clientResponse, utilsOverrides } = {}) {
|
|
|
129
135
|
},
|
|
130
136
|
);
|
|
131
137
|
dockerClient.stream = sinon.stub();
|
|
138
|
+
const fsStub = {
|
|
139
|
+
createReadStream: sinon.stub(),
|
|
140
|
+
lstatSync: sinon.stub(),
|
|
141
|
+
readdirSync: sinon.stub().returns([]),
|
|
142
|
+
readFileSync: sinon.stub(),
|
|
143
|
+
...fsOverrides,
|
|
144
|
+
};
|
|
132
145
|
const gotStub = {
|
|
133
146
|
extend: sinon.stub().returns(dockerClient),
|
|
134
147
|
get: sinon.stub().resolves({ body: "OK" }),
|
|
@@ -140,7 +153,13 @@ async function loadDockerModule({ clientResponse, utilsOverrides } = {}) {
|
|
|
140
153
|
getAllFiles: sinon.stub().returns([]),
|
|
141
154
|
getTmpDir: sinon.stub().returns("/tmp"),
|
|
142
155
|
isDryRun: false,
|
|
156
|
+
readEnvironmentVariable: sinon
|
|
157
|
+
.stub()
|
|
158
|
+
.callsFake((varName) => process.env[varName]),
|
|
143
159
|
recordActivity: sinon.stub(),
|
|
160
|
+
recordDecisionActivity: sinon.stub(),
|
|
161
|
+
recordSensitiveFileRead: sinon.stub(),
|
|
162
|
+
safeExtractArchive: sinon.stub().resolves(true),
|
|
144
163
|
safeExistsSync: sinon.stub().returns(false),
|
|
145
164
|
safeMkdirSync: sinon.stub(),
|
|
146
165
|
safeMkdtempSync: sinon.stub().returns("/tmp/docker-images-test"),
|
|
@@ -150,12 +169,115 @@ async function loadDockerModule({ clientResponse, utilsOverrides } = {}) {
|
|
|
150
169
|
...utilsOverrides,
|
|
151
170
|
};
|
|
152
171
|
const dockerModule = await esmock("./docker.js", {
|
|
172
|
+
"node:fs": fsStub,
|
|
173
|
+
"node:stream/promises": {
|
|
174
|
+
pipeline: sinon.stub().resolves(),
|
|
175
|
+
...streamOverrides,
|
|
176
|
+
},
|
|
153
177
|
got: { default: gotStub },
|
|
178
|
+
tar: {
|
|
179
|
+
x: sinon.stub().returns("extractor"),
|
|
180
|
+
...tarOverrides,
|
|
181
|
+
},
|
|
154
182
|
"../helpers/utils.js": utilsStub,
|
|
155
183
|
});
|
|
156
|
-
return { dockerClient, dockerModule, gotStub, utilsStub };
|
|
184
|
+
return { dockerClient, dockerModule, fsStub, gotStub, utilsStub };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const decodeRegistryAuthHeader = (header) =>
|
|
188
|
+
JSON.parse(Buffer.from(header, "base64url").toString("utf-8"));
|
|
189
|
+
|
|
190
|
+
const dockerConfigExistsStub = () =>
|
|
191
|
+
sinon.stub().callsFake((filePath) => filePath.endsWith("config.json"));
|
|
192
|
+
|
|
193
|
+
const encodedAuth = Buffer.from("trusted-user:trusted-pass").toString("base64");
|
|
194
|
+
|
|
195
|
+
const authConfigData = (configuredRegistry) =>
|
|
196
|
+
JSON.stringify({
|
|
197
|
+
auths: {
|
|
198
|
+
[configuredRegistry]: {
|
|
199
|
+
auth: encodedAuth,
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const credHelperConfigData = (configuredRegistry) =>
|
|
205
|
+
JSON.stringify({
|
|
206
|
+
credHelpers: {
|
|
207
|
+
[configuredRegistry]: "osxkeychain",
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
const credHelperExe = (helperSuffix) =>
|
|
212
|
+
isWin
|
|
213
|
+
? `docker-credential-${helperSuffix}.exe`
|
|
214
|
+
: `docker-credential-${helperSuffix}`;
|
|
215
|
+
|
|
216
|
+
async function loadDockerModuleWithAuths(configuredRegistry) {
|
|
217
|
+
return await loadDockerModule({
|
|
218
|
+
fsOverrides: {
|
|
219
|
+
readFileSync: sinon.stub().returns(authConfigData(configuredRegistry)),
|
|
220
|
+
},
|
|
221
|
+
utilsOverrides: {
|
|
222
|
+
safeExistsSync: dockerConfigExistsStub(),
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
async function loadDockerModuleWithCredHelpers(
|
|
228
|
+
configuredRegistry,
|
|
229
|
+
safeSpawnSync,
|
|
230
|
+
) {
|
|
231
|
+
return await loadDockerModule({
|
|
232
|
+
fsOverrides: {
|
|
233
|
+
readFileSync: sinon
|
|
234
|
+
.stub()
|
|
235
|
+
.returns(credHelperConfigData(configuredRegistry)),
|
|
236
|
+
},
|
|
237
|
+
utilsOverrides: {
|
|
238
|
+
safeExistsSync: dockerConfigExistsStub(),
|
|
239
|
+
safeSpawnSync,
|
|
240
|
+
},
|
|
241
|
+
});
|
|
157
242
|
}
|
|
158
243
|
|
|
244
|
+
const withDockerConfig = async (callback) => {
|
|
245
|
+
const originalDockerConfig = process.env.DOCKER_CONFIG;
|
|
246
|
+
process.env.DOCKER_CONFIG = "/tmp/cdxgen-docker-config";
|
|
247
|
+
try {
|
|
248
|
+
await callback();
|
|
249
|
+
} finally {
|
|
250
|
+
if (originalDockerConfig === undefined) {
|
|
251
|
+
delete process.env.DOCKER_CONFIG;
|
|
252
|
+
} else {
|
|
253
|
+
process.env.DOCKER_CONFIG = originalDockerConfig;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const withEnv = async (updates, callback) => {
|
|
259
|
+
const originalEnv = {};
|
|
260
|
+
for (const envKey of Object.keys(updates)) {
|
|
261
|
+
originalEnv[envKey] = process.env[envKey];
|
|
262
|
+
if (updates[envKey] === undefined) {
|
|
263
|
+
delete process.env[envKey];
|
|
264
|
+
} else {
|
|
265
|
+
process.env[envKey] = updates[envKey];
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
try {
|
|
269
|
+
await callback();
|
|
270
|
+
} finally {
|
|
271
|
+
for (const envKey of Object.keys(updates)) {
|
|
272
|
+
if (originalEnv[envKey] === undefined) {
|
|
273
|
+
delete process.env[envKey];
|
|
274
|
+
} else {
|
|
275
|
+
process.env[envKey] = originalEnv[envKey];
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
|
|
159
281
|
await it("docker connection uses the detected daemon client", async () => {
|
|
160
282
|
const { dockerModule, gotStub, dockerClient } = await loadDockerModule();
|
|
161
283
|
const dockerConn = await dockerModule.getConnection();
|
|
@@ -280,12 +402,218 @@ await it("docker getConnection reports blocked network activity in dry-run mode"
|
|
|
280
402
|
});
|
|
281
403
|
});
|
|
282
404
|
|
|
283
|
-
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 () => {
|
|
284
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);
|
|
285
614
|
const { dockerModule } = await loadDockerModule({
|
|
286
615
|
utilsOverrides: {
|
|
287
|
-
|
|
288
|
-
recordActivity,
|
|
616
|
+
safeExtractArchive,
|
|
289
617
|
},
|
|
290
618
|
});
|
|
291
619
|
const result = await dockerModule.extractTar(
|
|
@@ -294,28 +622,131 @@ await it("docker extractTar reports a blocked untar activity in dry-run mode", a
|
|
|
294
622
|
{},
|
|
295
623
|
);
|
|
296
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,
|
|
664
|
+
},
|
|
665
|
+
});
|
|
666
|
+
const result = await dockerModule.extractTar(
|
|
667
|
+
"/tmp/image.tar",
|
|
668
|
+
"/tmp/out",
|
|
669
|
+
{},
|
|
670
|
+
);
|
|
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
|
+
});
|
|
297
696
|
sinon.assert.calledWithMatch(recordActivity, {
|
|
298
|
-
kind: "
|
|
697
|
+
kind: "container",
|
|
299
698
|
status: "blocked",
|
|
300
|
-
target: "
|
|
699
|
+
target: "alpine:3.20",
|
|
301
700
|
});
|
|
302
701
|
});
|
|
303
702
|
|
|
304
|
-
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 () => {
|
|
305
736
|
const recordActivity = sinon.stub();
|
|
737
|
+
const recordSensitiveFileRead = sinon.stub();
|
|
306
738
|
const { dockerModule } = await loadDockerModule({
|
|
307
739
|
utilsOverrides: {
|
|
308
740
|
isDryRun: true,
|
|
309
741
|
recordActivity,
|
|
742
|
+
recordSensitiveFileRead,
|
|
743
|
+
safeExistsSync: sinon.stub().returns(true),
|
|
310
744
|
},
|
|
311
745
|
});
|
|
312
|
-
const result = await dockerModule.exportImage("
|
|
746
|
+
const result = await dockerModule.exportImage("/tmp/image.tar", {});
|
|
313
747
|
assert.strictEqual(result, undefined);
|
|
314
|
-
sinon.assert.
|
|
315
|
-
|
|
316
|
-
status: "blocked",
|
|
317
|
-
target: "alpine:3.20",
|
|
318
|
-
});
|
|
748
|
+
sinon.assert.notCalled(recordActivity);
|
|
749
|
+
sinon.assert.notCalled(recordSensitiveFileRead);
|
|
319
750
|
});
|
|
320
751
|
|
|
321
752
|
await it("docker exportImage ignores local directories", async () => {
|
|
@@ -323,6 +754,1040 @@ await it("docker exportImage ignores local directories", async () => {
|
|
|
323
754
|
assert.strictEqual(imageData, undefined);
|
|
324
755
|
});
|
|
325
756
|
|
|
757
|
+
await it("docker makeRequest prefers DOCKER_AUTH_CONFIG over config.json entries for all registries", async () => {
|
|
758
|
+
await withDockerConfig(async () => {
|
|
759
|
+
await withEnv(
|
|
760
|
+
{
|
|
761
|
+
DOCKER_AUTH_CONFIG: "opaque-global-auth-token",
|
|
762
|
+
},
|
|
763
|
+
async () => {
|
|
764
|
+
const safeSpawnSync = sinon.stub().returns({
|
|
765
|
+
status: 0,
|
|
766
|
+
stdout: JSON.stringify({
|
|
767
|
+
username: "helper-user",
|
|
768
|
+
Secret: "helper-pass",
|
|
769
|
+
}),
|
|
770
|
+
stderr: "",
|
|
771
|
+
});
|
|
772
|
+
const { dockerClient, dockerModule } = await loadDockerModule({
|
|
773
|
+
fsOverrides: {
|
|
774
|
+
readFileSync: sinon.stub().returns(
|
|
775
|
+
JSON.stringify({
|
|
776
|
+
auths: {
|
|
777
|
+
"registry.example.com": {
|
|
778
|
+
auth: Buffer.from("trusted-user:trusted-pass").toString(
|
|
779
|
+
"base64",
|
|
780
|
+
),
|
|
781
|
+
},
|
|
782
|
+
},
|
|
783
|
+
credHelpers: {
|
|
784
|
+
"registry.example.com": "osxkeychain",
|
|
785
|
+
},
|
|
786
|
+
}),
|
|
787
|
+
),
|
|
788
|
+
},
|
|
789
|
+
utilsOverrides: {
|
|
790
|
+
safeExistsSync: dockerConfigExistsStub(),
|
|
791
|
+
safeSpawnSync,
|
|
792
|
+
},
|
|
793
|
+
});
|
|
794
|
+
|
|
795
|
+
await dockerModule.makeRequest(
|
|
796
|
+
"images/create?fromImage=registry.example.com/team/app:latest",
|
|
797
|
+
"POST",
|
|
798
|
+
"registry.example.com/team/app",
|
|
799
|
+
);
|
|
800
|
+
|
|
801
|
+
const requestOptions = dockerClient.firstCall.args[1];
|
|
802
|
+
assert.strictEqual(
|
|
803
|
+
requestOptions.headers["X-Registry-Auth"],
|
|
804
|
+
"opaque-global-auth-token",
|
|
805
|
+
);
|
|
806
|
+
sinon.assert.notCalled(safeSpawnSync);
|
|
807
|
+
},
|
|
808
|
+
);
|
|
809
|
+
});
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
await it("docker makeRequest prefers DOCKER_USER credentials over matching config.json entries", async () => {
|
|
813
|
+
await withDockerConfig(async () => {
|
|
814
|
+
await withEnv(
|
|
815
|
+
{
|
|
816
|
+
DOCKER_USER: "env-user",
|
|
817
|
+
DOCKER_PASSWORD: "env-pass",
|
|
818
|
+
DOCKER_EMAIL: "env@example.com",
|
|
819
|
+
},
|
|
820
|
+
async () => {
|
|
821
|
+
const { dockerClient, dockerModule } = await loadDockerModule({
|
|
822
|
+
fsOverrides: {
|
|
823
|
+
readFileSync: sinon.stub().returns(
|
|
824
|
+
JSON.stringify({
|
|
825
|
+
auths: {
|
|
826
|
+
"registry.example.com": {
|
|
827
|
+
auth: Buffer.from("trusted-user:trusted-pass").toString(
|
|
828
|
+
"base64",
|
|
829
|
+
),
|
|
830
|
+
},
|
|
831
|
+
},
|
|
832
|
+
}),
|
|
833
|
+
),
|
|
834
|
+
},
|
|
835
|
+
utilsOverrides: {
|
|
836
|
+
safeExistsSync: dockerConfigExistsStub(),
|
|
837
|
+
},
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
await dockerModule.makeRequest(
|
|
841
|
+
"images/create?fromImage=registry.example.com/team/app:latest",
|
|
842
|
+
"POST",
|
|
843
|
+
"registry.example.com/team/app",
|
|
844
|
+
);
|
|
845
|
+
|
|
846
|
+
const registryAuthHeader =
|
|
847
|
+
dockerClient.firstCall.args[1].headers["X-Registry-Auth"];
|
|
848
|
+
assert.deepStrictEqual(decodeRegistryAuthHeader(registryAuthHeader), {
|
|
849
|
+
username: "env-user",
|
|
850
|
+
password: "env-pass",
|
|
851
|
+
email: "env@example.com",
|
|
852
|
+
serveraddress: "registry.example.com",
|
|
853
|
+
});
|
|
854
|
+
},
|
|
855
|
+
);
|
|
856
|
+
});
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
await it("docker makeRequest applies DOCKER_USER credentials regardless of configured registry entries", async () => {
|
|
860
|
+
await withDockerConfig(async () => {
|
|
861
|
+
await withEnv(
|
|
862
|
+
{
|
|
863
|
+
DOCKER_USER: "env-user",
|
|
864
|
+
DOCKER_PASSWORD: "env-pass",
|
|
865
|
+
DOCKER_EMAIL: "env@example.com",
|
|
866
|
+
},
|
|
867
|
+
async () => {
|
|
868
|
+
const safeSpawnSync = sinon.stub().returns({
|
|
869
|
+
status: 0,
|
|
870
|
+
stdout: JSON.stringify({
|
|
871
|
+
username: "helper-user",
|
|
872
|
+
Secret: "helper-pass",
|
|
873
|
+
}),
|
|
874
|
+
stderr: "",
|
|
875
|
+
});
|
|
876
|
+
const { dockerClient, dockerModule } = await loadDockerModule({
|
|
877
|
+
fsOverrides: {
|
|
878
|
+
readFileSync: sinon.stub().returns(
|
|
879
|
+
JSON.stringify({
|
|
880
|
+
auths: {
|
|
881
|
+
"other-registry.example.com": {
|
|
882
|
+
auth: Buffer.from("trusted-user:trusted-pass").toString(
|
|
883
|
+
"base64",
|
|
884
|
+
),
|
|
885
|
+
},
|
|
886
|
+
},
|
|
887
|
+
credHelpers: {
|
|
888
|
+
"other-registry.example.com": "osxkeychain",
|
|
889
|
+
},
|
|
890
|
+
}),
|
|
891
|
+
),
|
|
892
|
+
},
|
|
893
|
+
utilsOverrides: {
|
|
894
|
+
safeExistsSync: dockerConfigExistsStub(),
|
|
895
|
+
safeSpawnSync,
|
|
896
|
+
},
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
await dockerModule.makeRequest(
|
|
900
|
+
"images/create?fromImage=registry.example.com/team/app:latest",
|
|
901
|
+
"POST",
|
|
902
|
+
"registry.example.com/team/app",
|
|
903
|
+
);
|
|
904
|
+
|
|
905
|
+
const registryAuthHeader =
|
|
906
|
+
dockerClient.firstCall.args[1].headers["X-Registry-Auth"];
|
|
907
|
+
assert.deepStrictEqual(decodeRegistryAuthHeader(registryAuthHeader), {
|
|
908
|
+
username: "env-user",
|
|
909
|
+
password: "env-pass",
|
|
910
|
+
email: "env@example.com",
|
|
911
|
+
serveraddress: "registry.example.com",
|
|
912
|
+
});
|
|
913
|
+
sinon.assert.notCalled(safeSpawnSync);
|
|
914
|
+
},
|
|
915
|
+
);
|
|
916
|
+
});
|
|
917
|
+
});
|
|
918
|
+
|
|
919
|
+
await it("docker makeRequest does not forward auth for substring-matched registries", async () => {
|
|
920
|
+
const originalDockerConfig = process.env.DOCKER_CONFIG;
|
|
921
|
+
process.env.DOCKER_CONFIG = "/tmp/cdxgen-docker-config";
|
|
922
|
+
try {
|
|
923
|
+
const { dockerClient, dockerModule } = await loadDockerModule({
|
|
924
|
+
fsOverrides: {
|
|
925
|
+
readFileSync: sinon.stub().returns(
|
|
926
|
+
JSON.stringify({
|
|
927
|
+
auths: {
|
|
928
|
+
"private-registry.example.com": {
|
|
929
|
+
auth: Buffer.from("trusted-user:trusted-pass").toString(
|
|
930
|
+
"base64",
|
|
931
|
+
),
|
|
932
|
+
},
|
|
933
|
+
},
|
|
934
|
+
}),
|
|
935
|
+
),
|
|
936
|
+
},
|
|
937
|
+
utilsOverrides: {
|
|
938
|
+
safeExistsSync: sinon
|
|
939
|
+
.stub()
|
|
940
|
+
.callsFake((filePath) => filePath.endsWith("config.json")),
|
|
941
|
+
},
|
|
942
|
+
});
|
|
943
|
+
|
|
944
|
+
await dockerModule.makeRequest(
|
|
945
|
+
"images/create?fromImage=registry.example.com/team/app:latest",
|
|
946
|
+
"POST",
|
|
947
|
+
"registry.example.com",
|
|
948
|
+
);
|
|
949
|
+
|
|
950
|
+
const requestOptions = dockerClient.firstCall.args[1];
|
|
951
|
+
assert.strictEqual(requestOptions.headers, undefined);
|
|
952
|
+
} finally {
|
|
953
|
+
if (originalDockerConfig === undefined) {
|
|
954
|
+
delete process.env.DOCKER_CONFIG;
|
|
955
|
+
} else {
|
|
956
|
+
process.env.DOCKER_CONFIG = originalDockerConfig;
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
await it("docker makeRequest accepts exact normalized registry matches from config auths", async () => {
|
|
962
|
+
const originalDockerConfig = process.env.DOCKER_CONFIG;
|
|
963
|
+
process.env.DOCKER_CONFIG = "/tmp/cdxgen-docker-config";
|
|
964
|
+
try {
|
|
965
|
+
const { dockerClient, dockerModule } = await loadDockerModule({
|
|
966
|
+
fsOverrides: {
|
|
967
|
+
readFileSync: sinon.stub().returns(
|
|
968
|
+
JSON.stringify({
|
|
969
|
+
auths: {
|
|
970
|
+
"https://registry.example.com/v2/": {
|
|
971
|
+
auth: Buffer.from("trusted-user:trusted-pass").toString(
|
|
972
|
+
"base64",
|
|
973
|
+
),
|
|
974
|
+
},
|
|
975
|
+
},
|
|
976
|
+
}),
|
|
977
|
+
),
|
|
978
|
+
},
|
|
979
|
+
utilsOverrides: {
|
|
980
|
+
safeExistsSync: sinon
|
|
981
|
+
.stub()
|
|
982
|
+
.callsFake((filePath) => filePath.endsWith("config.json")),
|
|
983
|
+
},
|
|
984
|
+
});
|
|
985
|
+
|
|
986
|
+
await dockerModule.makeRequest(
|
|
987
|
+
"images/create?fromImage=registry.example.com/team/app:latest",
|
|
988
|
+
"POST",
|
|
989
|
+
"registry.example.com/team/app",
|
|
990
|
+
);
|
|
991
|
+
|
|
992
|
+
const registryAuthHeader =
|
|
993
|
+
dockerClient.firstCall.args[1].headers["X-Registry-Auth"];
|
|
994
|
+
assert.deepStrictEqual(decodeRegistryAuthHeader(registryAuthHeader), {
|
|
995
|
+
username: "trusted-user",
|
|
996
|
+
password: "trusted-pass",
|
|
997
|
+
serveraddress: "https://registry.example.com/v2/",
|
|
998
|
+
});
|
|
999
|
+
} finally {
|
|
1000
|
+
if (originalDockerConfig === undefined) {
|
|
1001
|
+
delete process.env.DOCKER_CONFIG;
|
|
1002
|
+
} else {
|
|
1003
|
+
process.env.DOCKER_CONFIG = originalDockerConfig;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
});
|
|
1007
|
+
|
|
1008
|
+
await it("docker makeRequest accepts normalized exact matches across ipv4 ipv6 explicit ports and scoped subpaths from config auths", async () => {
|
|
1009
|
+
const cases = [
|
|
1010
|
+
{
|
|
1011
|
+
configuredRegistry: "127.0.0.1:5000",
|
|
1012
|
+
requestedRegistry: "127.0.0.1:5000/team/app",
|
|
1013
|
+
expectedServerAddress: "127.0.0.1:5000",
|
|
1014
|
+
},
|
|
1015
|
+
{
|
|
1016
|
+
configuredRegistry: "[::1]:5000",
|
|
1017
|
+
requestedRegistry: "[::1]:5000/team/app",
|
|
1018
|
+
expectedServerAddress: "[::1]:5000",
|
|
1019
|
+
},
|
|
1020
|
+
{
|
|
1021
|
+
configuredRegistry: "https://[2001:db8::1]:5000/v2/",
|
|
1022
|
+
requestedRegistry: "[2001:db8::1]:5000/team/app",
|
|
1023
|
+
expectedServerAddress: "https://[2001:db8::1]:5000/v2/",
|
|
1024
|
+
},
|
|
1025
|
+
{
|
|
1026
|
+
configuredRegistry: "HTTPS://REGISTRY.EXAMPLE.COM/V2/",
|
|
1027
|
+
requestedRegistry: "registry.example.com/team/app",
|
|
1028
|
+
expectedServerAddress: "HTTPS://REGISTRY.EXAMPLE.COM/V2/",
|
|
1029
|
+
},
|
|
1030
|
+
{
|
|
1031
|
+
configuredRegistry: "https://registry.example.com:443/v2/",
|
|
1032
|
+
requestedRegistry: "registry.example.com:443/team/app",
|
|
1033
|
+
expectedServerAddress: "https://registry.example.com:443/v2/",
|
|
1034
|
+
},
|
|
1035
|
+
{
|
|
1036
|
+
configuredRegistry: "http://registry.example.com:80/v2/",
|
|
1037
|
+
requestedRegistry: "registry.example.com:80/team/app",
|
|
1038
|
+
expectedServerAddress: "http://registry.example.com:80/v2/",
|
|
1039
|
+
},
|
|
1040
|
+
{
|
|
1041
|
+
configuredRegistry: "https://registry.example.com/custom/subpath",
|
|
1042
|
+
requestedRegistry: "registry.example.com/custom/subpath/team/app",
|
|
1043
|
+
expectedServerAddress: "https://registry.example.com/custom/subpath",
|
|
1044
|
+
},
|
|
1045
|
+
{
|
|
1046
|
+
configuredRegistry: "https://registry.example.com/custom/subpath/v2/",
|
|
1047
|
+
requestedRegistry: "registry.example.com/custom/subpath/team/app",
|
|
1048
|
+
expectedServerAddress: "https://registry.example.com/custom/subpath/v2/",
|
|
1049
|
+
},
|
|
1050
|
+
];
|
|
1051
|
+
|
|
1052
|
+
await withDockerConfig(async () => {
|
|
1053
|
+
for (const testCase of cases) {
|
|
1054
|
+
const { dockerClient, dockerModule } = await loadDockerModuleWithAuths(
|
|
1055
|
+
testCase.configuredRegistry,
|
|
1056
|
+
);
|
|
1057
|
+
|
|
1058
|
+
await dockerModule.makeRequest(
|
|
1059
|
+
`images/create?fromImage=${testCase.requestedRegistry}:latest`,
|
|
1060
|
+
"POST",
|
|
1061
|
+
testCase.requestedRegistry,
|
|
1062
|
+
);
|
|
1063
|
+
|
|
1064
|
+
const registryAuthHeader =
|
|
1065
|
+
dockerClient.firstCall.args[1].headers["X-Registry-Auth"];
|
|
1066
|
+
assert.deepStrictEqual(decodeRegistryAuthHeader(registryAuthHeader), {
|
|
1067
|
+
username: "trusted-user",
|
|
1068
|
+
password: "trusted-pass",
|
|
1069
|
+
serveraddress: testCase.expectedServerAddress,
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
});
|
|
1073
|
+
});
|
|
1074
|
+
|
|
1075
|
+
await it("docker makeRequest rejects wildcard unicode bidi explicit-default-port port-boundary and unrelated scoped-path mismatches from config auths", async () => {
|
|
1076
|
+
const bidiRegistry = "reg\u202eistry.example.com";
|
|
1077
|
+
const unicodeConfusableRegistry = "reg\u0456stry.example.com";
|
|
1078
|
+
const cases = [
|
|
1079
|
+
{
|
|
1080
|
+
configuredRegistry: "*.example.com",
|
|
1081
|
+
requestedRegistry: "team.example.com/app",
|
|
1082
|
+
},
|
|
1083
|
+
{
|
|
1084
|
+
configuredRegistry: "registry.example.com",
|
|
1085
|
+
requestedRegistry: "registry.example.com:80/team/app",
|
|
1086
|
+
},
|
|
1087
|
+
{
|
|
1088
|
+
configuredRegistry: "registry.example.com:443",
|
|
1089
|
+
requestedRegistry: "registry.example.com/team/app",
|
|
1090
|
+
},
|
|
1091
|
+
{
|
|
1092
|
+
configuredRegistry: "127.0.0.1:5001",
|
|
1093
|
+
requestedRegistry: "127.0.0.1:5000/team/app",
|
|
1094
|
+
},
|
|
1095
|
+
{
|
|
1096
|
+
configuredRegistry: "[::1]:5001",
|
|
1097
|
+
requestedRegistry: "[::1]:5000/team/app",
|
|
1098
|
+
},
|
|
1099
|
+
{
|
|
1100
|
+
configuredRegistry: "https://registry.example.com.evil.invalid/v2/",
|
|
1101
|
+
requestedRegistry: "registry.example.com/team/app",
|
|
1102
|
+
},
|
|
1103
|
+
{
|
|
1104
|
+
configuredRegistry: "https://registry.example.com/custom/subpath",
|
|
1105
|
+
requestedRegistry: "registry.example.com/team/app",
|
|
1106
|
+
},
|
|
1107
|
+
{
|
|
1108
|
+
configuredRegistry: "https://registry.example.com/custom/subpath",
|
|
1109
|
+
requestedRegistry: "registry.example.com/custom/subpathology/team/app",
|
|
1110
|
+
},
|
|
1111
|
+
{
|
|
1112
|
+
configuredRegistry: "https://registry.example.com:443/v2/",
|
|
1113
|
+
requestedRegistry: "registry.example.com:444/team/app",
|
|
1114
|
+
},
|
|
1115
|
+
{
|
|
1116
|
+
configuredRegistry: unicodeConfusableRegistry,
|
|
1117
|
+
requestedRegistry: "registry.example.com/team/app",
|
|
1118
|
+
},
|
|
1119
|
+
{
|
|
1120
|
+
configuredRegistry: bidiRegistry,
|
|
1121
|
+
requestedRegistry: "registry.example.com/team/app",
|
|
1122
|
+
},
|
|
1123
|
+
];
|
|
1124
|
+
|
|
1125
|
+
await withDockerConfig(async () => {
|
|
1126
|
+
for (const testCase of cases) {
|
|
1127
|
+
const { dockerClient, dockerModule } = await loadDockerModuleWithAuths(
|
|
1128
|
+
testCase.configuredRegistry,
|
|
1129
|
+
);
|
|
1130
|
+
|
|
1131
|
+
await dockerModule.makeRequest(
|
|
1132
|
+
`images/create?fromImage=${testCase.requestedRegistry}:latest`,
|
|
1133
|
+
"POST",
|
|
1134
|
+
testCase.requestedRegistry,
|
|
1135
|
+
);
|
|
1136
|
+
|
|
1137
|
+
const requestOptions = dockerClient.firstCall.args[1];
|
|
1138
|
+
assert.strictEqual(requestOptions.headers, undefined);
|
|
1139
|
+
}
|
|
1140
|
+
});
|
|
1141
|
+
});
|
|
1142
|
+
|
|
1143
|
+
await it("docker makeRequest accepts raw host:port registry matches from config auths", async () => {
|
|
1144
|
+
await withDockerConfig(async () => {
|
|
1145
|
+
const { dockerClient, dockerModule } = await loadDockerModule({
|
|
1146
|
+
fsOverrides: {
|
|
1147
|
+
readFileSync: sinon.stub().returns(
|
|
1148
|
+
JSON.stringify({
|
|
1149
|
+
auths: {
|
|
1150
|
+
"localhost:5000": {
|
|
1151
|
+
auth: Buffer.from("trusted-user:trusted-pass").toString(
|
|
1152
|
+
"base64",
|
|
1153
|
+
),
|
|
1154
|
+
},
|
|
1155
|
+
},
|
|
1156
|
+
}),
|
|
1157
|
+
),
|
|
1158
|
+
},
|
|
1159
|
+
utilsOverrides: {
|
|
1160
|
+
safeExistsSync: sinon
|
|
1161
|
+
.stub()
|
|
1162
|
+
.callsFake((filePath) => filePath.endsWith("config.json")),
|
|
1163
|
+
},
|
|
1164
|
+
});
|
|
1165
|
+
|
|
1166
|
+
await dockerModule.makeRequest(
|
|
1167
|
+
"images/create?fromImage=localhost:5000/team/app:latest",
|
|
1168
|
+
"POST",
|
|
1169
|
+
"localhost:5000/team/app",
|
|
1170
|
+
);
|
|
1171
|
+
|
|
1172
|
+
const registryAuthHeader =
|
|
1173
|
+
dockerClient.firstCall.args[1].headers["X-Registry-Auth"];
|
|
1174
|
+
assert.deepStrictEqual(decodeRegistryAuthHeader(registryAuthHeader), {
|
|
1175
|
+
username: "trusted-user",
|
|
1176
|
+
password: "trusted-pass",
|
|
1177
|
+
serveraddress: "localhost:5000",
|
|
1178
|
+
});
|
|
1179
|
+
});
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
await it("docker makeRequest keeps raw host:port registries separated by port", async () => {
|
|
1183
|
+
await withDockerConfig(async () => {
|
|
1184
|
+
const { dockerClient, dockerModule } = await loadDockerModule({
|
|
1185
|
+
fsOverrides: {
|
|
1186
|
+
readFileSync: sinon.stub().returns(
|
|
1187
|
+
JSON.stringify({
|
|
1188
|
+
auths: {
|
|
1189
|
+
"localhost:5001": {
|
|
1190
|
+
auth: Buffer.from("trusted-user:trusted-pass").toString(
|
|
1191
|
+
"base64",
|
|
1192
|
+
),
|
|
1193
|
+
},
|
|
1194
|
+
},
|
|
1195
|
+
}),
|
|
1196
|
+
),
|
|
1197
|
+
},
|
|
1198
|
+
utilsOverrides: {
|
|
1199
|
+
safeExistsSync: sinon
|
|
1200
|
+
.stub()
|
|
1201
|
+
.callsFake((filePath) => filePath.endsWith("config.json")),
|
|
1202
|
+
},
|
|
1203
|
+
});
|
|
1204
|
+
|
|
1205
|
+
await dockerModule.makeRequest(
|
|
1206
|
+
"images/create?fromImage=localhost:5000/team/app:latest",
|
|
1207
|
+
"POST",
|
|
1208
|
+
"localhost:5000/team/app",
|
|
1209
|
+
);
|
|
1210
|
+
|
|
1211
|
+
const requestOptions = dockerClient.firstCall.args[1];
|
|
1212
|
+
assert.strictEqual(requestOptions.headers, undefined);
|
|
1213
|
+
});
|
|
1214
|
+
});
|
|
1215
|
+
|
|
1216
|
+
await it("docker makeRequest preserves Docker Hub auth aliases without substring matching", async () => {
|
|
1217
|
+
const originalDockerConfig = process.env.DOCKER_CONFIG;
|
|
1218
|
+
process.env.DOCKER_CONFIG = "/tmp/cdxgen-docker-config";
|
|
1219
|
+
try {
|
|
1220
|
+
const { dockerClient, dockerModule } = await loadDockerModule({
|
|
1221
|
+
fsOverrides: {
|
|
1222
|
+
readFileSync: sinon.stub().returns(
|
|
1223
|
+
JSON.stringify({
|
|
1224
|
+
auths: {
|
|
1225
|
+
"https://index.docker.io/v1/": {
|
|
1226
|
+
auth: Buffer.from("hub-user:hub-pass").toString("base64"),
|
|
1227
|
+
},
|
|
1228
|
+
},
|
|
1229
|
+
}),
|
|
1230
|
+
),
|
|
1231
|
+
},
|
|
1232
|
+
utilsOverrides: {
|
|
1233
|
+
safeExistsSync: sinon
|
|
1234
|
+
.stub()
|
|
1235
|
+
.callsFake((filePath) => filePath.endsWith("config.json")),
|
|
1236
|
+
},
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
await dockerModule.makeRequest(
|
|
1240
|
+
"images/create?fromImage=docker.io/library/alpine:latest",
|
|
1241
|
+
"POST",
|
|
1242
|
+
"docker.io",
|
|
1243
|
+
);
|
|
1244
|
+
|
|
1245
|
+
const registryAuthHeader =
|
|
1246
|
+
dockerClient.firstCall.args[1].headers["X-Registry-Auth"];
|
|
1247
|
+
assert.deepStrictEqual(decodeRegistryAuthHeader(registryAuthHeader), {
|
|
1248
|
+
username: "hub-user",
|
|
1249
|
+
password: "hub-pass",
|
|
1250
|
+
serveraddress: "https://index.docker.io/v1/",
|
|
1251
|
+
});
|
|
1252
|
+
} finally {
|
|
1253
|
+
if (originalDockerConfig === undefined) {
|
|
1254
|
+
delete process.env.DOCKER_CONFIG;
|
|
1255
|
+
} else {
|
|
1256
|
+
process.env.DOCKER_CONFIG = originalDockerConfig;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
});
|
|
1260
|
+
|
|
1261
|
+
await it("docker makeRequest resolves unqualified image pulls to Docker Hub auth entries", async () => {
|
|
1262
|
+
const requestedImages = ["myorg/app:latest", "alpine:latest"];
|
|
1263
|
+
|
|
1264
|
+
await withDockerConfig(async () => {
|
|
1265
|
+
for (const requestedImage of requestedImages) {
|
|
1266
|
+
const { dockerClient, dockerModule } = await loadDockerModule({
|
|
1267
|
+
fsOverrides: {
|
|
1268
|
+
readFileSync: sinon.stub().returns(
|
|
1269
|
+
JSON.stringify({
|
|
1270
|
+
auths: {
|
|
1271
|
+
"https://index.docker.io/v1/": {
|
|
1272
|
+
auth: Buffer.from("hub-user:hub-pass").toString("base64"),
|
|
1273
|
+
},
|
|
1274
|
+
},
|
|
1275
|
+
}),
|
|
1276
|
+
),
|
|
1277
|
+
},
|
|
1278
|
+
utilsOverrides: {
|
|
1279
|
+
safeExistsSync: dockerConfigExistsStub(),
|
|
1280
|
+
},
|
|
1281
|
+
});
|
|
1282
|
+
|
|
1283
|
+
await dockerModule.makeRequest(
|
|
1284
|
+
`images/create?fromImage=${requestedImage}`,
|
|
1285
|
+
"POST",
|
|
1286
|
+
"",
|
|
1287
|
+
);
|
|
1288
|
+
|
|
1289
|
+
const registryAuthHeader =
|
|
1290
|
+
dockerClient.firstCall.args[1].headers["X-Registry-Auth"];
|
|
1291
|
+
assert.deepStrictEqual(decodeRegistryAuthHeader(registryAuthHeader), {
|
|
1292
|
+
username: "hub-user",
|
|
1293
|
+
password: "hub-pass",
|
|
1294
|
+
serveraddress: "https://index.docker.io/v1/",
|
|
1295
|
+
});
|
|
1296
|
+
}
|
|
1297
|
+
});
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
await it("docker makeRequest skips credHelpers for substring-matched registries", async () => {
|
|
1301
|
+
const originalDockerConfig = process.env.DOCKER_CONFIG;
|
|
1302
|
+
process.env.DOCKER_CONFIG = "/tmp/cdxgen-docker-config";
|
|
1303
|
+
try {
|
|
1304
|
+
const safeSpawnSync = sinon.stub().returns({
|
|
1305
|
+
status: 0,
|
|
1306
|
+
stdout: JSON.stringify({
|
|
1307
|
+
Username: "trusted-user",
|
|
1308
|
+
Secret: "trusted-pass",
|
|
1309
|
+
}),
|
|
1310
|
+
stderr: "",
|
|
1311
|
+
});
|
|
1312
|
+
const { dockerClient, dockerModule } = await loadDockerModule({
|
|
1313
|
+
fsOverrides: {
|
|
1314
|
+
readFileSync: sinon.stub().returns(
|
|
1315
|
+
JSON.stringify({
|
|
1316
|
+
credHelpers: {
|
|
1317
|
+
"private-registry.example.com": "osxkeychain",
|
|
1318
|
+
},
|
|
1319
|
+
}),
|
|
1320
|
+
),
|
|
1321
|
+
},
|
|
1322
|
+
utilsOverrides: {
|
|
1323
|
+
safeExistsSync: sinon
|
|
1324
|
+
.stub()
|
|
1325
|
+
.callsFake((filePath) => filePath.endsWith("config.json")),
|
|
1326
|
+
safeSpawnSync,
|
|
1327
|
+
},
|
|
1328
|
+
});
|
|
1329
|
+
|
|
1330
|
+
await dockerModule.makeRequest(
|
|
1331
|
+
"images/create?fromImage=registry.example.com/team/app:latest",
|
|
1332
|
+
"POST",
|
|
1333
|
+
"registry.example.com",
|
|
1334
|
+
);
|
|
1335
|
+
|
|
1336
|
+
const requestOptions = dockerClient.firstCall.args[1];
|
|
1337
|
+
assert.strictEqual(requestOptions.headers, undefined);
|
|
1338
|
+
sinon.assert.notCalled(safeSpawnSync);
|
|
1339
|
+
} finally {
|
|
1340
|
+
if (originalDockerConfig === undefined) {
|
|
1341
|
+
delete process.env.DOCKER_CONFIG;
|
|
1342
|
+
} else {
|
|
1343
|
+
process.env.DOCKER_CONFIG = originalDockerConfig;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
});
|
|
1347
|
+
|
|
1348
|
+
await it("docker makeRequest accepts raw host:port registry matches from credHelpers", async () => {
|
|
1349
|
+
await withDockerConfig(async () => {
|
|
1350
|
+
const safeSpawnSync = sinon.stub().returns({
|
|
1351
|
+
status: 0,
|
|
1352
|
+
stdout: JSON.stringify({
|
|
1353
|
+
username: "trusted-user",
|
|
1354
|
+
Secret: "trusted-pass",
|
|
1355
|
+
}),
|
|
1356
|
+
stderr: "",
|
|
1357
|
+
});
|
|
1358
|
+
const { dockerClient, dockerModule } = await loadDockerModule({
|
|
1359
|
+
fsOverrides: {
|
|
1360
|
+
readFileSync: sinon.stub().returns(
|
|
1361
|
+
JSON.stringify({
|
|
1362
|
+
credHelpers: {
|
|
1363
|
+
"localhost:5000": "osxkeychain",
|
|
1364
|
+
},
|
|
1365
|
+
}),
|
|
1366
|
+
),
|
|
1367
|
+
},
|
|
1368
|
+
utilsOverrides: {
|
|
1369
|
+
safeExistsSync: sinon
|
|
1370
|
+
.stub()
|
|
1371
|
+
.callsFake((filePath) => filePath.endsWith("config.json")),
|
|
1372
|
+
safeSpawnSync,
|
|
1373
|
+
},
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1376
|
+
await dockerModule.makeRequest(
|
|
1377
|
+
"images/create?fromImage=localhost:5000/team/app:latest",
|
|
1378
|
+
"POST",
|
|
1379
|
+
"localhost:5000/team/app",
|
|
1380
|
+
);
|
|
1381
|
+
|
|
1382
|
+
sinon.assert.calledOnceWithExactly(
|
|
1383
|
+
safeSpawnSync,
|
|
1384
|
+
credHelperExe("osxkeychain"),
|
|
1385
|
+
["get"],
|
|
1386
|
+
{
|
|
1387
|
+
input: "localhost:5000",
|
|
1388
|
+
},
|
|
1389
|
+
);
|
|
1390
|
+
const registryAuthHeader =
|
|
1391
|
+
dockerClient.firstCall.args[1].headers["X-Registry-Auth"];
|
|
1392
|
+
assert.deepStrictEqual(decodeRegistryAuthHeader(registryAuthHeader), {
|
|
1393
|
+
username: "trusted-user",
|
|
1394
|
+
password: "trusted-pass",
|
|
1395
|
+
email: "trusted-user",
|
|
1396
|
+
serveraddress: "localhost:5000",
|
|
1397
|
+
});
|
|
1398
|
+
});
|
|
1399
|
+
});
|
|
1400
|
+
|
|
1401
|
+
await it("docker getCredsFromHelper normalizes cache keys for equivalent registry hosts", async () => {
|
|
1402
|
+
const safeSpawnSync = sinon.stub().returns({
|
|
1403
|
+
status: 0,
|
|
1404
|
+
stdout: JSON.stringify({
|
|
1405
|
+
username: "trusted-user",
|
|
1406
|
+
Secret: "trusted-pass",
|
|
1407
|
+
}),
|
|
1408
|
+
stderr: "",
|
|
1409
|
+
});
|
|
1410
|
+
const { dockerModule } = await loadDockerModule({
|
|
1411
|
+
utilsOverrides: {
|
|
1412
|
+
safeSpawnSync,
|
|
1413
|
+
},
|
|
1414
|
+
});
|
|
1415
|
+
|
|
1416
|
+
const firstToken = dockerModule.getCredsFromHelper(
|
|
1417
|
+
"osxkeychain",
|
|
1418
|
+
"registry.example.com",
|
|
1419
|
+
);
|
|
1420
|
+
const secondToken = dockerModule.getCredsFromHelper(
|
|
1421
|
+
"osxkeychain",
|
|
1422
|
+
"https://registry.example.com/v2/",
|
|
1423
|
+
);
|
|
1424
|
+
|
|
1425
|
+
assert.strictEqual(firstToken, secondToken);
|
|
1426
|
+
sinon.assert.calledOnceWithExactly(
|
|
1427
|
+
safeSpawnSync,
|
|
1428
|
+
credHelperExe("osxkeychain"),
|
|
1429
|
+
["get"],
|
|
1430
|
+
{
|
|
1431
|
+
input: "registry.example.com",
|
|
1432
|
+
},
|
|
1433
|
+
);
|
|
1434
|
+
});
|
|
1435
|
+
|
|
1436
|
+
await it("docker getCredsFromHelper keeps scoped path cache keys isolated", async () => {
|
|
1437
|
+
const safeSpawnSync = sinon.stub().returns({
|
|
1438
|
+
status: 0,
|
|
1439
|
+
stdout: JSON.stringify({
|
|
1440
|
+
username: "trusted-user",
|
|
1441
|
+
Secret: "trusted-pass",
|
|
1442
|
+
}),
|
|
1443
|
+
stderr: "",
|
|
1444
|
+
});
|
|
1445
|
+
const { dockerModule } = await loadDockerModule({
|
|
1446
|
+
utilsOverrides: {
|
|
1447
|
+
safeSpawnSync,
|
|
1448
|
+
},
|
|
1449
|
+
});
|
|
1450
|
+
|
|
1451
|
+
const firstToken = dockerModule.getCredsFromHelper(
|
|
1452
|
+
"osxkeychain",
|
|
1453
|
+
"https://registry.example.com/custom/subpath/v2/",
|
|
1454
|
+
);
|
|
1455
|
+
const secondToken = dockerModule.getCredsFromHelper(
|
|
1456
|
+
"osxkeychain",
|
|
1457
|
+
"https://registry.example.com/custom/subpath/v2/",
|
|
1458
|
+
);
|
|
1459
|
+
const thirdToken = dockerModule.getCredsFromHelper(
|
|
1460
|
+
"osxkeychain",
|
|
1461
|
+
"https://registry.example.com/other/subpath/v2/",
|
|
1462
|
+
);
|
|
1463
|
+
|
|
1464
|
+
assert.strictEqual(firstToken, secondToken);
|
|
1465
|
+
assert.notStrictEqual(firstToken, thirdToken);
|
|
1466
|
+
assert.deepStrictEqual(decodeRegistryAuthHeader(firstToken), {
|
|
1467
|
+
username: "trusted-user",
|
|
1468
|
+
password: "trusted-pass",
|
|
1469
|
+
email: "trusted-user",
|
|
1470
|
+
serveraddress: "https://registry.example.com/custom/subpath/v2/",
|
|
1471
|
+
});
|
|
1472
|
+
sinon.assert.calledTwice(safeSpawnSync);
|
|
1473
|
+
});
|
|
1474
|
+
|
|
1475
|
+
await it("docker makeRequest accepts ipv4 ipv6 explicit-port and scoped-subpath registry matches from credHelpers", async () => {
|
|
1476
|
+
const cases = [
|
|
1477
|
+
{
|
|
1478
|
+
configuredRegistry: "127.0.0.1:5000",
|
|
1479
|
+
requestedRegistry: "127.0.0.1:5000/team/app",
|
|
1480
|
+
},
|
|
1481
|
+
{
|
|
1482
|
+
configuredRegistry: "[::1]:5000",
|
|
1483
|
+
requestedRegistry: "[::1]:5000/team/app",
|
|
1484
|
+
},
|
|
1485
|
+
{
|
|
1486
|
+
configuredRegistry: "https://registry.example.com:443/v2/",
|
|
1487
|
+
requestedRegistry: "registry.example.com:443/team/app",
|
|
1488
|
+
},
|
|
1489
|
+
{
|
|
1490
|
+
configuredRegistry: "http://registry.example.com:80/v2/",
|
|
1491
|
+
requestedRegistry: "registry.example.com:80/team/app",
|
|
1492
|
+
},
|
|
1493
|
+
{
|
|
1494
|
+
configuredRegistry: "https://registry.example.com/custom/subpath/v2/",
|
|
1495
|
+
requestedRegistry: "registry.example.com/custom/subpath/team/app",
|
|
1496
|
+
},
|
|
1497
|
+
];
|
|
1498
|
+
|
|
1499
|
+
await withDockerConfig(async () => {
|
|
1500
|
+
for (const testCase of cases) {
|
|
1501
|
+
const safeSpawnSync = sinon.stub().returns({
|
|
1502
|
+
status: 0,
|
|
1503
|
+
stdout: JSON.stringify({
|
|
1504
|
+
username: "trusted-user",
|
|
1505
|
+
Secret: "trusted-pass",
|
|
1506
|
+
}),
|
|
1507
|
+
stderr: "",
|
|
1508
|
+
});
|
|
1509
|
+
const { dockerClient, dockerModule } =
|
|
1510
|
+
await loadDockerModuleWithCredHelpers(
|
|
1511
|
+
testCase.configuredRegistry,
|
|
1512
|
+
safeSpawnSync,
|
|
1513
|
+
);
|
|
1514
|
+
|
|
1515
|
+
await dockerModule.makeRequest(
|
|
1516
|
+
`images/create?fromImage=${testCase.requestedRegistry}:latest`,
|
|
1517
|
+
"POST",
|
|
1518
|
+
testCase.requestedRegistry,
|
|
1519
|
+
);
|
|
1520
|
+
|
|
1521
|
+
sinon.assert.calledOnceWithExactly(
|
|
1522
|
+
safeSpawnSync,
|
|
1523
|
+
credHelperExe("osxkeychain"),
|
|
1524
|
+
["get"],
|
|
1525
|
+
{
|
|
1526
|
+
input: testCase.configuredRegistry,
|
|
1527
|
+
},
|
|
1528
|
+
);
|
|
1529
|
+
const registryAuthHeader =
|
|
1530
|
+
dockerClient.firstCall.args[1].headers["X-Registry-Auth"];
|
|
1531
|
+
assert.deepStrictEqual(decodeRegistryAuthHeader(registryAuthHeader), {
|
|
1532
|
+
username: "trusted-user",
|
|
1533
|
+
password: "trusted-pass",
|
|
1534
|
+
email: "trusted-user",
|
|
1535
|
+
serveraddress: testCase.configuredRegistry,
|
|
1536
|
+
});
|
|
1537
|
+
}
|
|
1538
|
+
});
|
|
1539
|
+
});
|
|
1540
|
+
|
|
1541
|
+
await it("docker makeRequest does not invoke credHelpers for wildcard unicode bidi explicit-default-port or port-boundary mismatches", async () => {
|
|
1542
|
+
const bidiRegistry = "reg\u202eistry.example.com";
|
|
1543
|
+
const unicodeConfusableRegistry = "reg\u0456stry.example.com";
|
|
1544
|
+
const cases = [
|
|
1545
|
+
{
|
|
1546
|
+
configuredRegistry: "*.example.com",
|
|
1547
|
+
requestedRegistry: "team.example.com/app",
|
|
1548
|
+
},
|
|
1549
|
+
{
|
|
1550
|
+
configuredRegistry: "registry.example.com",
|
|
1551
|
+
requestedRegistry: "registry.example.com:80/team/app",
|
|
1552
|
+
},
|
|
1553
|
+
{
|
|
1554
|
+
configuredRegistry: "registry.example.com:443",
|
|
1555
|
+
requestedRegistry: "registry.example.com/team/app",
|
|
1556
|
+
},
|
|
1557
|
+
{
|
|
1558
|
+
configuredRegistry: "127.0.0.1:5001",
|
|
1559
|
+
requestedRegistry: "127.0.0.1:5000/team/app",
|
|
1560
|
+
},
|
|
1561
|
+
{
|
|
1562
|
+
configuredRegistry: "[::1]:5001",
|
|
1563
|
+
requestedRegistry: "[::1]:5000/team/app",
|
|
1564
|
+
},
|
|
1565
|
+
{
|
|
1566
|
+
configuredRegistry: "https://registry.example.com/custom/subpath/v2/",
|
|
1567
|
+
requestedRegistry: "registry.example.com/team/app",
|
|
1568
|
+
},
|
|
1569
|
+
{
|
|
1570
|
+
configuredRegistry: "https://registry.example.com/custom/subpath/v2/",
|
|
1571
|
+
requestedRegistry: "registry.example.com/custom/subpathology/team/app",
|
|
1572
|
+
},
|
|
1573
|
+
{
|
|
1574
|
+
configuredRegistry: "https://registry.example.com:443/v2/",
|
|
1575
|
+
requestedRegistry: "registry.example.com:444/team/app",
|
|
1576
|
+
},
|
|
1577
|
+
{
|
|
1578
|
+
configuredRegistry: unicodeConfusableRegistry,
|
|
1579
|
+
requestedRegistry: "registry.example.com/team/app",
|
|
1580
|
+
},
|
|
1581
|
+
{
|
|
1582
|
+
configuredRegistry: bidiRegistry,
|
|
1583
|
+
requestedRegistry: "registry.example.com/team/app",
|
|
1584
|
+
},
|
|
1585
|
+
];
|
|
1586
|
+
|
|
1587
|
+
await withDockerConfig(async () => {
|
|
1588
|
+
for (const testCase of cases) {
|
|
1589
|
+
const safeSpawnSync = sinon.stub().returns({
|
|
1590
|
+
status: 0,
|
|
1591
|
+
stdout: JSON.stringify({
|
|
1592
|
+
username: "trusted-user",
|
|
1593
|
+
Secret: "trusted-pass",
|
|
1594
|
+
}),
|
|
1595
|
+
stderr: "",
|
|
1596
|
+
});
|
|
1597
|
+
const { dockerClient, dockerModule } =
|
|
1598
|
+
await loadDockerModuleWithCredHelpers(
|
|
1599
|
+
testCase.configuredRegistry,
|
|
1600
|
+
safeSpawnSync,
|
|
1601
|
+
);
|
|
1602
|
+
|
|
1603
|
+
await dockerModule.makeRequest(
|
|
1604
|
+
`images/create?fromImage=${testCase.requestedRegistry}:latest`,
|
|
1605
|
+
"POST",
|
|
1606
|
+
testCase.requestedRegistry,
|
|
1607
|
+
);
|
|
1608
|
+
|
|
1609
|
+
const requestOptions = dockerClient.firstCall.args[1];
|
|
1610
|
+
assert.strictEqual(requestOptions.headers, undefined);
|
|
1611
|
+
sinon.assert.notCalled(safeSpawnSync);
|
|
1612
|
+
}
|
|
1613
|
+
});
|
|
1614
|
+
});
|
|
1615
|
+
|
|
1616
|
+
await it("docker makeRequest resolves unqualified image pulls to Docker Hub credHelpers", async () => {
|
|
1617
|
+
const requestedImages = ["myorg/app:latest", "alpine:latest"];
|
|
1618
|
+
|
|
1619
|
+
await withDockerConfig(async () => {
|
|
1620
|
+
for (const requestedImage of requestedImages) {
|
|
1621
|
+
const safeSpawnSync = sinon.stub().returns({
|
|
1622
|
+
status: 0,
|
|
1623
|
+
stdout: JSON.stringify({
|
|
1624
|
+
username: "hub-user",
|
|
1625
|
+
Secret: "hub-pass",
|
|
1626
|
+
}),
|
|
1627
|
+
stderr: "",
|
|
1628
|
+
});
|
|
1629
|
+
const { dockerClient, dockerModule } = await loadDockerModule({
|
|
1630
|
+
fsOverrides: {
|
|
1631
|
+
readFileSync: sinon.stub().returns(
|
|
1632
|
+
JSON.stringify({
|
|
1633
|
+
credHelpers: {
|
|
1634
|
+
"docker.io": "osxkeychain",
|
|
1635
|
+
},
|
|
1636
|
+
}),
|
|
1637
|
+
),
|
|
1638
|
+
},
|
|
1639
|
+
utilsOverrides: {
|
|
1640
|
+
safeExistsSync: dockerConfigExistsStub(),
|
|
1641
|
+
safeSpawnSync,
|
|
1642
|
+
},
|
|
1643
|
+
});
|
|
1644
|
+
|
|
1645
|
+
await dockerModule.makeRequest(
|
|
1646
|
+
`images/create?fromImage=${requestedImage}`,
|
|
1647
|
+
"POST",
|
|
1648
|
+
"",
|
|
1649
|
+
);
|
|
1650
|
+
|
|
1651
|
+
sinon.assert.calledOnceWithExactly(
|
|
1652
|
+
safeSpawnSync,
|
|
1653
|
+
credHelperExe("osxkeychain"),
|
|
1654
|
+
["get"],
|
|
1655
|
+
{
|
|
1656
|
+
input: "docker.io",
|
|
1657
|
+
},
|
|
1658
|
+
);
|
|
1659
|
+
const registryAuthHeader =
|
|
1660
|
+
dockerClient.firstCall.args[1].headers["X-Registry-Auth"];
|
|
1661
|
+
assert.deepStrictEqual(decodeRegistryAuthHeader(registryAuthHeader), {
|
|
1662
|
+
username: "hub-user",
|
|
1663
|
+
password: "hub-pass",
|
|
1664
|
+
email: "hub-user",
|
|
1665
|
+
serveraddress: "docker.io",
|
|
1666
|
+
});
|
|
1667
|
+
}
|
|
1668
|
+
});
|
|
1669
|
+
});
|
|
1670
|
+
|
|
1671
|
+
await it("docker makeRequest accepts normalized exact matches for common public registries without aliasing hosts", async () => {
|
|
1672
|
+
const cases = [
|
|
1673
|
+
{
|
|
1674
|
+
configuredRegistry: "https://ghcr.io/v2/",
|
|
1675
|
+
requestedRegistry: "ghcr.io/org/image",
|
|
1676
|
+
},
|
|
1677
|
+
{
|
|
1678
|
+
configuredRegistry: "https://quay.io/v2/",
|
|
1679
|
+
requestedRegistry: "quay.io/org/image",
|
|
1680
|
+
},
|
|
1681
|
+
{
|
|
1682
|
+
configuredRegistry: "https://public.ecr.aws/v2/",
|
|
1683
|
+
requestedRegistry: "public.ecr.aws/alias/image",
|
|
1684
|
+
},
|
|
1685
|
+
{
|
|
1686
|
+
configuredRegistry: "https://gcr.io/v2/",
|
|
1687
|
+
requestedRegistry: "gcr.io/project/image",
|
|
1688
|
+
},
|
|
1689
|
+
];
|
|
1690
|
+
|
|
1691
|
+
await withDockerConfig(async () => {
|
|
1692
|
+
for (const { configuredRegistry, requestedRegistry } of cases) {
|
|
1693
|
+
const { dockerClient, dockerModule } = await loadDockerModule({
|
|
1694
|
+
fsOverrides: {
|
|
1695
|
+
readFileSync: sinon.stub().returns(
|
|
1696
|
+
JSON.stringify({
|
|
1697
|
+
auths: {
|
|
1698
|
+
[configuredRegistry]: {
|
|
1699
|
+
auth: Buffer.from("trusted-user:trusted-pass").toString(
|
|
1700
|
+
"base64",
|
|
1701
|
+
),
|
|
1702
|
+
},
|
|
1703
|
+
},
|
|
1704
|
+
}),
|
|
1705
|
+
),
|
|
1706
|
+
},
|
|
1707
|
+
utilsOverrides: {
|
|
1708
|
+
safeExistsSync: sinon
|
|
1709
|
+
.stub()
|
|
1710
|
+
.callsFake((filePath) => filePath.endsWith("config.json")),
|
|
1711
|
+
},
|
|
1712
|
+
});
|
|
1713
|
+
|
|
1714
|
+
await dockerModule.makeRequest(
|
|
1715
|
+
`images/create?fromImage=${requestedRegistry}:latest`,
|
|
1716
|
+
"POST",
|
|
1717
|
+
requestedRegistry,
|
|
1718
|
+
);
|
|
1719
|
+
|
|
1720
|
+
const registryAuthHeader =
|
|
1721
|
+
dockerClient.firstCall.args[1].headers["X-Registry-Auth"];
|
|
1722
|
+
assert.deepStrictEqual(decodeRegistryAuthHeader(registryAuthHeader), {
|
|
1723
|
+
username: "trusted-user",
|
|
1724
|
+
password: "trusted-pass",
|
|
1725
|
+
serveraddress: configuredRegistry,
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1728
|
+
});
|
|
1729
|
+
});
|
|
1730
|
+
|
|
1731
|
+
await it("docker makeRequest keeps ghcr quay aws and gcp registries on separate trust boundaries", async () => {
|
|
1732
|
+
const cases = [
|
|
1733
|
+
{
|
|
1734
|
+
configuredRegistry: "https://tenant.ghcr.io/v2/",
|
|
1735
|
+
requestedRegistry: "ghcr.io",
|
|
1736
|
+
},
|
|
1737
|
+
{
|
|
1738
|
+
configuredRegistry: "https://quay.io.evil.example/v2/",
|
|
1739
|
+
requestedRegistry: "quay.io",
|
|
1740
|
+
},
|
|
1741
|
+
{
|
|
1742
|
+
configuredRegistry:
|
|
1743
|
+
"https://123456789012.dkr.ecr.us-east-1.amazonaws.com/v2/",
|
|
1744
|
+
requestedRegistry: "public.ecr.aws",
|
|
1745
|
+
},
|
|
1746
|
+
{
|
|
1747
|
+
configuredRegistry: "https://mirror.gcr.io/v2/",
|
|
1748
|
+
requestedRegistry: "gcr.io",
|
|
1749
|
+
},
|
|
1750
|
+
{
|
|
1751
|
+
configuredRegistry: "https://us-docker.pkg.dev/v2/",
|
|
1752
|
+
requestedRegistry: "gcr.io",
|
|
1753
|
+
},
|
|
1754
|
+
];
|
|
1755
|
+
|
|
1756
|
+
await withDockerConfig(async () => {
|
|
1757
|
+
for (const { configuredRegistry, requestedRegistry } of cases) {
|
|
1758
|
+
const { dockerClient, dockerModule } = await loadDockerModule({
|
|
1759
|
+
fsOverrides: {
|
|
1760
|
+
readFileSync: sinon.stub().returns(
|
|
1761
|
+
JSON.stringify({
|
|
1762
|
+
auths: {
|
|
1763
|
+
[configuredRegistry]: {
|
|
1764
|
+
auth: Buffer.from("trusted-user:trusted-pass").toString(
|
|
1765
|
+
"base64",
|
|
1766
|
+
),
|
|
1767
|
+
},
|
|
1768
|
+
},
|
|
1769
|
+
}),
|
|
1770
|
+
),
|
|
1771
|
+
},
|
|
1772
|
+
utilsOverrides: {
|
|
1773
|
+
safeExistsSync: sinon
|
|
1774
|
+
.stub()
|
|
1775
|
+
.callsFake((filePath) => filePath.endsWith("config.json")),
|
|
1776
|
+
},
|
|
1777
|
+
});
|
|
1778
|
+
|
|
1779
|
+
await dockerModule.makeRequest(
|
|
1780
|
+
`images/create?fromImage=${requestedRegistry}/team/app:latest`,
|
|
1781
|
+
"POST",
|
|
1782
|
+
requestedRegistry,
|
|
1783
|
+
);
|
|
1784
|
+
|
|
1785
|
+
const requestOptions = dockerClient.firstCall.args[1];
|
|
1786
|
+
assert.strictEqual(requestOptions.headers, undefined);
|
|
1787
|
+
}
|
|
1788
|
+
});
|
|
1789
|
+
});
|
|
1790
|
+
|
|
326
1791
|
await it("extractFromManifest derives PATH metadata from archive config", async () => {
|
|
327
1792
|
const tempDir = mkdtempSync(join(tmpdir(), "cdxgen-docker-"));
|
|
328
1793
|
try {
|