Skip to content

Commit 127d809

Browse files
pranavcodeantclaude
andcommitted
Merge main into pranav/scans-dismiss-fp-overrides
Keeps 0.5.9 for this branch, with main's 0.5.8 changelog entry below it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2 parents c6757c2 + b309575 commit 127d809

11 files changed

Lines changed: 158 additions & 32 deletions

File tree

‎changelog.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
- Removed a debug dump of full scan responses to stderr
1111
- Fixed numeric options with defaults (`scans results --limit`, `scans history --limit`, PR comment `--limit`) being parsed in the wrong radix
1212

13+
## [0.5.8] - 02/10/2026
14+
- `findings antipatterns` and VM/container `findings cloud list` are paged (`--limit`, `--offset`, `--all`), so large organizations and scans no longer fail with a 502
15+
1316
## [0.5.7] - 28/09/2026
1417
- Login tokens are prefixed with `cli___` so CLI logins show up as CLI on the IDE users page
1518

‎findings.md‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,9 +75,12 @@ codeant findings antipatterns --org CodeAnt-AI --service github
7575
codeant findings antipatterns \
7676
--org CodeAnt-AI --service github \
7777
--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
7881
```
7982

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.
8184

8285
## Cloud security findings
8386

@@ -98,8 +101,9 @@ codeant findings cloud history --provider all --kind container
98101
codeant findings cloud list --provider aws --scan-id <scan-id> --account-id <account-id>
99102
codeant findings cloud get --provider aws --scan-id <scan-id> --uid <finding-uid> --cloud-service iam
100103

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)
102105
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
103107
codeant findings cloud get --provider gcp --kind container --scan-id <scan-id> --uid <finding-uid>
104108

105109
# Azure requires the tenant ID
@@ -113,7 +117,7 @@ codeant findings cloud list \
113117
--framework cis
114118
```
115119

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.
117121

118122
## Pentest findings
119123

‎mcp.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ The CodeAnt CLI ships an MCP (Model Context Protocol) server that exposes CodeAn
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. |
22-
| `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`). |
2323
| `codeant_cloud_scan_history` | read | AWS/Azure/GCP CSPM, VM, or container scan history. |
24-
| `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`). |
2525
| `codeant_cloud_finding_get` | read | Full detail for one cloud finding UID. |
2626
| `codeant_pentest_history` | read | Pentest engagement history. |
2727
| `codeant_pentest_issues` | read | All available issues for a pentest engagement. |

‎src/commands/findings/index.js‎

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,17 @@ function hotlistOptions(options) {
5454
};
5555
}
5656

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+
5768
function addCloudScopeOptions(command) {
5869
return addTenantOptions(command)
5970
.requiredOption('--provider <provider>', 'Cloud provider: aws, azure, or gcp')
@@ -157,9 +168,9 @@ export default function registerFindingsCommands(program, { runCmd }) {
157168
maxWaitSeconds: options.maxWait,
158169
})));
159170

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')))
161172
.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 })));
163174

164175
const cloud = findings.command('cloud').description('Cloud security CSPM, VM, and container findings');
165176
addTenantOptions(cloud.command('history').description('List cloud scan history, or latest scans'))
@@ -168,14 +179,14 @@ export default function registerFindingsCommands(program, { runCmd }) {
168179
.option('--latest', 'Return only latest CSPM scans')
169180
.action((options) => runCmd(() => runCloudHistory({ ...tenantOptions(options), provider: options.provider, kind: options.kind, latest: options.latest })));
170181

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)')))
172183
.option('--severity <value>', 'Severity filter')
173184
.option('--status <value>', 'Finding status filter')
174185
.option('--framework <value>', 'Compliance framework filter')
175186
.option('--subscription-id <id>', 'Azure subscription ID filter')
176187
.option('--exploit-attempted-only', 'AWS findings with validation attempts only')
177188
.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) })));
179190

180191
addCloudScopeOptions(cloud.command('get').description('Get one cloud finding with full detail'))
181192
.requiredOption('--uid <uid>', 'Finding UID')

