Skip to content

Commit c7e9d5c

Browse files
pranavcodeantclaude
andcommitted
Validate numeric options, accept dismissed aliases in MCP, and keep dismissal comments
- Numeric options reject non-integers, negatives, zero counts, and values above the documented maximum instead of passing NaN or 0 through - codeant_scans_dismissed accepts sast and anti_patterns like the CLI - metadata carries comment_for_dismiss, and secrets with a FALSE_POSITIVE confidence are marked false_positive Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 127d809 commit c7e9d5c

9 files changed

Lines changed: 85 additions & 25 deletions

File tree

‎changelog.md‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
- SCA severity is read from the advisory rating instead of defaulting to medium
99
- Extra dead-code file paths no longer end in the result file name
1010
- Removed a debug dump of full scan responses to stderr
11-
- Fixed numeric options with defaults (`scans results --limit`, `scans history --limit`, PR comment `--limit`) being parsed in the wrong radix
11+
- Fixed numeric options with defaults (`scans results --limit`, `scans history --limit`, PR comment `--limit`) being parsed in the wrong radix; numeric options now reject non-integers, negative values, zero counts, and values above the documented maximum
12+
- The dismissal comment (`metadata.comment_for_dismiss`) and secrets with a `FALSE_POSITIVE` confidence are reported in `metadata`
1213

1314
## [0.5.8] - 02/10/2026
1415
- `findings antipatterns` and VM/container `findings cloud list` are paged (`--limit`, `--offset`, `--all`), so large organizations and scans no longer fail with a 502

