codeant-cli 0.5.4 → 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/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
+ }