‎src/findings/antipatterns.js‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { resolveCliTenant } from '../api/tenant.js';
22
import { runRepos } from '../commands/scans/repos.js';
3-
import { fetchAppApi } from '../utils/fetchApi.js';
3+
import { fetchPaginated } from './paginate.js';
44

55
function splitRepos(value) {
66
const values = Array.isArray(value) ? value : value ? [value] : [];
@@ -19,11 +19,12 @@ export async function runOrganizationAntipatterns(options = {}) {
1919
}
2020
if (repos.length === 0) throw new Error('No repositories are available for this organization.');
2121

22-
const result = await fetchAppApi(
23-
'/explorer/quality/antipatterns',
24-
'POST',
22+
const result = await fetchPaginated(
23+
'/explorer/quality/antipatterns/paginated',
2524
{ ...tenant.requestBody, org: tenant.organization, repos },
2625
tenant,
26+
'antipatterns',
27+
options,
2728
);
2829
return { tenant: tenant.requestBody, ...result };
2930
}

‎src/findings/cloud.js‎

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { resolveCliTenant } from '../api/tenant.js';
22
import { fetchAppApi } from '../utils/fetchApi.js';
3+
import { fetchPaginated } from './paginate.js';
34

45
const PROVIDERS = new Set(['aws', 'azure', 'gcp']);
56
const KINDS = new Set(['cspm', 'vm', 'container']);
@@ -53,18 +54,13 @@ function optionalFilters(provider, options = {}) {
5354
return filters;
5455
}
5556

56-
async function request(provider, kind, action, options = {}, extra = {}, resolvedTenant = null) {
57+
async function request(provider, kind, action, options = {}, extra = {}, resolvedTenant = null, pageKey = null) {
5758
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);
6864
return { tenant: tenant.requestBody, provider, kind, ...data };
6965
}
7066

@@ -96,10 +92,14 @@ export async function runCloudFindings(options = {}) {
9692
const provider = normalizeProvider(options.provider);
9793
const kind = normalizeKind(options.kind);
9894
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, {
100100
scan_id: scanId,
101-
...(kind === 'cspm' ? providerScope(provider, options) : {}),
102-
...(kind === 'cspm' ? optionalFilters(provider, options) : {}),
101+
...providerScope(provider, options),
102+
...optionalFilters(provider, options),
103103
});
104104
}
105105

‎src/findings/paginate.js‎

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

‎src/mcp/server.js‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -289,12 +289,15 @@ export async function startMcpServer() {
289289
'codeant_findings_antipatterns',
290290
{
291291
title: 'List organization anti-pattern findings',
292-
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.',
293293
inputSchema: {
294294
org: z.string().optional(),
295295
service: z.enum(['github', 'gitlab', 'bitbucket', 'azuredevops']).optional(),
296296
providerBaseUrl: z.string().url().optional(),
297297
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.'),
298301
},
299302
annotations: READ,
300303
},
@@ -327,7 +330,7 @@ export async function startMcpServer() {
327330
'codeant_cloud_findings_list',
328331
{
329332
title: 'List cloud security findings',
330-
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.',
331334
inputSchema: {
332335
org: z.string().optional(),
333336
service: z.enum(['github', 'gitlab', 'bitbucket', 'azuredevops']).optional(),
@@ -345,6 +348,9 @@ export async function startMcpServer() {
345348
subscriptionId: z.string().optional(),
346349
exploitAttemptedOnly: z.boolean().optional(),
347350
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.'),
348354
},
349355
annotations: READ,
350356
},

‎tests/cloudFindings.test.js‎

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ describe('cloud findings client', () => {
7272
subscription_id: 'sub-1',
7373
min_days_unused: 30,
7474
}), tenant);
75+
expect(fetchAppApi.mock.calls[0][2]).not.toHaveProperty('limit');
7576
});
7677

7778
it('requires provider-specific scope and fetches detail by UID', async () => {
@@ -86,10 +87,12 @@ describe('cloud findings client', () => {
8687
}), tenant);
8788
});
8889

