codeant-cli 0.5.7 → 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 +7 -3
- package/mcp.md +2 -2
- package/package.json +1 -1
- package/src/commands/findings/index.js +15 -4
- package/src/findings/antipatterns.js +5 -4
- package/src/findings/cloud.js +14 -14
- package/src/findings/paginate.js +32 -0
- package/src/mcp/server.js +8 -2
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
|
|
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
|
|
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
|
@@ -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')
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { resolveCliTenant } from '../api/tenant.js';
|
|
2
2
|
import { runRepos } from '../commands/scans/repos.js';
|
|
3
|
-
import {
|
|
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
|
|
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
|
}
|
package/src/findings/cloud.js
CHANGED
|
@@ -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
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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
|
-
...
|
|
102
|
-
...
|
|
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
|
|
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
|
},
|