@sparkleideas/security 3.0.0-alpha.10

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 (34) hide show
  1. package/README.md +234 -0
  2. package/__tests__/acceptance/security-compliance.test.ts +674 -0
  3. package/__tests__/credential-generator.test.ts +310 -0
  4. package/__tests__/fixtures/configurations.ts +419 -0
  5. package/__tests__/fixtures/index.ts +21 -0
  6. package/__tests__/helpers/create-mock.ts +469 -0
  7. package/__tests__/helpers/index.ts +32 -0
  8. package/__tests__/input-validator.test.ts +381 -0
  9. package/__tests__/integration/security-flow.test.ts +606 -0
  10. package/__tests__/password-hasher.test.ts +239 -0
  11. package/__tests__/path-validator.test.ts +302 -0
  12. package/__tests__/safe-executor.test.ts +292 -0
  13. package/__tests__/token-generator.test.ts +371 -0
  14. package/__tests__/unit/credential-generator.test.ts +182 -0
  15. package/__tests__/unit/password-hasher.test.ts +359 -0
  16. package/__tests__/unit/path-validator.test.ts +509 -0
  17. package/__tests__/unit/safe-executor.test.ts +667 -0
  18. package/__tests__/unit/token-generator.test.ts +310 -0
  19. package/package.json +28 -0
  20. package/src/CVE-REMEDIATION.ts +251 -0
  21. package/src/application/index.ts +10 -0
  22. package/src/application/services/security-application-service.ts +193 -0
  23. package/src/credential-generator.ts +368 -0
  24. package/src/domain/entities/security-context.ts +173 -0
  25. package/src/domain/index.ts +17 -0
  26. package/src/domain/services/security-domain-service.ts +296 -0
  27. package/src/index.ts +271 -0
  28. package/src/input-validator.ts +466 -0
  29. package/src/password-hasher.ts +270 -0
  30. package/src/path-validator.ts +525 -0
  31. package/src/safe-executor.ts +525 -0
  32. package/src/token-generator.ts +463 -0
  33. package/tmp.json +0 -0
  34. package/tsconfig.json +9 -0
