codeant-cli 0.2.3 → 0.2.5
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 +0 -45
- package/dist/178.index.js +1 -1
- package/dist/239.index.js +1 -1
- package/dist/701.index.js +1 -1
- package/dist/880.index.js +1 -1
- package/dist/935.index.js +1 -1
- package/dist/966.index.js +1 -1
- package/dist/index.js +4 -4
- package/package.json +1 -1
- package/src/commands/secrets.js +40 -160
- package/src/components/SecretsUI.js +366 -0
- package/src/index.js +23 -93
- package/src/utils/secretsApiHelper.js +109 -1
- package/src/commands/securityAnalysis.js +0 -284
- package/src/commands/staticAnalysis.js +0 -405
- package/src/utils/securityAnalysisApiHelper.js +0 -119
- package/src/utils/staticAnalysisApiHelper.js +0 -119
package/src/index.js
CHANGED
|
@@ -9,8 +9,6 @@ import SetBaseUrl from './commands/setBaseUrl.js';
|
|
|
9
9
|
import GetBaseUrl from './commands/getBaseUrl.js';
|
|
10
10
|
import Login from './commands/login.js';
|
|
11
11
|
import Logout from './commands/logout.js';
|
|
12
|
-
import StaticAnalysis from './commands/staticAnalysis.js';
|
|
13
|
-
import SecurityAnalysis from './commands/securityAnalysis.js';
|
|
14
12
|
import Review from './commands/review.js';
|
|
15
13
|
import Welcome from './components/Welcome.js';
|
|
16
14
|
import * as scm from './scm/index.js';
|
|
@@ -51,67 +49,39 @@ if (process.argv.length === 2) {
|
|
|
51
49
|
program
|
|
52
50
|
.command('secrets')
|
|
53
51
|
.description('Scan for secrets in your code')
|
|
54
|
-
.option('--
|
|
55
|
-
.option('--
|
|
56
|
-
.option('--uncommitted', 'Scan uncommitted changes')
|
|
57
|
-
.option('--
|
|
58
|
-
.option('--last-n-commits <n>', 'Scan last n commits (max 5)', parseInt)
|
|
59
|
-
.option('--fail-on <level>', 'Fail only on HIGH, MEDIUM, or all (default: HIGH)', 'HIGH')
|
|
60
|
-
.option('--include <paths>', 'Comma-separated list of file paths regex to include')
|
|
61
|
-
.option('--exclude <paths>', 'Comma-separated list of file paths regex to exclude')
|
|
62
|
-
.action((options) => {
|
|
63
|
-
let scanType = 'staged-only';
|
|
64
|
-
let lastNCommits = 1;
|
|
65
|
-
|
|
66
|
-
// Check !== undefined, not truthy — passing 0 is valid and shouldn't fall through
|
|
67
|
-
if (options.lastNCommits !== undefined) {
|
|
68
|
-
scanType = 'last-n-commits';
|
|
69
|
-
lastNCommits = Math.min(Math.max(1, options.lastNCommits), 5);
|
|
70
|
-
} else if (options.all) {
|
|
71
|
-
scanType = 'branch-diff';
|
|
72
|
-
} else if (options.uncommitted) {
|
|
73
|
-
scanType = 'uncommitted';
|
|
74
|
-
} else if (options.lastCommit) {
|
|
75
|
-
scanType = 'last-commit';
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const include = options.include
|
|
79
|
-
? (Array.isArray(options.include) ? options.include : splitGlobs(options.include))
|
|
80
|
-
: [];
|
|
81
|
-
|
|
82
|
-
const exclude = options.exclude
|
|
83
|
-
? (Array.isArray(options.exclude) ? options.exclude : splitGlobs(options.exclude))
|
|
84
|
-
: [];
|
|
85
|
-
|
|
86
|
-
const failOn = options.failOn?.toUpperCase() || 'HIGH';
|
|
87
|
-
|
|
88
|
-
render(React.createElement(Secrets, { scanType, failOn, include, exclude, lastNCommits }));
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
program
|
|
92
|
-
.command('static-analysis')
|
|
93
|
-
.description('Run static analysis on your code')
|
|
94
|
-
.option('--staged', 'Scan only staged files (default)')
|
|
95
|
-
.option('--all', 'Scan all changed files')
|
|
96
|
-
.option('--uncommitted', 'Scan uncommitted changes')
|
|
52
|
+
.option('--all', 'Scan committed + uncommitted changes (default)')
|
|
53
|
+
.option('--committed', 'Scan only unpushed commits')
|
|
54
|
+
.option('--uncommitted', 'Scan only uncommitted changes (staged + unstaged + untracked)')
|
|
55
|
+
.option('--staged', 'Scan only staged files')
|
|
97
56
|
.option('--last-commit', 'Scan last commit')
|
|
98
57
|
.option('--last-n-commits <n>', 'Scan last n commits (max 5)', parseInt)
|
|
58
|
+
.option('--base <branch>', 'Compare against a specific base branch (e.g. --base develop)')
|
|
59
|
+
.option('--base-commit <commit>', 'Compare against a specific commit (e.g. --base-commit HEAD~3)')
|
|
99
60
|
.option('--fail-on <level>', 'Fail on issues at or above this level: BLOCKER, CRITICAL, MAJOR, MINOR, INFO (default: CRITICAL)', 'CRITICAL')
|
|
100
|
-
.option('--
|
|
101
|
-
.option('--
|
|
102
|
-
.option('--exclude <paths>', 'Comma-separated list of file paths regex to exclude')
|
|
61
|
+
.option('--include <paths>', 'Comma-separated list of file paths glob patterns to include')
|
|
62
|
+
.option('--exclude <paths>', 'Comma-separated list of file paths glob patterns to exclude')
|
|
103
63
|
.action((options) => {
|
|
104
|
-
let scanType = '
|
|
64
|
+
let scanType = 'all';
|
|
105
65
|
let lastNCommits = 1;
|
|
66
|
+
let baseBranch = null;
|
|
67
|
+
let baseCommit = null;
|
|
106
68
|
|
|
69
|
+
if (options.base) {
|
|
70
|
+
scanType = 'base-branch';
|
|
71
|
+
baseBranch = options.base;
|
|
72
|
+
} else if (options.baseCommit) {
|
|
73
|
+
scanType = 'base-commit';
|
|
74
|
+
baseCommit = options.baseCommit;
|
|
107
75
|
// Check !== undefined, not truthy — passing 0 is valid and shouldn't fall through
|
|
108
|
-
if (options.lastNCommits !== undefined) {
|
|
76
|
+
} else if (options.lastNCommits !== undefined) {
|
|
109
77
|
scanType = 'last-n-commits';
|
|
110
78
|
lastNCommits = Math.min(Math.max(1, options.lastNCommits), 5);
|
|
111
|
-
} else if (options.
|
|
112
|
-
scanType = '
|
|
79
|
+
} else if (options.committed) {
|
|
80
|
+
scanType = 'committed';
|
|
113
81
|
} else if (options.uncommitted) {
|
|
114
82
|
scanType = 'uncommitted';
|
|
83
|
+
} else if (options.staged) {
|
|
84
|
+
scanType = 'staged-only';
|
|
115
85
|
} else if (options.lastCommit) {
|
|
116
86
|
scanType = 'last-commit';
|
|
117
87
|
}
|
|
@@ -125,48 +95,8 @@ program
|
|
|
125
95
|
: [];
|
|
126
96
|
|
|
127
97
|
const failOn = options.failOn?.toUpperCase() || 'CRITICAL';
|
|
128
|
-
const autoFix = options.autoFix || false;
|
|
129
|
-
render(React.createElement(StaticAnalysis, {scanType, failOn, include, exclude, autoFix, lastNCommits}));
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
program
|
|
133
|
-
.command('security-analysis')
|
|
134
|
-
.description('Run security analysis on your code')
|
|
135
|
-
.option('--staged', 'Scan only staged files (default)')
|
|
136
|
-
.option('--all', 'Scan all changed files')
|
|
137
|
-
.option('--uncommitted', 'Scan uncommitted changes')
|
|
138
|
-
.option('--last-commit', 'Scan last commit')
|
|
139
|
-
.option('--last-n-commits <n>', 'Scan last n commits (max 5)', parseInt)
|
|
140
|
-
.option('--fail-on <level>', 'Fail on issues at or above this level: BLOCKER, CRITICAL, MAJOR, MINOR, INFO (default: CRITICAL)', 'CRITICAL')
|
|
141
|
-
.option('--include <paths>', 'Comma-separated list of file paths regex to include')
|
|
142
|
-
.option('--exclude <paths>', 'Comma-separated list of file paths regex to exclude')
|
|
143
|
-
.action((options) => {
|
|
144
|
-
let scanType = 'staged-only';
|
|
145
|
-
let lastNCommits = 1;
|
|
146
|
-
|
|
147
|
-
// Check !== undefined, not truthy — passing 0 is valid and shouldn't fall through
|
|
148
|
-
if (options.lastNCommits !== undefined) {
|
|
149
|
-
scanType = 'last-n-commits';
|
|
150
|
-
lastNCommits = Math.min(Math.max(1, options.lastNCommits), 5);
|
|
151
|
-
} else if (options.all) {
|
|
152
|
-
scanType = 'branch-diff';
|
|
153
|
-
} else if (options.uncommitted) {
|
|
154
|
-
scanType = 'uncommitted';
|
|
155
|
-
} else if (options.lastCommit) {
|
|
156
|
-
scanType = 'last-commit';
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const include = options.include
|
|
160
|
-
? (Array.isArray(options.include) ? options.include : splitGlobs(options.include))
|
|
161
|
-
: [];
|
|
162
|
-
|
|
163
|
-
const exclude = options.exclude
|
|
164
|
-
? (Array.isArray(options.exclude) ? options.exclude : splitGlobs(options.exclude))
|
|
165
|
-
: [];
|
|
166
|
-
|
|
167
|
-
const failOn = options.failOn?.toUpperCase() || 'HIGH';
|
|
168
98
|
|
|
169
|
-
render(React.createElement(
|
|
99
|
+
render(React.createElement(Secrets, { scanType, failOn, include, exclude, lastNCommits, baseBranch, baseCommit }));
|
|
170
100
|
});
|
|
171
101
|
|
|
172
102
|
program
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import CommonApiHelper from './commonApiHelper.js';
|
|
2
|
+
import path from 'path';
|
|
2
3
|
import { minimatch } from 'minimatch';
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -15,6 +16,58 @@ import { minimatch } from 'minimatch';
|
|
|
15
16
|
* ]
|
|
16
17
|
* }
|
|
17
18
|
*/
|
|
19
|
+
|
|
20
|
+
const EXCLUDED_FILENAMES = new Set([
|
|
21
|
+
'package-lock.json',
|
|
22
|
+
'yarn.lock',
|
|
23
|
+
'pnpm-lock.yaml',
|
|
24
|
+
'composer.lock',
|
|
25
|
+
'Gemfile.lock',
|
|
26
|
+
'Cargo.lock',
|
|
27
|
+
'poetry.lock',
|
|
28
|
+
'Pipfile.lock',
|
|
29
|
+
'go.sum',
|
|
30
|
+
'.DS_Store',
|
|
31
|
+
'Thumbs.db',
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
const EXCLUDED_EXTENSIONS = new Set([
|
|
35
|
+
// Images
|
|
36
|
+
'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.svg', '.webp', '.avif',
|
|
37
|
+
// Fonts
|
|
38
|
+
'.woff', '.woff2', '.ttf', '.eot', '.otf',
|
|
39
|
+
// Binary / compiled
|
|
40
|
+
'.exe', '.dll', '.so', '.dylib', '.bin', '.o', '.a', '.class', '.pyc', '.pyo',
|
|
41
|
+
'.wasm', '.jar', '.war', '.ear',
|
|
42
|
+
// Archives
|
|
43
|
+
'.zip', '.tar', '.gz', '.bz2', '.7z', '.rar',
|
|
44
|
+
// Media
|
|
45
|
+
'.mp3', '.mp4', '.wav', '.avi', '.mov', '.flv', '.wmv', '.ogg', '.webm',
|
|
46
|
+
// Data / generated
|
|
47
|
+
'.min.js', '.min.css', '.map',
|
|
48
|
+
'.pb', '.proto.bin',
|
|
49
|
+
// PDF / docs
|
|
50
|
+
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
|
|
51
|
+
]);
|
|
52
|
+
|
|
53
|
+
const MAX_FILE_LINES = 5000;
|
|
54
|
+
|
|
55
|
+
function isScannableFile(filePath) {
|
|
56
|
+
const basename = path.basename(filePath);
|
|
57
|
+
|
|
58
|
+
if (EXCLUDED_FILENAMES.has(basename)) return false;
|
|
59
|
+
|
|
60
|
+
const ext = path.extname(basename).toLowerCase();
|
|
61
|
+
if (EXCLUDED_EXTENSIONS.has(ext)) return false;
|
|
62
|
+
|
|
63
|
+
if (basename.endsWith('.min.js') || basename.endsWith('.min.css')) return false;
|
|
64
|
+
|
|
65
|
+
// Skip markdown files
|
|
66
|
+
if (ext === '.md') return false;
|
|
67
|
+
|
|
68
|
+
return true;
|
|
69
|
+
}
|
|
70
|
+
|
|
18
71
|
class SecretsApiHelper extends CommonApiHelper {
|
|
19
72
|
/**
|
|
20
73
|
* Transform diff info array to API format
|
|
@@ -124,10 +177,65 @@ class SecretsApiHelper extends CommonApiHelper {
|
|
|
124
177
|
files = [];
|
|
125
178
|
}
|
|
126
179
|
|
|
180
|
+
const totalChanged = files.length;
|
|
181
|
+
const skipped = [];
|
|
182
|
+
|
|
183
|
+
// Filter out non-scannable files (binary, lock files, markdown, etc.)
|
|
184
|
+
files = files.filter(fileObj => {
|
|
185
|
+
const filePath = fileObj.file_path;
|
|
186
|
+
|
|
187
|
+
if (!isScannableFile(filePath)) {
|
|
188
|
+
skipped.push({ file: filePath, reason: 'binary/generated' });
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Skip test files — backend filters these out anyway
|
|
193
|
+
if (filePath.toLowerCase().includes('test')) {
|
|
194
|
+
skipped.push({ file: filePath, reason: 'test file' });
|
|
195
|
+
return false;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Skip deleted files (empty code)
|
|
199
|
+
if (fileObj.code === '' || fileObj.code === undefined) {
|
|
200
|
+
skipped.push({ file: filePath, reason: 'deleted' });
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Skip files exceeding line limit
|
|
205
|
+
if (fileObj.code) {
|
|
206
|
+
const lineCount = fileObj.code.split('\n').length;
|
|
207
|
+
if (lineCount > MAX_FILE_LINES) {
|
|
208
|
+
skipped.push({ file: filePath, reason: `too large (${lineCount} lines, max ${MAX_FILE_LINES})` });
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return true;
|
|
214
|
+
});
|
|
215
|
+
|
|
127
216
|
// Apply include/exclude filters
|
|
217
|
+
const beforePatternFilter = files.length;
|
|
128
218
|
files = this._filterFiles(files, includePatterns, excludePatterns);
|
|
129
219
|
|
|
130
|
-
|
|
220
|
+
// Track pattern-excluded files
|
|
221
|
+
if (files.length < beforePatternFilter) {
|
|
222
|
+
// We don't have exact file names for pattern-excluded, but the count is in skipped
|
|
223
|
+
const patternExcluded = beforePatternFilter - files.length;
|
|
224
|
+
if (patternExcluded > 0) {
|
|
225
|
+
skipped.push({ file: `${patternExcluded} file(s)`, reason: 'excluded by pattern' });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const scannedFiles = files.map(f => f.file_path);
|
|
230
|
+
|
|
231
|
+
return {
|
|
232
|
+
files,
|
|
233
|
+
_meta: {
|
|
234
|
+
scanned_files: scannedFiles,
|
|
235
|
+
total_changed: totalChanged,
|
|
236
|
+
skipped,
|
|
237
|
+
},
|
|
238
|
+
};
|
|
131
239
|
}
|
|
132
240
|
}
|
|
133
241
|
|
|
@@ -1,284 +0,0 @@
|
|
|
1
|
-
import React, { useEffect, useState } from 'react';
|
|
2
|
-
import { Box, Text, useApp } from 'ink';
|
|
3
|
-
import { getConfigValue } from '../utils/config.js';
|
|
4
|
-
import { fetchApi } from '../utils/fetchApi.js';
|
|
5
|
-
import SecurityAnalysisApiHelper from '../utils/securityAnalysisApiHelper.js';
|
|
6
|
-
|
|
7
|
-
export default function SecurityAnalysis({ scanType = 'staged-only', failOn = 'HIGH', include = [], exclude = [], lastNCommits = 1 }) {
|
|
8
|
-
const { exit } = useApp();
|
|
9
|
-
const [status, setStatus] = useState('initializing');
|
|
10
|
-
const [issues, setIssues] = useState([]);
|
|
11
|
-
const [error, setError] = useState(null);
|
|
12
|
-
const [fileCount, setFileCount] = useState(0);
|
|
13
|
-
|
|
14
|
-
const apiKey = getConfigValue('apiKey');
|
|
15
|
-
|
|
16
|
-
// Handle not logged in state
|
|
17
|
-
useEffect(() => {
|
|
18
|
-
if (!apiKey) {
|
|
19
|
-
const id = setTimeout(() => exit(new Error('Not logged in')), 0);
|
|
20
|
-
return () => clearTimeout(id);
|
|
21
|
-
}
|
|
22
|
-
}, [apiKey, exit]);
|
|
23
|
-
|
|
24
|
-
// Check if logged in
|
|
25
|
-
if (!apiKey) {
|
|
26
|
-
return React.createElement(
|
|
27
|
-
Box,
|
|
28
|
-
{ flexDirection: 'column', padding: 1 },
|
|
29
|
-
React.createElement(Text, { color: 'red' }, '✗ Not logged in.'),
|
|
30
|
-
React.createElement(Text, { color: 'gray' }, 'Run "codeant login" to authenticate.'),
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
// Helper to check if an issue should cause failure based on failOn level
|
|
35
|
-
const shouldFailOn = (severity) => {
|
|
36
|
-
const score = severity?.toUpperCase();
|
|
37
|
-
if (score === 'FALSE_POSITIVE') return false;
|
|
38
|
-
if (failOn === 'CRITICAL') return score === 'CRITICAL';
|
|
39
|
-
if (failOn === 'HIGH') return score === 'HIGH' || score === 'CRITICAL';
|
|
40
|
-
if (failOn === 'MEDIUM') return score === 'HIGH' || score === 'MEDIUM';
|
|
41
|
-
return true; // 'all' - fail on any non-false-positive
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
useEffect(() => {
|
|
45
|
-
let cancelled = false;
|
|
46
|
-
|
|
47
|
-
async function scanForIssues() {
|
|
48
|
-
try {
|
|
49
|
-
if (cancelled) return;
|
|
50
|
-
setStatus('scanning');
|
|
51
|
-
|
|
52
|
-
// Initialize git helper and get staged files
|
|
53
|
-
const helper = new SecurityAnalysisApiHelper(process.cwd());
|
|
54
|
-
await helper.init();
|
|
55
|
-
|
|
56
|
-
if (cancelled) return;
|
|
57
|
-
const requestBody = await helper.buildSecurityAnalysisApiRequest(scanType, include, exclude, { lastNCommits });
|
|
58
|
-
if (cancelled) return;
|
|
59
|
-
|
|
60
|
-
if (requestBody.files.length === 0) {
|
|
61
|
-
if (!cancelled) setStatus('no_files');
|
|
62
|
-
return;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// Call the security analysis API
|
|
66
|
-
const response = await fetchApi(
|
|
67
|
-
'/extension/pr-review/security-analysis',
|
|
68
|
-
'POST',
|
|
69
|
-
requestBody,
|
|
70
|
-
);
|
|
71
|
-
|
|
72
|
-
if (cancelled) return;
|
|
73
|
-
|
|
74
|
-
// Transform security issues response to the expected format
|
|
75
|
-
const detectedIssues = (response.securityIssues || []).map(file => ({
|
|
76
|
-
file_path: file.file_path,
|
|
77
|
-
issues: (file.security_issues || []).map(issue => ({
|
|
78
|
-
line_number: issue.line_number,
|
|
79
|
-
end_line: issue.end_line,
|
|
80
|
-
type: issue.issue_text,
|
|
81
|
-
confidence_score: issue.false_positive ? 'FALSE_POSITIVE' : issue.severity,
|
|
82
|
-
test_id: issue.test_id,
|
|
83
|
-
confidence: issue.confidence,
|
|
84
|
-
likelihood: issue.likelihood,
|
|
85
|
-
cwe: issue.cwe,
|
|
86
|
-
owasp: issue.owasp,
|
|
87
|
-
})),
|
|
88
|
-
}));
|
|
89
|
-
|
|
90
|
-
// Filter to only include files with actual issues
|
|
91
|
-
const filesWithIssues = detectedIssues.filter(
|
|
92
|
-
file => file.issues && file.issues.length > 0,
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
if (!cancelled) {
|
|
96
|
-
setIssues(filesWithIssues);
|
|
97
|
-
setStatus('done');
|
|
98
|
-
}
|
|
99
|
-
} catch (err) {
|
|
100
|
-
if (!cancelled) {
|
|
101
|
-
setError(err.message);
|
|
102
|
-
setStatus('error');
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
scanForIssues();
|
|
108
|
-
|
|
109
|
-
return () => {
|
|
110
|
-
cancelled = true;
|
|
111
|
-
};
|
|
112
|
-
}, [scanType, include, exclude, lastNCommits]);
|
|
113
|
-
|
|
114
|
-
// Handle exit after status changes
|
|
115
|
-
useEffect(() => {
|
|
116
|
-
if (status === 'done') {
|
|
117
|
-
// Check if any issues should cause failure based on failOn level
|
|
118
|
-
const hasBlockingIssues = issues.some(file =>
|
|
119
|
-
file.issues.some(issue => issue.false_positive !== true &&shouldFailOn(issue.severity)),
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
if (hasBlockingIssues) {
|
|
123
|
-
setTimeout(() => {
|
|
124
|
-
process.exitCode = 1;
|
|
125
|
-
exit(new Error('Security issues detected'));
|
|
126
|
-
}, 100);
|
|
127
|
-
} else {
|
|
128
|
-
setTimeout(() => exit(), 100);
|
|
129
|
-
}
|
|
130
|
-
} else if (status === 'no_files') {
|
|
131
|
-
setTimeout(() => exit(), 100);
|
|
132
|
-
} else if (status === 'error') {
|
|
133
|
-
setTimeout(() => exit(new Error(error)), 100);
|
|
134
|
-
}
|
|
135
|
-
}, [status, issues]);
|
|
136
|
-
|
|
137
|
-
// Render: Initializing
|
|
138
|
-
if (status === 'initializing') {
|
|
139
|
-
return React.createElement(
|
|
140
|
-
Box,
|
|
141
|
-
{ flexDirection: 'column', padding: 1 },
|
|
142
|
-
React.createElement(Text, { color: 'cyan' }, 'Initializing...'),
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Render: Scanning
|
|
147
|
-
if (status === 'scanning') {
|
|
148
|
-
return React.createElement(
|
|
149
|
-
Box,
|
|
150
|
-
{ flexDirection: 'column', padding: 1 },
|
|
151
|
-
React.createElement(Text, { color: 'cyan' }, `Scanning ${fileCount} file(s) for security issues...`),
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// Render: No files to scan
|
|
156
|
-
if (status === 'no_files') {
|
|
157
|
-
const noFilesMessages = {
|
|
158
|
-
'staged-only': 'Stage some changes first with "git add".',
|
|
159
|
-
'branch-diff': 'No changes found compared to the base branch.',
|
|
160
|
-
'uncommitted': 'No uncommitted changes found.',
|
|
161
|
-
'last-commit': 'No files found in the last commit.',
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
return React.createElement(
|
|
165
|
-
Box,
|
|
166
|
-
{ flexDirection: 'column', padding: 1 },
|
|
167
|
-
React.createElement(Text, { color: 'yellow' }, 'No files to scan.'),
|
|
168
|
-
React.createElement(Text, { color: 'gray' }, noFilesMessages[scanType] || 'No files found.'),
|
|
169
|
-
);
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
// Render: Error
|
|
173
|
-
if (status === 'error') {
|
|
174
|
-
return React.createElement(
|
|
175
|
-
Box,
|
|
176
|
-
{ flexDirection: 'column', padding: 1 },
|
|
177
|
-
React.createElement(Text, { color: 'red' }, '✗ Error: ', error),
|
|
178
|
-
);
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
// Render: Done with issues found
|
|
182
|
-
if (status === 'done' && issues.length > 0) {
|
|
183
|
-
const allIssues = issues.flatMap(file =>
|
|
184
|
-
file.issues.map(s => ({ ...s, file_path: file.file_path })),
|
|
185
|
-
);
|
|
186
|
-
|
|
187
|
-
const blockingIssues = allIssues.filter(s => s.false_positive !== true && shouldFailOn(s.severity));
|
|
188
|
-
const falsePositives = allIssues.filter(s => s.false_positive !== true && !shouldFailOn(s.severity));
|
|
189
|
-
|
|
190
|
-
const hasBlockingIssues = blockingIssues.length > 0;
|
|
191
|
-
|
|
192
|
-
// Group by file for display
|
|
193
|
-
const groupByFile = (issuesList) => {
|
|
194
|
-
const grouped = {};
|
|
195
|
-
issuesList.forEach(s => {
|
|
196
|
-
if (!grouped[s.file_path]) grouped[s.file_path] = [];
|
|
197
|
-
grouped[s.file_path].push(s);
|
|
198
|
-
});
|
|
199
|
-
return grouped;
|
|
200
|
-
};
|
|
201
|
-
|
|
202
|
-
const blockingByFile = groupByFile(blockingIssues);
|
|
203
|
-
const falsePositivesByFile = groupByFile(falsePositives);
|
|
204
|
-
|
|
205
|
-
const elements = [
|
|
206
|
-
// Header
|
|
207
|
-
React.createElement(
|
|
208
|
-
Text,
|
|
209
|
-
{ key: 'header', color: hasBlockingIssues ? 'red' : 'yellow', bold: true },
|
|
210
|
-
hasBlockingIssues
|
|
211
|
-
? `✗ ${blockingIssues.length} security issue(s) found!`
|
|
212
|
-
: `⚠ ${falsePositives.length} potential security issue(s) found (ignored)`,
|
|
213
|
-
),
|
|
214
|
-
React.createElement(Text, { key: 'spacer1' }, ''),
|
|
215
|
-
];
|
|
216
|
-
|
|
217
|
-
// Blocking issues
|
|
218
|
-
if (blockingIssues.length > 0) {
|
|
219
|
-
Object.entries(blockingByFile).forEach(([filePath, fileIssues]) => {
|
|
220
|
-
elements.push(
|
|
221
|
-
React.createElement(Text, { key: `file-${filePath}`, color: 'yellow', bold: true }, filePath),
|
|
222
|
-
);
|
|
223
|
-
fileIssues.forEach((issue, idx) => {
|
|
224
|
-
elements.push(
|
|
225
|
-
React.createElement(
|
|
226
|
-
Text,
|
|
227
|
-
{ key: `issue-${filePath}-${idx}`, color: 'red' },
|
|
228
|
-
` Line ${issue.line_number}: ${issue.type} (${issue.confidence_score})`,
|
|
229
|
-
),
|
|
230
|
-
);
|
|
231
|
-
});
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// False positives / ignored
|
|
236
|
-
if (falsePositives.length > 0) {
|
|
237
|
-
if (blockingIssues.length > 0) {
|
|
238
|
-
elements.push(React.createElement(Text, { key: 'spacer2' }, ''));
|
|
239
|
-
}
|
|
240
|
-
elements.push(
|
|
241
|
-
React.createElement(Text, { key: 'fp-header', color: 'gray' }, 'Ignored (false positives):'),
|
|
242
|
-
);
|
|
243
|
-
Object.entries(falsePositivesByFile).forEach(([filePath, fileIssues]) => {
|
|
244
|
-
elements.push(
|
|
245
|
-
React.createElement(Text, { key: `fp-file-${filePath}`, color: 'gray' }, ` ${filePath}`),
|
|
246
|
-
);
|
|
247
|
-
fileIssues.forEach((issue, idx) => {
|
|
248
|
-
elements.push(
|
|
249
|
-
React.createElement(
|
|
250
|
-
Text,
|
|
251
|
-
{ key: `fp-issue-${filePath}-${idx}`, color: 'gray', dimColor: true },
|
|
252
|
-
` Line ${issue.line_number}: ${issue.type} (${issue.confidence_score})`,
|
|
253
|
-
),
|
|
254
|
-
);
|
|
255
|
-
});
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
elements.push(React.createElement(Text, { key: 'spacer3' }, ''));
|
|
260
|
-
|
|
261
|
-
if (hasBlockingIssues) {
|
|
262
|
-
elements.push(
|
|
263
|
-
React.createElement(Text, { key: 'footer', color: 'gray' }, 'Fix security issues before committing.'),
|
|
264
|
-
);
|
|
265
|
-
} else {
|
|
266
|
-
elements.push(
|
|
267
|
-
React.createElement(Text, { key: 'footer', color: 'green' }, '✓ Commit allowed (only false positives found)'),
|
|
268
|
-
);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
return React.createElement(
|
|
272
|
-
Box,
|
|
273
|
-
{ flexDirection: 'column', padding: 1 },
|
|
274
|
-
...elements,
|
|
275
|
-
);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// Render: Done with no issues
|
|
279
|
-
return React.createElement(
|
|
280
|
-
Box,
|
|
281
|
-
{ flexDirection: 'column', padding: 1 },
|
|
282
|
-
React.createElement(Text, { color: 'green' }, '✓ No security issues found'),
|
|
283
|
-
);
|
|
284
|
-
}
|