codeslick-cli 1.2.2 → 1.2.4

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 (66) hide show
  1. package/__tests__/threshold-handler.test.ts +175 -0
  2. package/dist/packages/cli/src/commands/scan.d.ts +11 -0
  3. package/dist/packages/cli/src/commands/scan.d.ts.map +1 -1
  4. package/dist/packages/cli/src/commands/scan.js +74 -5
  5. package/dist/packages/cli/src/commands/scan.js.map +1 -1
  6. package/dist/packages/cli/src/config/config-loader.d.ts +11 -0
  7. package/dist/packages/cli/src/config/config-loader.d.ts.map +1 -1
  8. package/dist/packages/cli/src/config/config-loader.js.map +1 -1
  9. package/dist/packages/cli/src/reporters/cli-reporter.d.ts +18 -0
  10. package/dist/packages/cli/src/reporters/cli-reporter.d.ts.map +1 -1
  11. package/dist/packages/cli/src/reporters/cli-reporter.js +115 -0
  12. package/dist/packages/cli/src/reporters/cli-reporter.js.map +1 -1
  13. package/dist/packages/cli/src/utils/test-runner.d.ts +84 -0
  14. package/dist/packages/cli/src/utils/test-runner.d.ts.map +1 -0
  15. package/dist/packages/cli/src/utils/test-runner.js +209 -0
  16. package/dist/packages/cli/src/utils/test-runner.js.map +1 -0
  17. package/dist/packages/cli/src/utils/threshold-handler.d.ts +40 -0
  18. package/dist/packages/cli/src/utils/threshold-handler.d.ts.map +1 -0
  19. package/dist/packages/cli/src/utils/threshold-handler.js +85 -0
  20. package/dist/packages/cli/src/utils/threshold-handler.js.map +1 -0
  21. package/dist/src/lib/analyzers/go-analyzer.d.ts +5 -0
  22. package/dist/src/lib/analyzers/go-analyzer.d.ts.map +1 -1
  23. package/dist/src/lib/analyzers/go-analyzer.js +47 -0
  24. package/dist/src/lib/analyzers/go-analyzer.js.map +1 -1
  25. package/dist/src/lib/analyzers/java-analyzer.d.ts +5 -0
  26. package/dist/src/lib/analyzers/java-analyzer.d.ts.map +1 -1
  27. package/dist/src/lib/analyzers/java-analyzer.js +48 -0
  28. package/dist/src/lib/analyzers/java-analyzer.js.map +1 -1
  29. package/dist/src/lib/analyzers/javascript-analyzer.d.ts +5 -0
  30. package/dist/src/lib/analyzers/javascript-analyzer.d.ts.map +1 -1
  31. package/dist/src/lib/analyzers/javascript-analyzer.js +48 -0
  32. package/dist/src/lib/analyzers/javascript-analyzer.js.map +1 -1
  33. package/dist/src/lib/analyzers/python-analyzer.d.ts +5 -0
  34. package/dist/src/lib/analyzers/python-analyzer.d.ts.map +1 -1
  35. package/dist/src/lib/analyzers/python-analyzer.js +55 -0
  36. package/dist/src/lib/analyzers/python-analyzer.js.map +1 -1
  37. package/dist/src/lib/analyzers/types.d.ts +4 -0
  38. package/dist/src/lib/analyzers/types.d.ts.map +1 -1
  39. package/dist/src/lib/analyzers/typescript-analyzer.d.ts +5 -0
  40. package/dist/src/lib/analyzers/typescript-analyzer.d.ts.map +1 -1
  41. package/dist/src/lib/analyzers/typescript-analyzer.js +48 -0
  42. package/dist/src/lib/analyzers/typescript-analyzer.js.map +1 -1
  43. package/dist/src/lib/github/types.d.ts +112 -0
  44. package/dist/src/lib/github/types.d.ts.map +1 -0
  45. package/dist/src/lib/github/types.js +34 -0
  46. package/dist/src/lib/github/types.js.map +1 -0
  47. package/dist/src/lib/security/epss-service.d.ts +63 -0
  48. package/dist/src/lib/security/epss-service.d.ts.map +1 -0
  49. package/dist/src/lib/security/epss-service.js +256 -0
  50. package/dist/src/lib/security/epss-service.js.map +1 -0
  51. package/dist/src/lib/security/threshold-evaluator.d.ts +73 -0
  52. package/dist/src/lib/security/threshold-evaluator.d.ts.map +1 -0
  53. package/dist/src/lib/security/threshold-evaluator.js +234 -0
  54. package/dist/src/lib/security/threshold-evaluator.js.map +1 -0
  55. package/dist/src/lib/security/triage-service.d.ts +76 -0
  56. package/dist/src/lib/security/triage-service.d.ts.map +1 -0
  57. package/dist/src/lib/security/triage-service.js +318 -0
  58. package/dist/src/lib/security/triage-service.js.map +1 -0
  59. package/dist/src/lib/types/index.d.ts +4 -0
  60. package/dist/src/lib/types/index.d.ts.map +1 -1
  61. package/package.json +1 -1
  62. package/src/commands/scan.ts +100 -7
  63. package/src/config/config-loader.ts +15 -0
  64. package/src/reporters/cli-reporter.ts +132 -0
  65. package/src/utils/test-runner.ts +249 -0
  66. package/src/utils/threshold-handler.ts +99 -0
