- Introduced AGENTS.md, README.md, TASKS.md, and implementation_plan.md for Vexer, detailing mission, responsibilities, key components, and operational notes. - Established similar documentation structure for Vulnerability Explorer and Zastava modules, including their respective workflows, integrations, and observability notes. - Created risk scoring profiles documentation outlining the core workflow, factor model, governance, and deliverables. - Ensured all modules adhere to the Aggregation-Only Contract and maintain determinism and provenance in outputs.
936 lines
40 KiB
Markdown
Executable File
936 lines
40 KiB
Markdown
Executable File
# API & CLI Reference
|
||
|
||
*Purpose* – give operators and integrators a single, authoritative spec for REST/GRPC calls **and** first‑party CLI tools (`stella-cli`, `zastava`, `stella`).
|
||
Everything here is *source‑of‑truth* for generated Swagger/OpenAPI and the `--help` screens in the CLIs.
|
||
|
||
---
|
||
|
||
## 0 Quick Glance
|
||
|
||
| Area | Call / Flag | Notes |
|
||
| ------------------ | ------------------------------------------- | ------------------------------------------------------------------------------ |
|
||
| Scan entry | `POST /scan` | Accepts SBOM or image; sub‑5 s target |
|
||
| Delta check | `POST /layers/missing` | <20 ms reply; powers *delta SBOM* feature |
|
||
| Rate‑limit / quota | — | Headers **`X‑Stella‑Quota‑Remaining`**, **`X‑Stella‑Reset`** on every response |
|
||
| Policy I/O | `GET /policy/export`, `POST /policy/import` | YAML now; Rego coming |
|
||
| Policy lint | `POST /policy/validate` | Returns 200 OK if ruleset passes |
|
||
| Auth | `POST /connect/token` (OpenIddict) | Client‑credentials preferred |
|
||
| Health | `GET /healthz` | Simple liveness probe |
|
||
| Attestation * | `POST /attest` (TODO Q1‑2026) | SLSA provenance + Rekor log |
|
||
| CLI flags | `--sbom-type` `--delta` `--policy-file` | Added to `stella` |
|
||
|
||
\* Marked **TODO** → delivered after sixth month (kept on Feature Matrix “To Do” list).
|
||
|
||
---
|
||
|
||
## 1 Authentication
|
||
|
||
Stella Ops uses **OAuth 2.0 / OIDC** (token endpoint mounted via OpenIddict).
|
||
|
||
```
|
||
POST /connect/token
|
||
Content‑Type: application/x-www-form-urlencoded
|
||
|
||
grant_type=client_credentials&
|
||
client_id=ci‑bot&
|
||
client_secret=REDACTED&
|
||
scope=stella.api
|
||
```
|
||
|
||
Successful response:
|
||
|
||
```json
|
||
{
|
||
"access_token": "eyJraWQi...",
|
||
"token_type": "Bearer",
|
||
"expires_in": 3600
|
||
}
|
||
```
|
||
|
||
> **Tip** – pass the token via `Authorization: Bearer <token>` on every call.
|
||
|
||
---
|
||
|
||
## 2 REST API
|
||
|
||
### 2.0 Obtain / Refresh Offline‑Token
|
||
|
||
```text
|
||
POST /token/offline
|
||
Authorization: Bearer <admin‑token>
|
||
```
|
||
|
||
| Body field | Required | Example | Notes |
|
||
|------------|----------|---------|-------|
|
||
| `expiresDays` | no | `30` | Max 90 days |
|
||
|
||
```json
|
||
{
|
||
"jwt": "eyJhbGciOiJSUzI1NiIsInR5cCI6...",
|
||
"expires": "2025‑08‑17T00:00:00Z"
|
||
}
|
||
```
|
||
|
||
Token is signed with the backend’s private key and already contains
|
||
`"maxScansPerDay": {{ quota_token }}`.
|
||
|
||
|
||
### 2.1 Scan – Upload SBOM **or** Image
|
||
|
||
```
|
||
POST /scan
|
||
```
|
||
|
||
| Param / Header | In | Required | Description |
|
||
| -------------------- | ------ | -------- | --------------------------------------------------------------------- |
|
||
| `X‑Stella‑Sbom‑Type` | header | no | `trivy-json-v2`, `spdx-json`, `cyclonedx-json`; omitted ➞ auto‑detect |
|
||
| `?threshold` | query | no | `low`, `medium`, `high`, `critical`; default **critical** |
|
||
| body | body | yes | *Either* SBOM JSON *or* Docker image tarball/upload URL |
|
||
|
||
Every successful `/scan` response now includes:
|
||
|
||
| Header | Example |
|
||
|--------|---------|
|
||
| `X‑Stella‑Quota‑Remaining` | `129` |
|
||
| `X‑Stella‑Reset` | `2025‑07‑18T23:59:59Z` |
|
||
| `X‑Stella‑Token‑Expires` | `2025‑08‑17T00:00:00Z` |
|
||
|
||
**Response 200** (scan completed):
|
||
|
||
```json
|
||
{
|
||
"digest": "sha256:…",
|
||
"summary": {
|
||
"Critical": 0,
|
||
"High": 3,
|
||
"Medium": 12,
|
||
"Low": 41
|
||
},
|
||
"policyStatus": "pass",
|
||
"quota": {
|
||
"remaining": 131,
|
||
"reset": "2025-07-18T00:00:00Z"
|
||
}
|
||
}
|
||
```
|
||
|
||
**Response 202** – queued; polling URL in `Location` header.
|
||
|
||
---
|
||
|
||
### 2.2 Delta SBOM – Layer Cache Check
|
||
|
||
```
|
||
POST /layers/missing
|
||
Content‑Type: application/json
|
||
Authorization: Bearer <token>
|
||
```
|
||
|
||
```json
|
||
{
|
||
"layers": [
|
||
"sha256:d38b...",
|
||
"sha256:af45..."
|
||
]
|
||
}
|
||
```
|
||
|
||
**Response 200** — <20 ms target:
|
||
|
||
```json
|
||
{
|
||
"missing": [
|
||
"sha256:af45..."
|
||
]
|
||
}
|
||
```
|
||
|
||
Client then generates SBOM **only** for the `missing` layers and re‑posts `/scan`.
|
||
|
||
---
|
||
|
||
### 2.3 Policy Endpoints *(preview feature flag: `scanner.features.enablePolicyPreview`)*
|
||
|
||
All policy APIs require **`scanner.reports`** scope (or anonymous access while auth is disabled).
|
||
|
||
**Fetch schema**
|
||
|
||
```
|
||
GET /api/v1/policy/schema
|
||
Authorization: Bearer <token>
|
||
Accept: application/schema+json
|
||
```
|
||
|
||
Returns the embedded `policy-schema@1` JSON schema used by the binder.
|
||
|
||
**Run diagnostics**
|
||
|
||
```
|
||
POST /api/v1/policy/diagnostics
|
||
Content-Type: application/json
|
||
Authorization: Bearer <token>
|
||
```
|
||
|
||
```json
|
||
{
|
||
"policy": {
|
||
"format": "yaml",
|
||
"actor": "cli",
|
||
"description": "dev override",
|
||
"content": "version: \"1.0\"\nrules:\n - name: Quiet Dev\n environments: [dev]\n action:\n type: ignore\n justification: dev waiver\n"
|
||
}
|
||
}
|
||
```
|
||
|
||
**Response 200**:
|
||
|
||
```json
|
||
{
|
||
"success": false,
|
||
"version": "1.0",
|
||
"ruleCount": 1,
|
||
"errorCount": 0,
|
||
"warningCount": 1,
|
||
"generatedAt": "2025-10-19T03:25:14.112Z",
|
||
"issues": [
|
||
{ "code": "policy.rule.quiet.missing_vex", "message": "Quiet flag ignored: rule must specify requireVex justifications.", "severity": "Warning", "path": "$.rules[0]" }
|
||
],
|
||
"recommendations": [
|
||
"Review policy warnings and ensure intentional overrides are documented."
|
||
]
|
||
}
|
||
```
|
||
|
||
`success` is `false` when blocking issues remain; recommendations aggregate YAML ignore rules, VEX include/exclude hints, and vendor precedence guidance.
|
||
|
||
**Preview impact**
|
||
|
||
```
|
||
POST /api/v1/policy/preview
|
||
Authorization: Bearer <token>
|
||
Content-Type: application/json
|
||
```
|
||
|
||
```json
|
||
{
|
||
"imageDigest": "sha256:abc123",
|
||
"findings": [
|
||
{ "id": "finding-1", "severity": "Critical", "source": "NVD" }
|
||
],
|
||
"policy": {
|
||
"format": "yaml",
|
||
"content": "version: \"1.0\"\nrules:\n - name: Block Critical\n severity: [Critical]\n action: block\n"
|
||
}
|
||
}
|
||
```
|
||
|
||
**Response 200**:
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"policyDigest": "9c5e...",
|
||
"revisionId": "preview",
|
||
"changed": 1,
|
||
"diffs": [
|
||
{
|
||
"findingId": "finding-1",
|
||
"baseline": {"findingId": "finding-1", "status": "Pass"},
|
||
"projected": {
|
||
"findingId": "finding-1",
|
||
"status": "Blocked",
|
||
"ruleName": "Block Critical",
|
||
"ruleAction": "Block",
|
||
"score": 5.0,
|
||
"configVersion": "1.0",
|
||
"inputs": {"severityWeight": 5.0}
|
||
},
|
||
"changed": true
|
||
}
|
||
],
|
||
"issues": []
|
||
}
|
||
```
|
||
|
||
- Provide `policy` to preview staged changes; omit it to compare against the active snapshot.
|
||
- Baseline verdicts are optional; when omitted, the API synthesises pass baselines before computing diffs.
|
||
- Quieted verdicts include `quietedBy` and `quiet` flags; score inputs now surface reachability/vendor trust weights (`reachability.*`, `trustWeight.*`).
|
||
|
||
**OpenAPI**: the full API document (including these endpoints) is exposed at `/openapi/v1.json` and can be fetched for tooling or contract regeneration.
|
||
|
||
### 2.4 Scanner – Queue a Scan Job *(SP9 milestone)*
|
||
|
||
```
|
||
POST /api/v1/scans
|
||
Authorization: Bearer <token with scanner.scans.enqueue>
|
||
Content-Type: application/json
|
||
```
|
||
|
||
```json
|
||
{
|
||
"image": {
|
||
"reference": "registry.example.com/acme/app:1.2.3"
|
||
},
|
||
"force": false,
|
||
"clientRequestId": "ci-build-1845",
|
||
"metadata": {
|
||
"pipeline": "github",
|
||
"trigger": "pull-request"
|
||
}
|
||
}
|
||
```
|
||
|
||
| Field | Required | Notes |
|
||
| ------------------- | -------- | ------------------------------------------------------------------------------------------------ |
|
||
| `image.reference` | no\* | Full repo/tag (`registry/repo:tag`). Provide **either** `reference` or `digest` (sha256:…). |
|
||
| `image.digest` | no\* | OCI digest (e.g. `sha256:…`). |
|
||
| `force` | no | `true` forces a re-run even if an identical scan (`scanId`) already exists. Default **false**. |
|
||
| `clientRequestId` | no | Free-form string surfaced in audit logs. |
|
||
| `metadata` | no | Optional string map stored with the job and surfaced in observability feeds. |
|
||
|
||
\* At least one of `image.reference` or `image.digest` must be supplied.
|
||
|
||
**Response 202** – job accepted (idempotent):
|
||
|
||
```http
|
||
HTTP/1.1 202 Accepted
|
||
Location: /api/v1/scans/2f6c17f9b3f548e2a28b9c412f4d63f8
|
||
```
|
||
|
||
```json
|
||
{
|
||
"scanId": "2f6c17f9b3f548e2a28b9c412f4d63f8",
|
||
"status": "Pending",
|
||
"location": "/api/v1/scans/2f6c17f9b3f548e2a28b9c412f4d63f8",
|
||
"created": true
|
||
}
|
||
```
|
||
|
||
- `scanId` is deterministic – resubmitting an identical payload returns the same identifier with `"created": false`.
|
||
- API is cancellation-aware; aborting the HTTP request cancels the submission attempt.
|
||
- Required scope: **`scanner.scans.enqueue`**.
|
||
|
||
**Response 400** – validation problem (`Content-Type: application/problem+json`) when both `image.reference` and `image.digest` are blank.
|
||
|
||
### 2.5 Scanner – Fetch Scan Status
|
||
|
||
```
|
||
GET /api/v1/scans/{scanId}
|
||
Authorization: Bearer <token with scanner.scans.read>
|
||
Accept: application/json
|
||
```
|
||
|
||
**Response 200**:
|
||
|
||
```json
|
||
{
|
||
"scanId": "2f6c17f9b3f548e2a28b9c412f4d63f8",
|
||
"status": "Pending",
|
||
"image": {
|
||
"reference": "registry.example.com/acme/app:1.2.3",
|
||
"digest": null
|
||
},
|
||
"createdAt": "2025-10-18T20:15:12.482Z",
|
||
"updatedAt": "2025-10-18T20:15:12.482Z",
|
||
"failureReason": null
|
||
}
|
||
```
|
||
|
||
Statuses: `Pending`, `Running`, `Succeeded`, `Failed`, `Cancelled`.
|
||
|
||
### 2.6 Scanner – Stream Progress (SSE / JSONL)
|
||
|
||
```
|
||
GET /api/v1/scans/{scanId}/events?format=sse|jsonl
|
||
Authorization: Bearer <token with scanner.scans.read>
|
||
Accept: text/event-stream
|
||
```
|
||
|
||
When `format` is omitted the endpoint emits **Server-Sent Events** (SSE). Specify `format=jsonl` to receive newline-delimited JSON (`application/x-ndjson`). Response headers include `Cache-Control: no-store` and `X-Accel-Buffering: no` so intermediaries avoid buffering the stream.
|
||
|
||
**SSE frame** (default):
|
||
|
||
```
|
||
id: 1
|
||
event: pending
|
||
data: {"scanId":"2f6c17f9b3f548e2a28b9c412f4d63f8","sequence":1,"state":"Pending","message":"queued","timestamp":"2025-10-19T03:12:45.118Z","correlationId":"2f6c17f9b3f548e2a28b9c412f4d63f8:0001","data":{"force":false,"meta.pipeline":"github"}}
|
||
```
|
||
|
||
**JSONL frame** (`format=jsonl`):
|
||
|
||
```json
|
||
{"scanId":"2f6c17f9b3f548e2a28b9c412f4d63f8","sequence":1,"state":"Pending","message":"queued","timestamp":"2025-10-19T03:12:45.118Z","correlationId":"2f6c17f9b3f548e2a28b9c412f4d63f8:0001","data":{"force":false,"meta.pipeline":"github"}}
|
||
```
|
||
|
||
- `sequence` is monotonic starting at `1`.
|
||
- `correlationId` is deterministic (`{scanId}:{sequence:0000}`) unless a custom identifier is supplied by the publisher.
|
||
- `timestamp` is ISO‑8601 UTC with millisecond precision, ensuring deterministic ordering for consumers.
|
||
- The stream completes when the client disconnects or the coordinator stops publishing events.
|
||
|
||
### 2.7 Scanner – Assemble Report (Signed Envelope)
|
||
|
||
```
|
||
POST /api/v1/reports
|
||
Authorization: Bearer <token with scanner.reports>
|
||
Content-Type: application/json
|
||
```
|
||
|
||
Request body mirrors policy preview inputs (image digest plus findings). The service evaluates the active policy snapshot, assembles a verdict, and signs the canonical report payload.
|
||
|
||
**Response 200**:
|
||
|
||
```json
|
||
{
|
||
"report": {
|
||
"reportId": "report-9f8cde21aab54321",
|
||
"imageDigest": "sha256:7dbe0c9a5d4f1c8184007e9d94dbe55928f8a2db5ab9c1c2d4a2f7bbcdfe1234",
|
||
"generatedAt": "2025-10-23T15:32:22Z",
|
||
"verdict": "blocked",
|
||
"policy": {
|
||
"revisionId": "rev-42",
|
||
"digest": "8a0f72f8dc5c51c46991db3bba34e9b3c0c8e944a7a6d0a9c29a9aa6b8439876"
|
||
},
|
||
"summary": { "total": 2, "blocked": 1, "warned": 1, "ignored": 0, "quieted": 0 },
|
||
"verdicts": [
|
||
{
|
||
"findingId": "library:pkg/openssl@1.1.1w",
|
||
"status": "Blocked",
|
||
"ruleName": "Block vendor unknowns",
|
||
"ruleAction": "block",
|
||
"notes": "Unknown vendor telemetry — medium confidence band.",
|
||
"score": 19.5,
|
||
"configVersion": "1.0",
|
||
"inputs": {
|
||
"severityWeight": 50,
|
||
"trustWeight": 0.65,
|
||
"reachabilityWeight": 0.6,
|
||
"baseScore": 19.5,
|
||
"trustWeight.vendor": 0.65,
|
||
"reachability.unknown": 0.6,
|
||
"unknownConfidence": 0.55,
|
||
"unknownAgeDays": 5
|
||
},
|
||
"quietedBy": null,
|
||
"quiet": false,
|
||
"unknownConfidence": 0.55,
|
||
"confidenceBand": "medium",
|
||
"unknownAgeDays": 5,
|
||
"sourceTrust": "vendor",
|
||
"reachability": "unknown"
|
||
},
|
||
{
|
||
"findingId": "library:pkg/zlib@1.3.1",
|
||
"status": "Warned",
|
||
"ruleName": "Runtime mitigation required",
|
||
"ruleAction": "warn",
|
||
"notes": "Runtime reachable unknown — mitigation window required.",
|
||
"score": 18.75,
|
||
"configVersion": "1.0",
|
||
"inputs": {
|
||
"severityWeight": 75,
|
||
"trustWeight": 1,
|
||
"reachabilityWeight": 0.45,
|
||
"baseScore": 33.75,
|
||
"reachability.runtime": 0.45,
|
||
"warnPenalty": 15,
|
||
"unknownConfidence": 0.35,
|
||
"unknownAgeDays": 13
|
||
},
|
||
"quietedBy": null,
|
||
"quiet": false,
|
||
"unknownConfidence": 0.35,
|
||
"confidenceBand": "medium",
|
||
"unknownAgeDays": 13,
|
||
"sourceTrust": "NVD",
|
||
"reachability": "runtime"
|
||
}
|
||
],
|
||
"issues": []
|
||
},
|
||
"dsse": {
|
||
"payloadType": "application/vnd.stellaops.report+json",
|
||
"payload": "eyJyZXBvcnQiOnsicmVwb3J0SWQiOiJyZXBvcnQtOWY4Y2RlMjFhYWI1NDMyMSJ9fQ==",
|
||
"signatures": [
|
||
{
|
||
"keyId": "scanner-report-signing",
|
||
"algorithm": "hs256",
|
||
"signature": "MEQCIGHscnJ2bm9wYXlsb2FkZXIAIjANBgkqhkiG9w0BAQsFAAOCAQEASmFja3Nvbk1ldGE="
|
||
}
|
||
]
|
||
}
|
||
}
|
||
```
|
||
|
||
- The `report` object omits null fields and is deterministic (ISO timestamps, sorted keys) while surfacing `unknownConfidence`, `confidenceBand`, and `unknownAgeDays` for auditability.
|
||
- `dsse` follows the DSSE (Dead Simple Signing Envelope) shape; `payload` is the canonical UTF-8 JSON and `signatures[0].signature` is the base64 HMAC/Ed25519 value depending on configuration.
|
||
- Full offline samples live at `samples/policy/policy-report-unknown.json` (request + response) and `samples/api/reports/report-sample.dsse.json` (envelope fixture) for tooling tests or signature verification.
|
||
|
||
**Response 404** – `application/problem+json` payload with type `https://stellaops.org/problems/not-found` when the scan identifier is unknown.
|
||
|
||
> **Tip** – poll `Location` from the submission call until `status` transitions away from `Pending`/`Running`.
|
||
|
||
```yaml
|
||
# Example import payload (YAML)
|
||
version: "1.0"
|
||
rules:
|
||
- name: Ignore Low dev
|
||
severity: [Low, None]
|
||
environments: [dev, staging]
|
||
action: ignore
|
||
```
|
||
|
||
Validation errors come back as:
|
||
|
||
```json
|
||
{
|
||
"errors": [
|
||
{
|
||
"path": "$.rules[0].severity",
|
||
"msg": "Invalid level 'None'"
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
```json
|
||
# Preview response excerpt
|
||
{
|
||
"success": true,
|
||
"policyDigest": "9c5e...",
|
||
"revisionId": "rev-12",
|
||
"changed": 1,
|
||
"diffs": [
|
||
{
|
||
"baseline": {"findingId": "finding-1", "status": "pass"},
|
||
"projected": {"findingId": "finding-1", "status": "blocked", "ruleName": "Block Critical"},
|
||
"changed": true
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
### 2.4 Attestation (Planned – Q1‑2026)
|
||
|
||
```
|
||
POST /attest
|
||
```
|
||
|
||
| Param | Purpose |
|
||
| ----------- | ------------------------------------- |
|
||
| body (JSON) | SLSA v1.0 provenance doc |
|
||
| | Signed + stored in local Rekor mirror |
|
||
|
||
Returns `202 Accepted` and `Location: /attest/{id}` for async verify.
|
||
|
||
---
|
||
|
||
### 2.8 Runtime – Ingest Observer Events *(SCANNER-RUNTIME-12-301)*
|
||
|
||
```
|
||
POST /api/v1/runtime/events
|
||
Authorization: Bearer <token with scanner.runtime.ingest>
|
||
Content-Type: application/json
|
||
```
|
||
|
||
| Requirement | Details |
|
||
|-------------|---------|
|
||
| Auth scope | `scanner.runtime.ingest` |
|
||
| Batch size | ≤ **256** envelopes (`scanner.runtime.maxBatchSize`, configurable) |
|
||
| Payload cap | ≤ **1 MiB** serialized JSON (`scanner.runtime.maxPayloadBytes`) |
|
||
| Rate limits | Per-tenant and per-node token buckets (default 200 events/s tenant, 50 events/s node, burst 200) – excess returns **429** with `Retry-After`. |
|
||
| TTL | Runtime events retained **45 days** by default (`scanner.runtime.eventTtlDays`). |
|
||
|
||
**Request body**
|
||
|
||
```json
|
||
{
|
||
"batchId": "node-a-2025-10-20T15:03:12Z",
|
||
"events": [
|
||
{
|
||
"schemaVersion": "zastava.runtime.event@v1",
|
||
"event": {
|
||
"eventId": "evt-2f9c02b8",
|
||
"when": "2025-10-20T15:03:08Z",
|
||
"kind": "ContainerStart",
|
||
"tenant": "tenant-alpha",
|
||
"node": "cluster-a/node-01",
|
||
"runtime": { "engine": "containerd", "version": "1.7.19" },
|
||
"workload": {
|
||
"platform": "kubernetes",
|
||
"namespace": "payments",
|
||
"pod": "api-7c9fbbd8b7-ktd84",
|
||
"container": "api",
|
||
"containerId": "containerd://bead5...",
|
||
"imageRef": "ghcr.io/acme/api@sha256:deadbeef"
|
||
},
|
||
"process": { "pid": 12345, "entrypoint": ["/start.sh", "--serve"], "buildId": "5f0c7c3c..." },
|
||
"loadedLibs": [
|
||
{ "path": "/lib/x86_64-linux-gnu/libssl.so.3", "inode": 123456, "sha256": "abc123..." }
|
||
],
|
||
"posture": { "imageSigned": true, "sbomReferrer": "present" },
|
||
"delta": { "baselineImageDigest": "sha256:deadbeef" },
|
||
"evidence": [ { "signal": "proc.maps", "value": "libssl.so.3@0x7f..." } ],
|
||
"annotations": { "observerVersion": "1.0.0" }
|
||
}
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
**Responses**
|
||
|
||
| Code | Body | Notes |
|
||
|------|------|-------|
|
||
| `202 Accepted` | `{ "accepted": 128, "duplicates": 2 }` | Batch persisted; duplicates are ignored via unique `eventId`. |
|
||
| `400 Bad Request` | Problem+JSON | Validation failures – empty batch, duplicate IDs, unsupported schema version, payload too large. |
|
||
| `429 Too Many Requests` | Problem+JSON | Per-tenant/node rate limit exceeded; `Retry-After` header emitted in seconds. |
|
||
|
||
Persisted documents capture the canonical envelope (`payload` field), tenant/node metadata, and set an automatic TTL on `expiresAt`. Observers should retry rejected batches with exponential backoff honouring the provided `Retry-After` hint.
|
||
|
||
---
|
||
|
||
## 3 StellaOps CLI (`stellaops-cli`)
|
||
|
||
The new CLI is built on **System.CommandLine 2.0.0‑beta5** and mirrors the Concelier backend REST API.
|
||
Configuration follows the same precedence chain everywhere:
|
||
|
||
1. Environment variables (e.g. `API_KEY`, `STELLAOPS_BACKEND_URL`, `StellaOps:ApiKey`)
|
||
2. `appsettings.json` → `appsettings.local.json`
|
||
3. `appsettings.yaml` → `appsettings.local.yaml`
|
||
4. Defaults (`ApiKey = ""`, `BackendUrl = ""`, cache folders under the current working directory)
|
||
|
||
**Authority auth client resilience settings**
|
||
|
||
| Setting | Environment variable | Default | Purpose |
|
||
|---------|----------------------|---------|---------|
|
||
| `StellaOps:Authority:Resilience:EnableRetries` | `STELLAOPS_AUTHORITY_ENABLE_RETRIES` | `true` | Toggle Polly wait-and-retry handlers for discovery/token calls |
|
||
| `StellaOps:Authority:Resilience:RetryDelays` | `STELLAOPS_AUTHORITY_RETRY_DELAYS` | `1s,2s,5s` | Comma/space-separated backoff sequence (HH:MM:SS) |
|
||
| `StellaOps:Authority:Resilience:AllowOfflineCacheFallback` | `STELLAOPS_AUTHORITY_ALLOW_OFFLINE_CACHE_FALLBACK` | `true` | Reuse cached discovery/JWKS metadata when Authority is temporarily unreachable |
|
||
| `StellaOps:Authority:Resilience:OfflineCacheTolerance` | `STELLAOPS_AUTHORITY_OFFLINE_CACHE_TOLERANCE` | `00:10:00` | Additional tolerance window added to the discovery/JWKS cache lifetime |
|
||
|
||
See `docs/dev/32_AUTH_CLIENT_GUIDE.md` for recommended profiles (online vs. air-gapped) and testing guidance.
|
||
|
||
| Command | Purpose | Key Flags / Arguments | Notes |
|
||
|---------|---------|-----------------------|-------|
|
||
| `stellaops-cli scanner download` | Fetch and install scanner container | `--channel <stable\|beta\|nightly>` (default `stable`)<br>`--output <path>`<br>`--overwrite`<br>`--no-install` | Saves artefact under `ScannerCacheDirectory`, verifies digest/signature, and executes `docker load` unless `--no-install` is supplied. |
|
||
| `stellaops-cli scan run` | Execute scanner container against a directory (auto-upload) | `--target <directory>` (required)<br>`--runner <docker\|dotnet\|self>` (default from config)<br>`--entry <image-or-entrypoint>`<br>`[scanner-args...]` | Runs the scanner, writes results into `ResultsDirectory`, emits a structured `scan-run-*.json` metadata file, and automatically uploads the artefact when the exit code is `0`. |
|
||
| `stellaops-cli scan upload` | Re-upload existing scan artefact | `--file <path>` | Useful for retries when automatic upload fails or when operating offline. |
|
||
| `stellaops-cli db fetch` | Trigger connector jobs | `--source <id>` (e.g. `redhat`, `osv`)<br>`--stage <fetch\|parse\|map>` (default `fetch`)<br>`--mode <resume|init|cursor>` | Translates to `POST /jobs/source:{source}:{stage}` with `trigger=cli` |
|
||
| `stellaops-cli db merge` | Run canonical merge reconcile | — | Calls `POST /jobs/merge:reconcile`; exit code `0` on acceptance, `1` on failures/conflicts |
|
||
| `stellaops-cli db export` | Kick JSON / Trivy exports | `--format <json\|trivy-db>` (default `json`)<br>`--delta`<br>`--publish-full/--publish-delta`<br>`--bundle-full/--bundle-delta` | Sets `{ delta = true }` parameter when requested and can override ORAS/bundle toggles per run |
|
||
| `stellaops-cli auth <login\|logout\|status\|whoami>` | Manage cached tokens for StellaOps Authority | `auth login --force` (ignore cache)<br>`auth status`<br>`auth whoami` | Uses `StellaOps.Auth.Client`; honours `StellaOps:Authority:*` configuration, stores tokens under `~/.stellaops/tokens` by default, and `whoami` prints subject/scope/expiry |
|
||
| `stellaops-cli auth revoke export` | Export the Authority revocation bundle | `--output <directory>` (defaults to CWD) | Writes `revocation-bundle.json`, `.json.jws`, and `.json.sha256`; verifies the digest locally and includes key metadata in the log summary. |
|
||
| `stellaops-cli auth revoke verify` | Validate a revocation bundle offline | `--bundle <path>` `--signature <path>` `--key <path>`<br>`--verbose` | Verifies detached JWS signatures, reports the computed SHA-256, and can fall back to cached JWKS when `--key` is omitted. |
|
||
| `stellaops-cli offline kit pull` | Download the latest offline kit bundle and manifest | `--bundle-id <id>` (optional)<br>`--destination <dir>`<br>`--overwrite`<br>`--no-resume` | Streams the bundle + manifest from the configured mirror/backend, resumes interrupted downloads, verifies SHA-256, and writes signatures plus a `.metadata.json` manifest alongside the artefacts. |
|
||
| `stellaops-cli offline kit import` | Upload an offline kit bundle to the backend | `<bundle.tgz>` (argument)<br>`--manifest <path>`<br>`--bundle-signature <path>`<br>`--manifest-signature <path>` | Validates digests when metadata is present, then posts multipart payloads to `POST /api/offline-kit/import`; logs the submitted import ID/status for air-gapped rollout tracking. |
|
||
| `stellaops-cli offline kit status` | Display imported offline kit details | `--json` | Shows bundle id/kind, captured/imported timestamps, digests, and component versions; `--json` emits machine-readable output for scripting. |
|
||
| `stellaops-cli sources ingest --dry-run` | Dry-run guard validation for individual payloads | `--source <id>`<br>`--input <path\|uri>`<br>`--tenant <id>`<br>`--format table\|json`<br>`--output <file>` | Normalises gzip/base64 payloads, invokes `api/aoc/ingest/dry-run`, and maps guard failures to deterministic `ERR_AOC_00x` exit codes. |
|
||
| `stellaops-cli aoc verify` | Replay AOC guardrails over stored documents | `--since <ISO8601\|duration>`<br>`--limit <count>`<br>`--sources <list>`<br>`--codes <ERR_AOC_00x,...>`<br>`--format table\|json`<br>`--export <file>` | Summarises checked counts/violations, supports JSON evidence exports, and returns `0`, `11…17`, `18`, `70`, or `71` depending on guard outcomes. |
|
||
| `stellaops-cli config show` | Display resolved configuration | — | Masks secret values; helpful for air‑gapped installs |
|
||
| `stellaops-cli runtime policy test` | Ask Scanner.WebService for runtime verdicts (Webhook parity) | `--image/-i <digest>` (repeatable, comma/space lists supported)<br>`--file/-f <path>`<br>`--namespace/--ns <name>`<br>`--label/-l key=value` (repeatable)<br>`--json` | Posts to `POST /api/v1/scanner/policy/runtime`, deduplicates image digests, and prints TTL/policy revision plus per-image columns for signed state, SBOM referrers, quieted-by metadata, confidence, Rekor attestation (uuid + verified flag), and recently observed build IDs (shortened for readability). Accepts newline/whitespace-delimited stdin when piped; `--json` emits the raw response without additional logging. |
|
||
|
||
> Need to debug how the scanner resolves entry points? See the [entry-point documentation index](modules/scanner/operations/entrypoint.md), which links to static/dynamic reducers, ShellFlow, and runtime-specific guides.
|
||
|
||
#### Example: Pivot from runtime verdicts to debug symbols
|
||
|
||
```bash
|
||
$ stellaops-cli runtime policy test \
|
||
--image ghcr.io/acme/payments@sha256:4f7d55f6... \
|
||
--namespace payments
|
||
|
||
Image Digest Signed SBOM Build IDs TTL
|
||
ghcr.io/acme/payments@sha256:4f7d55f6... yes present 5f0c7c3c..., 1122aabbccddeeff... 04:59:55
|
||
```
|
||
|
||
1. Copy one of the hashes (e.g. `5f0c7c3cb4d9f8a4f1c1d5c6b7e8f90123456789`) and locate the bundled debug artefact:
|
||
```bash
|
||
ls offline-kit/debug/.build-id/5f/0c7c3cb4d9f8a4f1c1d5c6b7e8f90123456789.debug
|
||
```
|
||
2. Confirm the running binary advertises the same GNU build-id:
|
||
```bash
|
||
readelf -n /proc/$(pgrep -f payments-api | head -n1)/exe | grep -i 'Build ID'
|
||
```
|
||
3. If you operate a debuginfod mirror backed by the Offline Kit tree, resolve symbols with:
|
||
```bash
|
||
debuginfod-find debuginfo 5f0c7c3cb4d9f8a4f1c1d5c6b7e8f90123456789 >/tmp/payments-api.debug
|
||
```
|
||
|
||
See [Offline Kit step 0](24_OFFLINE_KIT.md#0-prepare-the-debug-store) for instructions on mirroring the debug store before packaging.
|
||
|
||
`POST /api/v1/scanner/policy/runtime` responds with one entry per digest. Each result now includes:
|
||
|
||
- `policyVerdict` (`pass|warn|fail|error`), `signed`, and `hasSbomReferrers` parity with the webhook contract.
|
||
- `confidence` (0-1 double) derived from canonical `PolicyPreviewService` evaluation and `quieted`/`quietedBy` flags for muted findings.
|
||
- `rekor` block carrying `uuid`, `url`, and the attestor-backed `verified` boolean when Rekor inclusion proofs have been confirmed.
|
||
- `metadata` (stringified JSON) capturing runtime heuristics, policy issues, evaluated findings, and timestamps for downstream audit.
|
||
- `buildIds` (array) lists up to three distinct GNU build-id hashes recently observed for that digest so debuggers can derive `/usr/lib/debug/.build-id/<aa>/<rest>.debug` paths for symbol stores.
|
||
|
||
When running on an interactive terminal without explicit override flags, the CLI uses Spectre.Console prompts to let you choose per-run ORAS/offline bundle behaviour.
|
||
|
||
Runtime verdict output reflects the SCANNER-RUNTIME-12-302 contract sign-off (quieted provenance, confidence band, attestation verification). CLI-RUNTIME-13-008 now mirrors those fields in both table and `--json` formats.
|
||
|
||
**Startup diagnostics**
|
||
|
||
- `stellaops-cli` now loads Authority plug-in manifests during startup (respecting `Authority:Plugins:*`) and surfaces analyzer warnings when a plug-in weakens the baseline password policy (minimum length **12** and all character classes required).
|
||
- Follow the log entry’s config path and raise `passwordPolicy.minimumLength` to at least 12 while keeping `requireUppercase`, `requireLowercase`, `requireDigit`, and `requireSymbol` set to `true` to clear the warning; weakened overrides are treated as actionable security deviations.
|
||
|
||
**Logging & exit codes**
|
||
|
||
- Structured logging via `Microsoft.Extensions.Logging` with single-line console output (timestamps in UTC).
|
||
- `--verbose / -v` raises log level to `Debug`.
|
||
- Command exit codes bubble up: backend conflict → `1`, cancelled via `CTRL+C` → `130`, scanner exit codes propagate as-is.
|
||
|
||
**Artifact validation**
|
||
|
||
- Downloads are verified against the `X-StellaOps-Digest` header (SHA-256). When `StellaOps:ScannerSignaturePublicKeyPath` points to a PEM-encoded RSA key, the optional `X-StellaOps-Signature` header is validated as well.
|
||
- Metadata for each bundle is written alongside the artefact (`*.metadata.json`) with digest, signature, source URL, and timestamps.
|
||
- Retry behaviour is controlled via `StellaOps:ScannerDownloadAttempts` (default **3** with exponential backoff).
|
||
- Successful `scan run` executions create timestamped JSON artefacts inside `ResultsDirectory` plus a `scan-run-*.json` metadata envelope documenting the runner, arguments, timing, and stdout/stderr. The artefact is posted back to Concelier automatically.
|
||
|
||
#### Trivy DB export metadata (`metadata.json`)
|
||
|
||
`stellaops-cli db export --format trivy-db` (and the backing `POST /jobs/export:trivy-db`) always emits a `metadata.json` document in the OCI layout root. Operators consuming the bundle or delta updates should inspect the following fields:
|
||
|
||
| Field | Type | Purpose |
|
||
| ----- | ---- | ------- |
|
||
| `mode` | `full` \| `delta` | Indicates whether the current run rebuilt the entire database (`full`) or only the changed files (`delta`). |
|
||
| `baseExportId` | string? | Export ID of the last full baseline that the delta builds upon. Only present for `mode = delta`. |
|
||
| `baseManifestDigest` | string? | SHA-256 digest of the manifest belonging to the baseline OCI layout. |
|
||
| `resetBaseline` | boolean | `true` when the exporter rotated the baseline (e.g., repo change, delta chain reset). Treat as a full refresh. |
|
||
| `treeDigest` | string | Canonical SHA-256 digest of the JSON tree used to build the database. |
|
||
| `treeBytes` | number | Total bytes across exported JSON files. |
|
||
| `advisoryCount` | number | Count of advisories included in the export. |
|
||
| `exporterVersion` | string | Version stamp of `StellaOps.Concelier.Exporter.TrivyDb`. |
|
||
| `builder` | object? | Raw metadata emitted by `trivy-db build` (version, update cadence, etc.). |
|
||
| `delta.changedFiles[]` | array | Present when `mode = delta`. Each entry lists `{ "path": "<relative json>", "length": <bytes>, "digest": "sha256:..." }`. |
|
||
| `delta.removedPaths[]` | array | Paths that existed in the previous manifest but were removed in the new run. |
|
||
|
||
When the planner opts for a delta run, the exporter copies unmodified blobs from the baseline layout identified by `baseManifestDigest`. Consumers that cache OCI blobs only need to fetch the `changedFiles` and the new manifest/metadata unless `resetBaseline` is true.
|
||
When pushing to ORAS, set `concelier:exporters:trivyDb:oras:publishFull` / `publishDelta` to control whether full or delta runs are copied to the registry. Offline bundles follow the analogous `includeFull` / `includeDelta` switches under `offlineBundle`.
|
||
|
||
Example configuration (`appsettings.yaml`):
|
||
|
||
```yaml
|
||
concelier:
|
||
exporters:
|
||
trivyDb:
|
||
oras:
|
||
enabled: true
|
||
publishFull: true
|
||
publishDelta: false
|
||
offlineBundle:
|
||
enabled: true
|
||
includeFull: true
|
||
includeDelta: false
|
||
```
|
||
|
||
|
||
**Authentication**
|
||
|
||
- API key is sent as `Authorization: Bearer <token>` automatically when configured.
|
||
- Anonymous operation is permitted only when Concelier runs with
|
||
`authority.allowAnonymousFallback: true`. This flag is temporary—plan to disable
|
||
it before **2025-12-31 UTC** so bearer tokens become mandatory.
|
||
|
||
Authority-backed auth workflow:
|
||
1. Configure Authority settings via config or env vars (see sample below). Minimum fields: `Url`, `ClientId`, and either `ClientSecret` (client credentials) or `Username`/`Password` (password grant).
|
||
2. Run `stellaops-cli auth login` to acquire and cache a token. Use `--force` if you need to ignore an existing cache entry.
|
||
3. Execute CLI commands as normal—the backend client injects the cached bearer token automatically and retries on transient 401/403 responses with operator guidance.
|
||
4. Inspect the cache with `stellaops-cli auth status` (shows expiry, scope, mode) or clear it via `stellaops-cli auth logout`.
|
||
5. Run `stellaops-cli auth whoami` to dump token subject, audience, issuer, scopes, and remaining lifetime (verbose mode prints additional claims).
|
||
6. Expect Concelier to emit audit logs for each `/jobs*` request showing `subject`,
|
||
`clientId`, `scopes`, `status`, and whether network bypass rules were applied.
|
||
|
||
Tokens live in `~/.stellaops/tokens` unless `StellaOps:Authority:TokenCacheDirectory` overrides it. Cached tokens are reused offline until they expire; the CLI surfaces clear errors if refresh fails.
|
||
|
||
For offline workflows, configure `StellaOps:Offline:KitsDirectory` (or `STELLAOPS_OFFLINE_KITS_DIR`) to control where bundles, manifests, and metadata are stored, and `StellaOps:Offline:KitMirror` (or `STELLAOPS_OFFLINE_MIRROR_URL`) to override the download base URL when pulling from a mirror.
|
||
|
||
**Configuration file template**
|
||
|
||
```jsonc
|
||
{
|
||
"StellaOps": {
|
||
"ApiKey": "your-api-token",
|
||
"BackendUrl": "https://concelier.example.org",
|
||
"ScannerCacheDirectory": "scanners",
|
||
"ResultsDirectory": "results",
|
||
"DefaultRunner": "docker",
|
||
"ScannerSignaturePublicKeyPath": "",
|
||
"ScannerDownloadAttempts": 3,
|
||
"Offline": {
|
||
"KitsDirectory": "offline-kits",
|
||
"KitMirror": "https://get.stella-ops.org/ouk/"
|
||
},
|
||
"Authority": {
|
||
"Url": "https://authority.example.org",
|
||
"ClientId": "concelier-cli",
|
||
"ClientSecret": "REDACTED",
|
||
"Username": "",
|
||
"Password": "",
|
||
"Scope": "concelier.jobs.trigger advisory:ingest advisory:read",
|
||
"TokenCacheDirectory": ""
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
Drop `appsettings.local.json` or `.yaml` beside the binary to override per environment.
|
||
|
||
---
|
||
|
||
### 2.5 Misc Endpoints
|
||
|
||
| Path | Method | Description |
|
||
| ---------- | ------ | ---------------------------- |
|
||
| `/healthz` | GET | Liveness; returns `"ok"` |
|
||
| `/metrics` | GET | Prometheus exposition (OTel) |
|
||
| `/version` | GET | Git SHA + build date |
|
||
|
||
---
|
||
|
||
### 2.6 Authority Admin APIs
|
||
|
||
Administrative endpoints live under `/internal/*` on the Authority host and require the bootstrap API key (`x-stellaops-bootstrap-key`). Responses are deterministic and audited via `AuthEventRecord`.
|
||
|
||
| Path | Method | Description |
|
||
| ---- | ------ | ----------- |
|
||
| `/internal/revocations/export` | GET | Returns the revocation bundle (JSON + detached JWS + digest). Mirrors the output of `stellaops-cli auth revoke export`. |
|
||
| `/internal/signing/rotate` | POST | Promotes a new signing key and marks the previous key as retired without restarting the service. |
|
||
|
||
**Rotate request body**
|
||
|
||
```json
|
||
{
|
||
"keyId": "authority-signing-2025",
|
||
"location": "../certificates/authority-signing-2025.pem",
|
||
"source": "file",
|
||
"provider": "default"
|
||
}
|
||
```
|
||
|
||
The API responds with the active `kid`, previous key (if any), and the set of retired key identifiers. Always export a fresh revocation bundle after rotation so downstream mirrors receive signatures from the new key.
|
||
|
||
---
|
||
|
||
## 3 First‑Party CLI Tools
|
||
|
||
### 3.1 `stella`
|
||
|
||
> *Package SBOM + Scan + Exit code* – designed for CI.
|
||
|
||
```
|
||
Usage: stella [OPTIONS] IMAGE_OR_SBOM
|
||
```
|
||
|
||
| Flag / Option | Default | Description |
|
||
| --------------- | ----------------------- | -------------------------------------------------- |
|
||
| `--server` | `http://localhost:8080` | API root |
|
||
| `--token` | *env `STELLA_TOKEN`* | Bearer token |
|
||
| `--sbom-type` | *auto* | Force `trivy-json-v2`/`spdx-json`/`cyclonedx-json` |
|
||
| `--delta` | `false` | Enable delta layer optimisation |
|
||
| `--policy-file` | *none* | Override server rules with local YAML/Rego |
|
||
| `--threshold` | `critical` | Fail build if ≥ level found |
|
||
| `--output-json` | *none* | Write raw scan result to file |
|
||
| `--wait-quota` | `true` | If 429 received, automatically wait `Retry‑After` and retry once. |
|
||
|
||
**Exit codes**
|
||
|
||
| Code | Meaning |
|
||
| ---- | ------------------------------------------- |
|
||
| 0 | Scan OK, policy passed |
|
||
| 1 | Vulnerabilities ≥ threshold OR policy block |
|
||
| 2 | Internal error (network etc.) |
|
||
|
||
---
|
||
|
||
### 3.2 `stella‑zastava`
|
||
|
||
> *Daemon / K8s DaemonSet* – watch container runtime, push SBOMs.
|
||
|
||
Core flags (excerpt):
|
||
|
||
| Flag | Purpose |
|
||
| ---------------- | ---------------------------------- |
|
||
| `--mode` | `listen` (default) / `enforce` |
|
||
| `--filter-image` | Regex; ignore infra/busybox images |
|
||
| `--threads` | Worker pool size |
|
||
|
||
---
|
||
|
||
### 3.3 `stellopsctl`
|
||
|
||
> *Admin utility* – policy snapshots, feed status, user CRUD.
|
||
|
||
Examples:
|
||
|
||
```
|
||
stellopsctl policy export > policies/backup-2025-07-14.yaml
|
||
stellopsctl feed refresh # force OSV merge
|
||
stellopsctl user add dev-team --role developer
|
||
```
|
||
|
||
---
|
||
|
||
## 4 Error Model
|
||
|
||
Uniform problem‑details object (RFC 7807):
|
||
|
||
```json
|
||
{
|
||
"type": "https://stella-ops.org/probs/validation",
|
||
"title": "Invalid request",
|
||
"status": 400,
|
||
"detail": "Layer digest malformed",
|
||
"traceId": "00-7c39..."
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 5 Rate Limits
|
||
|
||
Default **40 requests / second / token**.
|
||
429 responses include `Retry-After` seconds header.
|
||
|
||
---
|
||
|
||
## 6 FAQ & Tips
|
||
|
||
* **Skip SBOM generation in CI** – supply a *pre‑built* SBOM and add `?sbom-only=true` to `/scan` for <1 s path.
|
||
* **Air‑gapped?** – point `--server` to `http://oukgw:8080` inside the Offline Update Kit.
|
||
* **YAML vs Rego** – YAML simpler; Rego unlocks time‑based logic (see samples).
|
||
* **Cosign verify plug‑ins** – enable `SCANNER_VERIFY_SIG=true` env to refuse unsigned plug‑ins.
|
||
|
||
---
|
||
|
||
## 7 Planned Changes (Beyond 6 Months)
|
||
|
||
These stay in *Feature Matrix → To Do* until design is frozen.
|
||
|
||
| Epic / Feature | API Impact Sketch |
|
||
| ---------------------------- | ---------------------------------- |
|
||
| **SLSA L1‑L3** attestation | `/attest` (see §2.4) |
|
||
| Rekor transparency log | `/rekor/log/{id}` (GET) |
|
||
| Plug‑in Marketplace metadata | `/plugins/market` (catalog) |
|
||
| Horizontal scaling controls | `POST /cluster/node` (add/remove) |
|
||
| Windows agent support | Update LSAPI to PDE, no API change |
|
||
|
||
---
|
||
|
||
## 8 References
|
||
|
||
* OpenAPI YAML → `/openapi/v1.yaml` (served by backend)
|
||
* OAuth2 spec: <https://datatracker.ietf.org/doc/html/rfc6749>
|
||
* SLSA spec: <https://slsa.dev/spec/v1.0>
|
||
|
||
---
|
||
|
||
## 9 Changelog (truncated)
|
||
|
||
* **2025‑07‑14** – added *delta SBOM*, policy import/export, CLI `--sbom-type`.
|
||
* **2025‑07‑12** – initial public reference.
|
||
|
||
---
|