- Added Program.cs to set up the web application with Serilog for logging, health check endpoints, and a placeholder admission endpoint. - Configured Kestrel server to use TLS 1.3 and handle client certificates appropriately. - Created StellaOps.Zastava.Webhook.csproj with necessary dependencies including Serilog and Polly. - Documented tasks in TASKS.md for the Zastava Webhook project, outlining current work and exit criteria for each task.
		
			
				
	
	
		
			785 lines
		
	
	
		
			31 KiB
		
	
	
	
		
			Markdown
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			785 lines
		
	
	
		
			31 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-3def5f362aa475ef14b6",
 | ||
|     "imageDigest": "sha256:deadbeef",
 | ||
|     "verdict": "blocked",
 | ||
|     "policy": { "revisionId": "rev-1", "digest": "27d2ec2b34feedc304fc564d252ecee1c8fa14ea581a5ff5c1ea8963313d5c8d" },
 | ||
|     "summary": { "total": 1, "blocked": 1, "warned": 0, "ignored": 0, "quieted": 0 },
 | ||
|     "verdicts": [
 | ||
|       {
 | ||
|         "findingId": "finding-1",
 | ||
|         "status": "Blocked",
 | ||
|         "ruleName": "Block Critical",
 | ||
|         "ruleAction": "Block",
 | ||
|         "score": 40.5,
 | ||
|         "configVersion": "1.0",
 | ||
|         "inputs": {
 | ||
|           "reachabilityWeight": 0.45,
 | ||
|           "baseScore": 40.5,
 | ||
|           "severityWeight": 90,
 | ||
|           "trustWeight": 1,
 | ||
|           "trustWeight.NVD": 1,
 | ||
|           "reachability.runtime": 0.45
 | ||
|         },
 | ||
|         "quiet": false,
 | ||
|         "sourceTrust": "NVD",
 | ||
|         "reachability": "runtime"
 | ||
|       }
 | ||
|     ],
 | ||
|     "issues": []
 | ||
|   },
 | ||
|   "dsse": {
 | ||
|     "payloadType": "application/vnd.stellaops.report+json",
 | ||
|     "payload": "<base64 canonical report>",
 | ||
|     "signatures": [
 | ||
|       {
 | ||
|         "keyId": "scanner-report-signing",
 | ||
|         "algorithm": "hs256",
 | ||
|         "signature": "<base64 signature>"
 | ||
|       }
 | ||
|     ]
 | ||
|   }
 | ||
| }
 | ||
| ```
 | ||
| 
 | ||
| - The `report` object omits null fields and is deterministic (ISO timestamps, sorted keys).
 | ||
| - `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.
 | ||
| - A runnable sample envelope is available at `samples/api/reports/report-sample.dsse.json` 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.
 | ||
| 
 | ||
| ---
 | ||
| 
 | ||
| ## 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 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 + per-image verdict/signed/SBOM status. Accepts newline/whitespace-delimited stdin when piped; `--json` emits the raw response without additional logging. |
 | ||
| 
 | ||
| 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.
 | ||
| 
 | ||
| **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.
 | ||
| 
 | ||
| **Configuration file template**
 | ||
| 
 | ||
| ```jsonc
 | ||
| {
 | ||
|   "StellaOps": {
 | ||
|     "ApiKey": "your-api-token",
 | ||
|     "BackendUrl": "https://concelier.example.org",
 | ||
|     "ScannerCacheDirectory": "scanners",
 | ||
|     "ResultsDirectory": "results",
 | ||
|     "DefaultRunner": "docker",
 | ||
|     "ScannerSignaturePublicKeyPath": "",
 | ||
|     "ScannerDownloadAttempts": 3,
 | ||
|     "Authority": {
 | ||
|       "Url": "https://authority.example.org",
 | ||
|       "ClientId": "concelier-cli",
 | ||
|       "ClientSecret": "REDACTED",
 | ||
|       "Username": "",
 | ||
|       "Password": "",
 | ||
|       "Scope": "concelier.jobs.trigger",
 | ||
|       "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.
 | ||
| 
 | ||
| ---
 |