codeant-cli 0.5.6 → 0.5.8

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 CHANGED
@@ -75,9 +75,12 @@ codeant findings antipatterns --org CodeAnt-AI --service github
75
75
  codeant findings antipatterns \
76
76
  --org CodeAnt-AI --service github \
77
77
  --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
78
81
  ```
79
82
 
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.
81
84
 
82
85
  ## Cloud security findings
83
86
 
@@ -98,8 +101,9 @@ codeant findings cloud history --provider all --kind container
98
101
  codeant findings cloud list --provider aws --scan-id <scan-id> --account-id <account-id>
99
102
  codeant findings cloud get --provider aws --scan-id <scan-id> --uid <finding-uid> --cloud-service iam
100
103
 
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)
102
105
  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
103
107
  codeant findings cloud get --provider gcp --kind container --scan-id <scan-id> --uid <finding-uid>
104
108
 
105
109
  # Azure requires the tenant ID
@@ -113,7 +117,7 @@ codeant findings cloud list \
113
117
  --framework cis
114
118
  ```
115
119
 
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.
117
121
 
118
122
  ## Pentest findings
119
123
 
package/mcp.md CHANGED
@@ -18,9 +18,9 @@ The CodeAnt CLI ships an MCP (Model Context Protocol) server that exposes CodeAn
18
18
  | `codeant_scans_dismissed` | read | Dismissed alerts for a repo. |
19
19
  | `codeant_hotlist_list` | read | Prioritized organization-wide Hotlist findings with stable IDs. |
20
20
  | `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`). |
22
22
  | `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`). |
24
24
  | `codeant_cloud_finding_get` | read | Full detail for one cloud finding UID. |
25
25
  | `codeant_pentest_history` | read | Pentest engagement history. |
26
26
  | `codeant_pentest_issues` | read | All available issues for a pentest engagement. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeant-cli",
3
- "version": "0.5.6",
3
+ "version": "0.5.8",
4
4
  "description": "Code review CLI tool",
5
5
  "type": "module",
6
6
  "repository": {
@@ -54,6 +54,17 @@ function hotlistOptions(options) {
54
54
  };
55
55
  }
56
56
 
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
+
57
68
  function addCloudScopeOptions(command) {
58
69
  return addTenantOptions(command)
59
70
  .requiredOption('--provider <provider>', 'Cloud provider: aws, azure, or gcp')
@@ -157,9 +168,9 @@ export default function registerFindingsCommands(program, { runCmd }) {
157
168
  maxWaitSeconds: options.maxWait,
158
169
  })));
159
170
 
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')))
161
172
  .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 })));
163
174
 
164
175
  const cloud = findings.command('cloud').description('Cloud security CSPM, VM, and container findings');
165
176
  addTenantOptions(cloud.command('history').description('List cloud scan history, or latest scans'))
@@ -168,14 +179,14 @@ export default function registerFindingsCommands(program, { runCmd }) {
168
179
  .option('--latest', 'Return only latest CSPM scans')
169
180
  .action((options) => runCmd(() => runCloudHistory({ ...tenantOptions(options), provider: options.provider, kind: options.kind, latest: options.latest })));
170
181
 
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)')))
172
183
  .option('--severity <value>', 'Severity filter')
173
184
  .option('--status <value>', 'Finding status filter')
174
185
  .option('--framework <value>', 'Compliance framework filter')
175
186
  .option('--subscription-id <id>', 'Azure subscription ID filter')
176
187
  .option('--exploit-attempted-only', 'AWS findings with validation attempts only')
177
188
  .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) })));
179
190
 
180
191
  addCloudScopeOptions(cloud.command('get').description('Get one cloud finding with full detail'))
181
192
  .requiredOption('--uid <uid>', 'Finding UID')
@@ -22,7 +22,7 @@ export default function Login() {
22
22
  return;
23
23
  }
24
24
 
25
- const token = randomUUID();
25
+ const token = `cli___${randomUUID()}`;
26
26
  const baseUrl = getBaseUrl();
27
27
  const pollUrl = `${baseUrl}/extension/login/status?apiKey=${token}`;
28
28
 
@@ -1,6 +1,6 @@
1
1
  import { resolveCliTenant } from '../api/tenant.js';
