codeflow-hook 2.0.1 → 2.0.3

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,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  import { Command } from 'commander';
4
4
  import chalk from 'chalk';
@@ -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 }),
@@ -636,7 +678,7 @@ export class CLIIntegrationService {
636
678
  }
637
679
 
638
680
  /**
639
- * Make HTTP request to backend service with retry logic
681
+ * Make HTTP request to backend service with retry logic and security validation
640
682
  */
641
683
  private async makeBackendRequest(
642
684
  url: string,
@@ -645,21 +687,32 @@ export class CLIIntegrationService {
645
687
  ): Promise<any> {
646
688
  let lastError: any;
647
689
 
690
+ // Validate URL to prevent SSRF attacks
691
+ if (!this.isValidUrl(url)) {
692
+ throw new Error('Invalid URL provided');
693
+ }
694
+
695
+ // Sanitize headers to prevent header injection
696
+ const sanitizedHeaders = this.sanitizeHeaders(headers);
697
+
648
698
  for (let attempt = 1; attempt <= this.config.retries; attempt++) {
649
699
  try {
650
700
  const response = await axios.post(url, data, {
651
701
  headers: {
652
- ...headers,
653
- 'X-Attempt': attempt.toString()
702
+ ...sanitizedHeaders,
703
+ 'X-Attempt': attempt.toString(),
704
+ 'User-Agent': 'Codeflow-CLI-Integration/1.0'
654
705
  },
655
- timeout: this.config.timeout
706
+ timeout: this.config.timeout,
707
+ maxRedirects: 3,
708
+ validateStatus: (status) => status < 500 // Don't throw on 4xx errors
656
709
  });
657
710
 
658
711
  return response;
659
712
  } catch (error) {
660
713
  lastError = error;
661
714
  logger.warn(`Backend request attempt ${attempt} failed`, {
662
- url,
715
+ url: this.sanitizeUrlForLogging(url),
663
716
  attempt,
664
717
  error: this.formatError(error)
665
718
  });
@@ -674,6 +727,58 @@ export class CLIIntegrationService {
674
727
  throw lastError;
675
728
  }
676
729
 
730
+ /**
731
+ * Validate URL to prevent SSRF attacks
732
+ */
733
+ private isValidUrl(url: string): boolean {
734
+ try {
735
+ const urlObj = new URL(url);
736
+
737
+ // Block internal network ranges
738
+ const hostname = urlObj.hostname;
739
+ if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname.startsWith('10.') ||
740
+ hostname.startsWith('192.168.') || hostname.match(/^172\.(1[6-9]|2\d|3[01])\./)) {
741
+ return false;
742
+ }
743
+
744
+ // Only allow HTTP/HTTPS protocols
745
+ return urlObj.protocol === 'http:' || urlObj.protocol === 'https:';
746
+ } catch {
747
+ return false;
748
+ }
749
+ }
750
+
751
+ /**
752
+ * Sanitize headers to prevent header injection
753
+ */
754
+ private sanitizeHeaders(headers: Record<string, string>): Record<string, string> {
755
+ const sanitized: Record<string, string> = {};
756
+
757
+ for (const [key, value] of Object.entries(headers)) {
758
+ // Remove potentially dangerous headers
759
+ if (key.toLowerCase().includes('cookie') || key.toLowerCase().includes('authorization')) {
760
+ continue;
761
+ }
762
+
763
+ // Sanitize header values
764
+ sanitized[key] = value.replace(/[^\x20-\x7E]/g, '');
765
+ }
766
+
767
+ return sanitized;
768
+ }
769
+
770
+ /**
771
+ * Sanitize URL for logging to prevent log injection
772
+ */
773
+ private sanitizeUrlForLogging(url: string): string {
774
+ try {
775
+ const urlObj = new URL(url);
776
+ return `${urlObj.protocol}//${urlObj.hostname}${urlObj.pathname}`;
777
+ } catch {
778
+ return url.replace(/[\r\n]/g, '');
779
+ }
780
+ }
781
+
677
782
  /**
678
783
  * Make GraphQL request to Query Service
679
784
  */
@@ -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,7 +1,7 @@
1
1
 
2
2
  {
3
3
  "name": "codeflow-hook",
4
- "version": "2.0.1",
4
+ "version": "2.0.3",
5
5
  "description": "An interactive CI/CD simulator and lightweight pre-push code reviewer using Gemini AI",
6
6
  "type": "module",
7
7
  "main": "index.js",
@@ -1,128 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * CLI Integration Service - Phase 4
4
- *
5
- * Bridges the local CLI commands with EKG backend services.
6
- * Transforms CLI operations from local processing to backend-driven workflows.
7
- *
8
- * Key transformations:
9
- * - `codeflow index` → EKG Ingestion Service webhook simulation
10
- * - `codeflow analyze-diff` → EKG Query Service context-enhanced analysis
11
- */
12
- /**
13
- * CLI Integration Service
14
- * Provides methods that CLI commands can call to interact with EKG backend
15
- */
16
- export declare class CLIIntegrationService {
17
- private config;
18
- private git;
19
- constructor();
20
- /**
21
- * Index repository for EKG - equivalent to `codeflow index`
22
- *
23
- * Sends repository URL to EKG Ingestion Service for analysis and graph population
24
- */
25
- indexRepository(options?: {
26
- repositoryUrl?: string;
27
- dryRun?: boolean;
28
- }): Promise<{
29
- success: boolean;
30
- repositoryId?: string;
31
- message: string;
32
- stats?: {
33
- indexedFiles: number;
34
- analysisTime: number;
35
- webhookAccepted: boolean;
36
- };
37
- }>;
38
- /**
39
- * Analyze code diff with EKG context enhancement
40
- *
41
- * Sends diff to Query Service for EKG-enhanced analysis instead of local RAG
42
- */
43
- analyzeDiff(diffContent: string, options?: {
44
- legacy?: boolean;
45
- outputFormat?: 'console' | 'json';
46
- }): Promise<{
47
- success: boolean;
48
- analysis: any;
49
- message: string;
50
- stats?: {
51
- ekg_queries: number;
52
- similar_repos_found: number;
53
- analysis_time: number;
54
- };
55
- }>;
56
- /**
57
- * Analyze diff content and extract structured information
58
- */
59
- private analyzeDiffContent;
60
- /**
61
- * Query EKG for context on affected files
62
- */
63
- private getEKGContext;
64
- /**
65
- * Generate enhanced analysis using EKG context
66
- */
67
- private generateEKGEnhancedAnalysis;
68
- /**
69
- * Get current repository information
70
- */
71
- private getRepositoryInfo;
72
- /**
73
- * Get list of files that would be indexed
74
- */
75
- private getIndexableFiles;
76
- /**
77
- * Make HTTP request to backend service with retry logic
78
- */
79
- private makeBackendRequest;
80
- /**
81
- * Make GraphQL request to Query Service
82
- */
83
- private makeGraphQLRequest;
84
- /**
85
- * Generate repository ID (similar to ingestion service)
86
- */
87
- private generateRepositoryId;
88
- /**
89
- * Get current user information
90
- */
91
- private getCurrentUser;
92
- /**
93
- * Detect language from file extension
94
- */
95
- private detectLanguage;
96
- /**
97
- * Format error for logging and display
98
- */
99
- private formatError;
100
- }
101
- export declare const cliIntegrationService: CLIIntegrationService;
102
- export declare const indexProject: (options?: {
103
- repositoryUrl?: string;
104
- dryRun?: boolean;
105
- }) => Promise<{
106
- success: boolean;
107
- repositoryId?: string;
108
- message: string;
109
- stats?: {
110
- indexedFiles: number;
111
- analysisTime: number;
112
- webhookAccepted: boolean;
113
- };
114
- }>;
115
- export declare const analyzeDiff: (diffContent: string, options?: {
116
- legacy?: boolean;
117
- outputFormat?: 'console' | 'json';
118
- }) => Promise<{
119
- success: boolean;
120
- analysis: any;
121
- message: string;
122
- stats?: {
123
- ekg_queries: number;
124
- similar_repos_found: number;
125
- analysis_time: number;
126
- };
127
- }>;
128
- export default cliIntegrationService;