codeant-cli 0.5.7 → 0.5.9

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/findings.md CHANGED
@@ -49,9 +49,9 @@ codeant findings repo \
49
49
  --output codeant.sarif
50
50
  ```
51
51
 
52
- Supported repo types are `sast`, `sca`, `secrets`, `iac`, `dead_code`, `sbom`, `anti_patterns`, `docstring`, and `complex_functions`. Use `--types all` for all of them. Formats are `json`, `sarif`, `csv`, `md`, and `table`; JSON is the default.
52
+ Supported repo types are `sast`, `sca`, `secrets`, `iac`, `dead_code`, `duplicate_code`, `sbom`, `anti_patterns`, `docstring`, and `complex_functions`. Use `--types all` for all of them. Formats are `json`, `sarif`, `csv`, `md`, and `table`; JSON is the default.
53
53
 
54
- Use `--filter-dismissed` to exclude dismissed findings and `--no-false-positives` to exclude false positives. `--path`, `--check`, `--limit`, and `--offset` support agent-friendly filtering and pagination.
54
+ Use `--filter-dismissed` to exclude dismissed findings and `--no-false-positives` to exclude false positives, including ones users marked in the app. Without those flags, such findings are returned with `metadata.dismissed` or `metadata.false_positive`. `--path`, `--check`, `--limit`, and `--offset` support agent-friendly filtering and pagination.
55
55
 
56
56
  ## Organization Hotlist findings
57
57
 
@@ -75,9 +75,12 @@ codeant findings antipatterns --org CodeAnt-AI --service github
75
75
  codeant findings antipatterns \
76
76
  --org CodeAnt-AI --service github \
77
77
  --repos CodeAnt-AI/api,CodeAnt-AI/web
78
+
79
+ # Every page, merged into one list
80
+ codeant findings antipatterns --org CodeAnt-AI --service github --all
78
81
  ```
79
82
 
80
- When `--repos` is omitted, the CLI first lists the organization's repositories and sends all of them to the same aggregate anti-pattern endpoint used by the Quality Report UI.
83
+ When `--repos` is omitted, the CLI first lists the organization's repositories and sends all of them to the aggregate anti-pattern endpoint. Results are paged because an organization-wide list can exceed the API's 1 MB response limit: `--limit` is 25, 100, or 500 (default 500), `--offset` is a multiple of `--limit` (default 0), and `--all` fetches every page from `--offset` on. Responses carry `total`, `limit`, and `offset`; `summary` and `total_issues` always cover every issue.
81
84
 
82
85
  ## Cloud security findings
83
86
 
@@ -98,8 +101,9 @@ codeant findings cloud history --provider all --kind container
98
101
  codeant findings cloud list --provider aws --scan-id <scan-id> --account-id <account-id>
99
102
  codeant findings cloud get --provider aws --scan-id <scan-id> --uid <finding-uid> --cloud-service iam
100
103
 
101
- # VM and container vulnerabilities use the same list/detail flow
104
+ # VM and container vulnerabilities use the same list/detail flow (paged; --all merges every page)
102
105
  codeant findings cloud list --provider aws --kind vm --scan-id <scan-id>
106
+ codeant findings cloud list --provider aws --kind container --scan-id <scan-id> --all
103
107
  codeant findings cloud get --provider gcp --kind container --scan-id <scan-id> --uid <finding-uid>
104
108
 
105
109
  # Azure requires the tenant ID
@@ -113,7 +117,7 @@ codeant findings cloud list \
113
117
  --framework cis
