Skip to content

Commit d9ee124

Browse files
committed
Address CLI history and scan review feedback
1 parent 0044b3f commit d9ee124

9 files changed

Lines changed: 86 additions & 50 deletions

File tree

‎.gitignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,6 @@ coverage/
3333

3434
# Shell scripts
3535
*.sh
36+
37+
# Local Codex connection settings can contain credentials.
38+
.codex/

‎mcpb/manifest.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"display_name": "CodeAnt AI",
55
"version": "0.5.3",
66
"description": "Drive CodeAnt AI security scans and code review from Claude \u2014 org-wide secret triage, cross-repo SAST/SCA findings, on-demand scans, and local PR review.",
7-
"long_description": "CodeAnt AI inside Claude. Ask things like \"show my highest-priority Hotlist findings\", \"list AWS cloud findings\", \"show this pentest report\", or \"review my staged changes\" \u2014 Claude calls the CodeAnt API directly via this MCP server.\n\nIncludes 22 read-only tools, covering repository, Hotlist, cloud security, anti-pattern, and pentest findings plus an authenticated GET escape hatch for new APIs, 2 sign-in tools, and 3 opt-in write tools gated behind a setting.\n\nRequires a CodeAnt account. Sign up at https://codeant.ai. To authenticate, call the `codeant_login` tool \u2014 it returns a sign-in link (and opens it in your browser when it can), then saves the token once you finish.\n\nCollects usage telemetry via PostHog by default (events are tied to your CodeAnt token); turn on \"Disable telemetry\" in the extension settings to opt out.",
7+
"long_description": "CodeAnt AI inside Claude. Ask things like \"show my highest-priority Hotlist findings\", \"list AWS cloud findings\", \"show this pentest report\", or \"review my staged changes\" \u2014 Claude calls the CodeAnt API directly via this MCP server.\n\nIncludes 23 read-only tools, covering repository, Hotlist, cloud security, anti-pattern, and pentest findings plus an authenticated GET escape hatch for new APIs, 2 sign-in tools, and 3 opt-in write tools gated behind a setting.\n\nRequires a CodeAnt account. Sign up at https://codeant.ai. To authenticate, call the `codeant_login` tool \u2014 it returns a sign-in link (and opens it in your browser when it can), then saves the token once you finish.\n\nCollects usage telemetry via PostHog by default (events are tied to your CodeAnt token); turn on \"Disable telemetry\" in the extension settings to opt out.",
88
"author": {
99
"name": "CodeAnt AI",
1010
"email": "support@codeant.ai",

‎package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"start": "node src/index.js",
1515
"test": "vitest run",
1616
"test:watch": "vitest",
17+
"test:integration": "CODEANT_LIVE_TESTS=1 vitest run tests/scans tests/settings --no-file-parallelism --maxWorkers=1 --minWorkers=1",
1718
"mcpb:build": "node scripts/build-mcpb.mjs"
1819
},
1920
"keywords": [

‎scans.md‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,8 @@ codeant findings repo --repo acme/backend --types secrets \
295295
codeant scans results --repo acme/backend --types secrets --secrets-scope all
296296
```
297297

298+
Latest history results retain the last completed assessment while a newer run is pending or failed; the run status still describes the latest job. An explicit `--history-run` retrieves only that run.
299+
298300
`--secrets-scope` accepts `current` (default), `history`, or `all`. `--history-run` selects one immutable run identity; otherwise the latest history run is read. Status values are `not_started`, `queued`, `running`, `awaiting_verification`, `completed`, `failed`, and `disabled`. Failed and disabled runs are terminal outcomes, not clean scans. Jobs stalled for over one hour are reported as failed. When available, `last_completed_run_id` identifies the previous successful run. Results from incomplete runs must not be treated as a complete assessment.
299301

300302
History findings expose `metadata.commit`, `branches`, `tags`, `scanned_at`, `only_in_history`, and native `verification_status`. Branches/tags contain the commit at scan time; they do not identify where it originated. `only_in_history` compares the secret identity against the selected, unfiltered snapshot, including findings dismissed or marked false positive. It is `null` when the baseline is unavailable. History-only retrieval works without a snapshot scan; in that case `scan` is `null`.
@@ -333,7 +335,7 @@ A vulnerable package can now include:
333335
}
334336
```
335337

336-
Use the direct introducer and manifest to select the dependency to edit. `fix_version` fixes the vulnerable package; it does not promise a safe version of its direct introducer. There can be multiple introducers and manifest locations. Lockfiles are null when unknown or generated rather than committed. Path status distinguishes `pending`, `resolved`, `partial`, and `unavailable`; a resolved empty list differs from unavailable evidence. Existing scans are readable, but missing evidence requires a new scan.
338+
Use the direct introducer and manifest to select the dependency to edit. `fix_version` fixes the vulnerable package; it does not promise a safe version of its direct introducer. There can be multiple introducers and manifest locations. Very large path payloads are bounded; `dependency_paths_truncated: true` and a `partial` path status identify incomplete public evidence. Lockfiles are null when unknown or generated rather than committed. Path status distinguishes `pending`, `resolved`, `partial`, and `unavailable`; a resolved empty list differs from unavailable evidence. Existing scans are readable, but missing evidence requires a new scan.
337339

338340
Schema 1.1 adds `category_status` (including independent SCA `dependency_paths_status`), optional `history_run`, and category metadata. SCA IDs now include package/version/manifest context; history IDs include commit and secret identity. Existing current-secret IDs are unchanged. JSON and field projection preserve metadata; SARIF includes it in result properties, and CSV adds JSON-encoded `package` and `metadata` columns.
339341

@@ -350,3 +352,5 @@ npm test -- tests/scans/history-secrets.test.js
350352
```
351353

352354
This suite reads the specified fixture; it does not trigger scans or modify settings.
355+
356+
Scan-start aliases `anti_patterns` and `complex_functions` select the `antipatterns` scanner, which produces both result categories.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export function normalizeIssue(issue, category) {
9494
metadata.fix_version = issue.fix_version || issue.fix_details?.fixed_version || issue.fixed_version || issue.first_patched_version || null;
9595
metadata.fix_available = issue.fix_available ?? issue.fix_details?.fix_available ?? !!metadata.fix_version;
9696
if (issue.fix_details?.vulnerable_version_range !== undefined) metadata.vulnerable_version_range = issue.fix_details.vulnerable_version_range;
97-
for (const key of ['dependency_paths', 'dependency_paths_status', 'package_location', 'vulnerable_version_range', 'vulnerable_range', 'dependency_relationship']) {
97+
for (const key of ['dependency_paths', 'dependency_paths_status', 'dependency_paths_truncated', 'package_location', 'vulnerable_version_range', 'vulnerable_range', 'dependency_relationship']) {
9898
if (issue[key] !== undefined) metadata[key] = issue[key];
9999
}
100100
} else if (category === 'duplicate_code') {

‎src/commands/scans/start-scan.js‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ async function resolveRemoteCommit(branch) {
5555
}
5656

5757
export async function runStartScan({ repo, branch, commit, include, exclude, types, json = false, returnResult = false } = {}) {
58-
const scanTypes = types ? types.split(',').map((value) => value.trim()).filter(Boolean) : undefined;
58+
const aliases = { anti_patterns: 'antipatterns', complex_functions: 'antipatterns' };
59+
const scanTypes = types ? [...new Set(types.split(',').map((value) => value.trim()).filter(Boolean).map((value) => aliases[value] || value))] : undefined;
5960
if (types !== undefined && (!scanTypes?.length || scanTypes.some((value) => !SCAN_TYPES.has(value)))) throw new Error('Invalid --types for scan start');
6061
const resolvedRepo = repo || detectRepoName();
6162
if (!resolvedRepo) {

‎src/scans/fetchAdvancedScanResults.js‎

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,7 @@ export async function fetchAdvancedScanResults(repo, commitId, resultType, opts
291291
try {
292292
if (resultType === ADVANCED_RESULT_TYPES.SECRETS && opts.secretsScope && opts.secretsScope !== 'current') {
293293
const history = await getHistoryResults(repo, commitId, opts);
294+
const historyRun = { ...history.run, results_run_id: history.results_run_id, results_status: history.results_status };
294295
const occurrences = new Map();
295296
const historyIssues = (history.findings || []).map((item) => {
296297
const issue = normalizeAdvancedIssue({ ...item, finding_scope: 'history' }, resultType);
@@ -300,20 +301,25 @@ export async function fetchAdvancedScanResults(repo, commitId, resultType, opts
300301
return { ...issue, occurrence_index: occurrenceIndex };
301302
});
302303
if (opts.secretsScope === 'history') {
303-
return { success: true, issues: historyIssues, status: history.status, historyRun: history.run };
304+
return { success: true, issues: historyIssues, status: history.status, historyRun };
304305
}
305306
const current = await fetchAdvancedScanResults(repo, commitId, resultType, { ...opts, secretsScope: 'current' });
306307
if (!current.success) return current;
307308
const identity = (issue) => JSON.stringify([issue.file_path, issue.hashed_secret, issue.type || issue.secret_type]);
308-
const byIdentity = new Map(current.issues.filter((issue) => issue.hashed_secret).map((issue) => [identity(issue), issue]));
309+
const byIdentity = new Map();
310+
for (const issue of current.issues.filter((issue) => issue.hashed_secret)) {
311+
const key = identity(issue);
312+
if (!byIdentity.has(key)) byIdentity.set(key, []);
313+
byIdentity.get(key).push(issue);
314+
}
309315
const remaining = [];
310316
for (const issue of historyIssues) {
311-
const match = byIdentity.get(identity(issue));
312-
if (match) {
313-
(match.history_occurrences ??= []).push({ commit: issue.commit, file_path: issue.file_path, line_number: issue.line_number, branches: issue.branches, tags: issue.tags, verification_status: issue.verification_status, scanned_at: issue.scanned_at });
317+
const matches = byIdentity.get(identity(issue));
318+
if (matches) {
319+
for (const match of matches) (match.history_occurrences ??= []).push({ commit: issue.commit, file_path: issue.file_path, line_number: issue.line_number, branches: issue.branches, tags: issue.tags, verification_status: issue.verification_status, scanned_at: issue.scanned_at });
314320
} else remaining.push(issue);
315321
}
316-
return { ...current, issues: [...current.issues, ...remaining], historyRun: history.run };
322+
return { ...current, issues: [...current.issues, ...remaining], historyRun };
317323
}
318324
const response = await fetchApi('/extension/scans2/fetch-advanced-results', 'POST', {
319325
repo,

‎tests/historyResultsContract.test.js‎

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ describe('history results contract', () => {
3636
expect(JSON.stringify(envelope)).not.toContain('hashed_secret');
3737
});
3838

39+
it('identifies the completed result behind a newer failed history run', async () => {
40+
mocks.fetchApiResponse.mockResolvedValue({ ok: true, status: 200, data: { findings: [occurrence], status: 'failed', run: { scan_run_id: run, status: 'failed' }, results_run_id: 'previous-completed-run', results_status: 'completed' } });
41+
const envelope = await runResults({ repo, types: 'secrets', secretsScope: 'history', returnEnvelope: true });
42+
expect(envelope.findings).toHaveLength(1);
43+
expect(envelope.history_run).toMatchObject({ status: 'failed', results_run_id: 'previous-completed-run', results_status: 'completed' });
44+
});
45+
3946
it('works without a snapshot and keeps run metadata separate', async () => {
4047
mocks.resolveScan.mockRejectedValue(Object.assign(new Error('no scans found'), { exitCode: 2 }));
4148
const envelope = await runResults({ repo, types: 'secrets', secretsScope: 'history', returnEnvelope: true });
@@ -59,6 +66,20 @@ describe('history results contract', () => {
5966
expect(first.id).not.toBe(second.id);
6067
});
6168

69+
it('attaches history evidence to every matching current occurrence', async () => {
70+
mocks.fetchApi.mockResolvedValue({ secrets: { 'old.env': [8, 12].map((line_number) => ({ type: 'example', hashed_secret: 'hash', line_number })) }, status: 'done' });
71+
const result = await fetchSecretsResults(repo, commit, { secretsScope: 'all' });
72+
expect(result.issues).toHaveLength(2);
73+
for (const issue of result.issues) expect(issue.history_occurrences).toHaveLength(1);
74+
expect(result.issues[0].history_occurrences).toEqual(result.issues[1].history_occurrences);
75+
});
76+
77+
it('maps public scanner aliases to the scanner that produces both results', async () => {
78+
mocks.fetchApi.mockResolvedValue({ scan_run_id: run });
79+
await runStartScan({ repo, branch: 'main', commit, types: 'anti_patterns,complex_functions,antipatterns', returnResult: true });
80+
expect(mocks.fetchApi).toHaveBeenCalledWith('/extension/analysis/run', 'POST', expect.objectContaining({ scan_types: ['antipatterns'] }));
81+
});
82+
6283
it('projects metadata without dropping the run state', async () => {
6384
const envelope = await runResults({ repo, types: 'secrets', secretsScope: 'history', fields: 'id,metadata', returnEnvelope: true });
6485
expect(Object.keys(envelope.findings[0])).toEqual(['id', 'metadata']);
@@ -116,10 +137,10 @@ describe('history results contract', () => {
116137
describe('SCA remediation contract', () => {
117138
it('retains manifest, paths and fixed versions through fetch and normalization', async () => {
118139
const dependencyPaths = [{ manifest: 'apps/web/package.json', lockfile: 'pnpm-lock.yaml', packages: ['direct@1', 'vulnerable@1'], direct_dependency: 'direct@1', resolution: 'COMMITTED_LOCK' }];
119-
mocks.fetchApi.mockResolvedValue({ status: 'done', results: { dependency_paths_status: 'done', all_vulnerabilities: [{ package_name: 'vulnerable', package_version: '1', ecosystem: 'NPM', cve_id: 'CVE-EXAMPLE', relative_path: 'apps/web/package.json', dependency_paths: dependencyPaths, dependency_paths_status: 'resolved', fix_details: { fixed_version: '2', fix_available: true } }] } });
140+
mocks.fetchApi.mockResolvedValue({ status: 'done', results: { dependency_paths_status: 'done', all_vulnerabilities: [{ package_name: 'vulnerable', package_version: '1', ecosystem: 'NPM', cve_id: 'CVE-EXAMPLE', relative_path: 'apps/web/package.json', dependency_paths: dependencyPaths, dependency_paths_truncated: true, dependency_paths_status: 'resolved', fix_details: { fixed_version: '2', fix_available: true } }] } });
120141
const result = await fetchScaResults(repo, commit);
121142
const finding = normalizeIssue(result.issues[0], 'sca');
122-
expect(finding).toMatchObject({ file_path: 'apps/web/package.json', metadata: { dependency_paths: dependencyPaths, fix_version: '2', fix_available: true } });
143+
expect(finding).toMatchObject({ file_path: 'apps/web/package.json', metadata: { dependency_paths: dependencyPaths, dependency_paths_truncated: true, fix_version: '2', fix_available: true } });
123144
expect(result.dependencyPathsStatus).toBe('done');
124145
expect(finding.id).not.toBe(normalizeIssue({ ...result.issues[0], package_version: '3' }, 'sca').id);
125146
});

0 commit comments

Comments
 (0)