Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## [0.5.16] - 02/11/2026

- Added `findings cloud start --kind container` to start an AWS ECR, Azure ACR, or GCP Artifact Registry container scan without the UI. `--wait` waits up to `--timeout` seconds (default 1800), and `--fail-on <severity>` exits 2 when the scan has findings at or above that severity. A scan with failed images exits 3 and a timeout exits 4.

## [0.5.15] - 26/10/2026

- Added `--secrets-scope current|history|all` to `findings repo` and `scans results`, with `current` as the default. Use `--history-run` to retrieve a specific history scan, including when no snapshot scan exists.
Expand Down
61 changes: 60 additions & 1 deletion findings.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# CodeAnt findings CLI

`codeant findings` is the unified, read-only entry point for findings visible in the CodeAnt app. It reuses the same authenticated backend endpoints and authorization checks as the UI.
`codeant findings` is the unified entry point for findings visible in the CodeAnt app. It reuses the same authenticated backend endpoints and authorization checks as the UI. Every command is read-only except `findings cloud start`, which starts a container scan.

```bash
codeant login
Expand All @@ -20,6 +20,7 @@ When one login has multiple connections, pass the exact `--org` and `--service`
| Prioritized SAST/SCA/IaC/Secrets/Infrastructure/AI Exploitation | `codeant findings list/get` | organization Hotlist |
| Anti-patterns across repositories | `codeant findings antipatterns` | selected repos or organization |
| AWS/Azure/GCP CSPM, VM, and container findings | `codeant findings cloud history/list/get` | organization + cloud resource scope |
| Start an ECR/ACR/Artifact Registry container scan | `codeant findings cloud start` | organization + registry connection |
| Pentest engagements, issues, reports | `codeant findings pentest history/issues/report` | organization + engagement |

The existing `codeant scans repos`, `codeant scans results`, and `codeant hotlist list/get` commands remain supported. The unified commands are aliases or thin authenticated clients, so existing scripts do not need to migrate.
Expand Down Expand Up @@ -119,6 +120,64 @@ codeant findings cloud list \

`--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.

### Starting a container scan

