codeflow-hook 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/codeflow-hook.js +232 -164
- package/lib/ai-reviewer.cjs +422 -0
- package/lib/cli-integration/src/index.ts +133 -220
- package/lib/cli-integration/src/simulationEngine.ts +118 -4
- package/package.json +2 -2
- package/lib/cli-integration/dist/index.d.ts +0 -128
- package/lib/cli-integration/dist/index.js +0 -585
- package/lib/cli-integration/dist/pipelineConfigs.d.ts +0 -60
- package/lib/cli-integration/dist/pipelineConfigs.js +0 -549
- package/lib/cli-integration/dist/simulationEngine.d.ts +0 -86
- package/lib/cli-integration/dist/simulationEngine.js +0 -475
- package/lib/cli-integration/dist/types.d.ts +0 -156
- package/lib/cli-integration/dist/types.js +0 -15
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* CLI Integration Service
|
|
4
|
+
* CLI Integration Service
|
|
5
5
|
*
|
|
6
|
-
* Bridges
|
|
7
|
-
*
|
|
6
|
+
* Bridges CLI commands with local git operations.
|
|
7
|
+
* EKG backend integration (Phase 4) removed — not deployed.
|
|
8
8
|
*
|
|
9
|
-
* Key
|
|
10
|
-
* - `codeflow index` →
|
|
11
|
-
* - `codeflow analyze-diff` →
|
|
9
|
+
* Key operations:
|
|
10
|
+
* - `codeflow index` → Submit repository for indexing
|
|
11
|
+
* - `codeflow analyze-diff` → Local diff parsing only
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import axios from 'axios';
|
|
@@ -21,9 +21,51 @@ import fs from 'fs';
|
|
|
21
21
|
// Load environment variables
|
|
22
22
|
config();
|
|
23
23
|
|
|
24
|
+
// Validate and sanitize environment variables
|
|
25
|
+
const validateEnvironmentVariables = () => {
|
|
26
|
+
const envVars = {
|
|
27
|
+
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
|
|
28
|
+
INGESTION_SERVICE_URL: process.env.INGESTION_SERVICE_URL || 'http://localhost:3000',
|
|
29
|
+
QUERY_SERVICE_URL: process.env.QUERY_SERVICE_URL || 'http://localhost:4000',
|
|
30
|
+
REQUEST_TIMEOUT: process.env.REQUEST_TIMEOUT || '30000',
|
|
31
|
+
REQUEST_RETRIES: process.env.REQUEST_RETRIES || '3'
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Validate log level
|
|
35
|
+
const validLogLevels = ['error', 'warn', 'info', 'debug'];
|
|
36
|
+
if (!validLogLevels.includes(envVars.LOG_LEVEL)) {
|
|
37
|
+
throw new Error(`Invalid LOG_LEVEL: ${envVars.LOG_LEVEL}. Must be one of: ${validLogLevels.join(', ')}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Validate URLs
|
|
41
|
+
if (!envVars.INGESTION_SERVICE_URL.startsWith('http://') && !envVars.INGESTION_SERVICE_URL.startsWith('https://')) {
|
|
42
|
+
throw new Error(`Invalid INGESTION_SERVICE_URL: ${envVars.INGESTION_SERVICE_URL}. Must start with http:// or https://`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!envVars.QUERY_SERVICE_URL.startsWith('http://') && !envVars.QUERY_SERVICE_URL.startsWith('https://')) {
|
|
46
|
+
throw new Error(`Invalid QUERY_SERVICE_URL: ${envVars.QUERY_SERVICE_URL}. Must start with http:// or https://`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Validate timeout
|
|
50
|
+
const timeout = parseInt(envVars.REQUEST_TIMEOUT, 10);
|
|
51
|
+
if (isNaN(timeout) || timeout <= 0 || timeout > 300000) { // Max 5 minutes
|
|
52
|
+
throw new Error(`Invalid REQUEST_TIMEOUT: ${envVars.REQUEST_TIMEOUT}. Must be between 1 and 300000 milliseconds`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Validate retries
|
|
56
|
+
const retries = parseInt(envVars.REQUEST_RETRIES, 10);
|
|
57
|
+
if (isNaN(retries) || retries <= 0 || retries > 10) {
|
|
58
|
+
throw new Error(`Invalid REQUEST_RETRIES: ${envVars.REQUEST_RETRIES}. Must be between 1 and 10`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return envVars;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const envVars = validateEnvironmentVariables();
|
|
65
|
+
|
|
24
66
|
// Configure logger
|
|
25
67
|
const logger = winston.createLogger({
|
|
26
|
-
level:
|
|
68
|
+
level: envVars.LOG_LEVEL,
|
|
27
69
|
format: winston.format.combine(
|
|
28
70
|
winston.format.timestamp(),
|
|
29
71
|
winston.format.errors({ stack: true }),
|
|
@@ -193,9 +235,8 @@ export class CLIIntegrationService {
|
|
|
193
235
|
}
|
|
194
236
|
|
|
195
237
|
/**
|
|
196
|
-
* Analyze code diff
|
|
197
|
-
*
|
|
198
|
-
* Sends diff to Query Service for EKG-enhanced analysis instead of local RAG
|
|
238
|
+
* Analyze code diff — simplified to local diff parsing only.
|
|
239
|
+
* EKG backend integration removed (Phase 4 not deployed).
|
|
199
240
|
*/
|
|
200
241
|
async analyzeDiff(diffContent: string, options: {
|
|
201
242
|
legacy?: boolean;
|
|
@@ -221,22 +262,12 @@ export class CLIIntegrationService {
|
|
|
221
262
|
};
|
|
222
263
|
}
|
|
223
264
|
|
|
224
|
-
|
|
225
|
-
// Fallback to local analysis (would integrate with existing agents)
|
|
226
|
-
logger.warn('Legacy mode requested - falling back to local analysis');
|
|
227
|
-
return {
|
|
228
|
-
success: false,
|
|
229
|
-
analysis: null,
|
|
230
|
-
message: 'Legacy mode not yet implemented with EKG integration'
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
logger.info('Analyzing diff with EKG context enhancement', {
|
|
265
|
+
logger.info('Analyzing diff (local only — EKG backend not deployed)', {
|
|
235
266
|
diffSize: diffContent.length,
|
|
236
267
|
lines: diffContent.split('\n').length
|
|
237
268
|
});
|
|
238
269
|
|
|
239
|
-
//
|
|
270
|
+
// Parse diff content locally
|
|
240
271
|
const diffAnalysis = this.analyzeDiffContent(diffContent);
|
|
241
272
|
|
|
242
273
|
if (diffAnalysis.files.length === 0) {
|
|
@@ -247,28 +278,30 @@ export class CLIIntegrationService {
|
|
|
247
278
|
};
|
|
248
279
|
}
|
|
249
280
|
|
|
250
|
-
// Query EKG for context on affected files
|
|
251
|
-
const ekgContext = await this.getEKGContext(diffAnalysis);
|
|
252
|
-
|
|
253
|
-
// Generate enhanced analysis with EKG data
|
|
254
|
-
const enhancedAnalysis = await this.generateEKGEnhancedAnalysis(diffAnalysis, ekgContext);
|
|
255
|
-
|
|
256
281
|
const analysisTime = Date.now() - startTime;
|
|
257
282
|
|
|
258
|
-
logger.info('Diff analysis completed
|
|
283
|
+
logger.info('Diff analysis completed', {
|
|
259
284
|
affectedFiles: diffAnalysis.files.length,
|
|
260
|
-
ekgQueries: ekgContext.queriesMade,
|
|
261
|
-
similarReposFound: ekgContext.similarRepositories?.length || 0,
|
|
262
285
|
analysisTime
|
|
263
286
|
});
|
|
264
287
|
|
|
265
288
|
return {
|
|
266
289
|
success: true,
|
|
267
|
-
analysis:
|
|
268
|
-
|
|
290
|
+
analysis: {
|
|
291
|
+
summary: {
|
|
292
|
+
totalFiles: diffAnalysis.files.length,
|
|
293
|
+
totalAdditions: diffAnalysis.totalAdditions,
|
|
294
|
+
totalDeletions: diffAnalysis.totalDeletions,
|
|
295
|
+
ekgEnhanced: false
|
|
296
|
+
},
|
|
297
|
+
files: diffAnalysis.files,
|
|
298
|
+
issues: [],
|
|
299
|
+
recommendations: []
|
|
300
|
+
},
|
|
301
|
+
message: 'Diff analyzed (local analysis only)',
|
|
269
302
|
stats: {
|
|
270
|
-
ekg_queries:
|
|
271
|
-
similar_repos_found:
|
|
303
|
+
ekg_queries: 0,
|
|
304
|
+
similar_repos_found: 0,
|
|
272
305
|
analysis_time: analysisTime
|
|
273
306
|
}
|
|
274
307
|
};
|
|
@@ -368,178 +401,6 @@ export class CLIIntegrationService {
|
|
|
368
401
|
};
|
|
369
402
|
}
|
|
370
403
|
|
|
371
|
-
/**
|
|
372
|
-
* Query EKG for context on affected files
|
|
373
|
-
*/
|
|
374
|
-
private async getEKGContext(diffAnalysis: any): Promise<{
|
|
375
|
-
queriesMade: number;
|
|
376
|
-
repositoryIntelligence?: any;
|
|
377
|
-
similarRepositories: any[];
|
|
378
|
-
patterns: any[];
|
|
379
|
-
}> {
|
|
380
|
-
let queriesMade = 0;
|
|
381
|
-
|
|
382
|
-
try {
|
|
383
|
-
// Get current repository information
|
|
384
|
-
const repoInfo = await this.getRepositoryInfo();
|
|
385
|
-
const repositoryId = this.generateRepositoryId(repoInfo.fullName);
|
|
386
|
-
|
|
387
|
-
// Query repository intelligence if repository exists in EKG
|
|
388
|
-
let repositoryIntelligence = null;
|
|
389
|
-
try {
|
|
390
|
-
const response = await this.makeGraphQLRequest(
|
|
391
|
-
`
|
|
392
|
-
query GetRepositoryIntelligence($repoId: ID!) {
|
|
393
|
-
repositoryIntelligence(repositoryId: $repoId) {
|
|
394
|
-
repository {
|
|
395
|
-
id name fullName language
|
|
396
|
-
}
|
|
397
|
-
patterns {
|
|
398
|
-
name type confidence category
|
|
399
|
-
}
|
|
400
|
-
dependencies {
|
|
401
|
-
dependencyType currentVersion confidence
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
`,
|
|
406
|
-
{ repoId: repositoryId }
|
|
407
|
-
);
|
|
408
|
-
repositoryIntelligence = response.data?.repositoryIntelligence;
|
|
409
|
-
queriesMade++;
|
|
410
|
-
} catch (error) {
|
|
411
|
-
logger.warn('Repository not found in EKG, continuing analysis', { repositoryId });
|
|
412
|
-
}
|
|
413
|
-
|
|
414
|
-
// Find similar repositories and patterns for context
|
|
415
|
-
let similarRepositories: any[] = [];
|
|
416
|
-
try {
|
|
417
|
-
const response = await this.makeGraphQLRequest(
|
|
418
|
-
`
|
|
419
|
-
query FindSimilarRepositories($repoId: ID!, $limit: Int) {
|
|
420
|
-
similarRepositories(repositoryId: $repoId, limit: $limit) {
|
|
421
|
-
repository { name fullName language }
|
|
422
|
-
similarityScore reasons
|
|
423
|
-
sharedPatterns sizeComparison
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
`,
|
|
427
|
-
{ repoId: repositoryId, limit: 5 }
|
|
428
|
-
);
|
|
429
|
-
similarRepositories = response.data?.similarRepositories || [];
|
|
430
|
-
queriesMade++;
|
|
431
|
-
} catch (error) {
|
|
432
|
-
logger.warn('Could not fetch similar repositories', { error: this.formatError(error) });
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// Get enterprise-wide patterns that might be relevant
|
|
436
|
-
let patterns: any[] = [];
|
|
437
|
-
try {
|
|
438
|
-
const languages = [...new Set(diffAnalysis.files.map((f: any) => f.language))];
|
|
439
|
-
|
|
440
|
-
const response = await this.makeGraphQLRequest(
|
|
441
|
-
`
|
|
442
|
-
query GetRelevantPatterns($language: String, $limit: Int) {
|
|
443
|
-
patterns(language: $language, minConfidence: 0.7, limit: $limit) {
|
|
444
|
-
name type category confidence observationCount
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
`,
|
|
448
|
-
{ language: languages[0], limit: 10 }
|
|
449
|
-
);
|
|
450
|
-
patterns = response.data?.patterns || [];
|
|
451
|
-
queriesMade++;
|
|
452
|
-
} catch (error) {
|
|
453
|
-
logger.warn('Could not fetch patterns', { error: this.formatError(error) });
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
return {
|
|
457
|
-
queriesMade,
|
|
458
|
-
repositoryIntelligence,
|
|
459
|
-
similarRepositories,
|
|
460
|
-
patterns
|
|
461
|
-
};
|
|
462
|
-
|
|
463
|
-
} catch (error) {
|
|
464
|
-
logger.error('EKG context retrieval failed', { error: this.formatError(error) });
|
|
465
|
-
return {
|
|
466
|
-
queriesMade,
|
|
467
|
-
similarRepositories: [],
|
|
468
|
-
patterns: []
|
|
469
|
-
};
|
|
470
|
-
}
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
/**
|
|
474
|
-
* Generate enhanced analysis using EKG context
|
|
475
|
-
*/
|
|
476
|
-
private async generateEKGEnhancedAnalysis(diffAnalysis: any, ekgContext: any): Promise<any> {
|
|
477
|
-
// Analyze changes with EKG context
|
|
478
|
-
const issues: any[] = [];
|
|
479
|
-
const recommendations: any[] = [];
|
|
480
|
-
|
|
481
|
-
// Check against existing patterns
|
|
482
|
-
if (ekgContext.patterns && ekgContext.patterns.length > 0) {
|
|
483
|
-
for (const file of diffAnalysis.files) {
|
|
484
|
-
const relevantPatterns = ekgContext.patterns.filter((p: any) =>
|
|
485
|
-
p.type === 'security' || p.type === 'architecture'
|
|
486
|
-
);
|
|
487
|
-
|
|
488
|
-
if (relevantPatterns.length > 0) {
|
|
489
|
-
recommendations.push({
|
|
490
|
-
type: 'ekg_pattern_alignment',
|
|
491
|
-
description: `File ${file.path} modified - consider these established patterns: ${relevantPatterns.map((p: any) => p.name).join(', ')}`,
|
|
492
|
-
severity: 'info',
|
|
493
|
-
file: file.path
|
|
494
|
-
});
|
|
495
|
-
}
|
|
496
|
-
}
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
// Compare against similar repositories
|
|
500
|
-
if (ekgContext.similarRepositories && ekgContext.similarRepositories.length > 0) {
|
|
501
|
-
const similarRepoNames = ekgContext.similarRepositories.map((sr: any) => sr.repository.fullName);
|
|
502
|
-
recommendations.push({
|
|
503
|
-
type: 'similar_repositories',
|
|
504
|
-
description: `Changes similar to patterns seen in: ${similarRepoNames.slice(0, 3).join(', ')}`,
|
|
505
|
-
severity: 'info'
|
|
506
|
-
});
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
// Add repository-specific context if available
|
|
510
|
-
if (ekgContext.repositoryIntelligence) {
|
|
511
|
-
const repo = ekgContext.repositoryIntelligence.repository;
|
|
512
|
-
issues.push({
|
|
513
|
-
type: 'repository_context',
|
|
514
|
-
description: `Analyzing changes in repository ${repo.fullName} (${repo.language})`,
|
|
515
|
-
severity: 'info'
|
|
516
|
-
});
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
return {
|
|
520
|
-
summary: {
|
|
521
|
-
totalFiles: diffAnalysis.files.length,
|
|
522
|
-
totalAdditions: diffAnalysis.totalAdditions,
|
|
523
|
-
totalDeletions: diffAnalysis.totalDeletions,
|
|
524
|
-
ekgEnhanced: true
|
|
525
|
-
},
|
|
526
|
-
files: diffAnalysis.files.map((file: any) => ({
|
|
527
|
-
path: file.path,
|
|
528
|
-
language: file.language,
|
|
529
|
-
additions: file.additions,
|
|
530
|
-
deletions: file.deletions,
|
|
531
|
-
isNew: file.isNew
|
|
532
|
-
})),
|
|
533
|
-
issues,
|
|
534
|
-
recommendations,
|
|
535
|
-
ekg_context: {
|
|
536
|
-
patterns_analyzed: ekgContext.patterns?.length || 0,
|
|
537
|
-
similar_repositories_found: ekgContext.similarRepositories?.length || 0,
|
|
538
|
-
repository_known: !!ekgContext.repositoryIntelligence
|
|
539
|
-
}
|
|
540
|
-
};
|
|
541
|
-
}
|
|
542
|
-
|
|
543
404
|
/**
|
|
544
405
|
* Get current repository information
|
|
545
406
|
*/
|
|
@@ -636,7 +497,7 @@ export class CLIIntegrationService {
|
|
|
636
497
|
}
|
|
637
498
|
|
|
638
499
|
/**
|
|
639
|
-
* Make HTTP request to backend service with retry logic
|
|
500
|
+
* Make HTTP request to backend service with retry logic and security validation
|
|
640
501
|
*/
|
|
641
502
|
private async makeBackendRequest(
|
|
642
503
|
url: string,
|
|
@@ -645,21 +506,32 @@ export class CLIIntegrationService {
|
|
|
645
506
|
): Promise<any> {
|
|
646
507
|
let lastError: any;
|
|
647
508
|
|
|
509
|
+
// Validate URL to prevent SSRF attacks
|
|
510
|
+
if (!this.isValidUrl(url)) {
|
|
511
|
+
throw new Error('Invalid URL provided');
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Sanitize headers to prevent header injection
|
|
515
|
+
const sanitizedHeaders = this.sanitizeHeaders(headers);
|
|
516
|
+
|
|
648
517
|
for (let attempt = 1; attempt <= this.config.retries; attempt++) {
|
|
649
518
|
try {
|
|
650
519
|
const response = await axios.post(url, data, {
|
|
651
520
|
headers: {
|
|
652
|
-
...
|
|
653
|
-
'X-Attempt': attempt.toString()
|
|
521
|
+
...sanitizedHeaders,
|
|
522
|
+
'X-Attempt': attempt.toString(),
|
|
523
|
+
'User-Agent': 'Codeflow-CLI-Integration/1.0'
|
|
654
524
|
},
|
|
655
|
-
timeout: this.config.timeout
|
|
525
|
+
timeout: this.config.timeout,
|
|
526
|
+
maxRedirects: 3,
|
|
527
|
+
validateStatus: (status) => status < 500 // Don't throw on 4xx errors
|
|
656
528
|
});
|
|
657
529
|
|
|
658
530
|
return response;
|
|
659
531
|
} catch (error) {
|
|
660
532
|
lastError = error;
|
|
661
533
|
logger.warn(`Backend request attempt ${attempt} failed`, {
|
|
662
|
-
url,
|
|
534
|
+
url: this.sanitizeUrlForLogging(url),
|
|
663
535
|
attempt,
|
|
664
536
|
error: this.formatError(error)
|
|
665
537
|
});
|
|
@@ -675,18 +547,59 @@ export class CLIIntegrationService {
|
|
|
675
547
|
}
|
|
676
548
|
|
|
677
549
|
/**
|
|
678
|
-
*
|
|
550
|
+
* Validate URL to prevent SSRF attacks
|
|
551
|
+
*/
|
|
552
|
+
private isValidUrl(url: string): boolean {
|
|
553
|
+
try {
|
|
554
|
+
const urlObj = new URL(url);
|
|
555
|
+
|
|
556
|
+
// Block internal network ranges
|
|
557
|
+
const hostname = urlObj.hostname;
|
|
558
|
+
if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname.startsWith('10.') ||
|
|
559
|
+
hostname.startsWith('192.168.') || hostname.match(/^172\.(1[6-9]|2\d|3[01])\./)) {
|
|
560
|
+
return false;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// Only allow HTTP/HTTPS protocols
|
|
564
|
+
return urlObj.protocol === 'http:' || urlObj.protocol === 'https:';
|
|
565
|
+
} catch {
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Sanitize headers to prevent header injection
|
|
572
|
+
*/
|
|
573
|
+
private sanitizeHeaders(headers: Record<string, string>): Record<string, string> {
|
|
574
|
+
const sanitized: Record<string, string> = {};
|
|
575
|
+
|
|
576
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
577
|
+
// Remove potentially dangerous headers
|
|
578
|
+
if (key.toLowerCase().includes('cookie') || key.toLowerCase().includes('authorization')) {
|
|
579
|
+
continue;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Sanitize header values
|
|
583
|
+
sanitized[key] = value.replace(/[^\x20-\x7E]/g, '');
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
return sanitized;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
/**
|
|
590
|
+
* Sanitize URL for logging to prevent log injection
|
|
679
591
|
*/
|
|
680
|
-
private
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
{
|
|
684
|
-
|
|
685
|
-
|
|
592
|
+
private sanitizeUrlForLogging(url: string): string {
|
|
593
|
+
try {
|
|
594
|
+
const urlObj = new URL(url);
|
|
595
|
+
return `${urlObj.protocol}//${urlObj.hostname}${urlObj.pathname}`;
|
|
596
|
+
} catch {
|
|
597
|
+
return url.replace(/[\r\n]/g, '');
|
|
598
|
+
}
|
|
686
599
|
}
|
|
687
600
|
|
|
688
601
|
/**
|
|
689
|
-
* Generate repository ID
|
|
602
|
+
* Generate repository ID
|
|
690
603
|
*/
|
|
691
604
|
private generateRepositoryId(fullName: string): string {
|
|
692
605
|
return `${fullName.replace('/', '-')}-${Date.now().toString(36)}`;
|
|
@@ -26,6 +26,10 @@ export class SimulationEngine {
|
|
|
26
26
|
const executionId = this.generateExecutionId();
|
|
27
27
|
const startTime = Date.now();
|
|
28
28
|
|
|
29
|
+
// Validate configuration before execution
|
|
30
|
+
this.validatePipelineConfig(config);
|
|
31
|
+
this.validateEnvironmentVariables();
|
|
32
|
+
|
|
29
33
|
this.context = {
|
|
30
34
|
pipelineId: config.id,
|
|
31
35
|
executionId,
|
|
@@ -105,6 +109,103 @@ export class SimulationEngine {
|
|
|
105
109
|
}
|
|
106
110
|
}
|
|
107
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Validate environment variables for security
|
|
114
|
+
*/
|
|
115
|
+
private validateEnvironmentVariables(): void {
|
|
116
|
+
// Validate environment variables that might affect simulation
|
|
117
|
+
const envVars = {
|
|
118
|
+
NODE_ENV: process.env.NODE_ENV || 'development',
|
|
119
|
+
LOG_LEVEL: process.env.LOG_LEVEL || 'info',
|
|
120
|
+
SIMULATION_TIMEOUT: process.env.SIMULATION_TIMEOUT || '300000', // 5 minutes default
|
|
121
|
+
MAX_CONCURRENCY: process.env.MAX_CONCURRENCY || '5'
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// Validate NODE_ENV
|
|
125
|
+
const validNodeEnvs = ['development', 'test', 'production'];
|
|
126
|
+
if (!validNodeEnvs.includes(envVars.NODE_ENV)) {
|
|
127
|
+
throw new Error(`Invalid NODE_ENV: ${envVars.NODE_ENV}. Must be one of: ${validNodeEnvs.join(', ')}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Validate LOG_LEVEL
|
|
131
|
+
const validLogLevels = ['error', 'warn', 'info', 'debug'];
|
|
132
|
+
if (!validLogLevels.includes(envVars.LOG_LEVEL)) {
|
|
133
|
+
throw new Error(`Invalid LOG_LEVEL: ${envVars.LOG_LEVEL}. Must be one of: ${validLogLevels.join(', ')}`);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Validate simulation timeout
|
|
137
|
+
const timeout = parseInt(envVars.SIMULATION_TIMEOUT, 10);
|
|
138
|
+
if (isNaN(timeout) || timeout <= 0 || timeout > 3600000) { // Max 1 hour
|
|
139
|
+
throw new Error(`Invalid SIMULATION_TIMEOUT: ${envVars.SIMULATION_TIMEOUT}. Must be between 1 and 3600000 milliseconds`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Validate max concurrency
|
|
143
|
+
const concurrency = parseInt(envVars.MAX_CONCURRENCY, 10);
|
|
144
|
+
if (isNaN(concurrency) || concurrency <= 0 || concurrency > 20) {
|
|
145
|
+
throw new Error(`Invalid MAX_CONCURRENCY: ${envVars.MAX_CONCURRENCY}. Must be between 1 and 20`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Validate pipeline configuration for security and correctness
|
|
151
|
+
*/
|
|
152
|
+
private validatePipelineConfig(config: PipelineConfig): void {
|
|
153
|
+
// Validate pipeline ID format
|
|
154
|
+
if (!config.id || !/^[a-z0-9-]+$/.test(config.id)) {
|
|
155
|
+
throw new Error('Invalid pipeline ID format - must contain only lowercase letters, numbers, and hyphens');
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Validate stage configurations
|
|
159
|
+
if (!config.stages || config.stages.length === 0) {
|
|
160
|
+
throw new Error('Pipeline must contain at least one stage');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Validate each stage
|
|
164
|
+
const stageIds = new Set<string>();
|
|
165
|
+
for (const stage of config.stages) {
|
|
166
|
+
// Check for duplicate stage IDs
|
|
167
|
+
if (stageIds.has(stage.id)) {
|
|
168
|
+
throw new Error(`Duplicate stage ID found: ${stage.id}`);
|
|
169
|
+
}
|
|
170
|
+
stageIds.add(stage.id);
|
|
171
|
+
|
|
172
|
+
// Validate stage ID format
|
|
173
|
+
if (!/^[a-z0-9-]+$/.test(stage.id)) {
|
|
174
|
+
throw new Error(`Invalid stage ID format: ${stage.id}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Validate dependencies don't create cycles (basic check)
|
|
178
|
+
if (stage.dependencies.includes(stage.id)) {
|
|
179
|
+
throw new Error(`Stage ${stage.id} cannot depend on itself`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Validate timeout values
|
|
183
|
+
if (stage.timeout <= 0 || stage.timeout > 7200000) { // Max 2 hours
|
|
184
|
+
throw new Error(`Invalid timeout for stage ${stage.id}: must be between 1ms and 2 hours`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Validate success rate
|
|
188
|
+
if (stage.successRate < 0 || stage.successRate > 1) {
|
|
189
|
+
throw new Error(`Invalid success rate for stage ${stage.id}: must be between 0 and 1`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Validate duration range
|
|
193
|
+
if (stage.durationRange.min <= 0 || stage.durationRange.max <= 0 ||
|
|
194
|
+
stage.durationRange.min > stage.durationRange.max) {
|
|
195
|
+
throw new Error(`Invalid duration range for stage ${stage.id}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Validate pipeline settings
|
|
200
|
+
if (config.settings.maxConcurrency <= 0 || config.settings.maxConcurrency > 10) {
|
|
201
|
+
throw new Error('Invalid maxConcurrency setting: must be between 1 and 10');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (config.settings.timeout <= 0 || config.settings.timeout > 86400000) { // Max 24 hours
|
|
205
|
+
throw new Error('Invalid pipeline timeout: must be between 1ms and 24 hours');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
108
209
|
/**
|
|
109
210
|
* Execute stages respecting dependencies and concurrency limits
|
|
110
211
|
*/
|
|
@@ -381,18 +482,23 @@ export class SimulationEngine {
|
|
|
381
482
|
): { success: boolean; logs: string[]; metrics: StageMetrics; errors?: ErrorInfo[] } {
|
|
382
483
|
const logs: string[] = [];
|
|
383
484
|
|
|
384
|
-
|
|
385
|
-
|
|
485
|
+
// Sanitize input to prevent command injection
|
|
486
|
+
const imageName = this.sanitizeInput(stageConfig.config.imageName || 'app:latest');
|
|
487
|
+
const baseImage = this.sanitizeInput(stageConfig.config.baseImage || 'node:18-alpine');
|
|
488
|
+
|
|
489
|
+
logs.push(`[${new Date().toISOString()}] 🐳 Building Docker image \`${imageName}\``);
|
|
490
|
+
logs.push(`[${new Date().toISOString()}] 📦 Step 1/8 : FROM ${baseImage}`);
|
|
386
491
|
|
|
387
492
|
if (success) {
|
|
388
493
|
logs.push(`[${new Date().toISOString()}] 📦 Step 8/8 : CMD ["npm", "start"]`);
|
|
389
494
|
logs.push(`[${new Date().toISOString()}] ✅ Successfully built ${stageConfig.config.imageId || 'a1b2c3d4e5f6'}`);
|
|
390
495
|
logs.push(`[${new Date().toISOString()}] 🔍 Image size: ${50 + Math.random() * 200}MB`);
|
|
391
496
|
|
|
392
|
-
// Create artifact
|
|
497
|
+
// Create artifact with sanitized data
|
|
393
498
|
if (this.context) {
|
|
499
|
+
const sanitizedImageName = this.sanitizeInput(stageConfig.config.imageName || 'app');
|
|
394
500
|
this.context.artifacts.set(`${stageConfig.id}-image`, {
|
|
395
|
-
name: `${
|
|
501
|
+
name: `${sanitizedImageName}.tar.gz`,
|
|
396
502
|
type: 'docker-image',
|
|
397
503
|
size: Math.round(50 + Math.random() * 200) * 1024 * 1024,
|
|
398
504
|
path: `/artifacts/${stageConfig.id}`,
|
|
@@ -406,6 +512,14 @@ export class SimulationEngine {
|
|
|
406
512
|
return { success, logs, metrics };
|
|
407
513
|
}
|
|
408
514
|
|
|
515
|
+
/**
|
|
516
|
+
* Sanitize input to prevent injection attacks
|
|
517
|
+
*/
|
|
518
|
+
private sanitizeInput(input: string): string {
|
|
519
|
+
// Remove potentially dangerous characters
|
|
520
|
+
return input.replace(/[^\w\-\.\/:]/g, '');
|
|
521
|
+
}
|
|
522
|
+
|
|
409
523
|
/**
|
|
410
524
|
* Simulate unit test stage
|
|
411
525
|
*/
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
|
|
2
2
|
{
|
|
3
3
|
"name": "codeflow-hook",
|
|
4
|
-
"version": "2.
|
|
5
|
-
"description": "
|
|
4
|
+
"version": "2.2.0",
|
|
5
|
+
"description": "Local AI-powered code analysis and pre-push review for development workflows",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "index.js",
|
|
8
8
|
"bin": {
|