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/package.json
CHANGED
package/src/commands/secrets.js
CHANGED
|
@@ -1,34 +1,42 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useApp } from 'ink';
|
|
3
3
|
import { getConfigValue } from '../utils/config.js';
|
|
4
4
|
import { fetchApi } from '../utils/fetchApi.js';
|
|
5
5
|
import SecretsApiHelper from '../utils/secretsApiHelper.js';
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
import {
|
|
7
|
+
renderInitializing,
|
|
8
|
+
renderFetchingDiff,
|
|
9
|
+
renderScanning,
|
|
10
|
+
renderNoFiles,
|
|
11
|
+
renderError,
|
|
12
|
+
renderNotLoggedIn,
|
|
13
|
+
renderDone,
|
|
14
|
+
} from '../components/SecretsUI.js';
|
|
15
|
+
|
|
16
|
+
export default function Secrets({ scanType = 'all', failOn = 'CRITICAL', include = [], exclude = [], lastNCommits = 1, baseBranch = null, baseCommit = null }) {
|
|
8
17
|
const { exit } = useApp();
|
|
9
18
|
const [status, setStatus] = useState('initializing');
|
|
10
19
|
const [secrets, setSecrets] = useState([]);
|
|
11
20
|
const [error, setError] = useState(null);
|
|
12
21
|
const [fileCount, setFileCount] = useState(0);
|
|
22
|
+
const [scanMeta, setScanMeta] = useState(null);
|
|
23
|
+
const [startTime] = useState(() => Date.now());
|
|
13
24
|
|
|
14
25
|
const apiKey = getConfigValue('apiKey');
|
|
15
26
|
|
|
16
27
|
// Handle not logged in state
|
|
17
28
|
useEffect(() => {
|
|
18
29
|
if (!apiKey) {
|
|
30
|
+
if (process.stdin.isTTY) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
19
33
|
const id = setTimeout(() => exit(new Error('Not logged in')), 0);
|
|
20
34
|
return () => clearTimeout(id);
|
|
21
35
|
}
|
|
22
36
|
}, [apiKey, exit]);
|
|
23
37
|
|
|
24
|
-
// Check if logged in
|
|
25
38
|
if (!apiKey) {
|
|
26
|
-
return
|
|
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
|
-
);
|
|
39
|
+
return renderNotLoggedIn();
|
|
32
40
|
}
|
|
33
41
|
|
|
34
42
|
// Helper to check if a secret should cause failure based on failOn level
|
|
@@ -46,21 +54,30 @@ export default function Secrets({ scanType = 'staged-only', failOn = 'HIGH', inc
|
|
|
46
54
|
async function scanSecrets() {
|
|
47
55
|
try {
|
|
48
56
|
if (cancelled) return;
|
|
49
|
-
setStatus('
|
|
57
|
+
setStatus('fetching_diff');
|
|
50
58
|
|
|
51
|
-
// Initialize git helper and get
|
|
59
|
+
// Initialize git helper and get files
|
|
52
60
|
const helper = new SecretsApiHelper(process.cwd());
|
|
53
61
|
await helper.init();
|
|
54
62
|
|
|
55
63
|
if (cancelled) return;
|
|
56
|
-
const requestBody = await helper.buildSecretsApiRequest(scanType, include, exclude, { lastNCommits });
|
|
64
|
+
const requestBody = await helper.buildSecretsApiRequest(scanType, include, exclude, { lastNCommits, baseBranch, baseCommit });
|
|
57
65
|
if (cancelled) return;
|
|
58
66
|
|
|
67
|
+
const meta = requestBody._meta || null;
|
|
68
|
+
setScanMeta(meta);
|
|
69
|
+
|
|
70
|
+
// Strip _meta before sending to API
|
|
71
|
+
delete requestBody._meta;
|
|
72
|
+
|
|
59
73
|
if (requestBody.files.length === 0) {
|
|
60
74
|
if (!cancelled) setStatus('no_files');
|
|
61
75
|
return;
|
|
62
76
|
}
|
|
63
77
|
|
|
78
|
+
setFileCount(requestBody.files.length);
|
|
79
|
+
setStatus('scanning');
|
|
80
|
+
|
|
64
81
|
// Call the secrets detection API
|
|
65
82
|
const response = await fetchApi(
|
|
66
83
|
'/extension/pr-review/secrets-detection',
|
|
@@ -93,7 +110,7 @@ export default function Secrets({ scanType = 'staged-only', failOn = 'HIGH', inc
|
|
|
93
110
|
return () => {
|
|
94
111
|
cancelled = true;
|
|
95
112
|
};
|
|
96
|
-
}, [scanType, include, exclude, lastNCommits]);
|
|
113
|
+
}, [scanType, include, exclude, lastNCommits, baseBranch, baseCommit]);
|
|
97
114
|
|
|
98
115
|
// Handle exit after status changes
|
|
99
116
|
useEffect(() => {
|
|
@@ -118,151 +135,14 @@ export default function Secrets({ scanType = 'staged-only', failOn = 'HIGH', inc
|
|
|
118
135
|
}
|
|
119
136
|
}, [status, secrets]);
|
|
120
137
|
|
|
121
|
-
//
|
|
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;
|
|
138
|
+
// ── Renders ──────────────────────────────────────────────────────────────
|
|
175
139
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
}
|
|
140
|
+
if (status === 'initializing') return renderInitializing(startTime);
|
|
141
|
+
if (status === 'fetching_diff') return renderFetchingDiff(startTime);
|
|
142
|
+
if (status === 'scanning') return renderScanning(startTime, fileCount, scanMeta);
|
|
143
|
+
if (status === 'no_files') return renderNoFiles(scanType, lastNCommits, baseBranch, baseCommit);
|
|
144
|
+
if (status === 'error') return renderError(error);
|
|
145
|
+
if (status === 'done') return renderDone(secrets, failOn, shouldFailOn, startTime, fileCount, scanMeta);
|
|
261
146
|
|
|
262
|
-
|
|
263
|
-
return React.createElement(
|
|
264
|
-
Box,
|
|
265
|
-
{ flexDirection: 'column', padding: 1 },
|
|
266
|
-
React.createElement(Text, { color: 'green' }, '✓ No secrets found')
|
|
267
|
-
);
|
|
147
|
+
return null;
|
|
268
148
|
}
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
|
|
4
|
+
// ── Constants ────────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
7
|
+
const DIVIDER = '─'.repeat(55);
|
|
8
|
+
const STEPS = ['Init', 'Fetch', 'Scan'];
|
|
9
|
+
|
|
10
|
+
const CONFIDENCE_COLORS = {
|
|
11
|
+
HIGH: 'red',
|
|
12
|
+
MEDIUM: 'yellow',
|
|
13
|
+
LOW: 'cyan',
|
|
14
|
+
FALSE_POSITIVE: 'gray',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
function getConfidenceColor(score) {
|
|
20
|
+
return CONFIDENCE_COLORS[score?.toUpperCase()] || 'gray';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function padEnd(str, len) {
|
|
24
|
+
return str + ' '.repeat(Math.max(0, len - str.length));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function formatElapsed(startTime) {
|
|
28
|
+
return ((Date.now() - startTime) / 1000).toFixed(1) + 's';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ── Animated Components ──────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
function Spinner({ color = 'cyan' }) {
|
|
34
|
+
const [frame, setFrame] = useState(0);
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
const timer = setInterval(() => {
|
|
37
|
+
setFrame(f => (f + 1) % SPINNER_FRAMES.length);
|
|
38
|
+
}, 80);
|
|
39
|
+
return () => clearInterval(timer);
|
|
40
|
+
}, []);
|
|
41
|
+
return React.createElement(Text, { color }, SPINNER_FRAMES[frame]);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function ElapsedTime({ startTime }) {
|
|
45
|
+
const [, setTick] = useState(0);
|
|
46
|
+
useEffect(() => {
|
|
47
|
+
const timer = setInterval(() => setTick(t => t + 1), 100);
|
|
48
|
+
return () => clearInterval(timer);
|
|
49
|
+
}, []);
|
|
50
|
+
return React.createElement(Text, { color: 'gray' }, formatElapsed(startTime));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function StepProgress({ currentStep }) {
|
|
54
|
+
const items = STEPS.map((step, i) => {
|
|
55
|
+
if (i < currentStep) {
|
|
56
|
+
return React.createElement(Text, { key: step, color: 'green' }, `✓ ${step}`);
|
|
57
|
+
} else if (i === currentStep) {
|
|
58
|
+
return React.createElement(
|
|
59
|
+
Box, { key: step },
|
|
60
|
+
React.createElement(Spinner, { color: 'cyan' }),
|
|
61
|
+
React.createElement(Text, { color: 'cyan' }, ` ${step}`),
|
|
62
|
+
);
|
|
63
|
+
} else {
|
|
64
|
+
return React.createElement(Text, { key: step, color: 'gray', dimColor: true }, `· ${step}`);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
return React.createElement(Box, { gap: 2 }, ...items);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ── Loading Screen ───────────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
function SecretsLoading({ message, step, startTime, fileCount, meta }) {
|
|
73
|
+
const els = [
|
|
74
|
+
React.createElement(
|
|
75
|
+
Box, { key: 'status', gap: 1 },
|
|
76
|
+
React.createElement(Spinner, {}),
|
|
77
|
+
React.createElement(Text, { color: 'white', bold: true }, message),
|
|
78
|
+
startTime != null && React.createElement(ElapsedTime, { startTime }),
|
|
79
|
+
),
|
|
80
|
+
React.createElement(
|
|
81
|
+
Box, { key: 'steps', marginTop: 1 },
|
|
82
|
+
React.createElement(StepProgress, { currentStep: step }),
|
|
83
|
+
),
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
// Show file list and coverage info when we have metadata (during scan step)
|
|
87
|
+
if (meta && step >= 2) {
|
|
88
|
+
const scanned = meta.scanned_files || [];
|
|
89
|
+
const total = meta.total_changed || 0;
|
|
90
|
+
const skipped = meta.skipped || [];
|
|
91
|
+
|
|
92
|
+
els.push(React.createElement(Text, { key: 'meta-sp' }, ''));
|
|
93
|
+
els.push(React.createElement(
|
|
94
|
+
Text, { key: 'meta-hdr', color: 'gray', dimColor: true }, DIVIDER,
|
|
95
|
+
));
|
|
96
|
+
|
|
97
|
+
// Coverage line
|
|
98
|
+
if (total > 0) {
|
|
99
|
+
const coverageColor = scanned.length === total ? 'green' : 'yellow';
|
|
100
|
+
els.push(React.createElement(
|
|
101
|
+
Text, { key: 'coverage', color: coverageColor },
|
|
102
|
+
`Scanning ${scanned.length} of ${total} changed file${total !== 1 ? 's' : ''}`,
|
|
103
|
+
));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// File list
|
|
107
|
+
if (scanned.length > 0) {
|
|
108
|
+
els.push(React.createElement(Text, { key: 'files-sp' }, ''));
|
|
109
|
+
scanned.forEach((file, i) => {
|
|
110
|
+
const isLast = i === scanned.length - 1;
|
|
111
|
+
const branch = isLast ? '└─' : '├─';
|
|
112
|
+
els.push(React.createElement(
|
|
113
|
+
Text, { key: `file-${i}`, color: 'white' },
|
|
114
|
+
` ${branch} ${file}`,
|
|
115
|
+
));
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Skipped summary
|
|
120
|
+
if (skipped.length > 0) {
|
|
121
|
+
els.push(React.createElement(Text, { key: 'skip-sp' }, ''));
|
|
122
|
+
const reasons = {};
|
|
123
|
+
skipped.forEach(s => {
|
|
124
|
+
const r = s.reason.split(' (')[0];
|
|
125
|
+
reasons[r] = (reasons[r] || 0) + 1;
|
|
126
|
+
});
|
|
127
|
+
const parts = Object.entries(reasons).map(([r, n]) => `${n} ${r}`);
|
|
128
|
+
els.push(React.createElement(
|
|
129
|
+
Text, { key: 'skipped', color: 'gray', dimColor: true },
|
|
130
|
+
`Skipped: ${parts.join(' · ')}`,
|
|
131
|
+
));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return React.createElement(
|
|
136
|
+
Box, { flexDirection: 'column', paddingX: 1, paddingY: 1 }, ...els,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// ── Public Render Functions ──────────────────────────────────────────────────
|
|
141
|
+
|
|
142
|
+
export function renderInitializing(startTime) {
|
|
143
|
+
return React.createElement(SecretsLoading, {
|
|
144
|
+
message: 'Initializing...',
|
|
145
|
+
step: 0,
|
|
146
|
+
startTime,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export function renderFetchingDiff(startTime) {
|
|
151
|
+
return React.createElement(SecretsLoading, {
|
|
152
|
+
message: 'Fetching changes...',
|
|
153
|
+
step: 1,
|
|
154
|
+
startTime,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function renderScanning(startTime, fileCount, meta) {
|
|
159
|
+
return React.createElement(SecretsLoading, {
|
|
160
|
+
message: 'Scanning for secrets...',
|
|
161
|
+
step: 2,
|
|
162
|
+
startTime,
|
|
163
|
+
fileCount,
|
|
164
|
+
meta,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function renderNoFiles(scanType, lastNCommits, baseBranch, baseCommit) {
|
|
169
|
+
const hints = {
|
|
170
|
+
'staged-only': 'Stage some changes first with "git add".',
|
|
171
|
+
'branch-diff': 'No changes found compared to the base branch.',
|
|
172
|
+
'uncommitted': 'No uncommitted changes found.',
|
|
173
|
+
'committed': 'No unpushed commits found.',
|
|
174
|
+
'all': 'No changes found between your branch and the base.',
|
|
175
|
+
'last-commit': 'No files found in the last commit.',
|
|
176
|
+
'last-n-commits': `No changes found in the last ${lastNCommits} commit(s).`,
|
|
177
|
+
'base-branch': `No changes found compared to ${baseBranch}.`,
|
|
178
|
+
'base-commit': `No changes found compared to commit ${baseCommit}.`,
|
|
179
|
+
};
|
|
180
|
+
return React.createElement(
|
|
181
|
+
Box, { flexDirection: 'column', paddingX: 1, paddingY: 1 },
|
|
182
|
+
React.createElement(Text, { color: 'cyan', bold: true }, 'CodeAnt Secrets'),
|
|
183
|
+
React.createElement(Text, { color: 'gray', dimColor: true }, DIVIDER),
|
|
184
|
+
React.createElement(Text, {}, ''),
|
|
185
|
+
React.createElement(Text, { color: 'yellow' }, ' No changes to scan.'),
|
|
186
|
+
React.createElement(Text, { color: 'gray' }, ` ${hints[scanType] || 'No files found.'}`),
|
|
187
|
+
React.createElement(Text, {}, ''),
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function renderError(error) {
|
|
192
|
+
return React.createElement(
|
|
193
|
+
Box, { flexDirection: 'column', paddingX: 1, paddingY: 1 },
|
|
194
|
+
React.createElement(Text, { color: 'cyan', bold: true }, 'CodeAnt Secrets'),
|
|
195
|
+
React.createElement(Text, { color: 'gray', dimColor: true }, DIVIDER),
|
|
196
|
+
React.createElement(Text, {}, ''),
|
|
197
|
+
React.createElement(Text, { color: 'red', bold: true }, ` ✗ ${error}`),
|
|
198
|
+
React.createElement(Text, {}, ''),
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function renderNotLoggedIn() {
|
|
203
|
+
return React.createElement(
|
|
204
|
+
Box, { flexDirection: 'column', paddingX: 1, paddingY: 1 },
|
|
205
|
+
React.createElement(Text, { color: 'cyan', bold: true }, 'CodeAnt Secrets'),
|
|
206
|
+
React.createElement(Text, { color: 'gray', dimColor: true }, DIVIDER),
|
|
207
|
+
React.createElement(Text, {}, ''),
|
|
208
|
+
React.createElement(Text, { color: 'red', bold: true }, ' ✗ Not logged in'),
|
|
209
|
+
React.createElement(Text, { color: 'gray' }, ' Run "codeant login" to authenticate.'),
|
|
210
|
+
React.createElement(Text, {}, ''),
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function renderDone(secrets, failOn, shouldFailOn, startTime, fileCount, meta) {
|
|
215
|
+
const allSecrets = secrets.flatMap(file =>
|
|
216
|
+
file.secrets.map(s => ({ ...s, file_path: file.file_path }))
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const blockingSecrets = allSecrets.filter(s => shouldFailOn(s.confidence_score));
|
|
220
|
+
const hasBlocking = blockingSecrets.length > 0;
|
|
221
|
+
const elapsed = startTime ? formatElapsed(startTime) : null;
|
|
222
|
+
|
|
223
|
+
// Count by confidence
|
|
224
|
+
const counts = {};
|
|
225
|
+
allSecrets.forEach(s => {
|
|
226
|
+
const conf = s.confidence_score?.toUpperCase() || 'UNKNOWN';
|
|
227
|
+
counts[conf] = (counts[conf] || 0) + 1;
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
// Group by file
|
|
231
|
+
const grouped = {};
|
|
232
|
+
allSecrets.forEach(s => {
|
|
233
|
+
const file = s.file_path || 'Unknown';
|
|
234
|
+
(grouped[file] ??= []).push(s);
|
|
235
|
+
});
|
|
236
|
+
const sortedFiles = Object.keys(grouped).sort();
|
|
237
|
+
sortedFiles.forEach(f =>
|
|
238
|
+
grouped[f].sort((a, b) => (a.line_number || 0) - (b.line_number || 0))
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const els = [];
|
|
242
|
+
|
|
243
|
+
// ── Header ──
|
|
244
|
+
els.push(React.createElement(
|
|
245
|
+
Box, { key: 'hdr', gap: 1 },
|
|
246
|
+
React.createElement(Text, { color: 'cyan', bold: true }, 'CodeAnt Secrets'),
|
|
247
|
+
elapsed && React.createElement(Text, { color: 'gray' }, elapsed),
|
|
248
|
+
));
|
|
249
|
+
els.push(React.createElement(
|
|
250
|
+
Text, { key: 'div1', color: 'gray', dimColor: true }, DIVIDER,
|
|
251
|
+
));
|
|
252
|
+
|
|
253
|
+
if (allSecrets.length === 0) {
|
|
254
|
+
// ── Clean pass ──
|
|
255
|
+
els.push(React.createElement(Text, { key: 'sp' }, ''));
|
|
256
|
+
els.push(React.createElement(
|
|
257
|
+
Text, { key: 'ok', color: 'green', bold: true }, ' ✓ No secrets found',
|
|
258
|
+
));
|
|
259
|
+
els.push(React.createElement(Text, { key: 'sp2' }, ''));
|
|
260
|
+
} else {
|
|
261
|
+
// ── Secrets grouped by file ──
|
|
262
|
+
els.push(React.createElement(Text, { key: 'sp1' }, ''));
|
|
263
|
+
|
|
264
|
+
sortedFiles.forEach((file, fi) => {
|
|
265
|
+
const fileSecrets = grouped[file];
|
|
266
|
+
|
|
267
|
+
els.push(React.createElement(
|
|
268
|
+
Text, { key: `f-${fi}`, color: 'white', bold: true }, ` ${file}`,
|
|
269
|
+
));
|
|
270
|
+
|
|
271
|
+
fileSecrets.forEach((secret, si) => {
|
|
272
|
+
const conf = secret.confidence_score?.toUpperCase() || 'UNKNOWN';
|
|
273
|
+
const confColor = getConfidenceColor(conf);
|
|
274
|
+
const isBlocking = shouldFailOn(secret.confidence_score);
|
|
275
|
+
const isLast = si === fileSecrets.length - 1;
|
|
276
|
+
const branch = isLast ? '└─' : '├─';
|
|
277
|
+
const lineStr = secret.line_number ? `L${secret.line_number}` : '';
|
|
278
|
+
|
|
279
|
+
els.push(React.createElement(
|
|
280
|
+
Box, { key: `s-${fi}-${si}` },
|
|
281
|
+
React.createElement(Text, { color: 'gray' }, ` ${branch} `),
|
|
282
|
+
React.createElement(Text, {
|
|
283
|
+
color: confColor,
|
|
284
|
+
bold: isBlocking,
|
|
285
|
+
}, padEnd(conf, 16)),
|
|
286
|
+
React.createElement(Text, { color: 'gray' }, padEnd(lineStr, 6)),
|
|
287
|
+
React.createElement(Text, { color: isBlocking ? 'white' : 'gray', dimColor: !isBlocking }, secret.type || ''),
|
|
288
|
+
));
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
els.push(React.createElement(Text, { key: `g-${fi}` }, ''));
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ── Footer ──
|
|
296
|
+
els.push(React.createElement(
|
|
297
|
+
Text, { key: 'div2', color: 'gray', dimColor: true }, DIVIDER,
|
|
298
|
+
));
|
|
299
|
+
|
|
300
|
+
// Coverage line
|
|
301
|
+
const totalChanged = meta?.total_changed || 0;
|
|
302
|
+
const scannedCount = meta?.scanned_files?.length || fileCount;
|
|
303
|
+
if (totalChanged > 0) {
|
|
304
|
+
const coverageColor = scannedCount === totalChanged ? 'green' : 'yellow';
|
|
305
|
+
els.push(React.createElement(
|
|
306
|
+
Text, { key: 'coverage', color: coverageColor },
|
|
307
|
+
`Scanned ${scannedCount} of ${totalChanged} changed file${totalChanged !== 1 ? 's' : ''}`,
|
|
308
|
+
));
|
|
309
|
+
} else if (fileCount) {
|
|
310
|
+
els.push(React.createElement(
|
|
311
|
+
Text, { key: 'coverage', color: 'gray' },
|
|
312
|
+
`${fileCount} file${fileCount !== 1 ? 's' : ''} scanned`,
|
|
313
|
+
));
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Skipped files summary
|
|
317
|
+
const skipped = meta?.skipped || [];
|
|
318
|
+
if (skipped.length > 0) {
|
|
319
|
+
const reasons = {};
|
|
320
|
+
skipped.forEach(s => {
|
|
321
|
+
const r = s.reason.split(' (')[0];
|
|
322
|
+
reasons[r] = (reasons[r] || 0) + 1;
|
|
323
|
+
});
|
|
324
|
+
const parts = Object.entries(reasons).map(([r, n]) => `${n} ${r}`);
|
|
325
|
+
els.push(React.createElement(
|
|
326
|
+
Text, { key: 'skipped', color: 'gray', dimColor: true },
|
|
327
|
+
`Skipped: ${parts.join(' · ')}`,
|
|
328
|
+
));
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Secret stats line
|
|
332
|
+
if (allSecrets.length > 0) {
|
|
333
|
+
const stats = [`${allSecrets.length} secret${allSecrets.length !== 1 ? 's' : ''} found`];
|
|
334
|
+
for (const conf of ['HIGH', 'MEDIUM', 'LOW', 'FALSE_POSITIVE']) {
|
|
335
|
+
if (counts[conf]) {
|
|
336
|
+
const label = conf === 'FALSE_POSITIVE' ? 'false positive' : conf.toLowerCase();
|
|
337
|
+
stats.push(`${counts[conf]} ${label}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
els.push(React.createElement(
|
|
341
|
+
Text, { key: 'stats', color: 'gray' }, stats.join(' · '),
|
|
342
|
+
));
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Status line
|
|
346
|
+
if (hasBlocking) {
|
|
347
|
+
els.push(React.createElement(
|
|
348
|
+
Text, { key: 'status', color: 'red', bold: true },
|
|
349
|
+
`✗ ${blockingSecrets.length} blocking secret${blockingSecrets.length !== 1 ? 's' : ''} — remove before committing`,
|
|
350
|
+
));
|
|
351
|
+
} else if (allSecrets.length > 0) {
|
|
352
|
+
els.push(React.createElement(
|
|
353
|
+
Text, { key: 'status', color: 'green', bold: true },
|
|
354
|
+
'✓ No blocking secrets found',
|
|
355
|
+
));
|
|
356
|
+
} else {
|
|
357
|
+
els.push(React.createElement(
|
|
358
|
+
Text, { key: 'status', color: 'green', bold: true },
|
|
359
|
+
'✓ All clear',
|
|
360
|
+
));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return React.createElement(
|
|
364
|
+
Box, { flexDirection: 'column', paddingX: 1, paddingY: 1 }, ...els,
|
|
365
|
+
);
|
|
366
|
+
}
|