2
2
  import { runRepos } from '../commands/scans/repos.js';
3
- import { fetchAppApi } from '../utils/fetchApi.js';
3
+ import { fetchPaginated } from './paginate.js';
4
4
 
5
5
  function splitRepos(value) {
6
6
  const values = Array.isArray(value) ? value : value ? [value] : [];
@@ -19,11 +19,12 @@ export async function runOrganizationAntipatterns(options = {}) {
19
19
  }
20
20
  if (repos.length === 0) throw new Error('No repositories are available for this organization.');
21
21
 
22
- const result = await fetchAppApi(
23
- '/explorer/quality/antipatterns',
24
- 'POST',
22
+ const result = await fetchPaginated(
23
+ '/explorer/quality/antipatterns/paginated',
25
24
  { ...tenant.requestBody, org: tenant.organization, repos },
26
25
  tenant,
26
+ 'antipatterns',
27
+ options,
27
28
  );
28
29
  return { tenant: tenant.requestBody, ...result };
29
30
  }
@@ -1,5 +1,6 @@
1
1
  import { resolveCliTenant } from '../api/tenant.js';
2
2
  import { fetchAppApi } from '../utils/fetchApi.js';
3
+ import { fetchPaginated } from './paginate.js';
3
4
 
4
5
  const PROVIDERS = new Set(['aws', 'azure', 'gcp']);
5
6
  const KINDS = new Set(['cspm', 'vm', 'container']);
@@ -53,18 +54,13 @@ function optionalFilters(provider, options = {}) {
53
54
  return filters;
54
55
  }
55
56
 
56
- async function request(provider, kind, action, options = {}, extra = {}, resolvedTenant = null) {
57
+ async function request(provider, kind, action, options = {}, extra = {}, resolvedTenant = null, pageKey = null) {
57
58
  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);
68
64
  return { tenant: tenant.requestBody, provider, kind, ...data };
69
65
  }
70
66
 
@@ -96,10 +92,14 @@ export async function runCloudFindings(options = {}) {
96
92
  const provider = normalizeProvider(options.provider);
97
93
  const kind = normalizeKind(options.kind);
98
94
  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, {
100
100
  scan_id: scanId,
101
- ...(kind === 'cspm' ? providerScope(provider, options) : {}),
102
- ...(kind === 'cspm' ? optionalFilters(provider, options) : {}),
101
+ ...providerScope(provider, options),
102
+ ...optionalFilters(provider, options),
103
103
  });
104
104
  }
105
105
 
@@ -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
+ }
package/src/mcp/server.js CHANGED
@@ -273,12 +273,15 @@ export async function startMcpServer() {
273
273
  'codeant_findings_antipatterns',
274
274
  {
275
275
  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.',
277
277
  inputSchema: {
278
278
  org: z.string().optional(),
279
279
  service: z.enum(['github', 'gitlab', 'bitbucket', 'azuredevops']).optional(),
280
280
  providerBaseUrl: z.string().url().optional(),
281
281
  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.'),
282
285
  },
283
286
  annotations: READ,
284
287
  },
@@ -311,7 +314,7 @@ export async function startMcpServer() {
311
314
  'codeant_cloud_findings_list',
312
315
  {
313
316
  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.',
315
318
  inputSchema: {
316
319
  org: z.string().optional(),
317
320
  service: z.enum(['github', 'gitlab', 'bitbucket', 'azuredevops']).optional(),
@@ -329,6 +332,9 @@ export async function startMcpServer() {
329
332
  subscriptionId: z.string().optional(),
330
333
  exploitAttemptedOnly: z.boolean().optional(),
331
334
  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.'),
332
338
  },
333
339
  annotations: READ,
334
340
  },
@@ -10,7 +10,7 @@ export function isAlreadyLoggedIn() {
10
10
  }
11
11
 
12
12
  export async function startLoginFlow() {
13
- const token = randomUUID();
13
+ const token = `cli___${randomUUID()}`;
14
14
  const baseUrl = getBaseUrl();
15
15
  const dashboardUrl = await getDashboardUrl();
16
16
  const loginUrl = `${dashboardUrl}?ideLoginToken=${token}`;