`findings cloud start` scans images from an AWS, Azure, or GCP registry connection already set up in the CodeAnt app. For AWS, the CodeAnt IAM role needs the `AmazonEC2ContainerRegistryReadOnly` policy; the CSPM policies alone cannot pull images (see [Container Scanning](https://docs.codeant.ai/cloud_security/container-scanning)). It needs no UI and works with `CODEANT_API_TOKEN` in CI.

```bash
# Start and return immediately
codeant findings cloud start --provider aws --kind container \
--region ap-south-1 --repositories api,worker:v1.4.2

# Start, wait up to 30 minutes, and exit 2 on HIGH or CRITICAL findings
codeant findings cloud start --provider aws --kind container \
--region ap-south-1 --repositories api \
--wait --timeout 1800 --fail-on high

# Azure ACR and GCP Artifact Registry
codeant findings cloud start --provider azure --kind container --repositories myregistry/api:latest --wait
codeant findings cloud start --provider gcp --kind container --repositories my-project/us-central1/docker/api --wait

# Findings of the scan, with the existing command
codeant findings cloud list --provider aws --kind container --scan-id <scan-id> --all
```

| Provider | `--repositories` entry | Other inputs |
|---|---|---|
| `aws` | `<ecr-repository>` | `--region` (required), `--config-id` to pick one of several connected AWS accounts |
| `azure` | `<registry-name>/<repository>` | — |
| `gcp` | `<project>/<location>/<repository>/<image>` | — |

Append `:<tag>` to scan one tag; without it the newest tag is scanned. Without `--repositories`, every repository the connection can see is scanned. The `repositories` field of an earlier scan in `findings cloud history --kind container` shows exact names.

Without `--wait`, the response is `{ "scan_id", "status": "running", "repositories_total" }`. With `--wait`, the CLI checks the scan every 15 seconds and prints its summary once it finishes: `status` (`completed` or `completed_with_errors`), `severity_rollup`, `findings_total`, and one entry per image under `repositories` with its `status` and `error`. With `--fail-on`, the summary also carries `fail_on` and `threshold_breached`. `--fail-on` counts that severity and every higher one, from `critical`, `high`, `medium`, `low`, `unknown`.

| Exit code | Meaning |
|---|---|
| `0` | Started (no `--wait`), or finished below the `--fail-on` threshold |
| `1` | Request error: authentication, missing registry connection, denied cloud access, invalid input |
| `2` | Findings at or above `--fail-on` |
| `3` | At least one image failed to scan, for example an unknown repository or tag; see `repositories[].error` |
| `4` | Still running when `--timeout` ran out; the scan continues and its `scan_id` is in the error |

A threshold breach takes precedence over failed images. A repository name that does not exist does not fail the request; the scan finishes with that image failed (exit `3`).

#### CI example (GitHub Actions)

```yaml
- name: Scan container image
env:
CODEANT_API_TOKEN: ${{ secrets.CODEANT_API_TOKEN }}
run: |
npm install -g codeant-cli
codeant findings cloud start --provider aws --kind container \
--org my-org --service github \
--region us-east-1 --repositories api:${{ github.sha }} \
--wait --timeout 1800 --fail-on critical
```

`CODEANT_API_TOKEN` is the `apiKeyV2` value that `codeant login` saves in `~/.codeant/config.json`. Pass `--org` and `--service` when the login covers more than one organization.

## Pentest findings

```bash
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codeant-cli",
"version": "0.5.15",
"version": "0.5.16",
"description": "Code review CLI tool",
"type": "module",
"repository": {
Expand Down
34 changes: 33 additions & 1 deletion src/commands/findings/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { setQuiet, setNoColor } from '../scans/lib/log.js';
import { setNoColor as tableSetNoColor } from '../scans/formatters/table.js';
import { runHotlistGet, runHotlistList } from '../../hotlist/client.js';
import { runOrganizationAntipatterns } from '../../findings/antipatterns.js';
import { runCloudFindingGet, runCloudFindings, runCloudHistory } from '../../findings/cloud.js';
import { runCloudFindingGet, runCloudFindings, runCloudHistory, runCloudScanStart } from '../../findings/cloud.js';
import { intOption } from '../../utils/intOption.js';
import { runPentestHistory, runPentestIssues, runPentestReport } from '../../findings/pentest.js';

function addTenantOptions(command) {
Expand Down Expand Up @@ -196,6 +197,37 @@ export default function registerFindingsCommands(program, { runCmd }) {
.requiredOption('--uid <uid>', 'Finding UID')
.action((options) => runCmd(() => runCloudFindingGet({ ...cloudOptions(options), uid: options.uid })));

addTenantOptions(cloud.command('start').description('Start a container scan; --wait waits for it and applies --fail-on'))
.requiredOption('--provider <provider>', 'Cloud provider: aws, azure, or gcp')
.requiredOption('--kind <kind>', 'Scan kind: container')
.option('--region <region>', 'AWS region of the ECR registry (required for AWS)')
.option('--config-id <id>', 'AWS connection ID (default: the first connected AWS account)')
.option('--repositories <list>', 'Comma-separated repositories, each optionally :tag (default: every repository)')
.option('--wait', 'Wait until the scan finishes')
.option('--timeout <seconds>', 'Maximum --wait time (default: 1800)', intOption({ min: 1 }))
.option('--fail-on <severity>', 'With --wait, exit 2 when findings at or above: critical, high, medium, low, unknown')
.action(async (options) => {
try {
const result = await runCloudScanStart({
...tenantOptions(options),
provider: options.provider,
kind: options.kind,
region: options.region,
configId: options.configId,
repositories: options.repositories,
wait: options.wait,
timeout: options.timeout,
failOn: options.failOn,
});
console.log(JSON.stringify(result, null, 2));
if (result.threshold_breached) process.exitCode = 2;
else if (result.status === 'completed_with_errors') process.exitCode = 3;
} catch (err) {
console.error(JSON.stringify({ error: err.message }, null, 2));
process.exit(err.exitCode ?? 1);
}
});

const pentest = findings.command('pentest').description('Pentest histories, issues, and reports');
addTenantOptions(pentest.command('history').description('List pentest engagements'))
.action((options) => runCmd(() => runPentestHistory(tenantOptions(options))));
Expand Down
50 changes: 50 additions & 0 deletions src/findings/cloud.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { setTimeout as sleep } from 'node:timers/promises';

import { resolveCliTenant } from '../api/tenant.js';
import { fetchAppApi } from '../utils/fetchApi.js';
import { fetchPaginated } from './paginate.js';

const PROVIDERS = new Set(['aws', 'azure', 'gcp']);
const KINDS = new Set(['cspm', 'vm', 'container']);
const SEVERITIES = ['critical', 'high', 'medium', 'low', 'unknown'];
const POLL_INTERVAL_MS = 15_000;

function normalizeProvider(value, { allowAll = false } = {}) {
const provider = String(value || '').trim().toLowerCase();
Expand Down Expand Up @@ -116,4 +120,50 @@ export async function runCloudFindingGet(options = {}) {
});
}

function scanSummary(scan, failOn) {
const { findings, total, limit, offset, ...summary } = scan;
if (!failOn) return summary;
const blocking = SEVERITIES.slice(0, SEVERITIES.indexOf(failOn) + 1);
return {
...summary,
fail_on: failOn,
threshold_breached: blocking.some((severity) => summary.severity_rollup?.[severity] > 0),
};
}

export async function runCloudScanStart(options = {}) {
const provider = normalizeProvider(options.provider);
if (normalizeKind(options.kind) !== 'container') {
throw new Error('Only --kind container scans can be started from the CLI.');
}
const failOn = options.failOn?.toString().trim().toLowerCase();
if (failOn !== undefined && !SEVERITIES.includes(failOn)) {
throw new Error(`--fail-on must be one of: ${SEVERITIES.join(', ')}.`);
}
if (failOn !== undefined && !options.wait) throw new Error('--fail-on requires --wait.');
const target = {};
if (provider === 'aws') target.region = requireValue(options.region, '--region for AWS container scans');
if (options.configId) target.config_id = options.configId;
if (options.repositories) {
target.repositories = String(options.repositories).split(',').map((name) => name.trim()).filter(Boolean);
}

const tenant = await resolveCliTenant(options);
const started = await request(provider, 'container', 'start', options, target, tenant);
if (!options.wait) return started;

const timeout = options.timeout ?? 1800;
const deadline = Date.now() + timeout * 1000;
for (;;) {
const scan = await request(provider, 'container', 'results/paginated', options, { scan_id: started.scan_id, limit: 25 }, tenant);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: The deadline is checked only after this request returns, so a slow poll can finish after --timeout and prevent the CLI from exiting 4 on time.

Assessment: 🟠 Major · 🔁 Occurrence: Rarely · 🏷️ Logic error

Use CodeAnt Skill Fix in Cursor Fix in VSCode Claude

Prompt for AI Agent 🤖
This is a comment left during a code review.

**Path:** src/findings/cloud.js
**Line:** 158:158
**Comment:**
	*Logic Error: The deadline is checked only after this request returns, so a slow poll can finish after `--timeout` and prevent the CLI from exiting 4 on time.

Validate the correctness of the flagged issue. If correct, How can I resolve this? If you propose a fix, implement it and please make it concise.
Once fix is implemented, also check other comments on the same PR, and ask user if the user wants to fix the rest of the comments as well. if said yes, then fetch all the comments validate the correctness and implement a minimal fix
👍 | 👎

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CodeAnt-AI: While a scan is running, this poll only reads the scan meta.json and lists its repos/ prefix, and the API in front of it cuts requests off at about 60 s, so the overrun should be at most one short request. Is there a path where a single running-status poll could take much longer than that, enough to justify passing an AbortSignal through request() and fetchAppApi()?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The normal running-scan path—reading scan metadata and listing repositories—shouldn’t take much longer than the API’s roughly 60-second cutoff, so there’s no known backend operation that would routinely cause a large overrun.

However, that cutoff isn’t a client-side deadline: a stalled connection could leave fetch waiting, and even a 60-second response exceeds a valid --timeout 1. A late response could also report completion after the deadline and be returned as success. Since --timeout is documented as a maximum wait, I’d bound each poll by the remaining time with an AbortSignal and report exit 4 when that deadline expires. If the timeout is intended to be approximate, the signal plumbing isn’t necessary—but the docs should say an in-flight poll may overrun it.

if (scan.status !== 'running') return scanSummary(scan, failOn);
if (Date.now() >= deadline) {
const err = new Error(`Scan ${started.scan_id} is still running after ${timeout}s. Check it later with: codeant findings cloud list --provider ${provider} --kind container --scan-id ${started.scan_id}`);
err.exitCode = 4;
throw err;
}
await sleep(Math.min(POLL_INTERVAL_MS, deadline - Date.now()));
}
}

export { normalizeKind, normalizeProvider };