codeant-cli 0.1.5 → 0.1.7
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/dist/239.index.js +1 -0
- package/dist/701.index.js +1 -0
- package/dist/935.index.js +1 -0
- package/dist/index.js +20 -0
- package/dist/package.json +3 -0
- package/package.json +12 -4
- package/src/commands/getBaseUrl.js +0 -25
- package/src/commands/login.js +0 -124
- package/src/commands/logout.js +0 -30
- package/src/commands/review.js +0 -239
- package/src/commands/secrets.js +0 -268
- package/src/commands/securityAnalysis.js +0 -284
- package/src/commands/setBaseUrl.js +0 -37
- package/src/commands/staticAnalysis.js +0 -405
- package/src/components/ReviewUI.js +0 -127
- package/src/components/Welcome.js +0 -143
- package/src/index.js +0 -202
- package/src/tools/bashTool.js +0 -17
- package/src/tools/executeTool.js +0 -35
- package/src/tools/globTool.js +0 -9
- package/src/tools/grepTool.js +0 -18
- package/src/tools/lsTool.js +0 -7
- package/src/tools/readTool.js +0 -12
- package/src/utils/commonApiHelper.js +0 -101
- package/src/utils/config.js +0 -42
- package/src/utils/fetchApi.js +0 -42
- package/src/utils/gitDiffHelper.js +0 -774
- package/src/utils/reviewApiHelper.js +0 -139
- package/src/utils/secretsApiHelper.js +0 -134
- package/src/utils/securityAnalysisApiHelper.js +0 -119
- package/src/utils/staticAnalysisApiHelper.js +0 -119
package/src/commands/secrets.js
DELETED
|
@@ -1,268 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect } from 'react';
|
|
2
|
-
import { Text, Box, useApp } from 'ink';
|
|
3
|
-
import { getConfigValue } from '../utils/config.js';
|
|
4
|
-
import { fetchApi } from '../utils/fetchApi.js';
|
|
5
|
-
import SecretsApiHelper from '../utils/secretsApiHelper.js';
|
|
6
|
-
|
|
7
|
-
export default function Secrets({ scanType = 'staged-only', failOn = 'HIGH', include = [], exclude = [], lastNCommits = 1 }) {
|
|
8
|
-
const { exit } = useApp();
|
|
9
|
-
const [status, setStatus] = useState('initializing');
|
|
10
|
-
const [secrets, setSecrets] = 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 a secret should cause failure based on failOn level
|
|
35
|
-
const shouldFailOn = (confidenceScore) => {
|
|
36
|
-
const score = confidenceScore?.toUpperCase();
|
|
37
|
-
if (score === 'FALSE_POSITIVE') return false;
|
|
38
|
-
if (failOn === 'HIGH') return score === 'HIGH';
|
|
39
|
-
if (failOn === 'MEDIUM') return score === 'HIGH' || score === 'MEDIUM';
|
|
40
|
-
return true; // 'all' - fail on any non-false-positive
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
useEffect(() => {
|
|
44
|
-
let cancelled = false;
|
|
45
|
-
|
|
46
|
-
async function scanSecrets() {
|
|
47
|
-
try {
|
|
48
|
-
if (cancelled) return;
|
|
49
|
-
setStatus('scanning');
|
|
50
|
-
|
|
51
|
-
// Initialize git helper and get staged files
|
|
52
|
-
const helper = new SecretsApiHelper(process.cwd());
|
|
53
|
-
await helper.init();
|
|
54
|
-
|
|
55
|
-
if (cancelled) return;
|
|
56
|
-
const requestBody = await helper.buildSecretsApiRequest(scanType, include, exclude, { lastNCommits });
|
|
57
|
-
if (cancelled) return;
|
|
58
|
-
|
|
59
|
-
if (requestBody.files.length === 0) {
|
|
60
|
-
if (!cancelled) setStatus('no_files');
|
|
61
|
-
return;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Call the secrets detection API
|
|
65
|
-
const response = await fetchApi(
|
|
66
|
-
'/extension/pr-review/secrets-detection',
|
|
67
|
-
'POST',
|
|
68
|
-
requestBody
|
|
69
|
-
);
|
|
70
|
-
|
|
71
|
-
if (cancelled) return;
|
|
72
|
-
const detectedSecrets = response.secretsDetected || [];
|
|
73
|
-
|
|
74
|
-
// Filter to only include files with actual secrets
|
|
75
|
-
const filesWithSecrets = detectedSecrets.filter(
|
|
76
|
-
file => file.secrets && file.secrets.length > 0
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
if (!cancelled) {
|
|
80
|
-
setSecrets(filesWithSecrets);
|
|
81
|
-
setStatus('done');
|
|
82
|
-
}
|
|
83
|
-
} catch (err) {
|
|
84
|
-
if (!cancelled) {
|
|
85
|
-
setError(err.message);
|
|
86
|
-
setStatus('error');
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
scanSecrets();
|
|
92
|
-
|
|
93
|
-
return () => {
|
|
94
|
-
cancelled = true;
|
|
95
|
-
};
|
|
96
|
-
}, [scanType, include, exclude, lastNCommits]);
|
|
97
|
-
|
|
98
|
-
// Handle exit after status changes
|
|
99
|
-
useEffect(() => {
|
|
100
|
-
if (status === 'done') {
|
|
101
|
-
// Check if any secrets should cause failure based on failOn level
|
|
102
|
-
const hasBlockingSecrets = secrets.some(file =>
|
|
103
|
-
file.secrets.some(secret => shouldFailOn(secret.confidence_score))
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
if (hasBlockingSecrets) {
|
|
107
|
-
setTimeout(() => {
|
|
108
|
-
process.exitCode = 1;
|
|
109
|
-
exit(new Error('Secrets detected'));
|
|
110
|
-
}, 100);
|
|
111
|
-
} else {
|
|
112
|
-
setTimeout(() => exit(), 100);
|
|
113
|
-
}
|
|
114
|
-
} else if (status === 'no_files') {
|
|
115
|
-
setTimeout(() => exit(), 100);
|
|
116
|
-
} else if (status === 'error') {
|
|
117
|
-
setTimeout(() => exit(new Error(error)), 100);
|
|
118
|
-
}
|
|
119
|
-
}, [status, secrets]);
|
|
120
|
-
|
|
121
|
-
// Render: Initializing
|
|
122
|
-
if (status === 'initializing') {
|
|
123
|
-
return React.createElement(
|
|
124
|
-
Box,
|
|
125
|
-
{ flexDirection: 'column', padding: 1 },
|
|
126
|
-
React.createElement(Text, { color: 'cyan' }, 'Initializing...')
|
|
127
|
-
);
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
// Render: Scanning
|
|
131
|
-
if (status === 'scanning') {
|
|
132
|
-
return React.createElement(
|
|
133
|
-
Box,
|
|
134
|
-
{ flexDirection: 'column', padding: 1 },
|
|
135
|
-
React.createElement(Text, { color: 'cyan' }, `Scanning ${fileCount} file(s) for secrets...`)
|
|
136
|
-
);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// Render: No files to scan
|
|
140
|
-
if (status === 'no_files') {
|
|
141
|
-
const noFilesMessages = {
|
|
142
|
-
'staged-only': 'Stage some changes first with "git add".',
|
|
143
|
-
'branch-diff': 'No changes found compared to the base branch.',
|
|
144
|
-
'uncommitted': 'No uncommitted changes found.',
|
|
145
|
-
'last-commit': 'No files found in the last commit.'
|
|
146
|
-
};
|
|
147
|
-
|
|
148
|
-
return React.createElement(
|
|
149
|
-
Box,
|
|
150
|
-
{ flexDirection: 'column', padding: 1 },
|
|
151
|
-
React.createElement(Text, { color: 'yellow' }, 'No files to scan.'),
|
|
152
|
-
React.createElement(Text, { color: 'gray' }, noFilesMessages[scanType] || 'No files found.')
|
|
153
|
-
);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Render: Error
|
|
157
|
-
if (status === 'error') {
|
|
158
|
-
return React.createElement(
|
|
159
|
-
Box,
|
|
160
|
-
{ flexDirection: 'column', padding: 1 },
|
|
161
|
-
React.createElement(Text, { color: 'red' }, '✗ Error: ', error)
|
|
162
|
-
);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Render: Done with secrets found
|
|
166
|
-
if (status === 'done' && secrets.length > 0) {
|
|
167
|
-
const allSecrets = secrets.flatMap(file =>
|
|
168
|
-
file.secrets.map(s => ({ ...s, file_path: file.file_path }))
|
|
169
|
-
);
|
|
170
|
-
|
|
171
|
-
const blockingSecrets = allSecrets.filter(s => shouldFailOn(s.confidence_score));
|
|
172
|
-
const falsePositives = allSecrets.filter(s => !shouldFailOn(s.confidence_score));
|
|
173
|
-
|
|
174
|
-
const hasBlockingSecrets = blockingSecrets.length > 0;
|
|
175
|
-
|
|
176
|
-
// Group by file for display
|
|
177
|
-
const groupByFile = (secretsList) => {
|
|
178
|
-
const grouped = {};
|
|
179
|
-
secretsList.forEach(s => {
|
|
180
|
-
if (!grouped[s.file_path]) grouped[s.file_path] = [];
|
|
181
|
-
grouped[s.file_path].push(s);
|
|
182
|
-
});
|
|
183
|
-
return grouped;
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
const blockingByFile = groupByFile(blockingSecrets);
|
|
187
|
-
const falsePositivesByFile = groupByFile(falsePositives);
|
|
188
|
-
|
|
189
|
-
const elements = [
|
|
190
|
-
// Header
|
|
191
|
-
React.createElement(
|
|
192
|
-
Text,
|
|
193
|
-
{ key: 'header', color: hasBlockingSecrets ? 'red' : 'yellow', bold: true },
|
|
194
|
-
hasBlockingSecrets
|
|
195
|
-
? `✗ ${blockingSecrets.length} secret(s) found!`
|
|
196
|
-
: `⚠ ${falsePositives.length} potential secret(s) found (ignored)`
|
|
197
|
-
),
|
|
198
|
-
React.createElement(Text, { key: 'spacer1' }, '')
|
|
199
|
-
];
|
|
200
|
-
|
|
201
|
-
// Blocking secrets
|
|
202
|
-
if (blockingSecrets.length > 0) {
|
|
203
|
-
Object.entries(blockingByFile).forEach(([filePath, fileSecrets]) => {
|
|
204
|
-
elements.push(
|
|
205
|
-
React.createElement(Text, { key: `file-${filePath}`, color: 'yellow', bold: true }, filePath)
|
|
206
|
-
);
|
|
207
|
-
fileSecrets.forEach((secret, idx) => {
|
|
208
|
-
elements.push(
|
|
209
|
-
React.createElement(
|
|
210
|
-
Text,
|
|
211
|
-
{ key: `secret-${filePath}-${idx}`, color: 'red' },
|
|
212
|
-
` Line ${secret.line_number}: ${secret.type} (${secret.confidence_score})`
|
|
213
|
-
)
|
|
214
|
-
);
|
|
215
|
-
});
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// False positives / ignored
|
|
220
|
-
if (falsePositives.length > 0) {
|
|
221
|
-
if (blockingSecrets.length > 0) {
|
|
222
|
-
elements.push(React.createElement(Text, { key: 'spacer2' }, ''));
|
|
223
|
-
}
|
|
224
|
-
elements.push(
|
|
225
|
-
React.createElement(Text, { key: 'fp-header', color: 'gray' }, 'Ignored (false positives):')
|
|
226
|
-
);
|
|
227
|
-
Object.entries(falsePositivesByFile).forEach(([filePath, fileSecrets]) => {
|
|
228
|
-
elements.push(
|
|
229
|
-
React.createElement(Text, { key: `fp-file-${filePath}`, color: 'gray' }, ` ${filePath}`)
|
|
230
|
-
);
|
|
231
|
-
fileSecrets.forEach((secret, idx) => {
|
|
232
|
-
elements.push(
|
|
233
|
-
React.createElement(
|
|
234
|
-
Text,
|
|
235
|
-
{ key: `fp-secret-${filePath}-${idx}`, color: 'gray', dimColor: true },
|
|
236
|
-
` Line ${secret.line_number}: ${secret.type} (${secret.confidence_score})`
|
|
237
|
-
)
|
|
238
|
-
);
|
|
239
|
-
});
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
elements.push(React.createElement(Text, { key: 'spacer3' }, ''));
|
|
244
|
-
|
|
245
|
-
if (hasBlockingSecrets) {
|
|
246
|
-
elements.push(
|
|
247
|
-
React.createElement(Text, { key: 'footer', color: 'gray' }, 'Remove secrets before committing.')
|
|
248
|
-
);
|
|
249
|
-
} else {
|
|
250
|
-
elements.push(
|
|
251
|
-
React.createElement(Text, { key: 'footer', color: 'green' }, '✓ Commit allowed (only false positives found)')
|
|
252
|
-
);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
return React.createElement(
|
|
256
|
-
Box,
|
|
257
|
-
{ flexDirection: 'column', padding: 1 },
|
|
258
|
-
...elements
|
|
259
|
-
);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Render: Done with no secrets
|
|
263
|
-
return React.createElement(
|
|
264
|
-
Box,
|
|
265
|
-
{ flexDirection: 'column', padding: 1 },
|
|
266
|
-
React.createElement(Text, { color: 'green' }, '✓ No secrets found')
|
|
267
|
-
);
|
|
268
|
-
}
|
|
@@ -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
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import React, { useEffect } from 'react';
|
|
2
|
-
import { Text, Box, useApp } from 'ink';
|
|
3
|
-
import { setConfigValue, CONFIG_FILE } from '../utils/config.js';
|
|
4
|
-
|
|
5
|
-
export default function SetBaseUrl({ url }) {
|
|
6
|
-
const { exit } = useApp();
|
|
7
|
-
|
|
8
|
-
useEffect(() => {
|
|
9
|
-
if (!url) {
|
|
10
|
-
exit(new Error('URL is required'));
|
|
11
|
-
return;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
try {
|
|
15
|
-
setConfigValue('baseUrl', url);
|
|
16
|
-
exit();
|
|
17
|
-
} catch (err) {
|
|
18
|
-
exit(err);
|
|
19
|
-
}
|
|
20
|
-
}, []);
|
|
21
|
-
|
|
22
|
-
if (!url) {
|
|
23
|
-
return React.createElement(
|
|
24
|
-
Box,
|
|
25
|
-
{ flexDirection: 'column', padding: 1 },
|
|
26
|
-
React.createElement(Text, { color: 'red' }, '✗ Error: URL is required'),
|
|
27
|
-
React.createElement(Text, { color: 'gray' }, 'Usage: codeant set-base-url <url>')
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
return React.createElement(
|
|
32
|
-
Box,
|
|
33
|
-
{ flexDirection: 'column', padding: 1 },
|
|
34
|
-
React.createElement(Text, { color: 'green' }, '✓ Base URL set to: ', url),
|
|
35
|
-
React.createElement(Text, { color: 'gray' }, 'Saved to: ', CONFIG_FILE)
|
|
36
|
-
);
|
|
37
|
-
}
|