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.
@@ -1,14 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * CLI Integration Service - Phase 4
4
+ * CLI Integration Service
5
5
  *
6
- * Bridges the local CLI commands with EKG backend services.
7
- * Transforms CLI operations from local processing to backend-driven workflows.
6
+ * Bridges CLI commands with local git operations.
7
+ * EKG backend integration (Phase 4) removed not deployed.
8
8
  *
9
- * Key transformations:
10
- * - `codeflow index` → EKG Ingestion Service webhook simulation
11
- * - `codeflow analyze-diff` → EKG Query Service context-enhanced analysis
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: process.env.LOG_LEVEL || 'info',
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 with EKG context enhancement
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
- if (options.legacy) {
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
- // Analyze diff and extract context
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 with EKG enhancement', {
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: enhancedAnalysis,
268
- message: 'Diff analyzed with EKG context enhancement',
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: ekgContext.queriesMade,
271
- similar_repos_found: ekgContext.similarRepositories?.length || 0,
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
- ...headers,
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
- * Make GraphQL request to Query Service
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 async makeGraphQLRequest(query: string, variables: any = {}): Promise<any> {
681
- return this.makeBackendRequest(
682
- `${this.config.queryServiceUrl}/graphql`,
683
- { query, variables },
684
- { 'Content-Type': 'application/json' }
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 (similar to ingestion service)
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
- logs.push(`[${new Date().toISOString()}] 🐳 Building Docker image \`${stageConfig.config.imageName || 'app:latest'}\``);
385
- logs.push(`[${new Date().toISOString()}] 📦 Step 1/8 : FROM ${stageConfig.config.baseImage || 'node:18-alpine'}`);
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: `${stageConfig.config.imageName || 'app'}.tar.gz`,
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.1.0",
5
- "description": "An interactive CI/CD simulator and lightweight pre-push code reviewer using Gemini AI",
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": {