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.
@@ -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
- }