feat(scan): add scan status subcommand [PPSC-1127] - #286
feat(scan): add scan status subcommand [PPSC-1127]#286rushabh-thakkar-17 wants to merge 2 commits into
Conversation
New `armis-cli scan status [scan_id]` reports the current state of an initiated scan via GET /api/v1/ingest/status/. Handles every documented ArtifactScanStatus enum value (PENDING_UPLOAD, UPLOADED, INITIATED, IN_PROGRESS, COMPLETED, FAILED, STOPPED) with a state-specific hint line. When invoked without a scan_id, the command falls back to the most recent scan initiated locally for the current (base_url, tenant_id) pair. Every scan repo/image/sbom now records its scan_id in ~/.armis/scan-history.json (0600, capped at 20 entries) so the fallback works out of the box. - Guardrails: --format restricted to human/json, HTTPS enforcement inherited from the shared API client, per-tenant scoping to prevent cross-tenant leakage, and 404/403/422 errors surfaced with actionable hints instead of raw HTTP bodies. - Documentation: README section, updated help examples on `scan` and root. - Tests: unit coverage for all 7 status values, JSON envelope shape, history fallback, cross-env scoping, empty-data / 4xx paths, and history package round-trip / dedup / trimming behavior.
Test Coverage Reporttotal: (statements) 72.5% Coverage by function |
CI golangci-lint (fresh cache) surfaced the goconst rule for every "human" and "json" literal in root.go, scan.go, and scan_status.go once the shared statusFormatHuman/statusFormatJSON constants exist. Promote those constants to scan.go (next to validFormats) and reuse them in all three files.
|
| Flag | Values | Default | Description |
|---|---|---|---|
--format |
human, json |
human |
Output format. SARIF/JUnit are rejected. |
--help
Fetch the current status of a scan initiated via 'armis-cli scan'.
When invoked without a scan_id, the command uses the most recent scan_id
recorded locally for the current (base URL, tenant) pair. Every successful
'scan repo', 'scan image', and 'scan sbom' automatically records its
scan_id in ~/.armis/scan-history.json (created 0600) so that this fallback
works out of the box.
Usage:
armis-cli scan status [scan_id] [flags]
Examples:
# Look up a specific scan
$ armis-cli scan status a1b2c3d4-...
# Re-check the most recently initiated scan on this machine
$ armis-cli scan status
# Machine-readable output
$ armis-cli scan status --format json
Flags:
--format string Output format: human, json (default: human) (default "human")
-h, --help help for status
Happy-path outputs (exit 0)
Each of the seven ArtifactScanStatus enum values yields a labeled block
plus a state-specific one-liner. The FAILED state additionally renders a
red badge on the Status: line and surfaces the API-supplied error under
Last Error:.
PENDING_UPLOAD
Scan Status
Scan ID: demo-scan.PENDING_UPLOAD
Status: PENDING_UPLOAD
Artifact Type: repo
Scan Type: full
File: armis-cli.tar.gz
File Size: 2.0 MiB
Started: 2026-07-27T10:00:00Z
Updated: 2026-07-27T10:05:00Z
Expires: 2026-08-27T10:00:00Z
Waiting for the artifact upload to reach S3.
UPLOADED
Scan Status
Scan ID: demo-scan.UPLOADED
Status: UPLOADED
Artifact Type: repo
Scan Type: full
File: armis-cli.tar.gz
File Size: 2.0 MiB
Started: 2026-07-27T10:00:00Z
Updated: 2026-07-27T10:05:00Z
Expires: 2026-08-27T10:00:00Z
Upload complete; the API is preparing to dispatch the scan.
INITIATED
Scan Status
Scan ID: demo-scan.INITIATED
Status: INITIATED
Artifact Type: repo
Scan Type: full
File: armis-cli.tar.gz
File Size: 2.0 MiB
Started: 2026-07-27T10:00:00Z
Updated: 2026-07-27T10:05:00Z
Expires: 2026-08-27T10:00:00Z
Scan queued; analysis has not started yet.
IN_PROGRESS
Scan Status
Scan ID: demo-scan.IN_PROGRESS
Status: IN_PROGRESS
Artifact Type: repo
Scan Type: full
File: armis-cli.tar.gz
File Size: 2.0 MiB
Started: 2026-07-27T10:00:00Z
Updated: 2026-07-27T10:05:00Z
Expires: 2026-08-27T10:00:00Z
Analysis is running. Re-run this command to refresh.
COMPLETED
Scan Status
Scan ID: demo-scan.COMPLETED
Status: COMPLETED
Artifact Type: repo
Scan Type: full
File: armis-cli.tar.gz
File Size: 2.0 MiB
Started: 2026-07-27T10:00:00Z
Updated: 2026-07-27T10:05:00Z
Expires: 2026-08-27T10:00:00Z
Scan finished — retrieve findings with the scan command's output.
(The word COMPLETED is rendered in the accent-green style in the terminal.)
FAILED
Scan Status
Scan ID: demo-scan.FAILED
Status: FAILED
Artifact Type: repo
Scan Type: full
File: armis-cli.tar.gz
File Size: 2.0 MiB
Started: 2026-07-27T10:00:00Z
Updated: 2026-07-27T10:05:00Z
Expires: 2026-08-27T10:00:00Z
Last Error: ERROR worker crashed unexpectedly
Scan failed. See Last Error above for details.
(FAILED renders with a red badge; ERROR is a red inline badge before
the API-supplied error text.)
STOPPED
Scan Status
Scan ID: demo-scan.STOPPED
Status: STOPPED
Artifact Type: repo
Scan Type: full
File: armis-cli.tar.gz
File Size: 2.0 MiB
Started: 2026-07-27T10:00:00Z
Updated: 2026-07-27T10:05:00Z
Expires: 2026-08-27T10:00:00Z
Scan was stopped before it could complete.
JSON output (--format json)
The JSON envelope mirrors the API record verbatim under status, adds a
top-level scan_id for convenience, and — when the ID came from the local
history file rather than the CLI arg — sets from_scan_history: true.
{
"scan_id": "demo-scan.COMPLETED",
"status": {
"artifact_type": "repo",
"completed_at": null,
"expiration_time": "2026-08-27T10:00:00Z",
"file_bytes": 2048576,
"file_name": "armis-cli.tar.gz",
"last_error": null,
"scan_id": "demo-scan.COMPLETED",
"scan_status": "COMPLETED",
"scan_type": "full",
"started_at": "2026-07-27T10:00:00Z",
"tenant_id": "tenant-demo",
"updated_at": "2026-07-27T10:05:00Z"
}
}History-fallback output (no scan_id argument)
When invoked with no argument and the local ~/.armis/scan-history.json
has a matching entry for the current (base_url, tenant_id), the command
resolves the scan_id automatically and marks the resolution in the footer:
Scan Status
Scan ID: remembered-scan.IN_PROGRESS
Status: IN_PROGRESS
Artifact Type: repo
Scan Type: full
File: armis-cli.tar.gz
File Size: 2.0 MiB
Started: 2026-07-27T10:00:00Z
Updated: 2026-07-27T10:05:00Z
Expires: 2026-08-27T10:00:00Z
Analysis is running. Re-run this command to refresh.
Resolved scan_id from local history (armis-cli).
Error outputs (exit 1)
Every failure path produces a single-line, actionable error. Raw HTTP
bodies are never surfaced to the end user.
No scan_id, empty history
Error: no scan_id provided and no recent scan recorded for tenant
tenant-demo on http://127.0.0.1:38765. Run 'armis-cli scan repo|image|sbom'
first, or pass an explicit scan_id
Scan not found (404)
Error: scan some-id.NOTFOUND not found (check the scan_id and that the
tenant matches)
Access denied (403)
Error: access denied for scan some-id.FORBIDDEN: your role is not
permitted for this endpoint
API returned 200 with empty data
Error: no status data returned for scan some-id.EMPTY (check the scan_id
and tenant)
Empty-string scan_id argument
Error: scan_id argument is empty; pass a non-empty scan_id or omit the
argument to use the last scan on this machine
Invalid --format
Error: invalid --format value "sarif": must be one of [human json]
Guardrails
- HTTPS-only for non-loopback URLs — inherited from the shared API client.
- Auth-header stripped from non-loopback HTTP — the API client refuses
to attach credentials overhttp://for any non-loopback host. - Per-tenant + per-env scoping — history entries are keyed by
(base_url, tenant_id). A scan initiated against dev cannot be
returned as the "latest" scan when the CLI is pointed at prod. - On-disk history is bounded — the file is
0600, capped at 20 entries,
and any corrupted/oversized file is treated as empty (never crashes the
CLI). --formatis a strict allowlist — SARIF/JUnit are meaningless for a
single status record and are rejected before any network call.- 404 / 403 / 422 error translation — plain HTTP bodies are replaced
with actionable messages including the offending scan_id.
Test coverage
Automated (go test ./... all green):
- All seven
ArtifactScanStatusenum values verified viaRunEend-to-end. - JSON envelope shape locked (
scan_id,status.*,from_scan_history). - History-fallback path + cross-env scoping.
- 404/403/422 translations, empty-data guard, empty-arg guard, bad-format guard.
internal/scan/historypackage: round-trip, dedup, trim-to-max,
base-URL normalization, tenant scoping, permission bits, corrupt file
handling.
Manual (built binary + local HTTP mock, 16 scenarios): every case in this
document was run through ./bin/armis-cli before the PR was raised.
Summary
armis-cli scan status [scan_id]command that queriesGET /api/v1/ingest/status/and reports the current state of a scan.repo|image|sbomnow records itsscan_idin~/.armis/scan-history.json(0600, capped at 20 entries, scoped bybase_url+tenant_id) soscan statuswithout an argument re-checks the most recent local scan.ArtifactScanStatus(PENDING_UPLOAD, UPLOADED, INITIATED, IN_PROGRESS, COMPLETED, FAILED, STOPPED) with a state-specific hint line; supports--format human|json.Guardrails
--formatrestricted tohuman/json(SARIF/JUnit make no sense for a single record).(base_url, tenant_id)— a scan initiated against dev cannot leak into a prodscan statusfallback.Test plan
Automated:
TestScanStatus_AllStatusesexercises every enum value end-to-end through the RunE.TestScanStatus_JSONFormatlocks the JSON envelope shape (top-levelscan_id, nestedstatus.*,from_scan_historymarker).TestScanStatus_FallsBackToHistory/TestScanStatus_HistoryScopedByEnvcover the no-arg fallback and cross-env scoping.TestScanStatus_APIErrorscovers 404/403/422 translations;_EmptyData,_EmptyScanIDArg,_InvalidFormat,_NoArgNoHistorycover input-validation edges.internal/scan/historypackage round-trip / dedup / trimming / normalization / corrupt-file / permission tests.Manual (built binary + local mock exercising the real HTTP flow):
--format jsonoutput includesfrom_scan_history=trueon the fallback path and omits it on the explicit-arg path.--debugmode; emptydataarray; empty scan_id argument.