‎mcp.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ The CodeAnt CLI ships an MCP (Model Context Protocol) server that exposes CodeAn
1515
| `codeant_scans_history` | read | Recent scan runs for a repo. |
1616
| `codeant_scans_get` | read | Severity/category summary for one scan. |
1717
| `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). |
1919
| `codeant_scans_overrides` | read | User false-positive, confidence, and severity overrides for a repo. |
2020
| `codeant_hotlist_list` | read | Prioritized organization-wide Hotlist findings with stable IDs. |
2121
| `codeant_hotlist_get` | read | One complete Hotlist finding by stable ID. |

‎src/commands/scans/index.js‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import { runDismissed, runOverrides } from './dismissed.js';
77
import { runStartScan } from './start-scan.js';
88
import { setQuiet, setNoColor } from './lib/log.js';
99
import { setNoColor as tableSetNoColor } from './formatters/table.js';
10+
import { intOption } from '../../utils/intOption.js';
1011

11-
// Commander passes the default as the parser's second argument; keep it out of the radix.
12-
const toInt = (value) => Number.parseInt(value, 10);
12+
const toCount = intOption({ min: 1 });
13+
const toOffset = intOption();
1314

1415
/**
1516
* Register all `codeant scans <verb>` subcommands.
@@ -40,7 +41,7 @@ export default function registerScansCommands(program, { runCmd }) {
4041
.requiredOption('--repo <repo>', 'Repository (owner/repo)')
4142
.option('--branch <name>', 'Filter by branch name')
4243
.option('--since <iso>', 'Show scans since ISO date')
43-
.option('--limit <n>', 'Max results (default: 20)', toInt, 20)
44+
.option('--limit <n>', 'Max results (default: 20)', toCount, 20)
4445
.action((opts) =>
4546
runCmd(() => runHistory({ repo: opts.repo, branch: opts.branch, since: opts.since, limit: opts.limit }))
4647
);
@@ -79,8 +80,8 @@ export default function registerScansCommands(program, { runCmd }) {
7980
.option('--format <fmt>', 'Output format: json|sarif|csv|md|table (default: json)', 'json')
8081
.option('--output <path>', 'Write output to file instead of stdout')
8182
.option('--fields <list>', 'Project findings to subset of fields (comma-separated)')
82-
.option('--limit <n>', 'Max findings per page (default: 100)', toInt, 100)
83-
.option('--offset <n>', 'Pagination offset (default: 0)', toInt, 0)
83+
.option('--limit <n>', 'Max findings per page (default: 100)', toCount, 100)
84+
.option('--offset <n>', 'Pagination offset (default: 0)', toOffset, 0)
8485
.option('--fail-fast', 'Exit 3 on first category fetch failure')
8586
.option('--no-color', 'Disable ANSI color (auto-disabled when not a TTY)')
8687
.option('--quiet', 'Suppress progress output on stderr')

‎src/commands/scans/lib/normalize.js‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,8 +96,12 @@ export function normalizeIssue(issue, category) {
9696
if (issue.is_dismissed === true) {
9797
metadata.dismissed = true;
9898
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;
99104
}
100-
if (issue.is_false_positive === true) metadata.false_positive = true;
101105

102106
const id = buildId(category, filePath, lineNumber, checkId, message);
103107

‎src/index.js‎

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import ScanCenter from './components/ScanCenter.js';
2020
import Welcome from './components/Welcome.js';
2121
import * as scm from './scm/index.js';
2222
import { setConfigValue } from './utils/config.js';
23+
import { intOption } from './utils/intOption.js';
2324
import { track, shutdown as analyticsShutdown, isTelemetryDisabled } from './utils/analytics.js';
2425
import registerScansCommands from './commands/scans/index.js';
2526
import registerSettingsCommands from './commands/settings/index.js';
@@ -31,9 +32,8 @@ import registerFindingsCommands from './commands/findings/index.js';
3132
const require = createRequire(import.meta.url);
3233
const pkg = require('../package.json');
3334

34-
// Commander passes an option's default as the parser's second argument, so a
35-
// bare `parseInt` would use it as the radix (`--limit 30` with default 20 → 60).
36-
const toInt = (value) => Number.parseInt(value, 10);
35+
const toCount = intOption({ min: 1 });
36+
const toOffset = intOption();
3737

3838
// Split comma-separated globs while preserving commas inside {} brace expansions (e.g. "*.{js,ts}")
3939
function splitGlobs(input) {
@@ -71,7 +71,7 @@ program
7171
.option('--uncommitted', 'Scan only uncommitted changes (staged + unstaged + untracked)')
7272
.option('--staged', 'Scan only staged files')
7373
.option('--last-commit', 'Scan last commit')
74-
.option('--last-n-commits <n>', 'Scan last n commits (max 5)', toInt)
74+
.option('--last-n-commits <n>', 'Scan last n commits (max 5)', toCount)
7575
.option('--base <branch>', 'Compare against a specific base branch (e.g. --base develop)')
7676
.option('--base-commit <commit>', 'Compare against a specific commit (e.g. --base-commit HEAD~3)')
7777
.option('--include <paths>', 'Comma-separated list of file paths glob patterns to include')
@@ -122,7 +122,7 @@ program
122122
.option('--uncommitted', 'Review only uncommitted changes (staged + unstaged + untracked)')
123123
.option('--staged', 'Review only staged files')
124124
.option('--last-commit', 'Review last commit')
125-
.option('--last-n-commits <n>', 'Review last n commits (max 5)', toInt)
125+
.option('--last-n-commits <n>', 'Review last n commits (max 5)', toCount)
126126
.option('--base <branch>', 'Compare against a specific base branch (e.g. --base develop)')
127127
.option('--base-commit <commit>', 'Compare against a specific commit (e.g. --base-commit HEAD~3)')
128128
.option('--fail-on <level>', 'Fail on issues at or above this level: BLOCKER, CRITICAL, MAJOR, MINOR, INFO (default: CRITICAL)', 'CRITICAL')
@@ -303,8 +303,8 @@ program
303303
.option('--source-branch <branch>', 'Filter by source branch (partial match)')
304304
.option('--author <login>', 'Filter by author (fuzzy match)')
305305
.option('--state <state>', 'open or closed (default: open)', 'open')
306-
.option('--limit <n>', 'Max results (default: 20, max: 100)', toInt, 20)
307-
.option('--offset <n>', 'Pagination offset', toInt, 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)
308308
.action((options) => {
309309
const opts = resolveRepoOpts(options);
310310
runCmd(() => scm.listPullRequests({
@@ -319,7 +319,7 @@ program
319319
.option('--name <repo>', 'Repository (owner/repo)')
320320
.option('--remote <provider>', 'github, gitlab, bitbucket, azure')
321321
.option('--default-branch <branch>', 'Default branch name')
322-
.requiredOption('--pr-number <n>', 'PR number', toInt)
322+
.requiredOption('--pr-number <n>', 'PR number', toCount)
323323
.action((options) => {
324324
const opts = resolveRepoOpts(options);
325325
runCmd(() => scm.getPullRequest({
@@ -333,7 +333,7 @@ program
333333
.option('--name <repo>', 'Repository (owner/repo)')
334334
.option('--remote <provider>', 'github, gitlab, bitbucket, azure')
335335
.option('--default-branch <branch>', 'Default branch name')
336-
.requiredOption('--pr-number <n>', 'PR number', toInt)
336+
.requiredOption('--pr-number <n>', 'PR number', toCount)
337337
.option('--codeant-generated <bool>', 'Filter by CodeAnt authorship (true/false)', (v) => v === 'true')
338338
.option('--addressed', 'Filter by addressed/resolved status')
339339
.option('--created-after <date>', 'ISO 8601 date filter')
@@ -351,8 +351,8 @@ program
351351
.description('Resolve a conversation/comment thread on a PR')
352352
.option('--name <repo>', 'Repository (owner/repo)')
353353
.option('--remote <provider>', 'github, gitlab, bitbucket, azure')
354-
.requiredOption('--pr-number <n>', 'PR number', toInt)
355-
.option('--comment-id <id>', 'Comment ID (GitHub, Bitbucket)', toInt)
354+
.requiredOption('--pr-number <n>', 'PR number', toCount)
355+
.option('--comment-id <id>', 'Comment ID (GitHub, Bitbucket)', toCount)
356356
.option('--thread-id <id>', 'Thread/node ID (GitHub GraphQL, Azure)')
357357
.option('--discussion-id <id>', 'Discussion ID (GitLab)')
358358
.action((options) => {
@@ -374,10 +374,10 @@ program
374374
.option('--name <repo>', 'Repository (owner/repo)')
375375
.option('--remote <provider>', 'github, gitlab, bitbucket, azure')
376376
.option('--default-branch <branch>', 'Default branch name')
377-
.option('--pr-number <n>', 'Filter by PR number', toInt)
377+
.option('--pr-number <n>', 'Filter by PR number', toCount)
378378
.option('--status <status>', 'Filter by status (PENDING, COMPLETED, FAILED, etc.)')
379-
.option('--limit <n>', 'Max results (default: 20)', toInt, 20)
380-
.option('--offset <n>', 'Pagination offset', toInt, 0)
379+
.option('--limit <n>', 'Max results (default: 20)', toCount, 20)
380+
.option('--offset <n>', 'Pagination offset', toOffset, 0)
381381
.action((options) => {
382382
const opts = resolveRepoOpts(options);
383383
runCmd(() => scm.listCodeReviews({
@@ -391,7 +391,7 @@ program
391391
.description('Get detailed information for a specific code review')
392392
.option('--name <repo>', 'Repository (owner/repo)')
393393
.option('--remote <provider>', 'github, gitlab, bitbucket, azure')
394-
.requiredOption('--pr-number <n>', 'PR number', toInt)
394+
.requiredOption('--pr-number <n>', 'PR number', toCount)
395395
.requiredOption('--review-id <id>', 'Code review ID')
396396
.action((options) => {
397397
const opts = resolveRepoOpts(options);
@@ -409,7 +409,7 @@ program
409409
.option('--name <repo>', 'Repository (owner/repo)')
410410
.option('--remote <provider>', 'github, gitlab, bitbucket, azure')
411411
.requiredOption('--query <term>', 'Search term')
412-
.option('--limit <n>', 'Max results (default: 10, max: 50)', toInt, 10)
412+
.option('--limit <n>', 'Max results (default: 10, max: 50)', intOption({ min: 1, max: 50 }), 10)
413413
.option('--include-addressed', 'Include resolved comments (default: false)')
414414
.option('--created-after <date>', 'ISO 8601 date filter')
415415
.action((options) => {

‎src/mcp/server.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ export async function startMcpServer() {
212212
description: 'List dismissed alerts (false positives, accepted risk, etc.) for a repository. Useful when triaging to avoid re-surfacing already-handled findings.',
213213
inputSchema: {
214214
repo: z.string().describe('Repository in owner/repo form.'),
215-
analysisType: z.enum(['security', 'secrets', 'sca', 'iac', 'antipatterns', 'docstring', 'complex_functions', 'dead_code', 'duplicate_code']).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).'),
216216
},
217217
annotations: READ,
218218
},

‎src/utils/intOption.js‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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+
}

‎tests/intOption.test.js‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { Command } from 'commander';
2+
import { describe, expect, it } from 'vitest';
3+
import { intOption } from '../src/utils/intOption.js';
4+
5+
function parse(parser, defaultValue, argv) {
6+
const cmd = new Command().exitOverride().configureOutput({ writeErr: () => {} });
7+
cmd.option('--limit <n>', 'limit', parser, defaultValue);
8+
cmd.parse(argv, { from: 'user' });
9+
return cmd.opts().limit;
10+
}
11+
12+
describe('intOption', () => {
13+
it('parses base 10 even though commander passes the default as the second argument', () => {
14+
expect(parse(intOption({ min: 1 }), 20, ['--limit', '30'])).toBe(30);
15+
expect(parse(intOption({ min: 1 }), 20, [])).toBe(20);
16+
});
17+
18+
it.each([['0'], ['-1'], ['abc'], ['12abc'], ['1.5'], ['101']])('rejects %s', (value) => {
19+
expect(() => parse(intOption({ min: 1, max: 100 }), 20, ['--limit', value])).toThrow(/Must be an integer/);
20+
});
21+
22+
it('accepts zero for offsets', () => {
23+
expect(parse(intOption(), 0, ['--limit', '0'])).toBe(0);
24+
});
25+
});

‎tests/scansDismissFp.test.js‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,7 @@ describe('dismissed and false-positive markers', () => {
165165
duplicate_locations: ['src/b.py:30-40'],
166166
is_dismissed: true,
167167
reason_for_dismiss: 'intentional',
168+
comment_for_dismiss: 'kept for the v1 API',
168169
is_false_positive: true,
169170
}, 'duplicate_code');
170171

@@ -173,7 +174,17 @@ describe('dismissed and false-positive markers', () => {
173174
duplicate_locations: ['src/b.py:30-40'],
174175
dismissed: true,
175176
reason_for_dismiss: 'intentional',
177+
comment_for_dismiss: 'kept for the v1 API',
176178
false_positive: true,
177179
});
178180
});
181+
182+
it('marks secrets with a FALSE_POSITIVE confidence as false positives', () => {
183+
const finding = normalizeIssue(
184+
{ file_path: '.env', line_number: 1, type: 'AWS Key', confidence_score: 'false_positive' },
185+
'secrets'
186+
);
187+
188+
expect(finding.metadata.false_positive).toBe(true);
189+
});
179190
});

0 commit comments

Comments
 (0)