89-
it('uses the VM and container result endpoints without CSPM account fields', async () => {
90+
it('uses the paged VM and container result endpoints without CSPM account fields', async () => {
9091
await runCloudFindings({ provider: 'gcp', kind: 'vm', scanId: 'vm-scan' });
91-
expect(fetchAppApi).toHaveBeenNthCalledWith(1, '/cloud/gcp/vm-scanning/results', 'POST', expect.objectContaining({
92+
expect(fetchAppApi).toHaveBeenNthCalledWith(1, '/cloud/gcp/vm-scanning/results/paginated', 'POST', expect.objectContaining({
9293
scan_id: 'vm-scan',
94+
limit: 500,
95+
offset: 0,
9396
}), tenant);
9497
expect(fetchAppApi.mock.calls[0][2]).not.toHaveProperty('project_id');
9598

@@ -99,4 +102,19 @@ describe('cloud findings client', () => {
99102
uid: 'CVE-1',
100103
}), tenant);
101104
});
105+
106+
it('merges every container page with --all', async () => {
107+
fetchAppApi.mockImplementation(async (_path, _method, body) => ({
108+
status: 'completed',
109+
total: 1100,
110+
offset: body.offset,
111+
findings: Array.from({ length: Math.min(500, 1100 - body.offset) }, (_, index) => ({ uid: `f${body.offset + index}` })),
112+
}));
113+
114+
const result = await runCloudFindings({ provider: 'aws', kind: 'container', scanId: 'big', all: true });
115+
116+
expect(fetchAppApi.mock.calls.map((call) => call[2].offset)).toEqual([0, 500, 1000]);
117+
expect(result.findings.map((finding) => finding.uid)).toEqual(Array.from({ length: 1100 }, (_, index) => `f${index}`));
118+
expect(result).toMatchObject({ provider: 'aws', kind: 'container', total: 1100, status: 'completed' });
119+
});
102120
});

‎tests/findingsPaginate.test.js‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { beforeEach, describe, expect, it, vi } from 'vitest';
2+
3+
const { fetchAppApi } = vi.hoisted(() => ({ fetchAppApi: vi.fn() }));
4+
5+
vi.mock('../src/utils/fetchApi.js', () => ({ fetchAppApi }));
6+
7+
const { fetchPaginated } = await import('../src/findings/paginate.js');
8+
9+
const tenant = { organization: 'CodeAnt-AI' };
10+
const page = (body) => ({
11+
total: 260,
12+
limit: body.limit,
13+
offset: body.offset,
14+
items: Array.from({ length: Math.max(0, Math.min(body.limit, 260 - body.offset)) }, (_, index) => body.offset + index),
15+
});
16+
17+
describe('paged findings fetch', () => {
18+
beforeEach(() => {
19+
fetchAppApi.mockReset();
20+
fetchAppApi.mockImplementation(async (_path, _method, body) => page(body));
21+
});
22+
23+
it('fetches one page of 500 by default', async () => {
24+
const result = await fetchPaginated('/x/paginated', { org: 'o' }, tenant, 'items');
25+
26+
expect(fetchAppApi).toHaveBeenCalledTimes(1);
27+
expect(fetchAppApi).toHaveBeenCalledWith('/x/paginated', 'POST', { org: 'o', limit: 500, offset: 0 }, tenant);
28+
expect(result.items).toHaveLength(260);
29+
});
30+
31+
it('merges every page from the starting offset with all', async () => {
32+
const result = await fetchPaginated('/x/paginated', {}, tenant, 'items', { limit: 25, offset: 50, all: true });
33+
34+
expect(fetchAppApi.mock.calls.map((call) => call[2].offset)).toEqual(
35+
Array.from({ length: 9 }, (_, index) => 50 + index * 25),
36+
);
37+
expect(result.items).toEqual(Array.from({ length: 210 }, (_, index) => 50 + index));
38+
expect(result.total).toBe(260);
39+
});
40+
41+
it.each([
42+
[{ limit: 50 }, /--limit/],
43+
[{ limit: 25, offset: 30 }, /--offset/],
44+
[{ offset: -500 }, /--offset/],
45+
])('rejects %o before calling the API', async (options, message) => {
46+
await expect(fetchPaginated('/x/paginated', {}, tenant, 'items', options)).rejects.toThrow(message);
47+
expect(fetchAppApi).not.toHaveBeenCalled();
48+
});
49+
});

0 commit comments

Comments
 (0)