codeant-cli 0.5.8 → 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 +2 -2
- package/mcp.md +2 -1
- package/package.json +2 -1
- package/scans.md +16 -5
- package/src/commands/scans/dismissed.js +46 -6
- package/src/commands/scans/index.js +24 -6
- package/src/commands/scans/lib/categories.js +2 -0
- package/src/commands/scans/lib/normalize.js +14 -0
- package/src/index.js +17 -13
- package/src/mcp/server.js +21 -5
- package/src/scans/fetchAdvancedScanResults.js +81 -4
- package/src/scans/fetchDismissedAlerts.js +15 -1
- package/src/scans/fetchIssueOverrides.js +36 -0
- package/src/scans/fetchScanResults.js +0 -1
- package/src/utils/intOption.js +18 -0
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
|
|
package/mcp.md
CHANGED
|
@@ -15,7 +15,8 @@ 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
22
|
| `codeant_findings_antipatterns` | read | Anti-pattern findings across selected or all organization repos (paged: `limit`, `offset`, `all`). |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeant-cli",
|
|
3
|
-
"version": "0.5.
|
|
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
|
-
| `--
|
|
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
|
-
#
|
|
177
|
-
codeant scans results --repo acme/backend --
|
|
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>` |
|
|
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.
|
|
@@ -1,16 +1,34 @@
|
|
|
1
1
|
import { fetchDismissedAlerts } from '../../scans/fetchDismissedAlerts.js';
|
|
2
|
+
import { fetchIssueOverrides } from '../../scans/fetchIssueOverrides.js';
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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,
|
|
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:
|
|
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)',
|
|
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)',
|
|
80
|
-
.option('--offset <n>', 'Pagination offset (default: 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(
|
|
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);
|
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)',
|
|
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)',
|
|
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)',
|
|
303
|
-
.option('--offset <n>', 'Pagination offset',
|
|
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',
|
|
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',
|
|
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',
|
|
351
|
-
.option('--comment-id <id>', 'Comment ID (GitHub, Bitbucket)',
|
|
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',
|
|
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)',
|
|
376
|
-
.option('--offset <n>', 'Pagination offset',
|
|
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',
|
|
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)',
|
|
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',
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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('||::||'))
|
|
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
|
+
}
|