codeant-cli 0.1.0
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 +226 -0
- package/package.json +28 -0
- package/src/commands/getBaseUrl.js +25 -0
- package/src/commands/login.js +124 -0
- package/src/commands/logout.js +30 -0
- package/src/commands/secrets.js +251 -0
- package/src/commands/setBaseUrl.js +37 -0
- package/src/index.js +62 -0
- package/src/utils/config.js +42 -0
- package/src/utils/fetchApi.js +42 -0
- package/src/utils/gitDiffHelper.js +762 -0
- package/src/utils/secretsApiHelper.js +127 -0
package/README.md
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# CodeAnt CLI
|
|
2
|
+
|
|
3
|
+
A command-line tool for code review and security scanning.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g codeant-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or run locally:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
git clone https://github.com/codeantai/codeant-cli.git
|
|
15
|
+
cd codeant-cli
|
|
16
|
+
npm install
|
|
17
|
+
npm link
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# Login to CodeAnt
|
|
24
|
+
codeant login
|
|
25
|
+
|
|
26
|
+
# Scan staged files for secrets
|
|
27
|
+
codeant secrets
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Usage
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
codeant <command> [options]
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Commands
|
|
37
|
+
|
|
38
|
+
#### `login`
|
|
39
|
+
|
|
40
|
+
Authenticate with CodeAnt. Opens a browser window for login.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
codeant login
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
#### `logout`
|
|
47
|
+
|
|
48
|
+
Log out from CodeAnt.
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
codeant logout
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
#### `secrets`
|
|
55
|
+
|
|
56
|
+
Scan your code for exposed secrets, API keys, and credentials.
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
codeant secrets [options]
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**Options:**
|
|
63
|
+
|
|
64
|
+
| Option | Description |
|
|
65
|
+
|--------|-------------|
|
|
66
|
+
| `--staged` | Scan only staged files (default) |
|
|
67
|
+
| `--all` | Scan all changed files compared to base branch |
|
|
68
|
+
| `--uncommitted` | Scan all uncommitted changes |
|
|
69
|
+
| `--last-commit` | Scan files from the last commit |
|
|
70
|
+
| `--fail-on <level>` | Fail only on HIGH, MEDIUM, or all (default: HIGH) |
|
|
71
|
+
|
|
72
|
+
**Examples:**
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
# Scan staged files (default)
|
|
76
|
+
codeant secrets
|
|
77
|
+
|
|
78
|
+
# Scan all changed files
|
|
79
|
+
codeant secrets --all
|
|
80
|
+
|
|
81
|
+
# Scan last commit
|
|
82
|
+
codeant secrets --last-commit
|
|
83
|
+
|
|
84
|
+
# Only fail on HIGH confidence secrets (default)
|
|
85
|
+
codeant secrets --fail-on HIGH
|
|
86
|
+
|
|
87
|
+
# Fail on HIGH and MEDIUM confidence secrets
|
|
88
|
+
codeant secrets --fail-on MEDIUM
|
|
89
|
+
|
|
90
|
+
# Fail on all secrets (except false positives)
|
|
91
|
+
codeant secrets --fail-on all
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Exit codes:**
|
|
95
|
+
- `0` - No blocking secrets found (or only false positives)
|
|
96
|
+
- `1` - Secrets detected that match the `--fail-on` threshold
|
|
97
|
+
|
|
98
|
+
**Confidence Levels:**
|
|
99
|
+
- `HIGH` - High confidence, likely a real secret
|
|
100
|
+
- `MEDIUM` - Medium confidence, may need review
|
|
101
|
+
- `FALSE_POSITIVE` - Detected but likely not a real secret (always ignored)
|
|
102
|
+
|
|
103
|
+
#### `set-base-url <url>`
|
|
104
|
+
|
|
105
|
+
Set a custom API base URL.
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
codeant set-base-url https://api.example.com
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
#### `get-base-url`
|
|
112
|
+
|
|
113
|
+
Show the current API base URL and its source.
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
codeant get-base-url
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Global Options
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
codeant --version # Show version
|
|
123
|
+
codeant --help # Show help
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Configuration
|
|
127
|
+
|
|
128
|
+
Config is stored in `~/.codeant/config.json`.
|
|
129
|
+
|
|
130
|
+
You can also use environment variables:
|
|
131
|
+
|
|
132
|
+
| Variable | Description |
|
|
133
|
+
|----------|-------------|
|
|
134
|
+
| `CODEANT_API_URL` | API base URL (overrides config) |
|
|
135
|
+
| `CODEANT_API_TOKEN` | Authentication token (overrides config) |
|
|
136
|
+
|
|
137
|
+
**Priority order:**
|
|
138
|
+
1. Environment variables (highest)
|
|
139
|
+
2. Config file (`~/.codeant/config.json`)
|
|
140
|
+
3. Default values
|
|
141
|
+
|
|
142
|
+
## Git Hooks
|
|
143
|
+
|
|
144
|
+
Use CodeAnt as a pre-commit hook to prevent secrets from being committed.
|
|
145
|
+
|
|
146
|
+
### Manual Setup
|
|
147
|
+
|
|
148
|
+
Create `.git/hooks/pre-commit`:
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
#!/bin/sh
|
|
152
|
+
codeant secrets
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Make it executable:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
chmod +x .git/hooks/pre-commit
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### With Husky
|
|
162
|
+
|
|
163
|
+
```bash
|
|
164
|
+
npx husky add .husky/pre-commit "codeant secrets"
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### With lefthook
|
|
168
|
+
|
|
169
|
+
Add to `lefthook.yml`:
|
|
170
|
+
|
|
171
|
+
```yaml
|
|
172
|
+
pre-commit:
|
|
173
|
+
commands:
|
|
174
|
+
secrets:
|
|
175
|
+
run: codeant secrets
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Example Output
|
|
179
|
+
|
|
180
|
+
### Secrets Found (blocking)
|
|
181
|
+
|
|
182
|
+
```
|
|
183
|
+
✗ 2 secret(s) found!
|
|
184
|
+
|
|
185
|
+
src/config.js
|
|
186
|
+
Line 5: AWS Access Key (HIGH)
|
|
187
|
+
Line 12: API Key (HIGH)
|
|
188
|
+
|
|
189
|
+
Remove secrets before committing.
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Only False Positives (non-blocking)
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
⚠ 1 potential secret(s) found (ignored)
|
|
196
|
+
|
|
197
|
+
Ignored (false positives):
|
|
198
|
+
src/example.js
|
|
199
|
+
Line 10: Generic Secret (FALSE_POSITIVE)
|
|
200
|
+
|
|
201
|
+
✓ Commit allowed (only false positives found)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### No Secrets
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
✓ No secrets found
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
## Development
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
# Run locally
|
|
214
|
+
node src/index.js secrets
|
|
215
|
+
|
|
216
|
+
# Run with npm
|
|
217
|
+
npm start secrets
|
|
218
|
+
|
|
219
|
+
# Test different scan types
|
|
220
|
+
node src/index.js secrets --last-commit
|
|
221
|
+
node src/index.js secrets --all
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## License
|
|
225
|
+
|
|
226
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "codeant-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Code review CLI tool",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"codeant": "./src/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "node src/index.js"
|
|
11
|
+
},
|
|
12
|
+
"keywords": ["cli", "code-review", "secrets"],
|
|
13
|
+
"author": "",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": ""
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"src"
|
|
21
|
+
],
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"commander": "^12.1.0",
|
|
24
|
+
"ink": "^5.0.1",
|
|
25
|
+
"open": "^10.1.0",
|
|
26
|
+
"react": "^18.3.1"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { Text, Box, useApp } from 'ink';
|
|
3
|
+
import { getConfigValue } from '../utils/config.js';
|
|
4
|
+
|
|
5
|
+
export default function GetBaseUrl() {
|
|
6
|
+
const { exit } = useApp();
|
|
7
|
+
|
|
8
|
+
const envUrl = process.env.CODEANT_API_URL;
|
|
9
|
+
const configUrl = getConfigValue('baseUrl');
|
|
10
|
+
const defaultUrl = 'https://api.codeant.ai';
|
|
11
|
+
|
|
12
|
+
const activeUrl = envUrl || configUrl || defaultUrl;
|
|
13
|
+
const source = envUrl ? 'env' : configUrl ? 'config' : 'default';
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
exit();
|
|
17
|
+
}, []);
|
|
18
|
+
|
|
19
|
+
return React.createElement(
|
|
20
|
+
Box,
|
|
21
|
+
{ flexDirection: 'column', padding: 1 },
|
|
22
|
+
React.createElement(Text, { bold: true }, 'Base URL: ', activeUrl),
|
|
23
|
+
React.createElement(Text, { color: 'gray' }, 'Source: ', source)
|
|
24
|
+
);
|
|
25
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { Text, Box, useApp } from 'ink';
|
|
3
|
+
import { getConfigValue, setConfigValue } from '../utils/config.js';
|
|
4
|
+
import { randomUUID } from 'crypto';
|
|
5
|
+
|
|
6
|
+
const POLL_INTERVAL = 10000; // 10 seconds
|
|
7
|
+
const TIMEOUT = 10 * 60 * 1000; // 10 minutes
|
|
8
|
+
|
|
9
|
+
export default function Login() {
|
|
10
|
+
const { exit } = useApp();
|
|
11
|
+
const [status, setStatus] = useState('opening');
|
|
12
|
+
const [error, setError] = useState(null);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
// Check if already logged in
|
|
16
|
+
const existingToken = getConfigValue('apiKey');
|
|
17
|
+
if (existingToken) {
|
|
18
|
+
setStatus('already_logged_in');
|
|
19
|
+
setTimeout(() => exit(), 100);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const token = randomUUID();
|
|
24
|
+
const baseUrl = getConfigValue('baseUrl') || 'https://api.codeant.ai';
|
|
25
|
+
const loginUrl = `https://app.codeant.ai?ideLoginToken=${token}`;
|
|
26
|
+
const pollUrl = `${baseUrl}/extension/login/status?apiKey=${token}`;
|
|
27
|
+
|
|
28
|
+
// Open browser
|
|
29
|
+
import('open').then(({ default: open }) => {
|
|
30
|
+
open(loginUrl);
|
|
31
|
+
setStatus('waiting');
|
|
32
|
+
}).catch(() => {
|
|
33
|
+
// Fallback: just show the URL
|
|
34
|
+
console.log(`\nOpen this URL in your browser:\n${loginUrl}\n`);
|
|
35
|
+
setStatus('waiting');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Poll for login status
|
|
39
|
+
let timeoutId;
|
|
40
|
+
const intervalId = setInterval(async () => {
|
|
41
|
+
try {
|
|
42
|
+
const response = await fetch(pollUrl);
|
|
43
|
+
const data = await response.json();
|
|
44
|
+
|
|
45
|
+
if (data.status === 'yes') {
|
|
46
|
+
clearInterval(intervalId);
|
|
47
|
+
clearTimeout(timeoutId);
|
|
48
|
+
|
|
49
|
+
// Save the API key
|
|
50
|
+
setConfigValue('apiKey', token);
|
|
51
|
+
|
|
52
|
+
// Check for custom API URL based on email domain
|
|
53
|
+
if (data.item?.email?.includes('commvault.com')) {
|
|
54
|
+
setConfigValue('baseUrl', 'https://codeant-api.commvault.com');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
setStatus('success');
|
|
58
|
+
setTimeout(() => exit(), 100);
|
|
59
|
+
}
|
|
60
|
+
} catch (err) {
|
|
61
|
+
// Silently continue polling
|
|
62
|
+
}
|
|
63
|
+
}, POLL_INTERVAL);
|
|
64
|
+
|
|
65
|
+
// Timeout after 10 minutes
|
|
66
|
+
timeoutId = setTimeout(() => {
|
|
67
|
+
clearInterval(intervalId);
|
|
68
|
+
setError('Login timed out. Please try again.');
|
|
69
|
+
setStatus('error');
|
|
70
|
+
setTimeout(() => exit(new Error('Login timed out')), 100);
|
|
71
|
+
}, TIMEOUT);
|
|
72
|
+
|
|
73
|
+
// Cleanup
|
|
74
|
+
return () => {
|
|
75
|
+
clearInterval(intervalId);
|
|
76
|
+
clearTimeout(timeoutId);
|
|
77
|
+
};
|
|
78
|
+
}, []);
|
|
79
|
+
|
|
80
|
+
if (status === 'already_logged_in') {
|
|
81
|
+
return React.createElement(
|
|
82
|
+
Box,
|
|
83
|
+
{ flexDirection: 'column', padding: 1 },
|
|
84
|
+
React.createElement(Text, { color: 'yellow' }, 'Already logged in.'),
|
|
85
|
+
React.createElement(Text, { color: 'gray' }, 'Run "codeant logout" first to re-authenticate.')
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (status === 'opening') {
|
|
90
|
+
return React.createElement(
|
|
91
|
+
Box,
|
|
92
|
+
{ flexDirection: 'column', padding: 1 },
|
|
93
|
+
React.createElement(Text, null, 'Opening browser...')
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (status === 'waiting') {
|
|
98
|
+
return React.createElement(
|
|
99
|
+
Box,
|
|
100
|
+
{ flexDirection: 'column', padding: 1 },
|
|
101
|
+
React.createElement(Text, { color: 'cyan' }, 'Waiting for login...'),
|
|
102
|
+
React.createElement(Text, { color: 'gray' }, 'Complete the login in your browser.'),
|
|
103
|
+
React.createElement(Text, { color: 'gray' }, 'Checking every 10 seconds. Timeout in 10 minutes.')
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (status === 'success') {
|
|
108
|
+
return React.createElement(
|
|
109
|
+
Box,
|
|
110
|
+
{ flexDirection: 'column', padding: 1 },
|
|
111
|
+
React.createElement(Text, { color: 'green' }, '✓ Login successful!')
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (status === 'error') {
|
|
116
|
+
return React.createElement(
|
|
117
|
+
Box,
|
|
118
|
+
{ flexDirection: 'column', padding: 1 },
|
|
119
|
+
React.createElement(Text, { color: 'red' }, '✗ ', error)
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React, { useEffect } from 'react';
|
|
2
|
+
import { Text, Box, useApp } from 'ink';
|
|
3
|
+
import { getConfigValue, setConfigValue } from '../utils/config.js';
|
|
4
|
+
|
|
5
|
+
export default function Logout() {
|
|
6
|
+
const { exit } = useApp();
|
|
7
|
+
|
|
8
|
+
const wasLoggedIn = !!getConfigValue('apiKey');
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
if (wasLoggedIn) {
|
|
12
|
+
setConfigValue('apiKey', null);
|
|
13
|
+
}
|
|
14
|
+
exit();
|
|
15
|
+
}, []);
|
|
16
|
+
|
|
17
|
+
if (!wasLoggedIn) {
|
|
18
|
+
return React.createElement(
|
|
19
|
+
Box,
|
|
20
|
+
{ flexDirection: 'column', padding: 1 },
|
|
21
|
+
React.createElement(Text, { color: 'yellow' }, 'Not logged in.')
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return React.createElement(
|
|
26
|
+
Box,
|
|
27
|
+
{ flexDirection: 'column', padding: 1 },
|
|
28
|
+
React.createElement(Text, { color: 'green' }, '✓ Logged out successfully.')
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
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' }) {
|
|
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
|
+
// Check if logged in
|
|
17
|
+
if (!apiKey) {
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
exit(new Error('Not logged in'));
|
|
20
|
+
}, []);
|
|
21
|
+
|
|
22
|
+
return React.createElement(
|
|
23
|
+
Box,
|
|
24
|
+
{ flexDirection: 'column', padding: 1 },
|
|
25
|
+
React.createElement(Text, { color: 'red' }, '✗ Not logged in.'),
|
|
26
|
+
React.createElement(Text, { color: 'gray' }, 'Run "codeant login" to authenticate.')
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Helper to check if a secret should cause failure based on failOn level
|
|
31
|
+
const shouldFailOn = (confidenceScore) => {
|
|
32
|
+
const score = confidenceScore?.toUpperCase();
|
|
33
|
+
if (score === 'FALSE_POSITIVE') return false;
|
|
34
|
+
if (failOn === 'HIGH') return score === 'HIGH';
|
|
35
|
+
if (failOn === 'MEDIUM') return score === 'HIGH' || score === 'MEDIUM';
|
|
36
|
+
return true; // 'all' - fail on any non-false-positive
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
async function scanSecrets() {
|
|
41
|
+
try {
|
|
42
|
+
setStatus('scanning');
|
|
43
|
+
|
|
44
|
+
// Initialize git helper and get staged files
|
|
45
|
+
const helper = new SecretsApiHelper(process.cwd());
|
|
46
|
+
await helper.init();
|
|
47
|
+
|
|
48
|
+
const requestBody = await helper.buildSecretsApiRequest(scanType);
|
|
49
|
+
setFileCount(requestBody.files.length);
|
|
50
|
+
|
|
51
|
+
if (requestBody.files.length === 0) {
|
|
52
|
+
setStatus('no_files');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Call the secrets detection API
|
|
57
|
+
const response = await fetchApi(
|
|
58
|
+
'/extension/pr-review/secrets-detection',
|
|
59
|
+
'POST',
|
|
60
|
+
requestBody
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const detectedSecrets = response.secretsDetected || [];
|
|
64
|
+
|
|
65
|
+
// Filter to only include files with actual secrets
|
|
66
|
+
const filesWithSecrets = detectedSecrets.filter(
|
|
67
|
+
file => file.secrets && file.secrets.length > 0
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
setSecrets(filesWithSecrets);
|
|
71
|
+
setStatus('done');
|
|
72
|
+
} catch (err) {
|
|
73
|
+
setError(err.message);
|
|
74
|
+
setStatus('error');
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
scanSecrets();
|
|
79
|
+
}, []);
|
|
80
|
+
|
|
81
|
+
// Handle exit after status changes
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (status === 'done') {
|
|
84
|
+
// Check if any secrets should cause failure based on failOn level
|
|
85
|
+
const hasBlockingSecrets = secrets.some(file =>
|
|
86
|
+
file.secrets.some(secret => shouldFailOn(secret.confidence_score))
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
if (hasBlockingSecrets) {
|
|
90
|
+
setTimeout(() => {
|
|
91
|
+
process.exitCode = 1;
|
|
92
|
+
exit(new Error('Secrets detected'));
|
|
93
|
+
}, 100);
|
|
94
|
+
} else {
|
|
95
|
+
setTimeout(() => exit(), 100);
|
|
96
|
+
}
|
|
97
|
+
} else if (status === 'no_files') {
|
|
98
|
+
setTimeout(() => exit(), 100);
|
|
99
|
+
} else if (status === 'error') {
|
|
100
|
+
setTimeout(() => exit(new Error(error)), 100);
|
|
101
|
+
}
|
|
102
|
+
}, [status, secrets]);
|
|
103
|
+
|
|
104
|
+
// Render: Initializing
|
|
105
|
+
if (status === 'initializing') {
|
|
106
|
+
return React.createElement(
|
|
107
|
+
Box,
|
|
108
|
+
{ flexDirection: 'column', padding: 1 },
|
|
109
|
+
React.createElement(Text, { color: 'cyan' }, 'Initializing...')
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Render: Scanning
|
|
114
|
+
if (status === 'scanning') {
|
|
115
|
+
return React.createElement(
|
|
116
|
+
Box,
|
|
117
|
+
{ flexDirection: 'column', padding: 1 },
|
|
118
|
+
React.createElement(Text, { color: 'cyan' }, `Scanning ${fileCount} file(s) for secrets...`)
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Render: No files to scan
|
|
123
|
+
if (status === 'no_files') {
|
|
124
|
+
const noFilesMessages = {
|
|
125
|
+
'staged-only': 'Stage some changes first with "git add".',
|
|
126
|
+
'branch-diff': 'No changes found compared to the base branch.',
|
|
127
|
+
'uncommitted': 'No uncommitted changes found.',
|
|
128
|
+
'last-commit': 'No files found in the last commit.'
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
return React.createElement(
|
|
132
|
+
Box,
|
|
133
|
+
{ flexDirection: 'column', padding: 1 },
|
|
134
|
+
React.createElement(Text, { color: 'yellow' }, 'No files to scan.'),
|
|
135
|
+
React.createElement(Text, { color: 'gray' }, noFilesMessages[scanType] || 'No files found.')
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Render: Error
|
|
140
|
+
if (status === 'error') {
|
|
141
|
+
return React.createElement(
|
|
142
|
+
Box,
|
|
143
|
+
{ flexDirection: 'column', padding: 1 },
|
|
144
|
+
React.createElement(Text, { color: 'red' }, '✗ Error: ', error)
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Render: Done with secrets found
|
|
149
|
+
if (status === 'done' && secrets.length > 0) {
|
|
150
|
+
const allSecrets = secrets.flatMap(file =>
|
|
151
|
+
file.secrets.map(s => ({ ...s, file_path: file.file_path }))
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const blockingSecrets = allSecrets.filter(s => shouldFailOn(s.confidence_score));
|
|
155
|
+
const falsePositives = allSecrets.filter(s => !shouldFailOn(s.confidence_score));
|
|
156
|
+
|
|
157
|
+
const hasBlockingSecrets = blockingSecrets.length > 0;
|
|
158
|
+
|
|
159
|
+
// Group by file for display
|
|
160
|
+
const groupByFile = (secretsList) => {
|
|
161
|
+
const grouped = {};
|
|
162
|
+
secretsList.forEach(s => {
|
|
163
|
+
if (!grouped[s.file_path]) grouped[s.file_path] = [];
|
|
164
|
+
grouped[s.file_path].push(s);
|
|
165
|
+
});
|
|
166
|
+
return grouped;
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const blockingByFile = groupByFile(blockingSecrets);
|
|
170
|
+
const falsePositivesByFile = groupByFile(falsePositives);
|
|
171
|
+
|
|
172
|
+
const elements = [
|
|
173
|
+
// Header
|
|
174
|
+
React.createElement(
|
|
175
|
+
Text,
|
|
176
|
+
{ key: 'header', color: hasBlockingSecrets ? 'red' : 'yellow', bold: true },
|
|
177
|
+
hasBlockingSecrets
|
|
178
|
+
? `✗ ${blockingSecrets.length} secret(s) found!`
|
|
179
|
+
: `⚠ ${falsePositives.length} potential secret(s) found (ignored)`
|
|
180
|
+
),
|
|
181
|
+
React.createElement(Text, { key: 'spacer1' }, '')
|
|
182
|
+
];
|
|
183
|
+
|
|
184
|
+
// Blocking secrets
|
|
185
|
+
if (blockingSecrets.length > 0) {
|
|
186
|
+
Object.entries(blockingByFile).forEach(([filePath, fileSecrets]) => {
|
|
187
|
+
elements.push(
|
|
188
|
+
React.createElement(Text, { key: `file-${filePath}`, color: 'yellow', bold: true }, filePath)
|
|
189
|
+
);
|
|
190
|
+
fileSecrets.forEach((secret, idx) => {
|
|
191
|
+
elements.push(
|
|
192
|
+
React.createElement(
|
|
193
|
+
Text,
|
|
194
|
+
{ key: `secret-${filePath}-${idx}`, color: 'red' },
|
|
195
|
+
` Line ${secret.line_number}: ${secret.type} (${secret.confidence_score})`
|
|
196
|
+
)
|
|
197
|
+
);
|
|
198
|
+
});
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// False positives / ignored
|
|
203
|
+
if (falsePositives.length > 0) {
|
|
204
|
+
if (blockingSecrets.length > 0) {
|
|
205
|
+
elements.push(React.createElement(Text, { key: 'spacer2' }, ''));
|
|
206
|
+
}
|
|
207
|
+
elements.push(
|
|
208
|
+
React.createElement(Text, { key: 'fp-header', color: 'gray' }, 'Ignored (false positives):')
|
|
209
|
+
);
|
|
210
|
+
Object.entries(falsePositivesByFile).forEach(([filePath, fileSecrets]) => {
|
|
211
|
+
elements.push(
|
|
212
|
+
React.createElement(Text, { key: `fp-file-${filePath}`, color: 'gray' }, ` ${filePath}`)
|
|
213
|
+
);
|
|
214
|
+
fileSecrets.forEach((secret, idx) => {
|
|
215
|
+
elements.push(
|
|
216
|
+
React.createElement(
|
|
217
|
+
Text,
|
|
218
|
+
{ key: `fp-secret-${filePath}-${idx}`, color: 'gray', dimColor: true },
|
|
219
|
+
` Line ${secret.line_number}: ${secret.type} (${secret.confidence_score})`
|
|
220
|
+
)
|
|
221
|
+
);
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
elements.push(React.createElement(Text, { key: 'spacer3' }, ''));
|
|
227
|
+
|
|
228
|
+
if (hasBlockingSecrets) {
|
|
229
|
+
elements.push(
|
|
230
|
+
React.createElement(Text, { key: 'footer', color: 'gray' }, 'Remove secrets before committing.')
|
|
231
|
+
);
|
|
232
|
+
} else {
|
|
233
|
+
elements.push(
|
|
234
|
+
React.createElement(Text, { key: 'footer', color: 'green' }, '✓ Commit allowed (only false positives found)')
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return React.createElement(
|
|
239
|
+
Box,
|
|
240
|
+
{ flexDirection: 'column', padding: 1 },
|
|
241
|
+
...elements
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// Render: Done with no secrets
|
|
246
|
+
return React.createElement(
|
|
247
|
+
Box,
|
|
248
|
+
{ flexDirection: 'column', padding: 1 },
|
|
249
|
+
React.createElement(Text, { color: 'green' }, '✓ No secrets found')
|
|
250
|
+
);
|
|
251
|
+
}
|