codeant-cli 0.1.0 → 0.1.1
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 +15 -0
- package/package.json +7 -2
- package/src/commands/secrets.js +30 -13
- package/src/index.js +13 -1
- package/src/utils/secretsApiHelper.js +65 -1
package/README.md
CHANGED
|
@@ -68,6 +68,8 @@ codeant secrets [options]
|
|
|
68
68
|
| `--uncommitted` | Scan all uncommitted changes |
|
|
69
69
|
| `--last-commit` | Scan files from the last commit |
|
|
70
70
|
| `--fail-on <level>` | Fail only on HIGH, MEDIUM, or all (default: HIGH) |
|
|
71
|
+
| `--include <patterns>` | Comma-separated glob patterns to include files |
|
|
72
|
+
| `--exclude <patterns>` | Comma-separated glob patterns to exclude files |
|
|
71
73
|
|
|
72
74
|
**Examples:**
|
|
73
75
|
|
|
@@ -89,8 +91,21 @@ codeant secrets --fail-on MEDIUM
|
|
|
89
91
|
|
|
90
92
|
# Fail on all secrets (except false positives)
|
|
91
93
|
codeant secrets --fail-on all
|
|
94
|
+
|
|
95
|
+
# Filter files using glob patterns
|
|
96
|
+
codeant secrets --include '**/*.js' # Only JS files
|
|
97
|
+
codeant secrets --exclude 'node_modules/**,*.test.js' # Exclude patterns
|
|
98
|
+
codeant secrets --include 'src/**' --exclude '*.test.*' # Combine both
|
|
92
99
|
```
|
|
93
100
|
|
|
101
|
+
**File Filtering:**
|
|
102
|
+
|
|
103
|
+
Use `--include` and `--exclude` with glob patterns to filter files:
|
|
104
|
+
- `*` matches any characters except `/`
|
|
105
|
+
- `**` matches any characters including `/`
|
|
106
|
+
- `*.{js,ts}` matches multiple extensions
|
|
107
|
+
- Comma-separated for multiple patterns: `--exclude 'test/**,dist/**'`
|
|
108
|
+
|
|
94
109
|
**Exit codes:**
|
|
95
110
|
- `0` - No blocking secrets found (or only false positives)
|
|
96
111
|
- `1` - Secrets detected that match the `--fail-on` threshold
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeant-cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Code review CLI tool",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,7 +9,11 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"start": "node src/index.js"
|
|
11
11
|
},
|
|
12
|
-
"keywords": [
|
|
12
|
+
"keywords": [
|
|
13
|
+
"cli",
|
|
14
|
+
"code-review",
|
|
15
|
+
"secrets"
|
|
16
|
+
],
|
|
13
17
|
"author": "",
|
|
14
18
|
"license": "MIT",
|
|
15
19
|
"repository": {
|
|
@@ -22,6 +26,7 @@
|
|
|
22
26
|
"dependencies": {
|
|
23
27
|
"commander": "^12.1.0",
|
|
24
28
|
"ink": "^5.0.1",
|
|
29
|
+
"minimatch": "^10.1.1",
|
|
25
30
|
"open": "^10.1.0",
|
|
26
31
|
"react": "^18.3.1"
|
|
27
32
|
}
|
package/src/commands/secrets.js
CHANGED
|
@@ -4,7 +4,7 @@ import { getConfigValue } from '../utils/config.js';
|
|
|
4
4
|
import { fetchApi } from '../utils/fetchApi.js';
|
|
5
5
|
import SecretsApiHelper from '../utils/secretsApiHelper.js';
|
|
6
6
|
|
|
7
|
-
export default function Secrets({ scanType = 'staged-only', failOn = 'HIGH' }) {
|
|
7
|
+
export default function Secrets({ scanType = 'staged-only', failOn = 'HIGH', include = [], exclude = [] }) {
|
|
8
8
|
const { exit } = useApp();
|
|
9
9
|
const [status, setStatus] = useState('initializing');
|
|
10
10
|
const [secrets, setSecrets] = useState([]);
|
|
@@ -13,12 +13,16 @@ export default function Secrets({ scanType = 'staged-only', failOn = 'HIGH' }) {
|
|
|
13
13
|
|
|
14
14
|
const apiKey = getConfigValue('apiKey');
|
|
15
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
|
+
|
|
16
24
|
// Check if logged in
|
|
17
25
|
if (!apiKey) {
|
|
18
|
-
useEffect(() => {
|
|
19
|
-
exit(new Error('Not logged in'));
|
|
20
|
-
}, []);
|
|
21
|
-
|
|
22
26
|
return React.createElement(
|
|
23
27
|
Box,
|
|
24
28
|
{ flexDirection: 'column', padding: 1 },
|
|
@@ -37,19 +41,23 @@ export default function Secrets({ scanType = 'staged-only', failOn = 'HIGH' }) {
|
|
|
37
41
|
};
|
|
38
42
|
|
|
39
43
|
useEffect(() => {
|
|
44
|
+
let cancelled = false;
|
|
45
|
+
|
|
40
46
|
async function scanSecrets() {
|
|
41
47
|
try {
|
|
48
|
+
if (cancelled) return;
|
|
42
49
|
setStatus('scanning');
|
|
43
50
|
|
|
44
51
|
// Initialize git helper and get staged files
|
|
45
52
|
const helper = new SecretsApiHelper(process.cwd());
|
|
46
53
|
await helper.init();
|
|
47
54
|
|
|
48
|
-
|
|
49
|
-
|
|
55
|
+
if (cancelled) return;
|
|
56
|
+
const requestBody = await helper.buildSecretsApiRequest(scanType, include, exclude);
|
|
57
|
+
if (cancelled) return;
|
|
50
58
|
|
|
51
59
|
if (requestBody.files.length === 0) {
|
|
52
|
-
setStatus('no_files');
|
|
60
|
+
if (!cancelled) setStatus('no_files');
|
|
53
61
|
return;
|
|
54
62
|
}
|
|
55
63
|
|
|
@@ -60,6 +68,7 @@ export default function Secrets({ scanType = 'staged-only', failOn = 'HIGH' }) {
|
|
|
60
68
|
requestBody
|
|
61
69
|
);
|
|
62
70
|
|
|
71
|
+
if (cancelled) return;
|
|
63
72
|
const detectedSecrets = response.secretsDetected || [];
|
|
64
73
|
|
|
65
74
|
// Filter to only include files with actual secrets
|
|
@@ -67,16 +76,24 @@ export default function Secrets({ scanType = 'staged-only', failOn = 'HIGH' }) {
|
|
|
67
76
|
file => file.secrets && file.secrets.length > 0
|
|
68
77
|
);
|
|
69
78
|
|
|
70
|
-
|
|
71
|
-
|
|
79
|
+
if (!cancelled) {
|
|
80
|
+
setSecrets(filesWithSecrets);
|
|
81
|
+
setStatus('done');
|
|
82
|
+
}
|
|
72
83
|
} catch (err) {
|
|
73
|
-
|
|
74
|
-
|
|
84
|
+
if (!cancelled) {
|
|
85
|
+
setError(err.message);
|
|
86
|
+
setStatus('error');
|
|
87
|
+
}
|
|
75
88
|
}
|
|
76
89
|
}
|
|
77
90
|
|
|
78
91
|
scanSecrets();
|
|
79
|
-
|
|
92
|
+
|
|
93
|
+
return () => {
|
|
94
|
+
cancelled = true;
|
|
95
|
+
};
|
|
96
|
+
}, [scanType, include, exclude]);
|
|
80
97
|
|
|
81
98
|
// Handle exit after status changes
|
|
82
99
|
useEffect(() => {
|
package/src/index.js
CHANGED
|
@@ -22,13 +22,25 @@ program
|
|
|
22
22
|
.option('--uncommitted', 'Scan uncommitted changes')
|
|
23
23
|
.option('--last-commit', 'Scan last commit')
|
|
24
24
|
.option('--fail-on <level>', 'Fail only on HIGH, MEDIUM, or all (default: HIGH)', 'HIGH')
|
|
25
|
+
.option('--include <paths>', 'Comma-separated list of file paths regex to include')
|
|
26
|
+
.option('--exclude <paths>', 'Comma-separated list of file paths regex to exclude')
|
|
25
27
|
.action((options) => {
|
|
26
28
|
let scanType = 'staged-only';
|
|
27
29
|
if (options.all) scanType = 'branch-diff';
|
|
28
30
|
else if (options.uncommitted) scanType = 'uncommitted';
|
|
29
31
|
else if (options.lastCommit) scanType = 'last-commit';
|
|
30
32
|
|
|
31
|
-
|
|
33
|
+
const include = options.include
|
|
34
|
+
? (Array.isArray(options.include) ? options.include : options.include.split(',')).map(s => s.trim()).filter(Boolean)
|
|
35
|
+
: [];
|
|
36
|
+
|
|
37
|
+
const exclude = options.exclude
|
|
38
|
+
? (Array.isArray(options.exclude) ? options.exclude : options.exclude.split(',')).map(s => s.trim()).filter(Boolean)
|
|
39
|
+
: [];
|
|
40
|
+
|
|
41
|
+
const failOn = options.failOn?.toUpperCase() || 'HIGH';
|
|
42
|
+
|
|
43
|
+
render(React.createElement(Secrets, { scanType, failOn, include, exclude }));
|
|
32
44
|
});
|
|
33
45
|
|
|
34
46
|
program
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import GitDiffHelper from './gitDiffHelper.js';
|
|
2
|
+
import { minimatch } from 'minimatch';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Transforms git diff data into the format expected by the secrets detection API
|
|
@@ -93,10 +94,70 @@ class SecretsApiHelper {
|
|
|
93
94
|
return Array.from(fileMap.values());
|
|
94
95
|
}
|
|
95
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Filter files based on include and exclude glob patterns
|
|
99
|
+
* @param {Array} files - Array of file objects with file_path property
|
|
100
|
+
* @param {Array} includePatterns - Array of glob pattern strings to include
|
|
101
|
+
* @param {Array} excludePatterns - Array of glob pattern strings to exclude
|
|
102
|
+
* @returns {Array} Filtered array of files
|
|
103
|
+
*/
|
|
104
|
+
_filterFiles(files, includePatterns = [], excludePatterns = []) {
|
|
105
|
+
return files.filter(file => {
|
|
106
|
+
const filePath = file.file_path;
|
|
107
|
+
|
|
108
|
+
// If include patterns are specified, file must match at least one
|
|
109
|
+
if (includePatterns.length > 0) {
|
|
110
|
+
const matchesInclude = includePatterns.some(pattern => {
|
|
111
|
+
try {
|
|
112
|
+
// If pattern is a RegExp, test it directly against the file path.
|
|
113
|
+
if (pattern instanceof RegExp) {
|
|
114
|
+
return pattern.test(filePath);
|
|
115
|
+
}
|
|
116
|
+
// Use matchBase option to match basename patterns like '*.js' for glob strings
|
|
117
|
+
return minimatch(filePath, pattern, { matchBase: true });
|
|
118
|
+
} catch (e) {
|
|
119
|
+
console.warn(`Invalid include pattern: ${pattern}`, e.message);
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (!matchesInclude) {
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// If exclude patterns are specified, file must not match any
|
|
130
|
+
if (excludePatterns.length > 0) {
|
|
131
|
+
const matchesExclude = excludePatterns.some(pattern => {
|
|
132
|
+
try {
|
|
133
|
+
// If pattern is a RegExp, test it directly against the file path.
|
|
134
|
+
if (pattern instanceof RegExp) {
|
|
135
|
+
return pattern.test(filePath);
|
|
136
|
+
}
|
|
137
|
+
// Use matchBase option to match basename patterns like '*.js' for glob strings
|
|
138
|
+
return minimatch(filePath, pattern, { matchBase: true });
|
|
139
|
+
} catch (e) {
|
|
140
|
+
console.warn(`Invalid exclude pattern: ${pattern}`, e.message);
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (matchesExclude) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return true;
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
96
154
|
/**
|
|
97
155
|
* Build the complete request body for the secrets API
|
|
156
|
+
* @param {string} type - Type of scan (staged-only, branch-diff, etc.)
|
|
157
|
+
* @param {Array} includePatterns - Optional array of glob patterns to include files
|
|
158
|
+
* @param {Array} excludePatterns - Optional array of glob patterns to exclude files
|
|
98
159
|
*/
|
|
99
|
-
async buildSecretsApiRequest(type = 'staged-only') {
|
|
160
|
+
async buildSecretsApiRequest(type = 'staged-only', includePatterns = [], excludePatterns = []) {
|
|
100
161
|
let files;
|
|
101
162
|
|
|
102
163
|
switch (type) {
|
|
@@ -116,6 +177,9 @@ class SecretsApiHelper {
|
|
|
116
177
|
files = await this.getStagedFilesForApi();
|
|
117
178
|
}
|
|
118
179
|
|
|
180
|
+
// Apply include/exclude filters
|
|
181
|
+
files = this._filterFiles(files, includePatterns, excludePatterns);
|
|
182
|
+
|
|
119
183
|
return { files };
|
|
120
184
|
}
|
|
121
185
|
|