114
118
  ```
115
119
 
116
- `--kind` defaults to `cspm`; use `vm` or `container` for the other Cloud Security result views. CSPM `cloud list` supports `--cloud-service`, `--severity`, `--status`, `--framework`, and `--min-days-unused`. AWS additionally supports `--exploit-attempted-only`; Azure additionally supports `--subscription-id`. CSPM responses include `findings` and `dismissed_findings`; VM/container responses preserve their UI result payload unchanged.
120
+ `--kind` defaults to `cspm`; use `vm` or `container` for the other Cloud Security result views. CSPM `cloud list` supports `--cloud-service`, `--severity`, `--status`, `--framework`, and `--min-days-unused`. AWS additionally supports `--exploit-attempted-only`; Azure additionally supports `--subscription-id`. CSPM responses include `findings` and `dismissed_findings`. VM/container responses keep the UI result payload, but `findings` is paged with the same `--limit`/`--offset`/`--all` options as anti-patterns and the response carries `total`; `findings_total` and `severity_rollup` still describe the whole scan.
117
121
 
118
122
  ## Pentest findings
119
123
 
package/mcp.md CHANGED
@@ -15,12 +15,13 @@ The CodeAnt CLI ships an MCP (Model Context Protocol) server that exposes CodeAn
15
15
  | `codeant_scans_history` | read | Recent scan runs for a repo. |
16
16
  | `codeant_scans_get` | read | Severity/category summary for one scan. |
17
17
  | `codeant_scans_results` | read | Full findings (SAST, SCA, secrets, IaC, …) for one scan. |
18
- | `codeant_scans_dismissed` | read | Dismissed alerts for a repo. |
18
+ | `codeant_scans_dismissed` | read | Dismissed alerts for a repo (any analysis type; `sast` and `anti_patterns` are accepted as aliases). |
19
+ | `codeant_scans_overrides` | read | User false-positive, confidence, and severity overrides for a repo. |
19
20
  | `codeant_hotlist_list` | read | Prioritized organization-wide Hotlist findings with stable IDs. |
20
21
  | `codeant_hotlist_get` | read | One complete Hotlist finding by stable ID. |
21
- | `codeant_findings_antipatterns` | read | Anti-pattern findings across selected or all organization repos. |
22
+ | `codeant_findings_antipatterns` | read | Anti-pattern findings across selected or all organization repos (paged: `limit`, `offset`, `all`). |
22
23
  | `codeant_cloud_scan_history` | read | AWS/Azure/GCP CSPM, VM, or container scan history. |
23
- | `codeant_cloud_findings_list` | read | Findings for one CSPM, VM, or container scan. |
24
+ | `codeant_cloud_findings_list` | read | Findings for one CSPM, VM, or container scan (VM/container paged: `limit`, `offset`, `all`). |
24
25
  | `codeant_cloud_finding_get` | read | Full detail for one cloud finding UID. |
25
26
  | `codeant_pentest_history` | read | Pentest engagement history. |
26
27
  | `codeant_pentest_issues` | read | All available issues for a pentest engagement. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeant-cli",
3
- "version": "0.5.7",
3
+ "version": "0.5.9",
4
4
  "description": "Code review CLI tool",
5
5
  "type": "module",
6
6
  "repository": {
@@ -36,6 +36,7 @@
36
36
  "./scans/fetch-results": "./src/scans/fetchScanResults.js",
37
37
  "./scans/fetch-advanced-results": "./src/scans/fetchAdvancedScanResults.js",
38
38
  "./scans/dismissed-alerts": "./src/scans/fetchDismissedAlerts.js",
39
+ "./scans/issue-overrides": "./src/scans/fetchIssueOverrides.js",
39
40
  "./hotlist": "./src/hotlist/client.js",
40
41
  "./findings/cloud": "./src/findings/cloud.js",
41
42
  "./findings/pentest": "./src/findings/pentest.js",
package/scans.md CHANGED
@@ -135,11 +135,12 @@ codeant scans results --repo <owner/repo> [options]
135
135
  | `--repo <repo>` | **(required)** Repository in `owner/repo` format |
136
136
  | `--scan <sha>` | Specific commit SHA to use |
137
137
  | `--branch <name>` | Resolve latest scan on this branch |
138
- | `--types <list>` | Comma-separated types: `sast`, `sca`, `secrets`, `iac`, `dead_code`, `sbom`, `anti_patterns`, `docstring`, `complex_functions`, `all` (default: `all`) |
138
+ | `--types <list>` | Comma-separated types: `sast`, `sca`, `secrets`, `iac`, `dead_code`, `duplicate_code`, `sbom`, `anti_patterns`, `docstring`, `complex_functions`, `all` (default: `all`) |
139
139
  | `--severity <list>` | Filter by severity (e.g. `critical,high`) |
140
140
  | `--path <glob>` | Filter by file path glob |
141
141
  | `--check <regex>` | Filter by check ID or name (regex) |
142
- | `--include-dismissed` | Include dismissed findings (excluded by default) |
142
+ | `--filter-dismissed` | Exclude dismissed findings (included by default, marked `metadata.dismissed`) |
143
+ | `--no-false-positives` | Exclude false positives, including ones users marked in the app (included by default, marked `metadata.false_positive`) |
143
144
  | `--format <fmt>` | Output format: `json`, `sarif`, `csv`, `md`, `table` (default: `json`) |
144
145
  | `--output <path>` | Write output to file instead of stdout |
145
146
  | `--fields <list>` | Project findings to a subset of fields (comma-separated) |
@@ -173,8 +174,8 @@ codeant scans results --repo acme/backend --format md
173
174
  # Output as SARIF to a file
174
175
  codeant scans results --repo acme/backend --format sarif --output results.sarif
175
176
 
176
- # Include dismissed findings
177
- codeant scans results --repo acme/backend --include-dismissed
177
+ # Only open findings: drop dismissed findings and false positives
178
+ codeant scans results --repo acme/backend --filter-dismissed --no-false-positives
178
179
 
179
180
  # Paginate through results
180
181
  codeant scans results --repo acme/backend --limit 50 --offset 100
@@ -206,7 +207,7 @@ codeant scans dismissed --repo <owner/repo> [options]
206
207
  | Option | Description |
207
208
  |--------|-------------|
208
209
  | `--repo <repo>` | **(required)** Repository in `owner/repo` format |
209
- | `--analysis-type <type>` | Analysis type: `security` or `secrets` (default: `security`) |
210
+ | `--analysis-type <type>` | `security`, `secrets`, `sca`, `iac`, `antipatterns`, `docstring`, `complex_functions`, `dead_code`, or `duplicate_code` (default: `security`). Result-type names `sast` and `anti_patterns` are accepted too. |
210
211
 
211
212
  **Examples:**
212
213
 
@@ -220,6 +221,16 @@ codeant scans dismissed --repo acme/backend --analysis-type secrets
220
221
 
221
222
  ---
222
223
 
224
+ ### `scans overrides`
225
+
226
+ List the per-finding overrides users set in the CodeAnt app: Mark/Unmark false positive (`security`, `iac`), secrets confidence (`secrets`), and Change Severity (`security`, `sca`). `scans results` already applies them.
227
+
228
+ ```bash
229
+ codeant scans overrides --repo <owner/repo> [--analysis-type security|secrets|iac|sca]
230
+ ```
231
+
232
+ ---
233
+
223
234
  ### `scans start-scan`
224
235
 
225
236
  Trigger a new analysis run for a repository.
@@ -54,6 +54,17 @@ function hotlistOptions(options) {
54
54
  };
55
55
  }
56
56
 
57
+ function addPageOptions(command) {
58
+ return command
59
+ .option('--limit <n>', 'Page size: 25, 100, or 500', Number, 500)
60
+ .option('--offset <n>', 'Start offset, a multiple of --limit', Number, 0)
61
+ .option('--all', 'Fetch every page from --offset on', false);
62
+ }
63
+
64
+ function pageOptions(options) {
65
+ return { limit: options.limit, offset: options.offset, all: options.all };
66
+ }
67
+
57
68
  function addCloudScopeOptions(command) {
58
69
  return addTenantOptions(command)
59
70
  .requiredOption('--provider <provider>', 'Cloud provider: aws, azure, or gcp')
@@ -157,9 +168,9 @@ export default function registerFindingsCommands(program, { runCmd }) {
157
168
  maxWaitSeconds: options.maxWait,
158
169
  })));
159
170
 
160
- addTenantOptions(findings.command('antipatterns').description('List anti-pattern findings across selected or all organization repositories'))
171
+ addPageOptions(addTenantOptions(findings.command('antipatterns').description('List anti-pattern findings across selected or all organization repositories')))
161
172
  .option('--repos <repos>', 'Comma-separated owner/repo values; defaults to every repository')
162
- .action((options) => runCmd(() => runOrganizationAntipatterns({ ...tenantOptions(options), repos: options.repos })));
173
+ .action((options) => runCmd(() => runOrganizationAntipatterns({ ...tenantOptions(options), ...pageOptions(options), repos: options.repos })));
163
174
 
164
175
  const cloud = findings.command('cloud').description('Cloud security CSPM, VM, and container findings');
165
176
  addTenantOptions(cloud.command('history').description('List cloud scan history, or latest scans'))
@@ -168,14 +179,14 @@ export default function registerFindingsCommands(program, { runCmd }) {
168
179
  .option('--latest', 'Return only latest CSPM scans')
169
180
  .action((options) => runCmd(() => runCloudHistory({ ...tenantOptions(options), provider: options.provider, kind: options.kind, latest: options.latest })));
170
181
 
171
- addCloudScopeOptions(cloud.command('list').description('List findings for a cloud scan'))
182
+ addPageOptions(addCloudScopeOptions(cloud.command('list').description('List findings for a cloud scan (VM/container results are paged)')))
172
183
  .option('--severity <value>', 'Severity filter')
173
184
  .option('--status <value>', 'Finding status filter')
174
185
  .option('--framework <value>', 'Compliance framework filter')
175
186
  .option('--subscription-id <id>', 'Azure subscription ID filter')
176
187
  .option('--exploit-attempted-only', 'AWS findings with validation attempts only')
177
188
  .option('--min-days-unused <n>', 'Minimum unused age in days', Number)
178
- .action((options) => runCmd(() => runCloudFindings(cloudOptions(options))));
189
+ .action((options) => runCmd(() => runCloudFindings({ ...cloudOptions(options), ...pageOptions(options) })));
179
190
 
180
191
  addCloudScopeOptions(cloud.command('get').description('Get one cloud finding with full detail'))
181
192
  .requiredOption('--uid <uid>', 'Finding UID')
@@ -1,16 +1,34 @@
1
1
  import { fetchDismissedAlerts } from '../../scans/fetchDismissedAlerts.js';
2
+ import { fetchIssueOverrides } from '../../scans/fetchIssueOverrides.js';
2
3
 
3
- /**
4
- * codeant scans dismissed --repo <repo> [--analysis-type security|secrets]
5
- */
6
- export async function runDismissed({ repo, analysisType = 'security' } = {}) {
4
+ // Result-type names (`scans results --types`) → the names dismissals and
5
+ // overrides are stored under.
6
+ const STORE_ANALYSIS_TYPES = {
7
+ sast: 'security',
8
+ security_issues: 'security',
9
+ anti_patterns: 'antipatterns',
10
+ };
11
+
12
+ export function storeAnalysisType(analysisType) {
13
+ return STORE_ANALYSIS_TYPES[analysisType] || analysisType;
14
+ }
15
+
16
+ function requireRepo(repo) {
7
17
  if (!repo) {
8
18
  const err = new Error('--repo is required');
9
19
  err.exitCode = 1;
10
20
  throw err;
11
21
  }
22
+ }
23
+
24
+ /**
25
+ * codeant scans dismissed --repo <repo> [--analysis-type <type>]
26
+ */
27
+ export async function runDismissed({ repo, analysisType = 'security' } = {}) {
28
+ requireRepo(repo);
29
+ const type = storeAnalysisType(analysisType);
12
30
 
13
- const result = await fetchDismissedAlerts(repo, analysisType);
31
+ const result = await fetchDismissedAlerts(repo, type);
14
32
  if (!result.success) {
15
33
  const err = new Error(result.error || 'Failed to fetch dismissed alerts');
16
34
  err.exitCode = 1;
@@ -19,8 +37,30 @@ export async function runDismissed({ repo, analysisType = 'security' } = {}) {
19
37
 
20
38
  return {
21
39
  repo,
22
- analysis_type: analysisType,
40
+ analysis_type: type,
23
41
  total: result.dismissedAlerts.length,
24
42
  dismissed_alerts: result.dismissedAlerts,
25
43
  };
26
44
  }
45
+
46
+ /**
47
+ * codeant scans overrides --repo <repo> [--analysis-type security|secrets|iac|sca]
48
+ */
49
+ export async function runOverrides({ repo, analysisType = 'security' } = {}) {
50
+ requireRepo(repo);
51
+ const type = storeAnalysisType(analysisType);
52
+
53
+ const result = await fetchIssueOverrides(repo, type);
54
+ if (!result.success) {
55
+ const err = new Error(result.error || 'Failed to fetch issue overrides');
56
+ err.exitCode = 1;
57
+ throw err;
58
+ }
59
+
60
+ return {
61
+ repo,
62
+ analysis_type: type,
63
+ total: Object.keys(result.overrides).length,
64
+ overrides: result.overrides,
65
+ };
66
+ }
@@ -3,10 +3,14 @@ import { runRepos } from './repos.js';
3
3
  import { runHistory } from './history.js';
4
4
  import { runGet } from './get.js';
5
5
  import { runResults } from './results.js';
6
- import { runDismissed } from './dismissed.js';
6
+ import { runDismissed, runOverrides } from './dismissed.js';
7
7
  import { runStartScan } from './start-scan.js';
8
8
  import { setQuiet, setNoColor } from './lib/log.js';
9
9
  import { setNoColor as tableSetNoColor } from './formatters/table.js';
10
+ import { intOption } from '../../utils/intOption.js';
11
+
12
+ const toCount = intOption({ min: 1 });
13
+ const toOffset = intOption();
10
14
 
11
15
  /**
12
16
  * Register all `codeant scans <verb>` subcommands.
@@ -37,7 +41,7 @@ export default function registerScansCommands(program, { runCmd }) {
37
41
  .requiredOption('--repo <repo>', 'Repository (owner/repo)')
38
42
  .option('--branch <name>', 'Filter by branch name')
39
43
  .option('--since <iso>', 'Show scans since ISO date')
40
- .option('--limit <n>', 'Max results (default: 20)', parseInt, 20)
44
+ .option('--limit <n>', 'Max results (default: 20)', toCount, 20)
41
45
  .action((opts) =>
42
46
  runCmd(() => runHistory({ repo: opts.repo, branch: opts.branch, since: opts.since, limit: opts.limit }))
43
47
  );
@@ -65,7 +69,7 @@ export default function registerScansCommands(program, { runCmd }) {
65
69
  .option('--branch <name>', 'Resolve latest scan on this branch')
66
70
  .option(
67
71
  '--types <list>',
68
- 'Comma-separated types: sast,sca,secrets,iac,dead_code,sbom,anti_patterns,docstring,complex_functions,all',
72
+ 'Comma-separated types: sast,sca,secrets,iac,dead_code,duplicate_code,sbom,anti_patterns,docstring,complex_functions,all',
69
73
  'all'
70
74
  )
71
75
  .option('--severity <list>', 'Filter by severity (e.g. critical,high)')
@@ -76,8 +80,8 @@ export default function registerScansCommands(program, { runCmd }) {
76
80
  .option('--format <fmt>', 'Output format: json|sarif|csv|md|table (default: json)', 'json')
77
81
  .option('--output <path>', 'Write output to file instead of stdout')
78
82
  .option('--fields <list>', 'Project findings to subset of fields (comma-separated)')
79
- .option('--limit <n>', 'Max findings per page (default: 100)', parseInt, 100)
80
- .option('--offset <n>', 'Pagination offset (default: 0)', parseInt, 0)
83
+ .option('--limit <n>', 'Max findings per page (default: 100)', toCount, 100)
84
+ .option('--offset <n>', 'Pagination offset (default: 0)', toOffset, 0)
81
85
  .option('--fail-fast', 'Exit 3 on first category fetch failure')
82
86
  .option('--no-color', 'Disable ANSI color (auto-disabled when not a TTY)')
83
87
  .option('--quiet', 'Suppress progress output on stderr')
@@ -117,11 +121,25 @@ export default function registerScansCommands(program, { runCmd }) {
117
121
  .command('dismissed')
118
122
  .description('List dismissed alerts for a repository')
119
123
  .requiredOption('--repo <repo>', 'Repository (owner/repo)')
120
- .option('--analysis-type <type>', 'Analysis type: security|secrets (default: security)', 'security')
124
+ .option(
125
+ '--analysis-type <type>',
126
+ 'Analysis type: security|secrets|sca|iac|antipatterns|docstring|complex_functions|dead_code|duplicate_code (default: security)',
127
+ 'security'
128
+ )
121
129
  .action((opts) =>
122
130
  runCmd(() => runDismissed({ repo: opts.repo, analysisType: opts.analysisType }))
123
131
  );
124
132
 
133
+ // ── overrides ──────────────────────────────────────────────────────────────
134
+ scans
135
+ .command('overrides')
136
+ .description('List user overrides (false positive, confidence, severity) for a repository')
137
+ .requiredOption('--repo <repo>', 'Repository (owner/repo)')
138
+ .option('--analysis-type <type>', 'Analysis type: security|secrets|iac|sca (default: security)', 'security')
139
+ .action((opts) =>
140
+ runCmd(() => runOverrides({ repo: opts.repo, analysisType: opts.analysisType }))
141
+ );
142
+
125
143
  // ── start-scan ─────────────────────────────────────────────────────────────
126
144
  scans
127
145
  .command('start-scan')
@@ -10,6 +10,7 @@ import {
10
10
  fetchSecretsResults,
11
11
  fetchIacResults,
12
12
  fetchDeadCodeResults,
13
+ fetchDuplicateCodeResults,
13
14
  } from '../../../scans/fetchAdvancedScanResults.js';
14
15
 
15
16
  /**
@@ -26,6 +27,7 @@ export const CATEGORIES = {
26
27
  secrets: { fetcher: fetchSecretsResults, kind: 'secret' },
27
28
  iac: { fetcher: fetchIacResults, kind: 'config' },
28
29
  dead_code: { fetcher: fetchDeadCodeResults, kind: 'code' },
30
+ duplicate_code: { fetcher: fetchDuplicateCodeResults, kind: 'code' },
29
31
  };
30
32
 
31
33
  /**
@@ -87,6 +87,20 @@ export function normalizeIssue(issue, category) {
87
87
  } else if (category === 'sca') {
88
88
  if (issue.cvss_score !== undefined) metadata.cvss_score = issue.cvss_score;
89
89
  if (issue.fix_version) metadata.fix_version = issue.fix_version;
90
+ } else if (category === 'duplicate_code') {
91
+ if (issue.duplicate_group !== undefined) metadata.duplicate_group = issue.duplicate_group;
92
+ if (issue.duplicate_locations) metadata.duplicate_locations = issue.duplicate_locations;
93
+ }
94
+
95
+ // Backend dispositions, present when dismissed / false positives are not filtered out.
96
+ if (issue.is_dismissed === true) {
97
+ metadata.dismissed = true;
98
+ if (issue.reason_for_dismiss) metadata.reason_for_dismiss = issue.reason_for_dismiss;
99
+ if (issue.comment_for_dismiss) metadata.comment_for_dismiss = issue.comment_for_dismiss;
100
+ }
101
+ // Secrets from older backends only carry the FALSE_POSITIVE confidence.
102
+ if (issue.is_false_positive === true || String(issue.confidence_score || '').toUpperCase() === 'FALSE_POSITIVE') {
103
+ metadata.false_positive = true;
90
104
  }
91
105
 
92
106
  const id = buildId(category, filePath, lineNumber, checkId, message);
@@ -1,6 +1,6 @@
1
1
  import { resolveCliTenant } from '../api/tenant.js';
2
2
  import { runRepos } from '../commands/scans/repos.js';
3
- import { fetchAppApi } from '../utils/fetchApi.js';
3
+ import { fetchPaginated } from './paginate.js';
4
4
 
5
5
  function splitRepos(value) {
6
6
  const values = Array.isArray(value) ? value : value ? [value] : [];
@@ -19,11 +19,12 @@ export async function runOrganizationAntipatterns(options = {}) {
19
19
  }
20
20
  if (repos.length === 0) throw new Error('No repositories are available for this organization.');
21
21
 
22
- const result = await fetchAppApi(
23
- '/explorer/quality/antipatterns',
24
- 'POST',
22
+ const result = await fetchPaginated(
23
+ '/explorer/quality/antipatterns/paginated',
25
24
  { ...tenant.requestBody, org: tenant.organization, repos },
26
25
  tenant,
26
+ 'antipatterns',
27
+ options,
27
28
  );
28
29
  return { tenant: tenant.requestBody, ...result };
29
30
  }
@@ -1,5 +1,6 @@
1
1
  import { resolveCliTenant } from '../api/tenant.js';
2
2
  import { fetchAppApi } from '../utils/fetchApi.js';
3
+ import { fetchPaginated } from './paginate.js';
3
4
 
4
5
  const PROVIDERS = new Set(['aws', 'azure', 'gcp']);
5
6
  const KINDS = new Set(['cspm', 'vm', 'container']);
@@ -53,18 +54,13 @@ function optionalFilters(provider, options = {}) {
53
54
  return filters;
54
55
  }
55
56
 
56
- async function request(provider, kind, action, options = {}, extra = {}, resolvedTenant = null) {
57
+ async function request(provider, kind, action, options = {}, extra = {}, resolvedTenant = null, pageKey = null) {
57
58
  const tenant = resolvedTenant || await resolveCliTenant(options);
58
- const data = await fetchAppApi(
59
- endpoint(provider, kind, action),
60
- 'POST',
61
- {
62
- ...tenant.requestBody,
63
- username: tenant.organization,
64
- ...extra,
65
- },
66
- tenant,
67
- );
59
+ const path = endpoint(provider, kind, action);
60
+ const body = { ...tenant.requestBody, username: tenant.organization, ...extra };
61
+ const data = pageKey
62
+ ? await fetchPaginated(path, body, tenant, pageKey, options)
63
+ : await fetchAppApi(path, 'POST', body, tenant);
68
64
  return { tenant: tenant.requestBody, provider, kind, ...data };
69
65
  }
70
66
 
@@ -96,10 +92,14 @@ export async function runCloudFindings(options = {}) {
96
92
  const provider = normalizeProvider(options.provider);
97
93
  const kind = normalizeKind(options.kind);
98
94
  const scanId = requireValue(options.scanId, '--scan-id');
99
- return request(provider, kind, kind === 'cspm' ? 'findings' : 'results', options, {
95
+ // VM/container scans can exceed the API's 1 MB response cap, so they are paged.
96
+ if (kind !== 'cspm') {
97
+ return request(provider, kind, 'results/paginated', options, { scan_id: scanId }, null, 'findings');
98
+ }
99
+ return request(provider, kind, 'findings', options, {
100
100
  scan_id: scanId,
101
- ...(kind === 'cspm' ? providerScope(provider, options) : {}),
102
- ...(kind === 'cspm' ? optionalFilters(provider, options) : {}),
101
+ ...providerScope(provider, options),
102
+ ...optionalFilters(provider, options),
103
103
  });
104
104
  }
105
105
 
@@ -0,0 +1,32 @@
1
+ import { fetchAppApi } from '../utils/fetchApi.js';
2
+
3
+ const LIMITS = [25, 100, 500];
4
+ // Every page re-reads the whole result server-side, so fetch a few at a time.
5
+ const CONCURRENCY = 5;
6
+
7
+ function pageParams(options) {
8
+ const limit = options.limit === undefined ? 500 : Number(options.limit);
9
+ const offset = options.offset === undefined ? 0 : Number(options.offset);
10
+ if (!LIMITS.includes(limit)) throw new Error(`--limit must be one of: ${LIMITS.join(', ')}.`);
11
+ if (!Number.isInteger(offset) || offset < 0 || offset % limit) {
12
+ throw new Error('--offset must be a non-negative multiple of --limit.');
13
+ }
14
+ return { limit, offset };
15
+ }
16
+
17
+ // One page of `key` from a backend /paginated endpoint (with its `total`), or
18
+ // with `all` every page from `offset` on, merged into one list.
19
+ export async function fetchPaginated(endpoint, body, tenant, key, options = {}) {
20
+ const { limit, offset } = pageParams(options);
21
+ const fetchPage = (pageOffset) => fetchAppApi(endpoint, 'POST', { ...body, limit, offset: pageOffset }, tenant);
22
+ const first = await fetchPage(offset);
23
+ if (!options.all) return first;
24
+
25
+ const offsets = [];
26
+ for (let next = offset + limit; next < first.total; next += limit) offsets.push(next);
27
+ const pages = [first];
28
+ for (let index = 0; index < offsets.length; index += CONCURRENCY) {
29
+ pages.push(...await Promise.all(offsets.slice(index, index + CONCURRENCY).map(fetchPage)));
30
+ }
31
+ return { ...first, [key]: pages.flatMap((page) => page[key] || []) };
32
+ }
package/src/index.js CHANGED
@@ -20,6 +20,7 @@ import ScanCenter from './components/ScanCenter.js';
20
20
  import Welcome from './components/Welcome.js';
21
21
  import * as scm from './scm/index.js';
22
22
  import { setConfigValue } from './utils/config.js';
23
+ import { intOption } from './utils/intOption.js';
23
24
  import { track, shutdown as analyticsShutdown, isTelemetryDisabled } from './utils/analytics.js';
24
25
  import registerScansCommands from './commands/scans/index.js';
25
26
  import registerSettingsCommands from './commands/settings/index.js';
@@ -31,6 +32,9 @@ import registerFindingsCommands from './commands/findings/index.js';
31
32
  const require = createRequire(import.meta.url);
32
33
  const pkg = require('../package.json');
33
34
 
35
+ const toCount = intOption({ min: 1 });
36
+ const toOffset = intOption();
37
+
34
38
  // Split comma-separated globs while preserving commas inside {} brace expansions (e.g. "*.{js,ts}")
35
39
  function splitGlobs(input) {
36
40
  const parts = [];
@@ -67,7 +71,7 @@ program
67
71
  .option('--uncommitted', 'Scan only uncommitted changes (staged + unstaged + untracked)')
68
72
  .option('--staged', 'Scan only staged files')
69
73
  .option('--last-commit', 'Scan last commit')
70
- .option('--last-n-commits <n>', 'Scan last n commits (max 5)', parseInt)
74
+ .option('--last-n-commits <n>', 'Scan last n commits (max 5)', toCount)
71
75
  .option('--base <branch>', 'Compare against a specific base branch (e.g. --base develop)')
72
76
  .option('--base-commit <commit>', 'Compare against a specific commit (e.g. --base-commit HEAD~3)')
73
77
  .option('--include <paths>', 'Comma-separated list of file paths glob patterns to include')
@@ -118,7 +122,7 @@ program
118
122
  .option('--uncommitted', 'Review only uncommitted changes (staged + unstaged + untracked)')
119
123
  .option('--staged', 'Review only staged files')
120
124
  .option('--last-commit', 'Review last commit')
121
- .option('--last-n-commits <n>', 'Review last n commits (max 5)', parseInt)
125
+ .option('--last-n-commits <n>', 'Review last n commits (max 5)', toCount)
122
126
  .option('--base <branch>', 'Compare against a specific base branch (e.g. --base develop)')
123
127
  .option('--base-commit <commit>', 'Compare against a specific commit (e.g. --base-commit HEAD~3)')
124
128
  .option('--fail-on <level>', 'Fail on issues at or above this level: BLOCKER, CRITICAL, MAJOR, MINOR, INFO (default: CRITICAL)', 'CRITICAL')
@@ -299,8 +303,8 @@ program
299
303
  .option('--source-branch <branch>', 'Filter by source branch (partial match)')
300
304
  .option('--author <login>', 'Filter by author (fuzzy match)')
301
305
  .option('--state <state>', 'open or closed (default: open)', 'open')
302
- .option('--limit <n>', 'Max results (default: 20, max: 100)', parseInt, 20)
303
- .option('--offset <n>', 'Pagination offset', parseInt, 0)
306
+ .option('--limit <n>', 'Max results (default: 20, max: 100)', intOption({ min: 1, max: 100 }), 20)
307
+ .option('--offset <n>', 'Pagination offset', toOffset, 0)
304
308
  .action((options) => {
305
309
  const opts = resolveRepoOpts(options);
306
310
  runCmd(() => scm.listPullRequests({
@@ -315,7 +319,7 @@ program
315
319
  .option('--name <repo>', 'Repository (owner/repo)')
316
320
  .option('--remote <provider>', 'github, gitlab, bitbucket, azure')
317
321
  .option('--default-branch <branch>', 'Default branch name')
318
- .requiredOption('--pr-number <n>', 'PR number', parseInt)
322
+ .requiredOption('--pr-number <n>', 'PR number', toCount)
319
323
  .action((options) => {
320
324
  const opts = resolveRepoOpts(options);
321
325
  runCmd(() => scm.getPullRequest({
@@ -329,7 +333,7 @@ program
329
333
  .option('--name <repo>', 'Repository (owner/repo)')
330
334
  .option('--remote <provider>', 'github, gitlab, bitbucket, azure')
331
335
  .option('--default-branch <branch>', 'Default branch name')
332
- .requiredOption('--pr-number <n>', 'PR number', parseInt)
336
+ .requiredOption('--pr-number <n>', 'PR number', toCount)
333
337
  .option('--codeant-generated <bool>', 'Filter by CodeAnt authorship (true/false)', (v) => v === 'true')
334
338
  .option('--addressed', 'Filter by addressed/resolved status')
335
339
  .option('--created-after <date>', 'ISO 8601 date filter')
@@ -347,8 +351,8 @@ program
347
351
  .description('Resolve a conversation/comment thread on a PR')
348
352
  .option('--name <repo>', 'Repository (owner/repo)')
349
353
  .option('--remote <provider>', 'github, gitlab, bitbucket, azure')
350
- .requiredOption('--pr-number <n>', 'PR number', parseInt)
351
- .option('--comment-id <id>', 'Comment ID (GitHub, Bitbucket)', parseInt)
354
+ .requiredOption('--pr-number <n>', 'PR number', toCount)
355
+ .option('--comment-id <id>', 'Comment ID (GitHub, Bitbucket)', toCount)
352
356
  .option('--thread-id <id>', 'Thread/node ID (GitHub GraphQL, Azure)')
353
357
  .option('--discussion-id <id>', 'Discussion ID (GitLab)')
354
358
  .action((options) => {
@@ -370,10 +374,10 @@ program
370
374
  .option('--name <repo>', 'Repository (owner/repo)')
371
375
  .option('--remote <provider>', 'github, gitlab, bitbucket, azure')
372
376
  .option('--default-branch <branch>', 'Default branch name')
373
- .option('--pr-number <n>', 'Filter by PR number', parseInt)
377
+ .option('--pr-number <n>', 'Filter by PR number', toCount)
374
378
  .option('--status <status>', 'Filter by status (PENDING, COMPLETED, FAILED, etc.)')
375
- .option('--limit <n>', 'Max results (default: 20)', parseInt, 20)
376
- .option('--offset <n>', 'Pagination offset', parseInt, 0)
379
+ .option('--limit <n>', 'Max results (default: 20)', toCount, 20)
380
+ .option('--offset <n>', 'Pagination offset', toOffset, 0)
377
381
  .action((options) => {
378
382
  const opts = resolveRepoOpts(options);
379
383
  runCmd(() => scm.listCodeReviews({
@@ -387,7 +391,7 @@ program
387
391
  .description('Get detailed information for a specific code review')
388
392
  .option('--name <repo>', 'Repository (owner/repo)')
389
393
  .option('--remote <provider>', 'github, gitlab, bitbucket, azure')
390
- .requiredOption('--pr-number <n>', 'PR number', parseInt)
394
+ .requiredOption('--pr-number <n>', 'PR number', toCount)
391
395
  .requiredOption('--review-id <id>', 'Code review ID')
392
396
  .action((options) => {
393
397
  const opts = resolveRepoOpts(options);
@@ -405,7 +409,7 @@ program
405
409
  .option('--name <repo>', 'Repository (owner/repo)')
406
410
  .option('--remote <provider>', 'github, gitlab, bitbucket, azure')
407
411
  .requiredOption('--query <term>', 'Search term')
408
- .option('--limit <n>', 'Max results (default: 10, max: 50)', parseInt, 10)
412
+ .option('--limit <n>', 'Max results (default: 10, max: 50)', intOption({ min: 1, max: 50 }), 10)
409
413
  .option('--include-addressed', 'Include resolved comments (default: false)')
410
414
  .option('--created-after <date>', 'ISO 8601 date filter')
411
415
  .action((options) => {
package/src/mcp/server.js CHANGED
@@ -8,7 +8,7 @@ import { runRepos } from '../commands/scans/repos.js';
8
8
  import { runHistory } from '../commands/scans/history.js';
9
9
  import { runGet } from '../commands/scans/get.js';
10
10
  import { runResults } from '../commands/scans/results.js';
11
- import { runDismissed } from '../commands/scans/dismissed.js';
11
+ import { runDismissed, runOverrides } from '../commands/scans/dismissed.js';
12
12
  import { runStartScan } from '../commands/scans/start-scan.js';
13
13
  import { runReviewHeadless } from '../reviewHeadless.js';
14
14
  import * as scm from '../scm/index.js';
@@ -164,12 +164,12 @@ export async function startMcpServer() {
164
164
  repo: z.string().describe('Repository in owner/repo form.'),
165
165
  scan: z.string().optional().describe('Specific commit SHA.'),
166
166
  branch: z.string().optional().describe('Resolve the latest scan on this branch.'),
167
- types: z.string().optional().describe('Comma-separated types: sast,sca,secrets,iac,dead_code,sbom,anti_patterns,docstring,complex_functions,all (default "all").'),
167
+ types: z.string().optional().describe('Comma-separated types: sast,sca,secrets,iac,dead_code,duplicate_code,sbom,anti_patterns,docstring,complex_functions,all (default "all").'),
168
168
  severity: z.string().optional().describe('Comma-separated severities (e.g. "critical,high").'),
169
169
  path: z.string().optional().describe('File path glob filter.'),
170
170
  check: z.string().optional().describe('Filter by check ID or name (regex).'),
171
- filterDismissed: z.boolean().optional().describe('Exclude dismissed findings (default false).'),
172
- includeFalsePositives: z.boolean().optional().describe('Include false positives (default true).'),
171
+ filterDismissed: z.boolean().optional().describe('Exclude dismissed findings (default false). When false, dismissed findings carry metadata.dismissed.'),
172
+ includeFalsePositives: z.boolean().optional().describe('Include false positives, including user-marked ones (default true). When true, they carry metadata.false_positive.'),
173
173
  fields: z.string().optional().describe('Project findings to a subset of fields (comma-separated).'),
174
174
  limit: z.number().int().positive().max(500).optional().describe('Max findings per page (default 100).'),
175
175
  offset: z.number().int().nonnegative().optional().describe('Pagination offset (default 0).'),
@@ -212,7 +212,7 @@ export async function startMcpServer() {
212
212
  description: 'List dismissed alerts (false positives, accepted risk, etc.) for a repository. Useful when triaging to avoid re-surfacing already-handled findings.',
213
213
  inputSchema: {
214
214
  repo: z.string().describe('Repository in owner/repo form.'),
215
- analysisType: z.enum(['security', 'secrets']).optional().describe('Analysis type (default "security").'),
215
+ analysisType: z.enum(['security', 'sast', 'secrets', 'sca', 'iac', 'antipatterns', 'anti_patterns', 'docstring', 'complex_functions', 'dead_code', 'duplicate_code']).optional().describe('Analysis type (default "security"; sast and anti_patterns are aliases).'),
216
216
  },
217
217
  annotations: READ,
218
218
  },
@@ -221,6 +221,22 @@ export async function startMcpServer() {
221
221
  }
222
222
  );
223
223
 
224
+ server.registerTool(
225
+ 'codeant_scans_overrides',
226
+ {
227
+ title: 'List user issue overrides',
228
+ description: 'List per-finding overrides users set in the CodeAnt app for a repository: Mark/Unmark false positive (security, iac), secrets confidence, and Change Severity (security, sca). Results from codeant_scans_results already apply them; use this to explain why a finding is hidden or re-rated.',
229
+ inputSchema: {
230
+ repo: z.string().describe('Repository in owner/repo form.'),
231
+ analysisType: z.enum(['security', 'secrets', 'iac', 'sca']).optional().describe('Analysis type (default "security").'),
232
+ },
233
+ annotations: READ,
234
+ },
235
+ async ({ repo, analysisType }) => {
236
+ try { return ok(await runOverrides({ repo, analysisType: analysisType ?? 'security' })); } catch (err) { return fail(err); }
237
+ }
238
+ );
239
+
224
240
  // ─── Organization Hotlist findings (read-only) ──────────────────────────
225
241
  server.registerTool(
226
242
  'codeant_hotlist_list',
@@ -273,12 +289,15 @@ export async function startMcpServer() {
273
289
  'codeant_findings_antipatterns',
274
290
  {
275
291
  title: 'List organization anti-pattern findings',
276
- description: 'Fetch anti-pattern findings across selected repositories, or every repository in the organization when repos is omitted.',
292
+ description: 'Fetch anti-pattern findings across selected repositories, or every repository in the organization when repos is omitted. Results are paged; the response carries total.',
277
293
  inputSchema: {
278
294
  org: z.string().optional(),
279
295
  service: z.enum(['github', 'gitlab', 'bitbucket', 'azuredevops']).optional(),
280
296
  providerBaseUrl: z.string().url().optional(),
281
297
  repos: z.array(z.string()).optional().describe('Repositories in owner/repo form. Omit to query every repository.'),
298
+ limit: z.union([z.literal(25), z.literal(100), z.literal(500)]).optional().describe('Page size (default 500).'),
299
+ offset: z.number().int().nonnegative().optional().describe('Start offset, a multiple of limit (default 0).'),
300
+ all: z.boolean().optional().describe('Fetch every page from offset on instead of one page.'),
282
301
  },
283
302
  annotations: READ,
284
303
  },
@@ -311,7 +330,7 @@ export async function startMcpServer() {
311
330
  'codeant_cloud_findings_list',
312
331
  {
313
332
  title: 'List cloud security findings',
314
- description: 'Fetch findings for one AWS, Azure, or GCP CSPM, VM, or container scan using the same endpoint as the app.',
333
+ description: 'Fetch findings for one AWS, Azure, or GCP CSPM, VM, or container scan. VM and container results are paged (limit/offset/all) and carry total.',
315
334
  inputSchema: {
316
335
  org: z.string().optional(),
317
336
  service: z.enum(['github', 'gitlab', 'bitbucket', 'azuredevops']).optional(),
@@ -329,6 +348,9 @@ export async function startMcpServer() {
329
348
  subscriptionId: z.string().optional(),
330
349
  exploitAttemptedOnly: z.boolean().optional(),
331
350
  minDaysUnused: z.number().int().nonnegative().optional(),
351
+ limit: z.union([z.literal(25), z.literal(100), z.literal(500)]).optional().describe('Page size (default 500).'),
352
+ offset: z.number().int().nonnegative().optional().describe('Start offset, a multiple of limit (default 0).'),
353
+ all: z.boolean().optional().describe('Fetch every page from offset on instead of one page.'),
332
354
  },
333
355
  annotations: READ,
334
356
  },
@@ -6,8 +6,23 @@ export const ADVANCED_RESULT_TYPES = {
6
6
  SECRETS: 'secrets',
7
7
  IAC: 'iac',
8
8
  DEAD_CODE: 'dead_code',
9
+ DUPLICATE_CODE: 'duplicate_code',
9
10
  };
10
11
 
12
+ const BACKEND_MARKERS = ['is_dismissed', 'reason_for_dismiss', 'comment_for_dismiss', 'is_false_positive'];
13
+
14
+ /**
15
+ * Dismissed / false-positive markers the backend stamps on each finding. Kept
16
+ * when a finding is rebuilt so callers can hide or show those findings.
17
+ */
18
+ function backendMarkers(item) {
19
+ const markers = {};
20
+ for (const key of BACKEND_MARKERS) {
21
+ if (item?.[key] !== undefined) markers[key] = item[key];
22
+ }
23
+ return markers;
24
+ }
25
+
11
26
  const EXTRA_DEAD_CODE_MESSAGES = {
12
27
  S1481: 'Unused local variable',
13
28
  S1854: 'Unused assignment',
@@ -74,12 +89,16 @@ function flattenDeadCode(deadCodeData) {
74
89
  name,
75
90
  confidence: confMatch ? confMatch[1] : '90',
76
91
  check_name: `Unused ${issueType}: ${name}`,
92
+ ...backendMarkers(issue),
77
93
  });
78
94
  });
79
95
  });
80
96
 
81
- // JS dead code — unused files
97
+ // JS dead code — unused files (plain path strings; the backend lists dismissed ones separately)
82
98
  const jsDeadCode = deadCodeData.js_dead_code || {};
99
+ const dismissedFiles = Array.isArray(jsDeadCode.dismissed_unused_files)
100
+ ? new Set(jsDeadCode.dismissed_unused_files)
101
+ : null;
83
102
  (jsDeadCode.unused_files || []).forEach((filePath) => {
84
103
  if (!filePath) return;
85
104
  flat.push({
@@ -90,6 +109,7 @@ function flattenDeadCode(deadCodeData) {
90
109
  severity: 'warning',
91
110
  type: 'unused_file',
92
111
  check_name: 'Unused file',
112
+ ...(dismissedFiles ? { is_dismissed: dismissedFiles.has(filePath) } : {}),
93
113
  });
94
114
  });
95
115
 
@@ -110,6 +130,7 @@ function flattenDeadCode(deadCodeData) {
110
130
  type: 'unused_export',
111
131
  name: exp.name,
112
132
  check_name: `Unused export: ${exp.name || 'unknown'}`,
133
+ ...backendMarkers(exp),
113
134
  });
114
135
  });
115
136
  });
@@ -124,7 +145,8 @@ function flattenDeadCode(deadCodeData) {
124
145
  const messageId = issue['message-id'] || '';
125
146
  const msgKey = messageId.includes(':') ? messageId.split(':')[1] : messageId;
126
147
  flat.push({
127
- file_path: extractRelativeFilePath(filePath),
148
+ // Keys end with the per-file result name (".../app.py/dead_code.json").
149
+ file_path: extractRelativeFilePath(filePath).replace(/\/[^/]+\.json$/, ''),
128
150
  line_number: issue.line_number || 0,
129
151
  issue_text: issue.issue_text || EXTRA_DEAD_CODE_MESSAGES[msgKey] || 'Dead code detected',
130
152
  message: issue.issue_text || EXTRA_DEAD_CODE_MESSAGES[msgKey] || 'Dead code detected',
@@ -133,6 +155,7 @@ function flattenDeadCode(deadCodeData) {
133
155
  rule_id: msgKey,
134
156
  confidence: issue.confidence || '90',
135
157
  check_name: EXTRA_DEAD_CODE_MESSAGES[msgKey] || 'Dead code detected',
158
+ ...backendMarkers(issue),
136
159
  });
137
160
  });
138
161
  });
@@ -141,6 +164,49 @@ function flattenDeadCode(deadCodeData) {
141
164
  return flat;
142
165
  }
143
166
 
167
+ /** "[12-40]" → [12, 40]; anything unparsable → []. */
168
+ function parseWindow(window) {
169
+ const numbers = String(window ?? '').match(/\d+/g) || [];
170
+ return numbers.slice(0, 2).map(Number);
171
+ }
172
+
173
+ /**
174
+ * Flatten duplicate-code clone groups (each a list of { fileName, window }
175
+ * members) into one finding per member, linked by their group index.
176
+ */
177
+ function flattenDuplicateCode(groups) {
178
+ if (!Array.isArray(groups)) return [];
179
+ const flat = [];
180
+ groups.forEach((group, groupIndex) => {
181
+ const members = Array.isArray(group) ? group.filter((m) => m && typeof m === 'object') : [];
182
+ const locations = members.map((m) => {
183
+ const [start, end] = parseWindow(m.window);
184
+ return { file_path: extractRelativeFilePath(cleanLeadingSlash(m.fileName)), start, end };
185
+ });
186
+ members.forEach((member, i) => {
187
+ const { file_path, start, end } = locations[i];
188
+ const others = locations
189
+ .filter((_, j) => j !== i)
190
+ .map((o) => (o.start ? `${o.file_path}:${o.start}${o.end ? `-${o.end}` : ''}` : o.file_path));
191
+ const message = `Duplicated block (${members.length} locations)`;
192
+ flat.push({
193
+ file_path,
194
+ line_number: start || 1,
195
+ file_line_range: end ? [start, end] : [start || 1],
196
+ check_id: 'duplicate-code',
197
+ check_name: message,
198
+ issue_text: message,
199
+ message,
200
+ severity: 'low',
201
+ duplicate_group: member.originalIndex ?? groupIndex,
202
+ duplicate_locations: others,
203
+ ...backendMarkers(member),
204
+ });
205
+ });
206
+ });
207
+ return flat;
208
+ }
209
+
144
210
  /**
145
211
  * Normalize a single advanced issue to a consistent shape.
146
212
  */
@@ -163,7 +229,8 @@ function normalizeAdvancedIssue(item, resultType) {
163
229
  item.vulnerability_id || item.cve_id || item.advisory_id ||
164
230
  (item.package_name ? `Vulnerability in ${item.package_name}` : null) ||
165
231
  item.title || item.description || 'Package vulnerability detected';
166
- normalized.severity = item.severity || 'medium';
232
+ // SCA findings carry severity in ratings (where Change Severity overrides land).
233
+ normalized.severity = item.severity || item.ratings?.[0]?.severity || 'medium';
167
234
  break;
168
235
  case ADVANCED_RESULT_TYPES.SBOM:
169
236
  normalized.check_name = item.package_name
@@ -187,6 +254,10 @@ function normalizeAdvancedIssue(item, resultType) {
187
254
  item.check_name || item.name || item.function_name || item.symbol_name || item.description || 'Unused code detected';
188
255
  normalized.severity = item.severity || 'low';
189
256
  break;
257
+ case ADVANCED_RESULT_TYPES.DUPLICATE_CODE:
258
+ normalized.check_name = item.check_name || 'Duplicated block';
259
+ normalized.severity = item.severity || 'low';
260
+ break;
190
261
  default:
191
262
  normalized.check_name = item.description || item.message || item.name || 'Issue detected';
192
263
  }
@@ -195,7 +266,7 @@ function normalizeAdvancedIssue(item, resultType) {
195
266
  }
196
267
 
197
268
  /**
198
- * Fetch advanced scan results (SCA, SBOM, secrets, IaC, dead code).
269
+ * Fetch advanced scan results (SCA, SBOM, secrets, IaC, dead code, duplicate code).
199
270
  *
200
271
  * @param {string} repo - "org/repo-name"
201
272
  * @param {string} commitId - 40-char commit SHA
@@ -237,6 +308,8 @@ export async function fetchAdvancedScanResults(repo, commitId, resultType, opts
237
308
  resultsData = response.secrets;
238
309
  } else if (resultType === ADVANCED_RESULT_TYPES.DEAD_CODE) {
239
310
  resultsData = flattenDeadCode(response.dead_code);
311
+ } else if (resultType === ADVANCED_RESULT_TYPES.DUPLICATE_CODE) {
312
+ resultsData = flattenDuplicateCode(response.results);
240
313
  } else if (resultType === ADVANCED_RESULT_TYPES.SCA) {
241
314
  const scaResults = response.results;
242
315
  if (scaResults && typeof scaResults === 'object' && !Array.isArray(scaResults) && scaResults.all_vulnerabilities !== undefined) {
@@ -268,6 +341,7 @@ export async function fetchAdvancedScanResults(repo, commitId, resultType, opts
268
341
  guideline: check.guideline || '',
269
342
  code_block: check.code_block || [],
270
343
  resource: check.resource || '',
344
+ ...backendMarkers(check),
271
345
  });
272
346
  });
273
347
  });
@@ -328,3 +402,6 @@ export const fetchIacResults = (repo, commitId, opts) =>
328
402
 
329
403
  export const fetchDeadCodeResults = (repo, commitId, opts) =>
330
404
  fetchAdvancedScanResults(repo, commitId, ADVANCED_RESULT_TYPES.DEAD_CODE, opts);
405
+
406
+ export const fetchDuplicateCodeResults = (repo, commitId, opts) =>
407
+ fetchAdvancedScanResults(repo, commitId, ADVANCED_RESULT_TYPES.DUPLICATE_CODE, opts);
@@ -29,7 +29,21 @@ export async function fetchDismissedAlerts(repo, analysisType = 'security') {
29
29
  const dismissedAlerts = [];
30
30
 
31
31
  for (const [issueKey, dismissInfo] of Object.entries(dismissData)) {
32
- if (!issueKey.includes('||::||')) continue;
32
+ if (!issueKey.includes('||::||')) {
33
+ // SCA ("pkg@version[::manifest::cve]") and duplicate-code (group index)
34
+ // keys have no file/line parts; keep them so the count is complete.
35
+ dismissedAlerts.push({
36
+ file_path: '',
37
+ context_code_block: '',
38
+ test_id: '',
39
+ line_number: 0,
40
+ type: '',
41
+ issue_key: issueKey,
42
+ reason_for_dismiss: dismissInfo?.reason_for_dismiss || '',
43
+ comment_for_dismiss: dismissInfo?.comment_for_dismiss || '',
44
+ });
45
+ continue;
46
+ }
33
47
 
34
48
  const parts = issueKey.split('||::||');
35
49
  let file_path = parts[0] || '';
@@ -0,0 +1,36 @@
1
+ import { fetchApi } from '../utils/fetchApi.js';
2
+
3
+ /**
4
+ * Fetch the per-finding user overrides the web app saves for a repository
5
+ * (Mark/Unmark false positive, secrets confidence, Change Severity).
6
+ *
7
+ * Override keys and patches by analysis type:
8
+ * security — "id::<tracking id>|rule::<test_id>||::||<result key>||::||<line>" → { false_positive, severity }
9
+ * secrets — "<path>||::||<hashed_secret or line>||::||<type>" → { confidence_score }
10
+ * iac — "<file_path>||::||<check_id>||::||<start>-<end>" → { false_positive }
11
+ * sca — "<package>@<version>" → { severity }
12
+ *
13
+ * @param {string} repo - "org/repo-name"
14
+ * @param {string} analysisType - security | secrets | iac | sca
15
+ * @returns {Promise<{ success: boolean, overrides?: Object, error?: string }>}
16
+ */
17
+ export async function fetchIssueOverrides(repo, analysisType) {
18
+ try {
19
+ const response = await fetchApi('/extension/scans2/change-issue-details/get', 'POST', {
20
+ repo,
21
+ analysis_type: analysisType,
22
+ });
23
+
24
+ if (!response) {
25
+ return { success: false, error: 'Failed to connect to CodeAnt server' };
26
+ }
27
+
28
+ if (response.status === 'error') {
29
+ return { success: false, error: response.message || 'Failed to fetch issue overrides' };
30
+ }
31
+
32
+ return { success: true, overrides: response.data || {} };
33
+ } catch (error) {
34
+ return { success: false, error: error.message || 'Failed to fetch issue overrides' };
35
+ }
36
+ }
@@ -70,7 +70,6 @@ export async function fetchScanResults(repo, commitId, resultType, opts = {}) {
70
70
  filter_dismissed: filterDismissed,
71
71
  include_false_positives: includeFalsePositives,
72
72
  });
73
- console.error("dropship: ", JSON.stringify(response, null, 2));
74
73
 
75
74
  if (!response) {
76
75
  return { success: false, error: 'Failed to connect to CodeAnt server' };
@@ -0,0 +1,18 @@
1
+ import { InvalidArgumentError } from 'commander';
2
+
3
+ /**
4
+ * Commander parser for an integer option within [min, max]. Commander passes the
5
+ * option's default as the parser's second argument, so a bare `parseInt` would
6
+ * use it as the radix (`--limit 30` with default 20 → 60).
7
+ */
8
+ export function intOption({ min = 0, max = Infinity } = {}) {
9
+ return (value) => {
10
+ const n = Number(value);
11
+ if (!Number.isInteger(n) || n < min || n > max) {
12
+ throw new InvalidArgumentError(
13
+ max === Infinity ? `Must be an integer of at least ${min}.` : `Must be an integer from ${min} to ${max}.`
14
+ );
15
+ }
16
+ return n;
17
+ };
18
+ }