934 lines
		
	
	
		
			40 KiB
		
	
	
	
		
			Markdown
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			934 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. |
 | ||
| 
 | ||
| #### 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.
 | ||
| 
 | ||
| ---
 |