Skip to content

Commit b309575

Browse files
Merge pull request #45 from CodeAnt-AI/pranav/cli-paginated-findings
Page anti-pattern and VM/container cloud findings (0.5.8)
2 parents 40ecadb + 58f5657 commit b309575

13 files changed

Lines changed: 161 additions & 35 deletions

‎changelog.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Changelog
22

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

‎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
@@ -18,9 +18,9 @@ The CodeAnt CLI ships an MCP (Model Context Protocol) server that exposes CodeAn
1818
| `codeant_scans_dismissed` | read | Dismissed alerts for a repo. |
1919
| `codeant_hotlist_list` | read | Prioritized organization-wide Hotlist findings with stable IDs. |
2020
| `codeant_hotlist_get` | read | One complete Hotlist finding by stable ID. |
21-
| `codeant_findings_antipatterns` | read | Anti-pattern findings across selected or all organization repos. |
21+
| `codeant_findings_antipatterns` | read | Anti-pattern findings across selected or all organization repos (paged: `limit`, `offset`, `all`). |
2222
| `codeant_cloud_scan_history` | read | AWS/Azure/GCP CSPM, VM, or container scan history. |
23-
| `codeant_cloud_findings_list` | read | Findings for one CSPM, VM, or container scan. |
23+
| `codeant_cloud_findings_list` | read | Findings for one CSPM, VM, or container scan (VM/container paged: `limit`, `offset`, `all`). |
2424
| `codeant_cloud_finding_get` | read | Full detail for one cloud finding UID. |
2525
| `codeant_pentest_history` | read | Pentest engagement history. |
2626
| `codeant_pentest_issues` | read | All available issues for a pentest engagement. |

‎package-lock.json‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codeant-cli",
3-
"version": "0.5.7",
3+
"version": "0.5.8",
44
"description": "Code review CLI tool",
55
"type": "module",
66
"repository": {

‎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
@@ -273,12 +273,15 @@ export async function startMcpServer() {
273273
'codeant_findings_antipatterns',
274274
{
275275
title: 'List organization anti-pattern findings',
276-
description: 'Fetch anti-pattern findings across selected repositories, or every repository in the organization when repos is omitted.',
276+
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.',
277277
inputSchema: {
278278
org: z.string().optional(),
279279
service: z.enum(['github', 'gitlab', 'bitbucket', 'azuredevops']).optional(),
280280
providerBaseUrl: z.string().url().optional(),
281281
repos: z.array(z.string()).optional().describe('Repositories in owner/repo form. Omit to query every repository.'),
282+
limit: z.union([z.literal(25), z.literal(100), z.literal(500)]).optional().describe('Page size (default 500).'),
283+
offset: z.number().int().nonnegative().optional().describe('Start offset, a multiple of limit (default 0).'),
284+
all: z.boolean().optional().describe('Fetch every page from offset on instead of one page.'),
282285
},
283286
annotations: READ,
284287
},
@@ -311,7 +314,7 @@ export async function startMcpServer() {
311314
'codeant_cloud_findings_list',
312315
{
313316
title: 'List cloud security findings',
314-
description: 'Fetch findings for one AWS, Azure, or GCP CSPM, VM, or container scan using the same endpoint as the app.',
317+
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.',
315318
inputSchema: {
316319
org: z.string().optional(),
317320
service: z.enum(['github', 'gitlab', 'bitbucket', 'azuredevops']).optional(),
@@ -329,6 +332,9 @@ export async function startMcpServer() {
329332
subscriptionId: z.string().optional(),
330333
exploitAttemptedOnly: z.boolean().optional(),
331334
minDaysUnused: z.number().int().nonnegative().optional(),
335+
limit: z.union([z.literal(25), z.literal(100), z.literal(500)]).optional().describe('Page size (default 500).'),
336+
offset: z.number().int().nonnegative().optional().describe('Start offset, a multiple of limit (default 0).'),
337+
all: z.boolean().optional().describe('Fetch every page from offset on instead of one page.'),
332338
},
333339
annotations: READ,
334340
},

0 commit comments

Comments
 (0)