codeant-cli 0.5.3 → 0.5.5
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/README.md +37 -10
- package/cli-api.md +130 -0
- package/findings.md +163 -0
- package/mcp.md +207 -0
- package/package.json +18 -8
- package/scans.md +263 -0
- package/src/api/tenant.js +54 -0
- package/src/commands/api/index.js +33 -0
- package/src/commands/api/request.js +109 -0
- package/src/commands/findings/index.js +197 -0
- package/src/commands/getDashboardUrl.js +49 -0
- package/src/commands/hotlist/index.js +58 -0
- package/src/commands/login.js +9 -1
- package/src/commands/logout.js +14 -5
- package/src/commands/removeDashboardUrl.js +31 -0
- package/src/commands/setDashboardUrl.js +37 -0
- package/src/findings/antipatterns.js +31 -0
- package/src/findings/cloud.js +119 -0
- package/src/findings/pentest.js +42 -0
- package/src/hotlist/client.js +89 -0
- package/src/index.js +36 -0
- package/src/mcp/server.js +252 -5
- package/src/utils/baseUrl.js +27 -3
- package/src/utils/fetchApi.js +83 -9
- package/src/utils/logout.js +31 -0
package/scans.md
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
# `codeant scans`
|
|
2
|
+
|
|
3
|
+
Fetch and explore scan results from CodeAnt.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
codeant scans <subcommand> [options]
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Subcommands
|
|
12
|
+
|
|
13
|
+
### `scans orgs`
|
|
14
|
+
|
|
15
|
+
List authenticated organizations.
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
codeant scans orgs
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
### `scans repos`
|
|
24
|
+
|
|
25
|
+
List repositories for an organization.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
codeant scans repos [options]
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Options:**
|
|
32
|
+
|
|
33
|
+
| Option | Description |
|
|
34
|
+
|--------|-------------|
|
|
35
|
+
| `--org <org>` | Organization name (auto-picked when only one is authenticated) |
|
|
36
|
+
|
|
37
|
+
**Examples:**
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# List repos (auto-selects org if only one)
|
|
41
|
+
codeant scans repos
|
|
42
|
+
|
|
43
|
+
# List repos for a specific org
|
|
44
|
+
codeant scans repos --org my-org
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
### `scans history`
|
|
50
|
+
|
|
51
|
+
Show scan history for a repository.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
codeant scans history --repo <owner/repo> [options]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Options:**
|
|
58
|
+
|
|
59
|
+
| Option | Description |
|
|
60
|
+
|--------|-------------|
|
|
61
|
+
| `--repo <repo>` | **(required)** Repository in `owner/repo` format |
|
|
62
|
+
| `--branch <name>` | Filter by branch name |
|
|
63
|
+
| `--since <iso>` | Show scans since ISO date (e.g. `2024-01-01`) |
|
|
64
|
+
| `--limit <n>` | Max results (default: `20`) |
|
|
65
|
+
|
|
66
|
+
**Examples:**
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
# Show last 20 scans for a repo
|
|
70
|
+
codeant scans history --repo acme/backend
|
|
71
|
+
|
|
72
|
+
# Filter to a specific branch
|
|
73
|
+
codeant scans history --repo acme/backend --branch main
|
|
74
|
+
|
|
75
|
+
# Show scans since a date
|
|
76
|
+
codeant scans history --repo acme/backend --since 2024-06-01
|
|
77
|
+
|
|
78
|
+
# Show up to 50 results
|
|
79
|
+
codeant scans history --repo acme/backend --limit 50
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
### `scans get`
|
|
85
|
+
|
|
86
|
+
Show scan metadata and a severity/category summary. Does not include individual findings.
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
codeant scans get --repo <owner/repo> [options]
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Options:**
|
|
93
|
+
|
|
94
|
+
| Option | Description |
|
|
95
|
+
|--------|-------------|
|
|
96
|
+
| `--repo <repo>` | **(required)** Repository in `owner/repo` format |
|
|
97
|
+
| `--scan <sha>` | Specific commit SHA to use |
|
|
98
|
+
| `--branch <name>` | Resolve latest scan on this branch |
|
|
99
|
+
| `--types <list>` | Comma-separated scan types (default: `all`) |
|
|
100
|
+
| `--quiet` | Suppress progress output |
|
|
101
|
+
|
|
102
|
+
**Examples:**
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# Get latest scan summary for a repo
|
|
106
|
+
codeant scans get --repo acme/backend
|
|
107
|
+
|
|
108
|
+
# Get scan for a specific commit
|
|
109
|
+
codeant scans get --repo acme/backend --scan abc1234
|
|
110
|
+
|
|
111
|
+
# Get latest scan on a branch
|
|
112
|
+
codeant scans get --repo acme/backend --branch main
|
|
113
|
+
|
|
114
|
+
# Only include SAST and secrets types
|
|
115
|
+
codeant scans get --repo acme/backend --types sast,secrets
|
|
116
|
+
|
|
117
|
+
# Suppress progress output
|
|
118
|
+
codeant scans get --repo acme/backend --quiet
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
### `scans results`
|
|
124
|
+
|
|
125
|
+
Fetch full scan findings for a repository.
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
codeant scans results --repo <owner/repo> [options]
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Options:**
|
|
132
|
+
|
|
133
|
+
| Option | Description |
|
|
134
|
+
|--------|-------------|
|
|
135
|
+
| `--repo <repo>` | **(required)** Repository in `owner/repo` format |
|
|
136
|
+
| `--scan <sha>` | Specific commit SHA to use |
|
|
137
|
+
| `--branch <name>` | Resolve latest scan on this branch |
|
|
138
|
+
| `--types <list>` | Comma-separated types: `sast`, `sca`, `secrets`, `iac`, `dead_code`, `sbom`, `anti_patterns`, `docstring`, `complex_functions`, `all` (default: `all`) |
|
|
139
|
+
| `--severity <list>` | Filter by severity (e.g. `critical,high`) |
|
|
140
|
+
| `--path <glob>` | Filter by file path glob |
|
|
141
|
+
| `--check <regex>` | Filter by check ID or name (regex) |
|
|
142
|
+
| `--include-dismissed` | Include dismissed findings (excluded by default) |
|
|
143
|
+
| `--format <fmt>` | Output format: `json`, `sarif`, `csv`, `md`, `table` (default: `json`) |
|
|
144
|
+
| `--output <path>` | Write output to file instead of stdout |
|
|
145
|
+
| `--fields <list>` | Project findings to a subset of fields (comma-separated) |
|
|
146
|
+
| `--limit <n>` | Max findings per page (default: `100`) |
|
|
147
|
+
| `--offset <n>` | Pagination offset (default: `0`) |
|
|
148
|
+
| `--fail-fast` | Exit `3` on first category fetch failure |
|
|
149
|
+
| `--no-color` | Disable ANSI color (auto-disabled when not a TTY) |
|
|
150
|
+
| `--quiet` | Suppress progress output on stderr |
|
|
151
|
+
|
|
152
|
+
**Examples:**
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
# Fetch all findings (JSON)
|
|
156
|
+
codeant scans results --repo acme/backend
|
|
157
|
+
|
|
158
|
+
# Fetch only critical and high severity findings
|
|
159
|
+
codeant scans results --repo acme/backend --severity critical,high
|
|
160
|
+
|
|
161
|
+
# Fetch SAST findings only
|
|
162
|
+
codeant scans results --repo acme/backend --types sast
|
|
163
|
+
|
|
164
|
+
# Filter to a specific file path
|
|
165
|
+
codeant scans results --repo acme/backend --path 'src/**/*.ts'
|
|
166
|
+
|
|
167
|
+
# Filter by check name using regex
|
|
168
|
+
codeant scans results --repo acme/backend --check 'sql-injection'
|
|
169
|
+
|
|
170
|
+
# Output as a Markdown table
|
|
171
|
+
codeant scans results --repo acme/backend --format md
|
|
172
|
+
|
|
173
|
+
# Output as SARIF to a file
|
|
174
|
+
codeant scans results --repo acme/backend --format sarif --output results.sarif
|
|
175
|
+
|
|
176
|
+
# Include dismissed findings
|
|
177
|
+
codeant scans results --repo acme/backend --include-dismissed
|
|
178
|
+
|
|
179
|
+
# Paginate through results
|
|
180
|
+
codeant scans results --repo acme/backend --limit 50 --offset 100
|
|
181
|
+
|
|
182
|
+
# Project only specific fields
|
|
183
|
+
codeant scans results --repo acme/backend --fields id,severity,message,path
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Exit codes:**
|
|
187
|
+
|
|
188
|
+
| Code | Meaning |
|
|
189
|
+
|------|---------|
|
|
190
|
+
| `0` | Success |
|
|
191
|
+
| `1` | General error |
|
|
192
|
+
| `3` | Category fetch failure (with `--fail-fast`) |
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
### `scans dismissed`
|
|
197
|
+
|
|
198
|
+
List dismissed alerts for a repository.
|
|
199
|
+
|
|
200
|
+
```bash
|
|
201
|
+
codeant scans dismissed --repo <owner/repo> [options]
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**Options:**
|
|
205
|
+
|
|
206
|
+
| Option | Description |
|
|
207
|
+
|--------|-------------|
|
|
208
|
+
| `--repo <repo>` | **(required)** Repository in `owner/repo` format |
|
|
209
|
+
| `--analysis-type <type>` | Analysis type: `security` or `secrets` (default: `security`) |
|
|
210
|
+
|
|
211
|
+
**Examples:**
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
# List dismissed security alerts
|
|
215
|
+
codeant scans dismissed --repo acme/backend
|
|
216
|
+
|
|
217
|
+
# List dismissed secrets alerts
|
|
218
|
+
codeant scans dismissed --repo acme/backend --analysis-type secrets
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
### `scans start-scan`
|
|
224
|
+
|
|
225
|
+
Trigger a new analysis run for a repository.
|
|
226
|
+
|
|
227
|
+
```bash
|
|
228
|
+
codeant scans start-scan [options]
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
All options are optional — `repo`, `branch`, and `commit` are auto-detected from the local git context when not provided.
|
|
232
|
+
|
|
233
|
+
**Options:**
|
|
234
|
+
|
|
235
|
+
| Option | Description |
|
|
236
|
+
|--------|-------------|
|
|
237
|
+
| `--repo <repo>` | Repository in `owner/repo` format (auto-detected from git remote) |
|
|
238
|
+
| `--branch <name>` | Branch to scan (auto-detected from current checkout) |
|
|
239
|
+
| `--commit <sha>` | Commit SHA to scan (resolved from remote HEAD of branch if omitted) |
|
|
240
|
+
| `--include <paths>` | Comma-separated file path glob patterns to include |
|
|
241
|
+
| `--exclude <paths>` | Comma-separated file path glob patterns to exclude |
|
|
242
|
+
|
|
243
|
+
**Examples:**
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
# Zero-config — auto-detects repo, branch, and latest commit
|
|
247
|
+
codeant scans start-scan
|
|
248
|
+
|
|
249
|
+
# Explicit repo and branch
|
|
250
|
+
codeant scans start-scan --repo your-org/your-repo --branch main
|
|
251
|
+
|
|
252
|
+
# Explicit commit SHA
|
|
253
|
+
codeant scans start-scan \
|
|
254
|
+
--repo your-org/your-repo \
|
|
255
|
+
--branch main \
|
|
256
|
+
--commit b509bffa1721da442f35a7ccab969822711a67f0
|
|
257
|
+
|
|
258
|
+
# Scan only specific files
|
|
259
|
+
codeant scans start-scan \
|
|
260
|
+
--branch main \
|
|
261
|
+
--include "src/main.py,src/utils.py" \
|
|
262
|
+
--exclude "tests/,*.md"
|
|
263
|
+
```
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { validateConnection } from '../scans/connectionHandler.js';
|
|
2
|
+
|
|
3
|
+
const SERVICE_ALIASES = { azure_devops: 'azuredevops', ado: 'azuredevops' };
|
|
4
|
+
const PROVIDER_BASE_FIELDS = {
|
|
5
|
+
github: 'github_base_url',
|
|
6
|
+
gitlab: 'gitlab_base_url',
|
|
7
|
+
bitbucket: 'bitbucket_base_url',
|
|
8
|
+
azuredevops: 'azure_devops_base_url',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function normalizeService(value) {
|
|
12
|
+
const service = String(value || '').trim().toLowerCase();
|
|
13
|
+
return SERVICE_ALIASES[service] || service;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function resolveCliTenant({ org, service, providerBaseUrl } = {}) {
|
|
17
|
+
const validation = await validateConnection();
|
|
18
|
+
if (!validation.success) {
|
|
19
|
+
throw new Error(validation.error || 'Unable to load authenticated CodeAnt organizations.');
|
|
20
|
+
}
|
|
21
|
+
const requestedService = normalizeService(service);
|
|
22
|
+
const candidates = (validation.connections || []).filter((connection) => {
|
|
23
|
+
const orgMatches = !org || connection.organizationName === org;
|
|
24
|
+
const serviceMatches = !requestedService || normalizeService(connection.service) === requestedService;
|
|
25
|
+
return orgMatches && serviceMatches;
|
|
26
|
+
});
|
|
27
|
+
if (candidates.length === 0) {
|
|
28
|
+
throw new Error('No authenticated organization matches --org/--service. Run `codeant scans orgs` to list available connections.');
|
|
29
|
+
}
|
|
30
|
+
if (candidates.length > 1) {
|
|
31
|
+
throw new Error('More than one organization matches. Pass both --org and --service; run `codeant scans orgs` to list values.');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const connection = candidates[0];
|
|
35
|
+
const normalizedService = normalizeService(connection.service);
|
|
36
|
+
const baseField = PROVIDER_BASE_FIELDS[normalizedService];
|
|
37
|
+
if (!baseField) throw new Error(`Application APIs are not supported for service ${connection.service}.`);
|
|
38
|
+
const organization = org || connection.organizationName;
|
|
39
|
+
const baseUrl = providerBaseUrl || connection.baseUrl;
|
|
40
|
+
if (!baseUrl) {
|
|
41
|
+
throw new Error('The provider base URL is unavailable. Pass --provider-base-url explicitly.');
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
organization,
|
|
45
|
+
service: normalizedService,
|
|
46
|
+
providerBaseUrl: baseUrl,
|
|
47
|
+
requestBody: {
|
|
48
|
+
org: organization,
|
|
49
|
+
organization,
|
|
50
|
+
service: normalizedService,
|
|
51
|
+
[baseField]: baseUrl,
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { runApiRequest } from './request.js';
|
|
2
|
+
|
|
3
|
+
function collect(value, previous) {
|
|
4
|
+
return [...previous, value];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export default function registerApiCommands(program, { runCmd }) {
|
|
8
|
+
const api = program
|
|
9
|
+
.command('api')
|
|
10
|
+
.description('Call any authenticated CodeAnt application API');
|
|
11
|
+
|
|
12
|
+
api
|
|
13
|
+
.command('request <method> <path>')
|
|
14
|
+
.description('Send an authenticated request to a relative CodeAnt API path')
|
|
15
|
+
.option('--query <json>', 'Query parameters as a JSON object')
|
|
16
|
+
.option('--body <json>', 'JSON request body')
|
|
17
|
+
.option('--body-file <path>', 'Read the JSON request body from a file')
|
|
18
|
+
.option('--org <organization>', 'Authenticated CodeAnt organization')
|
|
19
|
+
.option('--service <provider>', 'SCM provider: github, gitlab, bitbucket, or azuredevops')
|
|
20
|
+
.option('--provider-base-url <url>', 'Provider base URL override for self-hosted SCMs')
|
|
21
|
+
.option('-H, --header <header>', 'Additional header (repeatable, "Name: value")', collect, [])
|
|
22
|
+
.action((method, path, options) => runCmd(() => runApiRequest({
|
|
23
|
+
method,
|
|
24
|
+
path,
|
|
25
|
+
query: options.query,
|
|
26
|
+
body: options.body,
|
|
27
|
+
bodyFile: options.bodyFile,
|
|
28
|
+
headers: options.header,
|
|
29
|
+
org: options.org,
|
|
30
|
+
service: options.service,
|
|
31
|
+
providerBaseUrl: options.providerBaseUrl,
|
|
32
|
+
})));
|
|
33
|
+
}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
|
|
3
|
+
import { fetchApiResponse } from '../../utils/fetchApi.js';
|
|
4
|
+
import { resolveCliTenant } from '../../api/tenant.js';
|
|
5
|
+
|
|
6
|
+
const METHODS = new Set(['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']);
|
|
7
|
+
const BLOCKED_HEADERS = new Set([
|
|
8
|
+
'authorization',
|
|
9
|
+
'cookie',
|
|
10
|
+
'host',
|
|
11
|
+
'content-length',
|
|
12
|
+
'x-codeant-cli-org',
|
|
13
|
+
'x-codeant-cli-service',
|
|
14
|
+
'x-codeant-cli-base-url',
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
export function normalizeApiPath(path) {
|
|
18
|
+
const value = String(path || '').trim();
|
|
19
|
+
if (!value.startsWith('/') || value.startsWith('//')) {
|
|
20
|
+
throw new Error('API path must be relative to the configured CodeAnt API URL and start with `/`.');
|
|
21
|
+
}
|
|
22
|
+
const resolved = new URL(value, 'https://codeant.invalid');
|
|
23
|
+
if (resolved.origin !== 'https://codeant.invalid') {
|
|
24
|
+
throw new Error('API path must stay on the configured CodeAnt API host.');
|
|
25
|
+
}
|
|
26
|
+
return `${resolved.pathname}${resolved.search}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function parseJsonObject(value, label) {
|
|
30
|
+
const parsed = parseJsonValue(value, label);
|
|
31
|
+
if (parsed === undefined) return undefined;
|
|
32
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
33
|
+
throw new Error(`${label} must be a JSON object.`);
|
|
34
|
+
}
|
|
35
|
+
return parsed;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function parseJsonValue(value, label) {
|
|
39
|
+
if (value === undefined || value === null || value === '') return undefined;
|
|
40
|
+
let parsed;
|
|
41
|
+
try {
|
|
42
|
+
parsed = typeof value === 'string' ? JSON.parse(value) : value;
|
|
43
|
+
} catch (error) {
|
|
44
|
+
throw new Error(`Invalid ${label} JSON: ${error.message}`);
|
|
45
|
+
}
|
|
46
|
+
return parsed;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function parseHeaders(values = []) {
|
|
50
|
+
const headers = {};
|
|
51
|
+
for (const entry of values || []) {
|
|
52
|
+
const separator = String(entry).indexOf(':');
|
|
53
|
+
if (separator < 1) throw new Error(`Invalid header ${JSON.stringify(entry)}; expected "Name: value".`);
|
|
54
|
+
const name = String(entry).slice(0, separator).trim();
|
|
55
|
+
const value = String(entry).slice(separator + 1).trim();
|
|
56
|
+
if (BLOCKED_HEADERS.has(name.toLowerCase())) {
|
|
57
|
+
throw new Error(`Header ${name} is managed by CodeAnt CLI and cannot be overridden.`);
|
|
58
|
+
}
|
|
59
|
+
headers[name] = value;
|
|
60
|
+
}
|
|
61
|
+
return headers;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function runApiRequest({
|
|
65
|
+
path,
|
|
66
|
+
method,
|
|
67
|
+
query,
|
|
68
|
+
body,
|
|
69
|
+
bodyFile,
|
|
70
|
+
headers,
|
|
71
|
+
org,
|
|
72
|
+
service,
|
|
73
|
+
providerBaseUrl,
|
|
74
|
+
} = {}) {
|
|
75
|
+
const normalizedMethod = String(method || 'GET').toUpperCase();
|
|
76
|
+
if (!METHODS.has(normalizedMethod)) {
|
|
77
|
+
throw new Error(`Unsupported method ${normalizedMethod}. Use ${[...METHODS].join(', ')}.`);
|
|
78
|
+
}
|
|
79
|
+
if (body !== undefined && bodyFile) {
|
|
80
|
+
throw new Error('Use either --body or --body-file, not both.');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let requestBody = parseJsonValue(body, 'body');
|
|
84
|
+
if (bodyFile) {
|
|
85
|
+
requestBody = parseJsonValue(await readFile(bodyFile, 'utf8'), 'body file');
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const tenant = await resolveCliTenant({ org, service, providerBaseUrl });
|
|
89
|
+
if (!['GET', 'HEAD'].includes(normalizedMethod)) {
|
|
90
|
+
if (requestBody !== undefined && (!requestBody || typeof requestBody !== 'object' || Array.isArray(requestBody))) {
|
|
91
|
+
throw new Error('Authenticated application API request bodies must be JSON objects.');
|
|
92
|
+
}
|
|
93
|
+
requestBody = { ...tenant.requestBody, ...(requestBody || {}) };
|
|
94
|
+
}
|
|
95
|
+
const response = await fetchApiResponse(normalizeApiPath(path), {
|
|
96
|
+
method: normalizedMethod,
|
|
97
|
+
query: parseJsonObject(query, 'query'),
|
|
98
|
+
body: requestBody,
|
|
99
|
+
headers: parseHeaders(headers),
|
|
100
|
+
allowHttpError: true,
|
|
101
|
+
tenant,
|
|
102
|
+
});
|
|
103
|
+
return {
|
|
104
|
+
ok: response.ok,
|
|
105
|
+
status: response.status,
|
|
106
|
+
tenant: tenant.requestBody,
|
|
107
|
+
data: response.data,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { runRepos } from '../scans/repos.js';
|
|
2
|
+
import { runResults } from '../scans/results.js';
|
|
3
|
+
import { setQuiet, setNoColor } from '../scans/lib/log.js';
|
|
4
|
+
import { setNoColor as tableSetNoColor } from '../scans/formatters/table.js';
|
|
5
|
+
import { runHotlistGet, runHotlistList } from '../../hotlist/client.js';
|
|
6
|
+
import { runOrganizationAntipatterns } from '../../findings/antipatterns.js';
|
|
7
|
+
import { runCloudFindingGet, runCloudFindings, runCloudHistory } from '../../findings/cloud.js';
|
|
8
|
+
import { runPentestHistory, runPentestIssues, runPentestReport } from '../../findings/pentest.js';
|
|
9
|
+
|
|
10
|
+
function addTenantOptions(command) {
|
|
11
|
+
return command
|
|
12
|
+
.option('--org <org>', 'Organization name (auto-picked when unambiguous)')
|
|
13
|
+
.option('--service <service>', 'github, gitlab, bitbucket, or azuredevops')
|
|
14
|
+
.option('--provider-base-url <url>', 'Override the authenticated provider base URL');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function tenantOptions(options) {
|
|
18
|
+
return {
|
|
19
|
+
org: options.org,
|
|
20
|
+
service: options.service,
|
|
21
|
+
providerBaseUrl: options.providerBaseUrl,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function addHotlistOptions(command) {
|
|
26
|
+
return addTenantOptions(command)
|
|
27
|
+
.option('--search <text>', 'Search title, repository, path, package, CVE, or check ID')
|
|
28
|
+
.option('--type <values>', 'Comma-separated types: SAST,SCA,Secrets,IaC,Infrastructure,AI Exploitation')
|
|
29
|
+
.option('--location <values>', 'Comma-separated repositories or cloud accounts')
|
|
30
|
+
.option('--severity <values>', 'Comma-separated severities')
|
|
31
|
+
.option('--ticket-status <values>', 'created,not_created')
|
|
32
|
+
.option('--compliance <values>', 'Comma-separated compliance frameworks')
|
|
33
|
+
.option('--validation <values>', 'Comma-separated validation flags')
|
|
34
|
+
.option('--limit <n>', 'Page size from 1 to 100', Number, 30)
|
|
35
|
+
.option('--cursor <cursor>', 'Continue from a previous next_cursor')
|
|
36
|
+
.option('--all', 'Fetch every matching page', false)
|
|
37
|
+
.option('--max-wait <seconds>', 'Wait for an initial Hotlist build', Number, 60);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function hotlistOptions(options) {
|
|
41
|
+
return {
|
|
42
|
+
...tenantOptions(options),
|
|
43
|
+
search: options.search,
|
|
44
|
+
types: options.type,
|
|
45
|
+
locations: options.location,
|
|
46
|
+
severities: options.severity,
|
|
47
|
+
ticketStatuses: options.ticketStatus,
|
|
48
|
+
compliance: options.compliance,
|
|
49
|
+
validation: options.validation,
|
|
50
|
+
limit: options.limit,
|
|
51
|
+
cursor: options.cursor,
|
|
52
|
+
all: options.all,
|
|
53
|
+
maxWaitSeconds: options.maxWait,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function addCloudScopeOptions(command) {
|
|
58
|
+
return addTenantOptions(command)
|
|
59
|
+
.requiredOption('--provider <provider>', 'Cloud provider: aws, azure, or gcp')
|
|
60
|
+
.option('--kind <kind>', 'Finding kind: cspm, vm, or container', 'cspm')
|
|
61
|
+
.requiredOption('--scan-id <id>', 'Cloud scan ID')
|
|
62
|
+
.option('--account-id <id>', 'AWS account ID')
|
|
63
|
+
.option('--tenant-id <id>', 'Azure tenant ID')
|
|
64
|
+
.option('--project-id <id>', 'GCP project ID')
|
|
65
|
+
.option('--cloud-service <name>', 'Provider service filter or finding service');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function cloudOptions(options) {
|
|
69
|
+
return {
|
|
70
|
+
...tenantOptions(options),
|
|
71
|
+
provider: options.provider,
|
|
72
|
+
kind: options.kind,
|
|
73
|
+
scanId: options.scanId,
|
|
74
|
+
accountId: options.accountId,
|
|
75
|
+
tenantId: options.tenantId,
|
|
76
|
+
projectId: options.projectId,
|
|
77
|
+
cloudService: options.cloudService,
|
|
78
|
+
severity: options.severity,
|
|
79
|
+
status: options.status,
|
|
80
|
+
framework: options.framework,
|
|
81
|
+
subscriptionId: options.subscriptionId,
|
|
82
|
+
exploitAttemptedOnly: options.exploitAttemptedOnly,
|
|
83
|
+
minDaysUnused: options.minDaysUnused,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function runRepoResults(options) {
|
|
88
|
+
setQuiet(options.quiet);
|
|
89
|
+
if (options.noColor) {
|
|
90
|
+
setNoColor(true);
|
|
91
|
+
tableSetNoColor(true);
|
|
92
|
+
}
|
|
93
|
+
return runResults({
|
|
94
|
+
repo: options.repo,
|
|
95
|
+
scan: options.scan,
|
|
96
|
+
branch: options.branch,
|
|
97
|
+
types: options.types,
|
|
98
|
+
severity: options.severity,
|
|
99
|
+
path: options.path,
|
|
100
|
+
check: options.check,
|
|
101
|
+
filterDismissed: options.filterDismissed || false,
|
|
102
|
+
includeFalsePositives: options.falsePositives ?? true,
|
|
103
|
+
format: options.format,
|
|
104
|
+
output: options.output,
|
|
105
|
+
fields: options.fields,
|
|
106
|
+
limit: options.limit,
|
|
107
|
+
offset: options.offset,
|
|
108
|
+
failFast: options.failFast || false,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export default function registerFindingsCommands(program, { runCmd }) {
|
|
113
|
+
const findings = program.command('findings').description('Access CodeAnt findings from repositories, Hotlist, cloud security, and pentesting');
|
|
114
|
+
|
|
115
|
+
findings
|
|
116
|
+
.command('repos')
|
|
117
|
+
.description('List repositories available for repo-level findings')
|
|
118
|
+
.option('--org <org>', 'Organization name (auto-picked when only one is authenticated)')
|
|
119
|
+
.action((options) => runCmd(() => runRepos({ org: options.org })));
|
|
120
|
+
|
|
121
|
+
findings
|
|
122
|
+
.command('repo')
|
|
123
|
+
.description('Fetch repo-level SAST, SCA, IaC, secrets, anti-pattern, and quality findings')
|
|
124
|
+
.requiredOption('--repo <repo>', 'Repository (owner/repo)')
|
|
125
|
+
.option('--scan <sha>', 'Specific commit SHA to use')
|
|
126
|
+
.option('--branch <name>', 'Resolve latest scan on this branch')
|
|
127
|
+
.option('--types <list>', 'Comma-separated types: sast,sca,secrets,iac,dead_code,sbom,anti_patterns,docstring,complex_functions,all', 'all')
|
|
128
|
+
.option('--severity <list>', 'Filter by severity')
|
|
129
|
+
.option('--path <glob>', 'Filter by file path glob')
|
|
130
|
+
.option('--check <regex>', 'Filter by check ID or name')
|
|
131
|
+
.option('--filter-dismissed', 'Exclude dismissed findings')
|
|
132
|
+
.option('--no-false-positives', 'Exclude false positives')
|
|
133
|
+
.option('--format <fmt>', 'json|sarif|csv|md|table', 'json')
|
|
134
|
+
.option('--output <path>', 'Write output to a file')
|
|
135
|
+
.option('--fields <list>', 'Project findings to a subset of fields')
|
|
136
|
+
.option('--limit <n>', 'Max findings per page', Number, 100)
|
|
137
|
+
.option('--offset <n>', 'Pagination offset', Number, 0)
|
|
138
|
+
.option('--fail-fast', 'Stop on the first category error')
|
|
139
|
+
.option('--no-color', 'Disable ANSI color')
|
|
140
|
+
.option('--quiet', 'Suppress progress output')
|
|
141
|
+
.action(async (options) => {
|
|
142
|
+
try { await runRepoResults(options); }
|
|
143
|
+
catch (err) {
|
|
144
|
+
process.stderr.write(JSON.stringify({ error: err.message }) + '\n');
|
|
145
|
+
process.exitCode = err.exitCode ?? 1;
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
addHotlistOptions(findings.command('list').description('List organization-wide Hotlist findings'))
|
|
150
|
+
.action((options) => runCmd(() => runHotlistList(hotlistOptions(options))));
|
|
151
|
+
|
|
152
|
+
addTenantOptions(findings.command('get <finding-id>').description('Get one Hotlist finding by its stable ID'))
|
|
153
|
+
.option('--max-wait <seconds>', 'Wait for an initial Hotlist build', Number, 60)
|
|
154
|
+
.action((findingId, options) => runCmd(() => runHotlistGet({
|
|
155
|
+
findingId,
|
|
156
|
+
...tenantOptions(options),
|
|
157
|
+
maxWaitSeconds: options.maxWait,
|
|
158
|
+
})));
|
|
159
|
+
|
|
160
|
+
addTenantOptions(findings.command('antipatterns').description('List anti-pattern findings across selected or all organization repositories'))
|
|
161
|
+
.option('--repos <repos>', 'Comma-separated owner/repo values; defaults to every repository')
|
|
162
|
+
.action((options) => runCmd(() => runOrganizationAntipatterns({ ...tenantOptions(options), repos: options.repos })));
|
|
163
|
+
|
|
164
|
+
const cloud = findings.command('cloud').description('Cloud security CSPM, VM, and container findings');
|
|
165
|
+
addTenantOptions(cloud.command('history').description('List cloud scan history, or latest scans'))
|
|
166
|
+
.option('--provider <provider>', 'aws, azure, gcp, or all', 'all')
|
|
167
|
+
.option('--kind <kind>', 'Finding kind: cspm, vm, or container', 'cspm')
|
|
168
|
+
.option('--latest', 'Return only latest CSPM scans')
|
|
169
|
+
.action((options) => runCmd(() => runCloudHistory({ ...tenantOptions(options), provider: options.provider, kind: options.kind, latest: options.latest })));
|
|
170
|
+
|
|
171
|
+
addCloudScopeOptions(cloud.command('list').description('List findings for a cloud scan'))
|
|
172
|
+
.option('--severity <value>', 'Severity filter')
|
|
173
|
+
.option('--status <value>', 'Finding status filter')
|
|
174
|
+
.option('--framework <value>', 'Compliance framework filter')
|
|
175
|
+
.option('--subscription-id <id>', 'Azure subscription ID filter')
|
|
176
|
+
.option('--exploit-attempted-only', 'AWS findings with validation attempts only')
|
|
177
|
+
.option('--min-days-unused <n>', 'Minimum unused age in days', Number)
|
|
178
|
+
.action((options) => runCmd(() => runCloudFindings(cloudOptions(options))));
|
|
179
|
+
|
|
180
|
+
addCloudScopeOptions(cloud.command('get').description('Get one cloud finding with full detail'))
|
|
181
|
+
.requiredOption('--uid <uid>', 'Finding UID')
|
|
182
|
+
.action((options) => runCmd(() => runCloudFindingGet({ ...cloudOptions(options), uid: options.uid })));
|
|
183
|
+
|
|
184
|
+
const pentest = findings.command('pentest').description('Pentest histories, issues, and reports');
|
|
185
|
+
addTenantOptions(pentest.command('history').description('List pentest engagements'))
|
|
186
|
+
.action((options) => runCmd(() => runPentestHistory(tenantOptions(options))));
|
|
187
|
+
|
|
188
|
+
addTenantOptions(pentest.command('issues').description('List all issues for a pentest engagement'))
|
|
189
|
+
.requiredOption('--report-id <id>', 'Pentest report/engagement ID')
|
|
190
|
+
.option('--variant <variant>', 'prod or test', 'prod')
|
|
191
|
+
.action((options) => runCmd(() => runPentestIssues({ ...tenantOptions(options), reportId: options.reportId, variant: options.variant })));
|
|
192
|
+
|
|
193
|
+
addTenantOptions(pentest.command('report').description('Get the full pentest customer report'))
|
|
194
|
+
.requiredOption('--report-id <id>', 'Pentest report/engagement ID')
|
|
195
|
+
.option('--variant <variant>', 'prod or test', 'prod')
|
|
196
|
+
.action((options) => runCmd(() => runPentestReport({ ...tenantOptions(options), reportId: options.reportId, variant: options.variant })));
|
|
197
|
+
}
|