@@ -731,3 +731,135 @@ export function printBriefSummary(
731
731
  console.log(chalk.gray(` Open: code ${reportPath}`));
732
732
  console.log('');
733
733
  }
734
+
735
+ /**
736
+ * Print test execution start
737
+ * Winter Roadmap WR2: Test Execution Integration
738
+ */
739
+ export function printTestStart(command: string): void {
740
+ console.log('');
741
+ console.log(chalk.bold('Running Tests'));
742
+ console.log(chalk.gray('─'.repeat(50)));
743
+ console.log(chalk.gray(` Command: ${command}`));
744
+ console.log('');
745
+ }
746
+
747
+ /**
748
+ * Print test execution results
749
+ * Winter Roadmap WR2: Test Execution Integration
750
+ */
751
+ export function printTestResult(result: {
752
+ success: boolean;
753
+ exitCode: number;
754
+ duration: number;
755
+ stdout: string;
756
+ stderr: string;
757
+ command: string;
758
+ timedOut: boolean;
759
+ }): void {
760
+ console.log('');
761
+ console.log(chalk.bold('Test Results') + chalk.gray(` (${(result.duration / 1000).toFixed(1)}s)`));
762
+ console.log(chalk.gray('─'.repeat(50)));
763
+
764
+ if (result.timedOut) {
765
+ console.log('');
766
+ console.log(chalk.red.bold(' ✖ TIMEOUT'));
767
+ console.log(chalk.red(` Tests exceeded timeout and were terminated`));
768
+ console.log('');
769
+ return;
770
+ }
771
+
772
+ if (result.success) {
773
+ console.log('');
774
+ console.log(chalk.green.bold(' ✓ PASSED'));
775
+ console.log(chalk.green(` All tests passed successfully`));
776
+ console.log('');
777
+
778
+ // Try to extract test count from output
779
+ const testInfo = extractTestInfo(result.stdout + result.stderr);
780
+ if (testInfo) {
781
+ console.log(chalk.gray(` Tests run: ${testInfo.total}`));
782
+ console.log(chalk.gray(` Passed: ${testInfo.passed}`));
783
+ if (testInfo.failed > 0) {
784
+ console.log(chalk.gray(` Failed: ${testInfo.failed}`));
785
+ }
786
+ console.log('');
787
+ }
788
+ } else {
789
+ console.log('');
790
+ console.log(chalk.red.bold(' ✖ FAILED'));
791
+ console.log(chalk.red(` Tests failed with exit code ${result.exitCode}`));
792
+ console.log('');
793
+
794
+ // Try to extract test count from output
795
+ const testInfo = extractTestInfo(result.stdout + result.stderr);
796
+ if (testInfo) {
797
+ console.log(chalk.gray(` Tests run: ${testInfo.total}`));
798
+ console.log(chalk.green(` Passed: ${testInfo.passed}`));
799
+ console.log(chalk.red(` Failed: ${testInfo.failed}`));
800
+ console.log('');
801
+ }
802
+
803
+ // Show stderr if available (first 500 chars)
804
+ if (result.stderr && result.stderr.length > 0) {
805
+ const errorPreview = result.stderr.substring(0, 500);
806
+ console.log(chalk.gray(' Error output (first 500 chars):'));
807
+ console.log(chalk.gray(' ' + errorPreview.replace(/\n/g, '\n ')));
808
+ if (result.stderr.length > 500) {
809
+ console.log(chalk.gray(' ...'));
810
+ }
811
+ console.log('');
812
+ }
813
+ }
814
+ }
815
+
816
+ /**
817
+ * Extract test count information from test output
818
+ */
819
+ function extractTestInfo(output: string): { total: number; passed: number; failed: number } | null {
820
+ // Jest/Vitest format: "Tests: 5 passed, 5 total"
821
+ const jestMatch = output.match(/Tests:\s+(\d+)\s+passed(?:,\s+(\d+)\s+failed)?.*?(\d+)\s+total/i);
822
+ if (jestMatch) {
823
+ return {
824
+ passed: parseInt(jestMatch[1]),
825
+ failed: parseInt(jestMatch[2] || '0'),
826
+ total: parseInt(jestMatch[3]),
827
+ };
828
+ }
829
+
830
+ // Pytest format: "10 passed in 2.5s" or "5 passed, 2 failed in 1.2s"
831
+ const pytestMatch = output.match(/(\d+)\s+passed(?:,\s+(\d+)\s+failed)?/i);
832
+ if (pytestMatch) {
833
+ const passed = parseInt(pytestMatch[1]);
834
+ const failed = parseInt(pytestMatch[2] || '0');
835
+ return {
836
+ passed,
837
+ failed,
838
+ total: passed + failed,
839
+ };
840
+ }
841
+
842
+ // Go test format: "ok" or "FAIL"
843
+ const goMatch = output.match(/ok.*?(\d+)\s+tests?/i);
844
+ if (goMatch) {
845
+ return {
846
+ passed: parseInt(goMatch[1]),
847
+ failed: 0,
848
+ total: parseInt(goMatch[1]),
849
+ };
850
+ }
851
+
852
+ // Maven format: "Tests run: 10, Failures: 2, Errors: 0"
853
+ const mavenMatch = output.match(/Tests run:\s+(\d+),\s+Failures:\s+(\d+)/i);
854
+ if (mavenMatch) {
855
+ const total = parseInt(mavenMatch[1]);
856
+ const failed = parseInt(mavenMatch[2]);
857
+ return {
858
+ passed: total - failed,
859
+ failed,
860
+ total,
861
+ };
862
+ }
863
+
864
+ return null;
865
+ }
@@ -0,0 +1,249 @@
1
+ /**
2
+ * Test Runner - Execute User Test Suite
3
+ *
4
+ * Runs user's existing test suite (npm test, pytest, go test, etc.)
5
+ * to verify that security scans or fixes don't break functionality.
6
+ *
7
+ * Features:
8
+ * - Detect test command automatically (package.json, pytest, go test)
9
+ * - Custom test command support
10
+ * - Timeout handling
11
+ * - Exit code interpretation
12
+ * - Test output capture
13
+ *
14
+ * Winter Roadmap WR2: Test Execution Integration
15
+ */
16
+
17
+ import { exec } from 'child_process';
18
+ import { promisify } from 'util';
19
+ import { existsSync, readFileSync } from 'fs';
20
+ import { resolve } from 'path';
21
+
22
+ const execAsync = promisify(exec);
23
+
24
+ /**
25
+ * Test execution result
26
+ */
27
+ export interface TestResult {
28
+ success: boolean;
29
+ exitCode: number;
30
+ duration: number; // milliseconds
31
+ stdout: string;
32
+ stderr: string;
33
+ command: string;
34
+ timedOut: boolean;
35
+ }
36
+
37
+ /**
38
+ * Auto-detect test command from project files
39
+ *
40
+ * Detection order:
41
+ * 1. package.json "test" script (npm test)
42
+ * 2. pytest.ini or setup.py (pytest)
43
+ * 3. go.mod (go test ./...)
44
+ * 4. pom.xml (mvn test)
45
+ *
46
+ * @param cwd - Current working directory
47
+ * @returns Detected test command or null
48
+ */
49
+ export function detectTestCommand(cwd: string = process.cwd()): string | null {
50
+ // 1. Check for package.json with test script
51
+ const packageJsonPath = resolve(cwd, 'package.json');
52
+ if (existsSync(packageJsonPath)) {
53
+ try {
54
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
55
+ if (packageJson.scripts?.test) {
56
+ return 'npm test';
57
+ }
58
+ } catch (error) {
59
+ // Invalid package.json, continue
60
+ }
61
+ }
62
+
63
+ // 2. Check for Python test setup
64
+ if (existsSync(resolve(cwd, 'pytest.ini')) ||
65
+ existsSync(resolve(cwd, 'setup.py')) ||
66
+ existsSync(resolve(cwd, 'pyproject.toml'))) {
67
+ return 'pytest';
68
+ }
69
+
70
+ // 3. Check for Go modules
71
+ if (existsSync(resolve(cwd, 'go.mod'))) {
72
+ return 'go test ./...';
73
+ }
74
+
75
+ // 4. Check for Maven/Java
76
+ if (existsSync(resolve(cwd, 'pom.xml'))) {
77
+ return 'mvn test';
78
+ }
79
+
80
+ // 5. Check for Gradle/Java
81
+ if (existsSync(resolve(cwd, 'build.gradle')) ||
82
+ existsSync(resolve(cwd, 'build.gradle.kts'))) {
83
+ return './gradlew test';
84
+ }
85
+
86
+ return null;
87
+ }
88
+
89
+ /**
90
+ * Run user's test suite
91
+ *
92
+ * @param command - Test command to execute (auto-detected if not provided)
93
+ * @param options - Execution options
94
+ * @returns Test execution result
95
+ *
96
+ * @example
97
+ * const result = await runTests('npm test', { timeout: 60000 });
98
+ * if (!result.success) {
99
+ * console.error('Tests failed:', result.stderr);
100
+ * }
101
+ */
102
+ export async function runTests(
103
+ command?: string,
104
+ options: {
105
+ cwd?: string;
106
+ timeout?: number; // milliseconds
107
+ verbose?: boolean;
108
+ } = {}
109
+ ): Promise<TestResult> {
110
+ const cwd = options.cwd || process.cwd();
111
+ const timeout = options.timeout || 300000; // Default: 5 minutes
112
+
113
+ // Auto-detect test command if not provided
114
+ const testCommand = command || detectTestCommand(cwd);
115
+
116
+ if (!testCommand) {
117
+ throw new Error('No test command found. Specify one using --test-command or add it to .codeslick.json');
118
+ }
119
+
120
+ const startTime = Date.now();
121
+ let timedOut = false;
122
+
123
+ try {
124
+ const { stdout, stderr } = await execAsync(testCommand, {
125
+ cwd,
126
+ timeout,
127
+ maxBuffer: 10 * 1024 * 1024, // 10MB buffer for large test output
128
+ env: {
129
+ ...process.env,
130
+ // Disable test coverage to speed up execution (optional)
131
+ NODE_ENV: process.env.NODE_ENV || 'test',
132
+ },
133
+ });
134
+
135
+ const duration = Date.now() - startTime;
136
+
137
+ return {
138
+ success: true,
139
+ exitCode: 0,
140
+ duration,
141
+ stdout: stdout.trim(),
142
+ stderr: stderr.trim(),
143
+ command: testCommand,
144
+ timedOut: false,
145
+ };
146
+ } catch (error: any) {
147
+ const duration = Date.now() - startTime;
148
+
149
+ // Check if timeout occurred
150
+ if (error.killed && error.signal === 'SIGTERM') {
151
+ timedOut = true;
152
+ }
153
+
154
+ return {
155
+ success: false,
156
+ exitCode: error.code || 1,
157
+ duration,
158
+ stdout: error.stdout?.trim() || '',
159
+ stderr: error.stderr?.trim() || '',
160
+ command: testCommand,
161
+ timedOut,
162
+ };
163
+ }
164
+ }
165
+
166
+ /**
167
+ * Format test duration for display
168
+ *
169
+ * @param duration - Duration in milliseconds
170
+ * @returns Formatted duration string (e.g., "2.5s", "1m 30s")
171
+ */
172
+ export function formatDuration(duration: number): string {
173
+ if (duration < 1000) {
174
+ return `${duration}ms`;
175
+ }
176
+
177
+ if (duration < 60000) {
178
+ return `${(duration / 1000).toFixed(1)}s`;
179
+ }
180
+
181
+ const minutes = Math.floor(duration / 60000);
182
+ const seconds = Math.floor((duration % 60000) / 1000);
183
+ return `${minutes}m ${seconds}s`;
184
+ }
185
+
186
+ /**
187
+ * Parse test output to extract test count information
188
+ *
189
+ * Supports common test framework output formats:
190
+ * - Jest/Vitest: "Tests: 10 passed, 10 total"
191
+ * - Pytest: "10 passed in 2.5s"
192
+ * - Go: "ok 10 tests"
193
+ * - Maven: "Tests run: 10, Failures: 0"
194
+ *
195
+ * @param output - Test stdout/stderr
196
+ * @param framework - Test framework (auto-detected if not provided)
197
+ * @returns Parsed test statistics or null
198
+ */
199
+ export function parseTestOutput(output: string, framework?: string): {
200
+ passed: number;
201
+ failed: number;
202
+ total: number;
203
+ } | null {
204
+ // Jest/Vitest format: "Tests: 5 passed, 5 total"
205
+ const jestMatch = output.match(/Tests:\s+(\d+)\s+passed(?:,\s+(\d+)\s+failed)?.*?(\d+)\s+total/i);
206
+ if (jestMatch) {
207
+ return {
208
+ passed: parseInt(jestMatch[1]),
209
+ failed: parseInt(jestMatch[2] || '0'),
210
+ total: parseInt(jestMatch[3]),
211
+ };
212
+ }
213
+
214
+ // Pytest format: "10 passed in 2.5s" or "5 passed, 2 failed in 1.2s"
215
+ const pytestMatch = output.match(/(\d+)\s+passed(?:,\s+(\d+)\s+failed)?/i);
216
+ if (pytestMatch) {
217
+ const passed = parseInt(pytestMatch[1]);
218
+ const failed = parseInt(pytestMatch[2] || '0');
219
+ return {
220
+ passed,
221
+ failed,
222
+ total: passed + failed,
223
+ };
224
+ }
225
+
226
+ // Go test format: "ok" or "FAIL"
227
+ const goMatch = output.match(/ok.*?(\d+)\s+tests?/i);
228
+ if (goMatch) {
229
+ return {
230
+ passed: parseInt(goMatch[1]),
231
+ failed: 0,
232
+ total: parseInt(goMatch[1]),
233
+ };
234
+ }
235
+
236
+ // Maven format: "Tests run: 10, Failures: 2, Errors: 0"
237
+ const mavenMatch = output.match(/Tests run:\s+(\d+),\s+Failures:\s+(\d+)/i);
238
+ if (mavenMatch) {
239
+ const total = parseInt(mavenMatch[1]);
240
+ const failed = parseInt(mavenMatch[2]);
241
+ return {
242
+ passed: total - failed,
243
+ failed,
244
+ total,
245
+ };
246
+ }
247
+
248
+ return null;
249
+ }
@@ -0,0 +1,99 @@
1
+ /**
2
+ * CLI Threshold Handler - WR2 Integration
3
+ *
4
+ * Integrates the comprehensive threshold evaluation system (WR2) into CLI scans.
5
+ * Provides advanced threshold checking beyond simple severity filtering.
6
+ *
7
+ * Features:
8
+ * - Critical/High vulnerability blocking
9
+ * - Maximum vulnerability count limits
10
+ * - EPSS score thresholds
11
+ * - Glob pattern exemptions
12
+ * - Custom failure messages
13
+ *
14
+ * Winter Roadmap WR2: Pass/Fail Thresholds
15
+ */
16
+
17
+ import type { FileScanResult } from '../scanner/local-scanner';
18
+ import {
19
+ evaluateThresholds,
20
+ formatCLIOutput,
21
+ type ThresholdConfig,
22
+ type ThresholdResult,
23
+ } from '../../../../src/lib/security/threshold-evaluator';
24
+ import type { AggregatedResults, FileAnalysis } from '../../../../src/lib/github/types';
25
+
26
+ /**
27
+ * Convert CLI scan results to AggregatedResults format
28
+ *
29
+ * The threshold evaluator expects AggregatedResults (used by GitHub App),
30
+ * but CLI uses FileScanResult. This function bridges the gap.
31
+ */
32
+ export function convertToAggregatedResults(results: FileScanResult[]): AggregatedResults {
33
+ // Calculate totals
34
+ const totalVulnerabilities = results.reduce(
35
+ (sum, r) => sum + r.critical + r.high + r.medium + r.low,
36
+ 0
37
+ );
38
+
39
+ const criticalCount = results.reduce((sum, r) => sum + r.critical, 0);
40
+ const highCount = results.reduce((sum, r) => sum + r.high, 0);
41
+ const mediumCount = results.reduce((sum, r) => sum + r.medium, 0);
42
+ const lowCount = results.reduce((sum, r) => sum + r.low, 0);
43
+
44
+ // Convert FileScanResult[] to FileAnalysis[]
45
+ const fileResults: FileAnalysis[] = results.map((result) => ({
46
+ filename: result.relativePath,
47
+ language: result.language,
48
+ vulnerabilities: result.result.security?.vulnerabilities || [],
49
+ criticalCount: result.critical,
50
+ highCount: result.high,
51
+ mediumCount: result.medium,
52
+ lowCount: result.low,
53
+ syntaxErrors: result.result.syntax?.lineErrors?.length || 0,
54
+ syntaxErrorCount: result.result.syntax?.lineErrors?.filter((e: any) => e.severity === 'error').length || 0,
55
+ syntaxWarningCount: result.result.syntax?.lineErrors?.filter((e: any) => e.severity === 'warning').length || 0,
56
+ syntaxInfoCount: result.result.syntax?.lineErrors?.filter((e: any) => e.severity === 'info').length || 0,
57
+ }));
58
+
59
+ return {
60
+ filesAnalyzed: results.length,
61
+ totalVulnerabilities,
62
+ criticalCount,
63
+ highCount,
64
+ mediumCount,
65
+ lowCount,
66
+ syntaxErrors: fileResults.reduce((sum, f) => sum + f.syntaxErrors, 0),
67
+ syntaxErrorCount: fileResults.reduce((sum, f) => sum + f.syntaxErrorCount, 0),
68
+ syntaxWarningCount: fileResults.reduce((sum, f) => sum + f.syntaxWarningCount, 0),
69
+ syntaxInfoCount: fileResults.reduce((sum, f) => sum + f.syntaxInfoCount, 0),
70
+ fileResults,
71
+ analyzedAt: new Date(),
72
+ prUrl: '', // Not applicable for CLI scans
73
+ };
74
+ }
75
+
76
+ /**
77
+ * Evaluate thresholds for CLI scan results
78
+ *
79
+ * @param results - CLI scan results
80
+ * @param config - Threshold configuration
81
+ * @returns Threshold evaluation result
82
+ */
83
+ export function evaluateCLIThresholds(
84
+ results: FileScanResult[],
85
+ config: ThresholdConfig
86
+ ): ThresholdResult {
87
+ const aggregatedResults = convertToAggregatedResults(results);
88
+ return evaluateThresholds(aggregatedResults, config);
89
+ }
90
+
91
+ /**
92
+ * Print threshold evaluation result to CLI
93
+ *
94
+ * @param result - Threshold evaluation result
95
+ */
96
+ export function printThresholdResult(result: ThresholdResult): void {
97
+ const output = formatCLIOutput(result);
98
+ console.log(output);
99
+ }