codeant-cli 0.4.9 → 0.4.11

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeant-cli",
3
- "version": "0.4.9",
3
+ "version": "0.4.11",
4
4
  "description": "Code review CLI tool",
5
5
  "type": "module",
6
6
  "bin": {
@@ -71,7 +71,8 @@ export default function registerScansCommands(program, { runCmd }) {
71
71
  .option('--severity <list>', 'Filter by severity (e.g. critical,high)')
72
72
  .option('--path <glob>', 'Filter by file path glob')
73
73
  .option('--check <regex>', 'Filter by check ID or name (regex)')
74
- .option('--include-dismissed', 'Include dismissed findings (excluded by default)')
74
+ .option('--filter-dismissed', 'Exclude dismissed findings (default: false)')
75
+ .option('--no-false-positives', 'Exclude false positives (default: included)')
75
76
  .option('--format <fmt>', 'Output format: json|sarif|csv|md|table (default: json)', 'json')
76
77
  .option('--output <path>', 'Write output to file instead of stdout')
77
78
  .option('--fields <list>', 'Project findings to subset of fields (comma-separated)')
@@ -96,7 +97,8 @@ export default function registerScansCommands(program, { runCmd }) {
96
97
  severity: opts.severity,
97
98
  path: opts.path,
98
99
  check: opts.check,
99
- includeDismissed: opts.includeDismissed || false,
100
+ filterDismissed: opts.filterDismissed || false,
101
+ includeFalsePositives: opts.falsePositives ?? true,
100
102
  format: opts.format,
101
103
  output: opts.output,
102
104
  fields: opts.fields,
@@ -1,29 +1,22 @@
1
1
  import { minimatch } from 'minimatch';
2
- import { isDismissed, findDismissMatch } from './dismissMatch.js';
3
2
  import { normalizeSeverity } from './normalize.js';
4
3
 
5
- const SEV_RANK = { critical: 5, high: 4, medium: 3, low: 2, info: 1, unknown: 0 };
6
-
7
4
  /**
8
- * Apply all filters to findings in-place (returns new array).
5
+ * Apply client-side filters to findings (returns new array).
6
+ * Dismissed / false-positive filtering is handled exclusively by the backend.
9
7
  *
10
8
  * @param {Array} findings - NormalizedFinding[]
11
9
  * @param {object} opts
12
- * @param {string[]|null} opts.severity - allowed severity levels (e.g. ['critical','high'])
13
- * @param {string|null} opts.pathGlob - minimatch glob for file_path
10
+ * @param {string[]|null} opts.severity - allowed severity levels (e.g. ['critical','high'])
11
+ * @param {string|null} opts.pathGlob - minimatch glob for file_path
14
12
  * @param {string|null} opts.checkRegex - regex applied to check_id + check_name
15
- * @param {Array} opts.dismissedAlerts - from fetchDismissedAlerts()
16
- * @param {boolean} opts.includeDismissed
17
- * @returns {Array} filtered NormalizedFinding[] (dismissed field annotated)
13
+ * @returns {Array} filtered NormalizedFinding[]
18
14
  */
19
15
  export function applyFilters(findings, {
20
16
  severity = null,
21
17
  pathGlob = null,
22
18
  checkRegex = null,
23
- dismissedAlerts = [],
24
- includeDismissed = false,
25
19
  } = {}) {
26
- // Pre-compile regex — strip Python/PCRE inline flag (?i) and fold into JS flag
27
20
  let checkRe = null;
28
21
  if (checkRegex) {
29
22
  try {
@@ -37,35 +30,15 @@ export function applyFilters(findings, {
37
30
  }
38
31
  }
39
32
 
40
- // Severity set
41
33
  const sevSet = severity && severity.length > 0
42
34
  ? new Set(severity.map((s) => normalizeSeverity(s)))
43
35
  : null;
44
36
 
45
37
  const result = [];
46
38
  for (const f of findings) {
47
- // Annotate dismiss status
48
- const match = findDismissMatch(f, dismissedAlerts);
49
- f.dismissed = match !== null;
50
- if (match) {
51
- f.dismiss_info = {
52
- reason: match.reason_for_dismiss || null,
53
- comment: match.comment_for_dismiss || null,
54
- };
55
- }
56
-
57
- // Filter dismissed
58
- if (f.dismissed && !includeDismissed) continue;
59
-
60
- // Filter by severity
61
39
  if (sevSet && !sevSet.has(f.severity)) continue;
62
-
63
- // Filter by path glob
64
40
  if (pathGlob && !minimatch(f.file_path, pathGlob, { matchBase: true })) continue;
65
-
66
- // Filter by check regex
67
41
  if (checkRe && !checkRe.test(f.check_id) && !checkRe.test(f.check_name)) continue;
68
-
69
42
  result.push(f);
70
43
  }
71
44
  return result;
@@ -106,8 +106,6 @@ export function normalizeIssue(issue, category) {
106
106
  cve: cve ? String(cve) : null,
107
107
  package: packageInfo,
108
108
  metadata,
109
- dismissed: false,
110
- dismiss_info: null,
111
109
  };
112
110
  }
113
111
 
@@ -7,7 +7,6 @@ import { paginate } from './lib/paginate.js';
7
7
  import { emit } from './lib/emit.js';
8
8
  import { progress, logError } from './lib/log.js';
9
9
  import { FORMATTERS } from './formatters/index.js';
10
- import { fetchDismissedAlerts } from '../../scans/fetchDismissedAlerts.js';
11
10
 
12
11
  /**
13
12
  * codeant scans results — full orchestration.
@@ -23,7 +22,8 @@ export async function runResults(opts = {}) {
23
22
  severity,
24
23
  path: pathGlob,
25
24
  check: checkRegex,
26
- includeDismissed = false,
25
+ filterDismissed = false,
26
+ includeFalsePositives = true,
27
27
  format = 'json',
28
28
  output: outputPath = null,
29
29
  fields = null,
@@ -53,16 +53,12 @@ export async function runResults(opts = {}) {
53
53
  // 2. Parse types
54
54
  const categories = parseTypes(types);
55
55
 
56
- // 3. Fetch in parallel + dismissed alerts
56
+ // 3. Fetch in parallel
57
57
  progress(`fetching ${categories.map((c) => c.key).join(', ')}…`);
58
- const [settled, dismissedResult] = await Promise.all([
59
- Promise.allSettled(categories.map((c) => c.fetcher(repo, scanMeta.commit_id))),
60
- includeDismissed
61
- ? Promise.resolve({ success: true, dismissedAlerts: [] })
62
- : fetchDismissedAlerts(repo, 'security'),
63
- ]);
64
-
65
- const dismissedAlerts = dismissedResult.success ? (dismissedResult.dismissedAlerts ?? []) : [];
58
+ const fetchOpts = { filterDismissed, includeFalsePositives };
59
+ const settled = await Promise.allSettled(
60
+ categories.map((c) => c.fetcher(repo, scanMeta.commit_id, fetchOpts))
61
+ );
66
62
 
67
63
  // 4. Collect findings + errors
68
64
  const allFindings = [];
@@ -100,8 +96,6 @@ export async function runResults(opts = {}) {
100
96
  severity: severityList,
101
97
  pathGlob: pathGlob || null,
102
98
  checkRegex: checkRegex || null,
103
- dismissedAlerts,
104
- includeDismissed,
105
99
  });
106
100
 
107
101
  // 6. Sort
@@ -115,7 +109,8 @@ export async function runResults(opts = {}) {
115
109
  severity: severityList,
116
110
  path: pathGlob || null,
117
111
  check: checkRegex || null,
118
- include_dismissed: includeDismissed,
112
+ filter_dismissed: filterDismissed,
113
+ include_false_positives: includeFalsePositives,
119
114
  };
120
115
 
121
116
  // Rebuild summary from all filtered (pre-page) findings
@@ -153,7 +153,7 @@ function ErrorView({ message, onBack, canGoBack }) {
153
153
 
154
154
  // ─── Main ScanCenter ─────────────────────────────────────────────────────────
155
155
 
156
- export default function ScanCenter() {
156
+ export default function ScanCenter({ filterDismissed = false, includeFalsePositives = true }) {
157
157
  const { exit } = useApp();
158
158
 
159
159
  const [step, setStep] = useState(STEPS.LOADING);
@@ -198,7 +198,7 @@ export default function ScanCenter() {
198
198
 
199
199
  // ── Step 5: result type selected → fetch exactly one endpoint ──
200
200
  const handleSelectResultType = (item) =>
201
- _handleSelectResultType({ STEPS, item, selectedRepo, selectedScan, setSelectedResultType, setStep, setLoadingMsg, setError, setResults });
201
+ _handleSelectResultType({ STEPS, item, selectedRepo, selectedScan, setSelectedResultType, setStep, setLoadingMsg, setError, setResults, filterDismissed, includeFalsePositives });
202
202
 
203
203
  // ── Back navigation ──
204
204
  const goBack = {
package/src/index.js CHANGED
@@ -211,8 +211,13 @@ program
211
211
  program
212
212
  .command('scan-center')
213
213
  .description('Browse scan results interactively')
214
- .action(() => {
215
- render(React.createElement(ScanCenter));
214
+ .option('--filter-dismissed', 'Exclude dismissed alerts from results (default: false)')
215
+ .option('--no-false-positives', 'Exclude false positives from results (default: included)')
216
+ .action((options) => {
217
+ render(React.createElement(ScanCenter, {
218
+ filterDismissed: options.filterDismissed ?? false,
219
+ includeFalsePositives: options.falsePositives ?? true,
220
+ }));
216
221
  });
217
222
 
218
223
  program
@@ -2,7 +2,7 @@ import { fetchScanResults } from '../scans/fetchScanResults.js';
2
2
  import { fetchAdvancedScanResults } from '../scans/fetchAdvancedScanResults.js';
3
3
  import { fetchDismissedAlerts } from '../scans/fetchDismissedAlerts.js';
4
4
 
5
- export async function handleSelectResultType({ STEPS, item, selectedRepo, selectedScan, setSelectedResultType, setStep, setLoadingMsg, setError, setResults }) {
5
+ export async function handleSelectResultType({ STEPS, item, selectedRepo, selectedScan, setSelectedResultType, setStep, setLoadingMsg, setError, setResults, filterDismissed = false, includeFalsePositives = true }) {
6
6
  const rt = item.value;
7
7
  setSelectedResultType(rt);
8
8
  setStep(STEPS.LOADING);
@@ -10,12 +10,13 @@ export async function handleSelectResultType({ STEPS, item, selectedRepo, select
10
10
 
11
11
  const repo = selectedRepo.full_name;
12
12
  const commitId = selectedScan.commitId;
13
+ const fetchOpts = { filterDismissed, includeFalsePositives };
13
14
  let res;
14
15
 
15
16
  if (rt.kind === 'basic') {
16
- res = await fetchScanResults(repo, commitId, rt.value);
17
+ res = await fetchScanResults(repo, commitId, rt.value, fetchOpts);
17
18
  } else if (rt.kind === 'advanced') {
18
- res = await fetchAdvancedScanResults(repo, commitId, rt.value);
19
+ res = await fetchAdvancedScanResults(repo, commitId, rt.value, fetchOpts);
19
20
  } else if (rt.value === 'dismissed_alerts') {
20
21
  const r = await fetchDismissedAlerts(repo, 'security');
21
22
  res = r.success ? { success: true, issues: r.dismissedAlerts } : r;
@@ -200,9 +200,12 @@ function normalizeAdvancedIssue(item, resultType) {
200
200
  * @param {string} repo - "org/repo-name"
201
201
  * @param {string} commitId - 40-char commit SHA
202
202
  * @param {string} resultType - one of ADVANCED_RESULT_TYPES values
203
+ * @param {{ filterDismissed?: boolean, includeFalsePositives?: boolean }} [opts]
203
204
  * @returns {Promise<{ success: boolean, issues?: Array, healthyPackages?: Array, status?: string, error?: string }>}
204
205
  */
205
- export async function fetchAdvancedScanResults(repo, commitId, resultType) {
206
+ export async function fetchAdvancedScanResults(repo, commitId, resultType, opts = {}) {
207
+ const { filterDismissed = false, includeFalsePositives = true } = opts;
208
+
206
209
  if (!Object.values(ADVANCED_RESULT_TYPES).includes(resultType)) {
207
210
  return {
208
211
  success: false,
@@ -215,6 +218,8 @@ export async function fetchAdvancedScanResults(repo, commitId, resultType) {
215
218
  repo,
216
219
  commit_id: commitId,
217
220
  result_type: resultType,
221
+ filter_dismissed: filterDismissed,
222
+ include_false_positives: includeFalsePositives,
218
223
  });
219
224
 
220
225
  if (!response) {
@@ -294,8 +299,8 @@ export async function fetchAdvancedScanResults(repo, commitId, resultType) {
294
299
  }
295
300
  }
296
301
 
297
- // Filter secrets false positives
298
- if (resultType === ADVANCED_RESULT_TYPES.SECRETS) {
302
+ // Filter secrets false positives only when caller explicitly excludes them
303
+ if (resultType === ADVANCED_RESULT_TYPES.SECRETS && !includeFalsePositives) {
299
304
  issues = issues.filter((issue) => String(issue.confidence_score || '').toLowerCase() !== 'false_positive');
300
305
  }
301
306
 
@@ -309,17 +314,17 @@ export async function fetchAdvancedScanResults(repo, commitId, resultType) {
309
314
  }
310
315
  }
311
316
 
312
- export const fetchScaResults = (repo, commitId) =>
313
- fetchAdvancedScanResults(repo, commitId, ADVANCED_RESULT_TYPES.SCA);
317
+ export const fetchScaResults = (repo, commitId, opts) =>
318
+ fetchAdvancedScanResults(repo, commitId, ADVANCED_RESULT_TYPES.SCA, opts);
314
319
 
315
- export const fetchSbomResults = (repo, commitId) =>
316
- fetchAdvancedScanResults(repo, commitId, ADVANCED_RESULT_TYPES.SBOM);
320
+ export const fetchSbomResults = (repo, commitId, opts) =>
321
+ fetchAdvancedScanResults(repo, commitId, ADVANCED_RESULT_TYPES.SBOM, opts);
317
322
 
318
- export const fetchSecretsResults = (repo, commitId) =>
319
- fetchAdvancedScanResults(repo, commitId, ADVANCED_RESULT_TYPES.SECRETS);
323
+ export const fetchSecretsResults = (repo, commitId, opts) =>
324
+ fetchAdvancedScanResults(repo, commitId, ADVANCED_RESULT_TYPES.SECRETS, opts);
320
325
 
321
- export const fetchIacResults = (repo, commitId) =>
322
- fetchAdvancedScanResults(repo, commitId, ADVANCED_RESULT_TYPES.IAC);
326
+ export const fetchIacResults = (repo, commitId, opts) =>
327
+ fetchAdvancedScanResults(repo, commitId, ADVANCED_RESULT_TYPES.IAC, opts);
323
328
 
324
- export const fetchDeadCodeResults = (repo, commitId) =>
325
- fetchAdvancedScanResults(repo, commitId, ADVANCED_RESULT_TYPES.DEAD_CODE);
329
+ export const fetchDeadCodeResults = (repo, commitId, opts) =>
330
+ fetchAdvancedScanResults(repo, commitId, ADVANCED_RESULT_TYPES.DEAD_CODE, opts);
@@ -49,9 +49,12 @@ function extractCleanPath(fullPath, commitId, suffix = null) {
49
49
  * @param {string} repo - "org/repo-name"
50
50
  * @param {string} commitId - 40-char commit SHA
51
51
  * @param {string} resultType - one of VALID_RESULT_TYPES values
52
+ * @param {{ filterDismissed?: boolean, includeFalsePositives?: boolean }} [opts]
52
53
  * @returns {Promise<{ success: boolean, issues?: Array, status?: string, error?: string }>}
53
54
  */
54
- export async function fetchScanResults(repo, commitId, resultType) {
55
+ export async function fetchScanResults(repo, commitId, resultType, opts = {}) {
56
+ const { filterDismissed = false, includeFalsePositives = true } = opts;
57
+
55
58
  if (!Object.values(VALID_RESULT_TYPES).includes(resultType)) {
56
59
  return {
57
60
  success: false,
@@ -64,6 +67,8 @@ export async function fetchScanResults(repo, commitId, resultType) {
64
67
  repo,
65
68
  commit_id: commitId,
66
69
  result_type: resultType,
70
+ filter_dismissed: filterDismissed,
71
+ include_false_positives: includeFalsePositives,
67
72
  });
68
73
 
69
74
  if (!response) {
@@ -102,14 +107,14 @@ export async function fetchScanResults(repo, commitId, resultType) {
102
107
  }
103
108
  }
104
109
 
105
- export const fetchSastResults = (repo, commitId) =>
106
- fetchScanResults(repo, commitId, VALID_RESULT_TYPES.SECURITY_ISSUES);
110
+ export const fetchSastResults = (repo, commitId, opts) =>
111
+ fetchScanResults(repo, commitId, VALID_RESULT_TYPES.SECURITY_ISSUES, opts);
107
112
 
108
- export const fetchAntiPatternsResults = (repo, commitId) =>
109
- fetchScanResults(repo, commitId, VALID_RESULT_TYPES.ANTI_PATTERNS);
113
+ export const fetchAntiPatternsResults = (repo, commitId, opts) =>
114
+ fetchScanResults(repo, commitId, VALID_RESULT_TYPES.ANTI_PATTERNS, opts);
110
115
 
111
- export const fetchDocstringResults = (repo, commitId) =>
112
- fetchScanResults(repo, commitId, VALID_RESULT_TYPES.DOCSTRING);
116
+ export const fetchDocstringResults = (repo, commitId, opts) =>
117
+ fetchScanResults(repo, commitId, VALID_RESULT_TYPES.DOCSTRING, opts);
113
118
 
114
- export const fetchComplexFunctionsResults = (repo, commitId) =>
115
- fetchScanResults(repo, commitId, VALID_RESULT_TYPES.COMPLEX_FUNCTIONS);
119
+ export const fetchComplexFunctionsResults = (repo, commitId, opts) =>
120
+ fetchScanResults(repo, commitId, VALID_RESULT_TYPES.COMPLEX_FUNCTIONS, opts);
@@ -3,7 +3,7 @@ import path from 'path';
3
3
  import fs from 'fs';
4
4
  import { minimatch } from 'minimatch';
5
5
 
6
- const MAX_REVIEW_FILES = 10;
6
+ const MAX_REVIEW_FILES = 15;
7
7
  const MAX_FILE_LINES = 5000;
8
8
  const MAX_FILE_SIZE_BYTES = 200 * 1024; // 200 KB
9
9
 
@@ -184,41 +184,20 @@ class ReviewApiHelper extends CommonApiHelper {
184
184
  return true;
185
185
  });
186
186
 
187
- // Dedupe by filename, keeping all hunks, but limit unique files to MAX_REVIEW_FILES
187
+ // Dedupe by filename, keeping all hunks (no cap yet — applied after size filtering)
188
188
  const fileOrder = [];
189
189
  const fileHunks = new Map();
190
- let capped = false;
191
190
  for (const d of reviewable) {
192
191
  const f = d.filename_str;
193
192
  if (!fileHunks.has(f)) {
194
- if (fileOrder.length >= MAX_REVIEW_FILES) {
195
- capped = true;
196
- if (!seenSkipped.has(f)) {
197
- dedupedSkipped.push({ file: f, reason: `exceeded ${MAX_REVIEW_FILES}-file limit` });
198
- seenSkipped.add(f);
199
- }
200
- continue;
201
- }
202
193
  fileOrder.push(f);
203
194
  fileHunks.set(f, []);
204
195
  }
205
196
  fileHunks.get(f).push(d);
206
197
  }
207
198
 
208
- // Reconstruct a combined diff string from the filtered hunks
209
- const parts = [];
210
- for (const f of fileOrder) {
211
- const hunks = fileHunks.get(f);
212
- // Build a file-level diff header + all hunk patches
213
- const header = `diff --git a/${f} b/${f}\n--- a/${f}\n+++ b/${f}`;
214
- const hunkPatches = hunks.map(h => h.patch_str).filter(Boolean);
215
- if (hunkPatches.length > 0) {
216
- parts.push(header + '\n' + hunkPatches.join('\n'));
217
- }
218
- }
219
-
220
199
  // Use head_file_str from diffs so file content matches the diff version (avoids working tree mismatch for --last-commit etc.)
221
- const fileContents = {};
200
+ // Filter out files that exceed size or line limits before applying the cap
222
201
  const skippedLargeFiles = new Set();
223
202
  for (const f of fileOrder) {
224
203
  const hunks = fileHunks.get(f);
@@ -234,21 +213,49 @@ class ReviewApiHelper extends CommonApiHelper {
234
213
  if (lineCount > MAX_FILE_LINES) {
235
214
  skippedLargeFiles.add(f);
236
215
  dedupedSkipped.push({ file: f, reason: `too large (${lineCount} lines, max ${MAX_FILE_LINES})` });
237
- continue;
238
216
  }
239
- fileContents[f] = content;
240
217
  }
241
218
  }
242
219
 
243
- // Remove diff sections for skipped files
244
- const filteredParts = parts.filter((_, i) => !skippedLargeFiles.has(fileOrder[i]));
220
+ // Apply MAX_REVIEW_FILES cap after all filtering (binary, deleted, pattern, size)
221
+ let capped = false;
222
+ const cappedFileOrder = [];
223
+ for (const f of fileOrder) {
224
+ if (skippedLargeFiles.has(f)) continue;
225
+ if (cappedFileOrder.length >= MAX_REVIEW_FILES) {
226
+ capped = true;
227
+ if (!seenSkipped.has(f)) {
228
+ dedupedSkipped.push({ file: f, reason: `exceeded ${MAX_REVIEW_FILES}-file limit` });
229
+ seenSkipped.add(f);
230
+ }
231
+ continue;
232
+ }
233
+ cappedFileOrder.push(f);
234
+ }
235
+
236
+ // Build file contents for the capped set
237
+ const fileContents = {};
238
+ for (const f of cappedFileOrder) {
239
+ const content = fileHunks.get(f)[0]?.head_file_str || '';
240
+ if (content) fileContents[f] = content;
241
+ }
242
+
243
+ // Reconstruct a combined diff string from the capped set
244
+ const parts = [];
245
+ for (const f of cappedFileOrder) {
246
+ const hunks = fileHunks.get(f);
247
+ const header = `diff --git a/${f} b/${f}\n--- a/${f}\n+++ b/${f}`;
248
+ const hunkPatches = hunks.map(h => h.patch_str).filter(Boolean);
249
+ if (hunkPatches.length > 0) {
250
+ parts.push(header + '\n' + hunkPatches.join('\n'));
251
+ }
252
+ }
245
253
 
246
- const reviewedFiles = fileOrder.filter(f => !skippedLargeFiles.has(f));
247
254
  return {
248
- diff_content: filteredParts.join('\n'),
255
+ diff_content: parts.join('\n'),
249
256
  file_contents: fileContents,
250
257
  _meta: {
251
- reviewed_files: reviewedFiles,
258
+ reviewed_files: cappedFileOrder,
252
259
  total_changed: allUniqueFiles.size,
253
260
  skipped: dedupedSkipped,
254
261
  capped,