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/src/index.js CHANGED
@@ -9,8 +9,6 @@ import SetBaseUrl from './commands/setBaseUrl.js';
9
9
  import GetBaseUrl from './commands/getBaseUrl.js';
10
10
  import Login from './commands/login.js';
11
11
  import Logout from './commands/logout.js';
12
- import StaticAnalysis from './commands/staticAnalysis.js';
13
- import SecurityAnalysis from './commands/securityAnalysis.js';
14
12
  import Review from './commands/review.js';
15
13
  import Welcome from './components/Welcome.js';
16
14
  import * as scm from './scm/index.js';
@@ -51,67 +49,39 @@ if (process.argv.length === 2) {
51
49
  program
52
50
  .command('secrets')
53
51
  .description('Scan for secrets in your code')
54
- .option('--staged', 'Scan only staged files (default)')
55
- .option('--all', 'Scan all changed files')
56
- .option('--uncommitted', 'Scan uncommitted changes')
57
- .option('--last-commit', 'Scan last commit')
58
- .option('--last-n-commits <n>', 'Scan last n commits (max 5)', parseInt)
59
- .option('--fail-on <level>', 'Fail only on HIGH, MEDIUM, or all (default: HIGH)', 'HIGH')
60
- .option('--include <paths>', 'Comma-separated list of file paths regex to include')
61
- .option('--exclude <paths>', 'Comma-separated list of file paths regex to exclude')
62
- .action((options) => {
63
- let scanType = 'staged-only';
64
- let lastNCommits = 1;
65
-
66
- // Check !== undefined, not truthy — passing 0 is valid and shouldn't fall through
67
- if (options.lastNCommits !== undefined) {
68
- scanType = 'last-n-commits';
69
- lastNCommits = Math.min(Math.max(1, options.lastNCommits), 5);
70
- } else if (options.all) {
71
- scanType = 'branch-diff';
72
- } else if (options.uncommitted) {
73
- scanType = 'uncommitted';
74
- } else if (options.lastCommit) {
75
- scanType = 'last-commit';
76
- }
77
-
78
- const include = options.include
79
- ? (Array.isArray(options.include) ? options.include : splitGlobs(options.include))
80
- : [];
81
-
82
- const exclude = options.exclude
83
- ? (Array.isArray(options.exclude) ? options.exclude : splitGlobs(options.exclude))
84
- : [];
85
-
86
- const failOn = options.failOn?.toUpperCase() || 'HIGH';
87
-
88
- render(React.createElement(Secrets, { scanType, failOn, include, exclude, lastNCommits }));
89
- });
90
-
91
- program
92
- .command('static-analysis')
93
- .description('Run static analysis on your code')
94
- .option('--staged', 'Scan only staged files (default)')
95
- .option('--all', 'Scan all changed files')
96
- .option('--uncommitted', 'Scan uncommitted changes')
52
+ .option('--all', 'Scan committed + uncommitted changes (default)')
53
+ .option('--committed', 'Scan only unpushed commits')
54
+ .option('--uncommitted', 'Scan only uncommitted changes (staged + unstaged + untracked)')
55
+ .option('--staged', 'Scan only staged files')
97
56
  .option('--last-commit', 'Scan last commit')
98
57
  .option('--last-n-commits <n>', 'Scan last n commits (max 5)', parseInt)
58
+ .option('--base <branch>', 'Compare against a specific base branch (e.g. --base develop)')
59
+ .option('--base-commit <commit>', 'Compare against a specific commit (e.g. --base-commit HEAD~3)')
99
60
  .option('--fail-on <level>', 'Fail on issues at or above this level: BLOCKER, CRITICAL, MAJOR, MINOR, INFO (default: CRITICAL)', 'CRITICAL')
100
- .option('--auto-fix', 'Automatically apply fixes when available')
101
- .option('--include <paths>', 'Comma-separated list of file paths regex to include')
102
- .option('--exclude <paths>', 'Comma-separated list of file paths regex to exclude')
61
+ .option('--include <paths>', 'Comma-separated list of file paths glob patterns to include')
62
+ .option('--exclude <paths>', 'Comma-separated list of file paths glob patterns to exclude')
103
63
  .action((options) => {
104
- let scanType = 'staged-only';
64
+ let scanType = 'all';
105
65
  let lastNCommits = 1;
66
+ let baseBranch = null;
67
+ let baseCommit = null;
106
68
 
69
+ if (options.base) {
70
+ scanType = 'base-branch';
71
+ baseBranch = options.base;
72
+ } else if (options.baseCommit) {
73
+ scanType = 'base-commit';
74
+ baseCommit = options.baseCommit;
107
75
  // Check !== undefined, not truthy — passing 0 is valid and shouldn't fall through
108
- if (options.lastNCommits !== undefined) {
76
+ } else if (options.lastNCommits !== undefined) {
109
77
  scanType = 'last-n-commits';
110
78
  lastNCommits = Math.min(Math.max(1, options.lastNCommits), 5);
111
- } else if (options.all) {
112
- scanType = 'branch-diff';
79
+ } else if (options.committed) {
80
+ scanType = 'committed';
113
81
  } else if (options.uncommitted) {
114
82
  scanType = 'uncommitted';
83
+ } else if (options.staged) {
84
+ scanType = 'staged-only';
115
85
  } else if (options.lastCommit) {
116
86
  scanType = 'last-commit';
117
87
  }
@@ -125,48 +95,8 @@ program
125
95
  : [];
126
96
 
127
97
  const failOn = options.failOn?.toUpperCase() || 'CRITICAL';
128
- const autoFix = options.autoFix || false;
129
- render(React.createElement(StaticAnalysis, {scanType, failOn, include, exclude, autoFix, lastNCommits}));
130
- });
131
-
132
- program
133
- .command('security-analysis')
134
- .description('Run security analysis on your code')
135
- .option('--staged', 'Scan only staged files (default)')
136
- .option('--all', 'Scan all changed files')
137
- .option('--uncommitted', 'Scan uncommitted changes')
138
- .option('--last-commit', 'Scan last commit')
139
- .option('--last-n-commits <n>', 'Scan last n commits (max 5)', parseInt)
140
- .option('--fail-on <level>', 'Fail on issues at or above this level: BLOCKER, CRITICAL, MAJOR, MINOR, INFO (default: CRITICAL)', 'CRITICAL')
141
- .option('--include <paths>', 'Comma-separated list of file paths regex to include')
142
- .option('--exclude <paths>', 'Comma-separated list of file paths regex to exclude')
143
- .action((options) => {
144
- let scanType = 'staged-only';
145
- let lastNCommits = 1;
146
-
147
- // Check !== undefined, not truthy — passing 0 is valid and shouldn't fall through
148
- if (options.lastNCommits !== undefined) {
149
- scanType = 'last-n-commits';
150
- lastNCommits = Math.min(Math.max(1, options.lastNCommits), 5);
151
- } else if (options.all) {
152
- scanType = 'branch-diff';
153
- } else if (options.uncommitted) {
154
- scanType = 'uncommitted';
155
- } else if (options.lastCommit) {
156
- scanType = 'last-commit';
157
- }
158
-
159
- const include = options.include
160
- ? (Array.isArray(options.include) ? options.include : splitGlobs(options.include))
161
- : [];
162
-
163
- const exclude = options.exclude
164
- ? (Array.isArray(options.exclude) ? options.exclude : splitGlobs(options.exclude))
165
- : [];
166
-
167
- const failOn = options.failOn?.toUpperCase() || 'HIGH';
168
98
 
169
- render(React.createElement(SecurityAnalysis, { scanType, failOn, include, exclude, lastNCommits }));
99
+ render(React.createElement(Secrets, { scanType, failOn, include, exclude, lastNCommits, baseBranch, baseCommit }));
170
100
  });
171
101
 
172
102
  program
@@ -1,4 +1,5 @@
1
1
  import CommonApiHelper from './commonApiHelper.js';
2
+ import path from 'path';
2
3
  import { minimatch } from 'minimatch';
3
4
 
4
5
  /**
@@ -15,6 +16,58 @@ import { minimatch } from 'minimatch';
15
16
  * ]
16
17
  * }
17
18
  */
19
+
20
+ const EXCLUDED_FILENAMES = new Set([
21
+ 'package-lock.json',
22
+ 'yarn.lock',
23
+ 'pnpm-lock.yaml',
24
+ 'composer.lock',
25
+ 'Gemfile.lock',
26
+ 'Cargo.lock',
27
+ 'poetry.lock',
28
+ 'Pipfile.lock',
29
+ 'go.sum',
30
+ '.DS_Store',
31
+ 'Thumbs.db',
32
+ ]);
33
+
34
+ const EXCLUDED_EXTENSIONS = new Set([
35
+ // Images
36
+ '.png', '.jpg', '.jpeg', '.gif', '.bmp', '.ico', '.svg', '.webp', '.avif',
37
+ // Fonts
38
+ '.woff', '.woff2', '.ttf', '.eot', '.otf',
39
+ // Binary / compiled
40
+ '.exe', '.dll', '.so', '.dylib', '.bin', '.o', '.a', '.class', '.pyc', '.pyo',
41
+ '.wasm', '.jar', '.war', '.ear',
42
+ // Archives
43
+ '.zip', '.tar', '.gz', '.bz2', '.7z', '.rar',
44
+ // Media
45
+ '.mp3', '.mp4', '.wav', '.avi', '.mov', '.flv', '.wmv', '.ogg', '.webm',
46
+ // Data / generated
47
+ '.min.js', '.min.css', '.map',
48
+ '.pb', '.proto.bin',
49
+ // PDF / docs
50
+ '.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
51
+ ]);
52
+
53
+ const MAX_FILE_LINES = 5000;
54
+
55
+ function isScannableFile(filePath) {
56
+ const basename = path.basename(filePath);
57
+
58
+ if (EXCLUDED_FILENAMES.has(basename)) return false;
59
+
60
+ const ext = path.extname(basename).toLowerCase();
61
+ if (EXCLUDED_EXTENSIONS.has(ext)) return false;
62
+
63
+ if (basename.endsWith('.min.js') || basename.endsWith('.min.css')) return false;
64
+
65
+ // Skip markdown files
66
+ if (ext === '.md') return false;
67
+
68
+ return true;
69
+ }
70
+
18
71
  class SecretsApiHelper extends CommonApiHelper {
19
72
  /**
20
73
  * Transform diff info array to API format
@@ -124,10 +177,65 @@ class SecretsApiHelper extends CommonApiHelper {
124
177
  files = [];
125
178
  }
126
179
 
180
+ const totalChanged = files.length;
181
+ const skipped = [];
182
+
183
+ // Filter out non-scannable files (binary, lock files, markdown, etc.)
184
+ files = files.filter(fileObj => {
185
+ const filePath = fileObj.file_path;
186
+
187
+ if (!isScannableFile(filePath)) {
188
+ skipped.push({ file: filePath, reason: 'binary/generated' });
189
+ return false;
190
+ }
191
+
192
+ // Skip test files — backend filters these out anyway
193
+ if (filePath.toLowerCase().includes('test')) {
194
+ skipped.push({ file: filePath, reason: 'test file' });
195
+ return false;
196
+ }
197
+
198
+ // Skip deleted files (empty code)
199
+ if (fileObj.code === '' || fileObj.code === undefined) {
200
+ skipped.push({ file: filePath, reason: 'deleted' });
201
+ return false;
202
+ }
203
+
204
+ // Skip files exceeding line limit
205
+ if (fileObj.code) {
206
+ const lineCount = fileObj.code.split('\n').length;
207
+ if (lineCount > MAX_FILE_LINES) {
208
+ skipped.push({ file: filePath, reason: `too large (${lineCount} lines, max ${MAX_FILE_LINES})` });
209
+ return false;
210
+ }
211
+ }
212
+
213
+ return true;
214
+ });
215
+
127
216
  // Apply include/exclude filters
217
+ const beforePatternFilter = files.length;
128
218
  files = this._filterFiles(files, includePatterns, excludePatterns);
129
219
 
130
- return { files };
220
+ // Track pattern-excluded files
221
+ if (files.length < beforePatternFilter) {
222
+ // We don't have exact file names for pattern-excluded, but the count is in skipped
223
+ const patternExcluded = beforePatternFilter - files.length;
224
+ if (patternExcluded > 0) {
225
+ skipped.push({ file: `${patternExcluded} file(s)`, reason: 'excluded by pattern' });
226
+ }
227
+ }
228
+
229
+ const scannedFiles = files.map(f => f.file_path);
230
+
231
+ return {
232
+ files,
233
+ _meta: {
234
+ scanned_files: scannedFiles,
235
+ total_changed: totalChanged,
236
+ skipped,
237
+ },
238
+ };
131
239
  }
132
240
  }
133
241
 
@@ -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
- }