@@ -0,0 +1,667 @@
1
+ /**
2
+ * V3 Claude-Flow Safe Executor Unit Tests
3
+ *
4
+ * London School TDD - Behavior Verification
5
+ * Tests secure command execution (CVE-3 prevention)
6
+ */
7
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
8
+ import { createMock, type MockedInterface } from '../../helpers/create-mock';
9
+ import { securityConfigs } from '../../fixtures/configurations';
10
+
11
+ /**
12
+ * Safe executor interface (to be implemented)
13
+ */
14
+ interface ISafeExecutor {
15
+ execute(command: string, args?: string[], options?: ExecuteOptions): Promise<ExecuteResult>;
16
+ isCommandAllowed(command: string): boolean;
17
+ sanitizeArgs(args: string[]): string[];
18
+ }
19
+
20
+ /**
21
+ * Process spawner interface (collaborator)
22
+ */
23
+ interface IProcessSpawner {
24
+ spawn(command: string, args: string[], options: SpawnOptions): Promise<SpawnResult>;
25
+ kill(pid: number): Promise<void>;
26
+ }
27
+
28
+ /**
29
+ * Command validator interface (collaborator)
30
+ */
31
+ interface ICommandValidator {
32
+ extractCommand(input: string): string;
33
+ isBuiltin(command: string): boolean;
34
+ resolveCommand(command: string): Promise<string | null>;
35
+ }
36
+
37
+ interface ExecuteOptions {
38
+ timeout?: number;
39
+ cwd?: string;
40
+ env?: Record<string, string>;
41
+ shell?: boolean;
42
+ }
43
+
44
+ interface ExecuteResult {
45
+ stdout: string;
46
+ stderr: string;
47
+ exitCode: number;
48
+ killed: boolean;
49
+ }
50
+
51
+ interface SpawnOptions {
52
+ cwd?: string;
53
+ env?: Record<string, string>;
54
+ timeout?: number;
55
+ shell?: boolean;
56
+ }
57
+
58
+ interface SpawnResult {
59
+ pid: number;
60
+ stdout: string;
61
+ stderr: string;
62
+ exitCode: number;
63
+ signal?: string;
64
+ }
65
+
66
+ /**
67
+ * Safe executor implementation for testing
68
+ */
69
+ class SafeExecutor implements ISafeExecutor {
70
+ constructor(
71
+ private readonly spawner: IProcessSpawner,
72
+ private readonly validator: ICommandValidator,
73
+ private readonly config: typeof securityConfigs.strict.execution
74
+ ) {}
75
+
76
+ async execute(
77
+ command: string,
78
+ args: string[] = [],
79
+ options: ExecuteOptions = {}
80
+ ): Promise<ExecuteResult> {
81
+ // Extract base command
82
+ const baseCommand = this.validator.extractCommand(command);
83
+
84
+ // Check if command is allowed
85
+ if (!this.isCommandAllowed(baseCommand)) {
86
+ throw new Error(`Command not allowed: ${baseCommand}`);
87
+ }
88
+
89
+ // Sanitize arguments
90
+ const sanitizedArgs = this.sanitizeArgs(args);
91
+
92
+ // Apply shell restriction from config
93
+ const spawnOptions: SpawnOptions = {
94
+ cwd: options.cwd,
95
+ env: options.env,
96
+ timeout: options.timeout ?? this.config.timeout,
97
+ shell: this.config.shell, // Always use config, ignore user option
98
+ };
99
+
100
+ const result = await this.spawner.spawn(baseCommand, sanitizedArgs, spawnOptions);
101
+
102
+ return {
103
+ stdout: result.stdout,
104
+ stderr: result.stderr,
105
+ exitCode: result.exitCode,
106
+ killed: result.signal !== undefined,
107
+ };
108
+ }
109
+
110
+ isCommandAllowed(command: string): boolean {
111
+ const baseCommand = this.validator.extractCommand(command);
112
+
113
+ // Check blocked commands first
114
+ if (this.config.blockedCommands.includes(baseCommand)) {
115
+ return false;
116
+ }
117
+
118
+ // Check allowed commands
119
+ return this.config.allowedCommands.includes(baseCommand);
120
+ }
121
+
122
+ sanitizeArgs(args: string[]): string[] {
123
+ return args.map(arg => {
124
+ // Remove shell metacharacters
125
+ let sanitized = arg
126
+ .replace(/[;&|`$()]/g, '')
127
+ .replace(/\n/g, '')
128
+ .replace(/\r/g, '');
129
+
130
+ // Remove command substitution
131
+ sanitized = sanitized.replace(/\$\([^)]*\)/g, '');
132
+ sanitized = sanitized.replace(/`[^`]*`/g, '');
133
+
134
+ // Remove redirection
135
+ sanitized = sanitized.replace(/[<>]/g, '');
136
+
137
+ return sanitized;
138
+ });
139
+ }
140
+ }
141
+
142
+ describe('SafeExecutor', () => {
143
+ let mockSpawner: MockedInterface<IProcessSpawner>;
144
+ let mockValidator: MockedInterface<ICommandValidator>;
145
+ let safeExecutor: SafeExecutor;
146
+ const executionConfig = securityConfigs.strict.execution;
147
+
148
+ beforeEach(() => {
149
+ mockSpawner = createMock<IProcessSpawner>();
150
+ mockValidator = createMock<ICommandValidator>();
151
+
152
+ // Configure default mock behavior
153
+ mockValidator.extractCommand.mockImplementation((input: string) => input.split(' ')[0]);
154
+ mockValidator.isBuiltin.mockReturnValue(false);
155
+ mockValidator.resolveCommand.mockResolvedValue('/usr/bin/npm');
156
+
157
+ mockSpawner.spawn.mockResolvedValue({
158
+ pid: 12345,
159
+ stdout: 'command output',
160
+ stderr: '',
161
+ exitCode: 0,
162
+ });
163
+
164
+ safeExecutor = new SafeExecutor(mockSpawner, mockValidator, executionConfig);
165
+ });
166
+
167
+ describe('execute', () => {
168
+ it('should extract base command before execution', async () => {
169
+ // Given
170
+ const command = 'npm install';
171
+
172
+ // When
173
+ await safeExecutor.execute(command);
174
+
175
+ // Then
176
+ expect(mockValidator.extractCommand).toHaveBeenCalledWith(command);
177
+ });
178
+
179
+ it('should spawn process with sanitized arguments', async () => {
180
+ // Given
181
+ const command = 'npm';
182
+ const args = ['install', '--save'];
183
+
184
+ // When
185
+ await safeExecutor.execute(command, args);
186
+
187
+ // Then
188
+ expect(mockSpawner.spawn).toHaveBeenCalledWith(
189
+ 'npm',
190
+ ['install', '--save'],
191
+ expect.any(Object)
192
+ );
193
+ });
194
+
195
+ it('should use config timeout by default', async () => {
196
+ // Given
197
+ const command = 'npm';
198
+
199
+ // When
200
+ await safeExecutor.execute(command);
201
+
202
+ // Then
203
+ expect(mockSpawner.spawn).toHaveBeenCalledWith(
204
+ 'npm',
205
+ [],
206
+ expect.objectContaining({
207
+ timeout: executionConfig.timeout,
208
+ })
209
+ );
210
+ });
211
+
212
+ it('should respect custom timeout option', async () => {
213
+ // Given
214
+ const command = 'npm';
215
+ const customTimeout = 60000;
216
+
217
+ // When
218
+ await safeExecutor.execute(command, [], { timeout: customTimeout });
219
+
220
+ // Then
221
+ expect(mockSpawner.spawn).toHaveBeenCalledWith(
222
+ 'npm',
223
+ [],
224
+ expect.objectContaining({
225
+ timeout: customTimeout,
226
+ })
227
+ );
228
+ });
229
+
230
+ it('should always use shell setting from config', async () => {
231
+ // Given
232
+ const command = 'npm';
233
+
234
+ // When
235
+ await safeExecutor.execute(command, [], { shell: true }); // User tries to enable shell
236
+
237
+ // Then
238
+ expect(mockSpawner.spawn).toHaveBeenCalledWith(
239
+ 'npm',
240
+ [],
241
+ expect.objectContaining({
242
+ shell: executionConfig.shell, // Should be config value (false)
243
+ })
244
+ );
245
+ });
246
+
247
+ it('should return execution result', async () => {
248
+ // Given
249
+ const command = 'npm';
250
+ mockSpawner.spawn.mockResolvedValue({
251
+ pid: 12345,
252
+ stdout: 'success output',
253
+ stderr: 'warning',
254
+ exitCode: 0,
255
+ });
256
+
257
+ // When
258
+ const result = await safeExecutor.execute(command);
259
+
260
+ // Then
261
+ expect(result).toEqual({
262
+ stdout: 'success output',
263
+ stderr: 'warning',
264
+ exitCode: 0,
265
+ killed: false,
266
+ });
267
+ });
268
+
269
+ it('should report killed status when signal present', async () => {
270
+ // Given
271
+ const command = 'npm';
272
+ mockSpawner.spawn.mockResolvedValue({
273
+ pid: 12345,
274
+ stdout: '',
275
+ stderr: 'timeout',
276
+ exitCode: 137,
277
+ signal: 'SIGKILL',
278
+ });
279
+
280
+ // When
281
+ const result = await safeExecutor.execute(command);
282
+
283
+ // Then
284
+ expect(result.killed).toBe(true);
285
+ });
286
+
287
+ it('should throw error for blocked command', async () => {
288
+ // Given
289
+ const command = 'rm';
290
+ mockValidator.extractCommand.mockReturnValue('rm');
291
+
292
+ // When/Then
293
+ await expect(safeExecutor.execute(command)).rejects.toThrow(
294
+ 'Command not allowed: rm'
295
+ );
296
+ expect(mockSpawner.spawn).not.toHaveBeenCalled();
297
+ });
298
+
299
+ it('should throw error for command not in allowed list', async () => {
300
+ // Given
301
+ const command = 'wget';
302
+ mockValidator.extractCommand.mockReturnValue('wget');
303
+
304
+ // When/Then
305
+ await expect(safeExecutor.execute(command)).rejects.toThrow(
306
+ 'Command not allowed: wget'
307
+ );
308
+ });
309
+ });
310
+
311
+ describe('isCommandAllowed', () => {
312
+ it('should allow npm command', () => {
313
+ // Given
314
+ mockValidator.extractCommand.mockReturnValue('npm');
315
+
316
+ // When
317
+ const result = safeExecutor.isCommandAllowed('npm');
318
+
319
+ // Then
320
+ expect(result).toBe(true);
321
+ });
322
+
323
+ it('should allow npx command', () => {
324
+ // Given
325
+ mockValidator.extractCommand.mockReturnValue('npx');
326
+
327
+ // When
328
+ const result = safeExecutor.isCommandAllowed('npx');
329
+
330
+ // Then
331
+ expect(result).toBe(true);
332
+ });
333
+
334
+ it('should allow node command', () => {
335
+ // Given
336
+ mockValidator.extractCommand.mockReturnValue('node');
337
+
338
+ // When
339
+ const result = safeExecutor.isCommandAllowed('node');
340
+
341
+ // Then
342
+ expect(result).toBe(true);
343
+ });
344
+
345
+ it('should allow git command', () => {
346
+ // Given
347
+ mockValidator.extractCommand.mockReturnValue('git');
348
+
349
+ // When
350
+ const result = safeExecutor.isCommandAllowed('git');
351
+
352
+ // Then
353
+ expect(result).toBe(true);
354
+ });
355
+
356
+ it('should block rm command', () => {
357
+ // Given
358
+ mockValidator.extractCommand.mockReturnValue('rm');
359
+
360
+ // When
361
+ const result = safeExecutor.isCommandAllowed('rm');
362
+
363
+ // Then
364
+ expect(result).toBe(false);
365
+ });
366
+
367
+ it('should block del command', () => {
368
+ // Given
369
+ mockValidator.extractCommand.mockReturnValue('del');
370
+
371
+ // When
372
+ const result = safeExecutor.isCommandAllowed('del');
373
+
374
+ // Then
375
+ expect(result).toBe(false);
376
+ });
377
+
378
+ it('should block format command', () => {
379
+ // Given
380
+ mockValidator.extractCommand.mockReturnValue('format');
381
+
382
+ // When
383
+ const result = safeExecutor.isCommandAllowed('format');
384
+
385
+ // Then
386
+ expect(result).toBe(false);
387
+ });
388
+
389
+ it('should block dd command', () => {
390
+ // Given
391
+ mockValidator.extractCommand.mockReturnValue('dd');
392
+
393
+ // When
394
+ const result = safeExecutor.isCommandAllowed('dd');
395
+
396
+ // Then
397
+ expect(result).toBe(false);
398
+ });
399
+
400
+ it('should block blocked command even if in allowed list', () => {
401
+ // Given - hypothetical scenario where command appears in both lists
402
+ // Blocked should take precedence
403
+ mockValidator.extractCommand.mockReturnValue('rm');
404
+
405
+ // When
406
+ const result = safeExecutor.isCommandAllowed('rm');
407
+
408
+ // Then
409
+ expect(result).toBe(false);
410
+ });
411
+ });
412
+
413
+ describe('sanitizeArgs', () => {
414
+ it('should remove semicolon from arguments', () => {
415
+ // Given
416
+ const args = ['install;rm -rf /'];
417
+
418
+ // When
419
+ const result = safeExecutor.sanitizeArgs(args);
420
+
421
+ // Then
422
+ expect(result[0]).not.toContain(';');
423
+ });
424
+
425
+ it('should remove pipe operator from arguments', () => {
426
+ // Given
427
+ const args = ['install | cat /etc/passwd'];
428
+
429
+ // When
430
+ const result = safeExecutor.sanitizeArgs(args);
431
+
432
+ // Then
433
+ expect(result[0]).not.toContain('|');
434
+ });
435
+
436
+ it('should remove ampersand from arguments', () => {
437
+ // Given
438
+ const args = ['install && rm -rf /'];
439
+
440
+ // When
441
+ const result = safeExecutor.sanitizeArgs(args);
442
+
443
+ // Then
444
+ expect(result[0]).not.toContain('&');
445
+ });
446
+
447
+ it('should remove backticks from arguments', () => {
448
+ // Given
449
+ const args = ['install `rm -rf /`'];
450
+
451
+ // When
452
+ const result = safeExecutor.sanitizeArgs(args);
453
+
454
+ // Then
455
+ expect(result[0]).not.toContain('`');
456
+ });
457
+
458
+ it('should remove $() command substitution from arguments', () => {
459
+ // Given
460
+ const args = ['install $(rm -rf /)'];
461
+
462
+ // When
463
+ const result = safeExecutor.sanitizeArgs(args);
464
+
465
+ // Then
466
+ expect(result[0]).not.toContain('$(');
467
+ expect(result[0]).not.toContain(')');
468
+ });
469
+
470
+ it('should remove newlines from arguments', () => {
471
+ // Given
472
+ const args = ['install\nrm -rf /'];
473
+
474
+ // When
475
+ const result = safeExecutor.sanitizeArgs(args);
476
+
477
+ // Then
478
+ expect(result[0]).not.toContain('\n');
479
+ });
480
+
481
+ it('should remove redirection operators from arguments', () => {
482
+ // Given
483
+ const args = ['install > /dev/null', 'test < /etc/passwd'];
484
+
485
+ // When
486
+ const result = safeExecutor.sanitizeArgs(args);
487
+
488
+ // Then
489
+ expect(result[0]).not.toContain('>');
490
+ expect(result[1]).not.toContain('<');
491
+ });
492
+
493
+ it('should preserve safe argument content', () => {
494
+ // Given
495
+ const args = ['--save-dev', 'lodash@4.17.21', './src/index.ts'];
496
+
497
+ // When
498
+ const result = safeExecutor.sanitizeArgs(args);
499
+
500
+ // Then
501
+ expect(result).toEqual(['--save-dev', 'lodash@4.17.21', './src/index.ts']);
502
+ });
503
+
504
+ it('should handle empty arguments array', () => {
505
+ // Given
506
+ const args: string[] = [];
507
+
508
+ // When
509
+ const result = safeExecutor.sanitizeArgs(args);
510
+
511
+ // Then
512
+ expect(result).toEqual([]);
513
+ });
514
+
515
+ it('should sanitize all arguments in array', () => {
516
+ // Given
517
+ const args = ['safe-arg', 'unsafe;arg', 'another|unsafe'];
518
+
519
+ // When
520
+ const result = safeExecutor.sanitizeArgs(args);
521
+
522
+ // Then
523
+ expect(result[0]).toBe('safe-arg');
524
+ expect(result[1]).not.toContain(';');
525
+ expect(result[2]).not.toContain('|');
526
+ });
527
+ });
528
+
529
+ describe('CVE-3 prevention scenarios', () => {
530
+ it('should prevent command injection via arguments', async () => {
531
+ // Given
532
+ const command = 'npm';
533
+ const maliciousArgs = ['install; rm -rf /'];
534
+
535
+ // When
536
+ await safeExecutor.execute(command, maliciousArgs);
537
+
538
+ // Then
539
+ expect(mockSpawner.spawn).toHaveBeenCalledWith(
540
+ 'npm',
541
+ ['install rm -rf /'], // Semicolon removed
542
+ expect.any(Object)
543
+ );
544
+ });
545
+
546
+ it('should prevent command substitution attack', async () => {
547
+ // Given
548
+ const command = 'npm';
549
+ const maliciousArgs = ['$(cat /etc/passwd)'];
550
+
551
+ // When
552
+ await safeExecutor.execute(command, maliciousArgs);
553
+
554
+ // Then
555
+ expect(mockSpawner.spawn).toHaveBeenCalledWith(
556
+ 'npm',
557
+ ['cat /etc/passwd'], // $() removed
558
+ expect.any(Object)
559
+ );
560
+ });
561
+
562
+ it('should prevent shell expansion attack', async () => {
563
+ // Given
564
+ const command = 'npm';
565
+ const maliciousArgs = ['`whoami`'];
566
+
567
+ // When
568
+ await safeExecutor.execute(command, maliciousArgs);
569
+
570
+ // Then
571
+ expect(mockSpawner.spawn).toHaveBeenCalledWith(
572
+ 'npm',
573
+ ['whoami'], // Backticks removed
574
+ expect.any(Object)
575
+ );
576
+ });
577
+
578
+ it('should block dangerous commands directly', async () => {
579
+ // Given
580
+ const dangerousCommands = ['rm', 'dd', 'format', 'del'];
581
+
582
+ // When/Then
583
+ for (const cmd of dangerousCommands) {
584
+ mockValidator.extractCommand.mockReturnValue(cmd);
585
+ await expect(safeExecutor.execute(cmd)).rejects.toThrow(
586
+ `Command not allowed: ${cmd}`
587
+ );
588
+ }
589
+ });
590
+
591
+ it('should prevent shell mode when disabled in config', async () => {
592
+ // Given
593
+ const command = 'npm';
594
+ const args = ['install && echo pwned'];
595
+
596
+ // When
597
+ await safeExecutor.execute(command, args, { shell: true });
598
+
599
+ // Then
600
+ expect(mockSpawner.spawn).toHaveBeenCalledWith(
601
+ 'npm',
602
+ expect.any(Array),
603
+ expect.objectContaining({
604
+ shell: false, // Config overrides user option
605
+ })
606
+ );
607
+ });
608
+ });
609
+
610
+ describe('error handling', () => {
611
+ it('should propagate spawner errors', async () => {
612
+ // Given
613
+ const command = 'npm';
614
+ const spawnError = new Error('Process spawn failed');
615
+ mockSpawner.spawn.mockRejectedValue(spawnError);
616
+
617
+ // When/Then
618
+ await expect(safeExecutor.execute(command)).rejects.toThrow(
619
+ 'Process spawn failed'
620
+ );
621
+ });
622
+
623
+ it('should handle validator errors', async () => {
624
+ // Given
625
+ const command = 'npm';
626
+ mockValidator.extractCommand.mockImplementation(() => {
627
+ throw new Error('Invalid command format');
628
+ });
629
+
630
+ // When/Then
631
+ await expect(safeExecutor.execute(command)).rejects.toThrow(
632
+ 'Invalid command format'
633
+ );
634
+ });
635
+ });
636
+
637
+ describe('interaction verification', () => {
638
+ it('should not spawn if command validation fails', async () => {
639
+ // Given
640
+ const command = 'rm';
641
+ mockValidator.extractCommand.mockReturnValue('rm');
642
+
643
+ // When
644
+ try {
645
+ await safeExecutor.execute(command);
646
+ } catch {
647
+ // Expected
648
+ }
649
+
650
+ // Then
651
+ expect(mockSpawner.spawn).not.toHaveBeenCalled();
652
+ });
653
+
654
+ it('should extract command before checking allowed list', async () => {
655
+ // Given
656
+ const command = 'npm install lodash';
657
+ mockValidator.extractCommand.mockReturnValue('npm');
658
+
659
+ // When
660
+ await safeExecutor.execute(command);
661
+
662
+ // Then
663
+ expect(mockValidator.extractCommand).toHaveBeenCalledWith(command);
664
+ expect(mockValidator.extractCommand).toHaveBeenCalledBefore(mockSpawner.spawn);
665
+ });
666
+ });
667
+ });