From a6ffb38ecf96074406a2ad9ea7a0f8f2715c312d Mon Sep 17 00:00:00 2001 From: master <> Date: Mon, 30 Mar 2026 17:25:37 +0300 Subject: [PATCH] Update module architecture docs and workflow tutorials - Module dossiers: attestor, authority, cli, graph, scanner - Policy assistant parameters guide - UI v2-rewire navigation rendering policy - Test suite overview update - Workflow engine requirements and tutorial series (01-08) Co-Authored-By: Claude Opus 4.6 (1M context) --- docs/modules/attestor/architecture.md | 5492 ++++++++--------- docs/modules/authority/architecture.md | 108 +- docs/modules/cli/architecture.md | 1338 ++-- docs/modules/graph/architecture.md | 146 +- .../policy/guides/assistant-parameters.md | 44 +- docs/modules/scanner/architecture.md | 1382 ++--- .../ui/v2-rewire/S00_nav_rendering_policy.md | 176 +- docs/technical/testing/TEST_SUITE_OVERVIEW.md | 26 +- .../engine/01-requirements-and-principles.md | 4 +- .../tutorials/01-hello-world/README.md | 18 +- .../tutorials/02-service-tasks/README.md | 16 +- .../workflow/tutorials/03-decisions/README.md | 14 +- .../tutorials/04-human-tasks/README.md | 28 +- .../tutorials/05-sub-workflows/README.md | 12 +- .../tutorials/06-advanced-patterns/README.md | 4 +- .../tutorials/07-shared-helpers/README.md | 10 +- .../tutorials/08-expressions/README.md | 4 +- 17 files changed, 4442 insertions(+), 4380 deletions(-) diff --git a/docs/modules/attestor/architecture.md b/docs/modules/attestor/architecture.md index f843cfa7e..a750fefd2 100644 --- a/docs/modules/attestor/architecture.md +++ b/docs/modules/attestor/architecture.md @@ -1,932 +1,932 @@ -# component_architecture_attestor.md — **Stella Ops Attestor** (2025Q4) - -> Derived from Epic 19 – Attestor Console with provenance hooks aligned to the Export Center bundle workflows scoped in Epic 10. - -> **Scope.** Implementation‑ready architecture for the **Attestor**: the service that **submits** DSSE envelopes to **Rekor v2**, retrieves/validates inclusion proofs, caches results, and exposes verification APIs. It accepts DSSE **only** from the **Signer** over mTLS, enforces chain‑of‑trust to Stella Ops roots, and returns `{uuid, index, proof, logURL}` to calling services (Scanner.WebService for SBOMs; backend for final reports; Excititor exports when configured). - ---- - -## 0) Mission & boundaries - -**Mission.** Turn a signed DSSE envelope from the Signer into a **transparency‑logged, verifiable fact** with a durable, replayable proof (Merkle inclusion + (optional) checkpoint anchoring). Provide **fast verification** for downstream consumers and a stable retrieval interface for UI/CLI. - -**Boundaries.** - -* Attestor **does not sign**; it **must not** accept unsigned or third‑party‑signed bundles. -* Attestor **does not decide PASS/FAIL**; it logs attestations for SBOMs, reports, and export artifacts. -* Rekor v2 backends may be **local** (self‑hosted) or **remote**; Attestor handles both with retries, backoff, and idempotency. - ---- - -## 1) Topology & dependencies - -**Process shape:** single stateless service `stellaops/attestor` behind mTLS. - -**Dependencies:** - -* **Signer** (caller) — authenticated via **mTLS** and **Authority** OpToks. -* **Rekor v2** — tile‑backed transparency log endpoint(s). -* **RustFS (S3-compatible)** — optional archive store for DSSE envelopes & verification bundles. -* **PostgreSQL** — local cache of `{uuid, index, proof, artifactSha256, bundleSha256}`; job state; audit. -* **Valkey** — dedupe/idempotency keys and short‑lived rate‑limit buckets. -* **Licensing Service (optional)** — “endorse” call for cross‑log publishing when customer opts‑in. - -Trust boundary: **Only the Signer** is allowed to call submission endpoints; enforced by **mTLS peer cert allowlist** + `aud=attestor` OpTok. - ---- - -### Roles, identities & scopes -- **Subjects** — immutable digests for artifacts (container images, SBOMs, reports) referenced in DSSE envelopes. -- **Issuers** — authenticated builders/scanners/policy engines signing evidence; tracked with mode (`keyless`, `kms`, `hsm`, `fido2`) and tenant scope. -- **Consumers** — Scanner, Export Center, CLI, Console, Policy Engine that verify proofs using Attestor APIs. -- **Authority scopes** — `attestor.write`, `attestor.verify`, `attestor.read`, and administrative scopes for key management; all calls mTLS/DPoP-bound. - -### Supported predicate types -- `StellaOps.BuildProvenance@1` -- `StellaOps.SBOMAttestation@1` -- `StellaOps.ScanResults@1` -- `StellaOps.PolicyEvaluation@1` -- `StellaOps.VEXAttestation@1` -- `StellaOps.RiskProfileEvidence@1` -- `StellaOps.SignedException@1` - -Each predicate embeds subject digests, issuer metadata, policy context, materials, and optional transparency hints. Unsupported predicates return `422 predicate_unsupported`. - -> **Golden fixtures:** Deterministic JSON statements for each predicate live in `src/Attestor/StellaOps.Attestor.Types/samples`. They are kept stable by the `StellaOps.Attestor.Types.Tests` project so downstream docs and contracts can rely on them without drifting. - -### Envelope & signature model -- DSSE envelopes canonicalised (stable JSON ordering) prior to hashing. -- Signature modes: keyless (Fulcio cert chain), keyful (KMS/HSM), hardware (FIDO2/WebAuthn). Multiple signatures allowed. -- Rekor entry stores bundle hash, certificate chain, and optional witness endorsements. -- Archive CAS retains original envelope plus metadata for offline verification. -- Envelope serializer emits **compact** (canonical, minified) and **expanded** (annotated, indented) JSON variants off the same canonical byte stream so hashing stays deterministic while humans get context. -- Payload handling supports **optional compression** (`gzip`, `brotli`) with compression metadata recorded in the expanded view and digesting always performed over the uncompressed bytes. -- Expanded envelopes surface **detached payload references** (URI, digest, media type, size) so large artifacts can live in CAS/object storage while the canonical payload remains embedded for verification. -- Payload previews auto-render JSON or UTF-8 text in the expanded output to simplify triage in air-gapped and offline review flows. - -### Verification pipeline overview -1. Fetch envelope (from request, cache, or storage) and validate DSSE structure. -2. Verify signature(s) against configured trust roots; evaluate issuer policy. -3. Retrieve or acquire inclusion proof from Rekor (primary + optional mirror). -4. Validate Merkle proof against checkpoint; optionally verify witness endorsement. -5. Return cached verification bundle including policy verdict and timestamps. - -### Rekor Inclusion Proof Verification (SPRINT_3000_0001_0001) - -The Attestor implements RFC 6962-compliant Merkle inclusion proof verification for Rekor transparency log entries: - -**Components:** -- `MerkleProofVerifier` — Verifies Merkle audit paths per RFC 6962 Section 2.1.1 -- `CheckpointSignatureVerifier` — Parses and verifies Rekor checkpoint signatures (ECDSA/Ed25519) -- `RekorVerificationOptions` — Configuration for public keys, offline mode, and checkpoint caching - -**Verification Flow:** -1. Parse checkpoint body (origin, tree size, root hash) -2. Verify checkpoint signature against Rekor public key -3. Compute leaf hash from canonicalized entry -4. Walk Merkle path from leaf to root using RFC 6962 interior node hashing -5. Compare computed root with checkpoint root hash (constant-time) - -**Offline Mode:** -- Bundled checkpoints can be used in air-gapped environments -- `EnableOfflineMode` and `OfflineCheckpointBundlePath` configuration options -- `AllowOfflineWithoutSignature` for fully disconnected scenarios (reduced security) - -**Metrics:** -- `attestor.rekor_inclusion_verify_total` — Verification attempts by result -- `attestor.rekor_checkpoint_verify_total` — Checkpoint signature verifications -- `attestor.rekor_offline_verify_total` — Offline mode verifications -- `attestor.rekor_checkpoint_cache_hits/misses` — Checkpoint cache performance - -### UI & CLI touchpoints -- Console: Evidence browser, verification report, chain-of-custody graph, issuer/key management, attestation workbench, bulk verification views. -- CLI: `stella attest sign|verify|list|fetch|key` with offline verification and export bundle support. -- SDKs expose sign/verify primitives for build pipelines. - -### Performance & observability targets -- Throughput goal: ≥1 000 envelopes/minute per worker with cached verification. -- Metrics: `attestor_submission_total`, `attestor_verify_seconds`, `attestor_rekor_latency_seconds`, `attestor_cache_hit_ratio`. -- Logs include `tenant`, `issuer`, `subjectDigest`, `rekorUuid`, `proofStatus`; traces cover submission → Rekor → cache → response path. - ---- - -## 2) Data model (PostgreSQL) - -Database: `attestor` - -**Tables & schemas** - -* `entries` table - - ```sql - CREATE TABLE attestor.entries ( - id UUID PRIMARY KEY, -- rekor-uuid - artifact_sha256 TEXT NOT NULL, - artifact_kind TEXT NOT NULL, -- sbom|report|vex-export - artifact_image_digest TEXT, - artifact_subject_uri TEXT, - bundle_sha256 TEXT NOT NULL, -- canonicalized DSSE - log_index INTEGER, -- log index/sequence if provided by backend - proof_checkpoint JSONB, -- { origin, size, rootHash, timestamp } - proof_inclusion JSONB, -- { leafHash, path[] } Merkle path (tiles) - log_url TEXT, - log_id TEXT, - created_at TIMESTAMPTZ DEFAULT NOW(), - status TEXT NOT NULL, -- included|pending|failed - signer_identity JSONB -- { mode, issuer, san?, kid? } - ); - ``` - -* `dedupe` table - - ```sql - CREATE TABLE attestor.dedupe ( - key TEXT PRIMARY KEY, -- bundle: idempotency key - rekor_uuid UUID NOT NULL, - created_at TIMESTAMPTZ DEFAULT NOW(), - ttl_at TIMESTAMPTZ NOT NULL -- for scheduled cleanup - ); - ``` - -* `audit` table - - ```sql - CREATE TABLE attestor.audit ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - ts TIMESTAMPTZ DEFAULT NOW(), - caller_cn TEXT, - caller_mtls_thumbprint TEXT, - caller_sub TEXT, - caller_aud TEXT, - action TEXT NOT NULL, -- submit|verify|fetch - artifact_sha256 TEXT, - bundle_sha256 TEXT, - rekor_uuid UUID, - log_index INTEGER, - result TEXT NOT NULL, - latency_ms INTEGER, - backend TEXT - ); - ``` - -Indexes: - -* `entries`: indexes on `artifact_sha256`, `bundle_sha256`, `created_at`, and composite `(status, created_at DESC)`. -* `dedupe`: unique index on `key`; scheduled job cleans rows where `ttl_at < NOW()` (24–48h retention). -* `audit`: index on `ts` for time‑range queries. - ---- - -## 2.1) Content-Addressed Identifier Formats - -The ProofChain library (`StellaOps.Attestor.ProofChain`) defines canonical content-addressed identifiers for all proof chain components. These IDs ensure determinism, tamper-evidence, and reproducibility. - -### Identifier Types - -| ID Type | Format | Source | Example | -|---------|--------|--------|---------| -| **ArtifactID** | `sha256:<64-hex>` | Container manifest or binary hash | `sha256:a1b2c3d4e5f6...` | -| **SBOMEntryID** | `:[@]` | SBOM hash + component PURL | `sha256:91f2ab3c:pkg:npm/lodash@4.17.21` | -| **EvidenceID** | `sha256:` | Canonical evidence JSON | `sha256:e7f8a9b0c1d2...` | -| **ReasoningID** | `sha256:` | Canonical reasoning JSON | `sha256:f0e1d2c3b4a5...` | -| **VEXVerdictID** | `sha256:` | Canonical VEX verdict JSON | `sha256:d4c5b6a7e8f9...` | -| **ProofBundleID** | `sha256:` | Merkle root of bundle components | `sha256:1a2b3c4d5e6f...` | -| **GraphRevisionID** | `grv_sha256:` | Merkle root of graph state | `grv_sha256:9f8e7d6c5b4a...` | - -### Canonicalization (RFC 8785) - -All JSON-based IDs use RFC 8785 (JCS) canonicalization: -- UTF-8 encoding -- Lexicographically sorted keys -- No whitespace (minified) -- No volatile fields (timestamps, random values excluded) - -**Implementation:** `StellaOps.Attestor.ProofChain.Json.Rfc8785JsonCanonicalizer` - -### Merkle Tree Construction - -ProofBundleID and GraphRevisionID use deterministic binary Merkle trees: -- SHA-256 hash function -- Lexicographically sorted leaf inputs -- Standard binary tree construction (pair-wise hashing) -- Odd leaves promoted to next level - -**Implementation:** `StellaOps.Attestor.ProofChain.Merkle.DeterministicMerkleTreeBuilder` - -### ID Generation Interface - -```csharp -// Core interface for ID generation -public interface IContentAddressedIdGenerator -{ - EvidenceId GenerateEvidenceId(EvidencePredicate predicate); - ReasoningId GenerateReasoningId(ReasoningPredicate predicate); - VexVerdictId GenerateVexVerdictId(VexPredicate predicate); - ProofBundleId GenerateProofBundleId(SbomEntryId sbom, EvidenceId[] evidence, - ReasoningId reasoning, VexVerdictId verdict); - GraphRevisionId GenerateGraphRevisionId(GraphState state); -} -``` - -### Predicate Types - -The ProofChain library defines DSSE predicates for proof chain attestations. All predicates follow the in-toto Statement/v1 format. - -#### Predicate Type Registry - -| Predicate | Type URI | Purpose | Signer Role | -|-----------|----------|---------|-------------| -| **Evidence** | `evidence.stella/v1` | Raw evidence from scanner/ingestor (findings, reachability data) | Scanner/Ingestor key | -| **Reasoning** | `reasoning.stella/v1` | Policy evaluation trace with inputs and intermediate findings | Policy/Authority key | -| **VEX Verdict** | `cdx-vex.stella/v1` | VEX verdict with status, justification, and provenance | VEXer/Vendor key | -| **Proof Spine** | `proofspine.stella/v1` | Merkle-aggregated proof spine linking evidence to verdict | Authority key | -| **Verdict Receipt** | `verdict.stella/v1` | Final surfaced decision receipt with policy rule reference | Authority key | -| **SBOM Linkage** | `https://stella-ops.org/predicates/sbom-linkage/v1` | SBOM-to-component linkage metadata | Generator key | -| **Signed Exception** | `https://stellaops.io/attestation/v1/signed-exception` | DSSE-signed budget exception with recheck policy | Authority key | - -#### Evidence Statement (`evidence.stella/v1`) - -Captures raw evidence collected from scanners or vulnerability feeds. - -| Field | Type | Description | -|-------|------|-------------| -| `source` | string | Scanner or feed name that produced this evidence | -| `sourceVersion` | string | Version of the source tool | -| `collectionTime` | DateTimeOffset | UTC timestamp when evidence was collected | -| `sbomEntryId` | string | Reference to the SBOM entry this evidence relates to | -| `vulnerabilityId` | string? | CVE or vulnerability identifier if applicable | -| `rawFinding` | object | Pointer to or inline representation of raw finding data | -| `evidenceId` | string | Content-addressed ID (sha256:<hash>) | - -#### Reasoning Statement (`reasoning.stella/v1`) - -Captures policy evaluation traces linking evidence to decisions. - -| Field | Type | Description | -|-------|------|-------------| -| `sbomEntryId` | string | SBOM entry this reasoning applies to | -| `evidenceIds` | string[] | Evidence IDs considered in this reasoning | -| `policyVersion` | string | Version of the policy used for evaluation | -| `inputs` | object | Inputs to the reasoning process (evaluation time, thresholds, lattice rules) | -| `intermediateFindings` | object? | Intermediate findings from the evaluation | -| `reasoningId` | string | Content-addressed ID (sha256:<hash>) | - -#### VEX Verdict Statement (`cdx-vex.stella/v1`) - -Captures VEX status determinations with provenance. - -| Field | Type | Description | -|-------|------|-------------| -| `sbomEntryId` | string | SBOM entry this verdict applies to | -| `vulnerabilityId` | string | CVE, GHSA, or other vulnerability identifier | -| `status` | string | VEX status: `not_affected`, `affected`, `fixed`, `under_investigation` | -| `justification` | string | Justification for the VEX status | -| `policyVersion` | string | Version of the policy used | -| `reasoningId` | string | Reference to the reasoning that led to this verdict | -| `vexVerdictId` | string | Content-addressed ID (sha256:<hash>) | - -#### Proof Spine Statement (`proofspine.stella/v1`) - -Merkle-aggregated proof bundle linking all chain components. - -| Field | Type | Description | -|-------|------|-------------| -| `sbomEntryId` | string | SBOM entry this proof spine covers | -| `evidenceIds` | string[] | Sorted list of evidence IDs included in this proof bundle | -| `reasoningId` | string | Reasoning ID linking evidence to verdict | -| `vexVerdictId` | string | VEX verdict ID for this entry | -| `policyVersion` | string | Version of the policy used | -| `proofBundleId` | string | Content-addressed ID (sha256:<merkle_root>) | - -#### Verdict Receipt Statement (`verdict.stella/v1`) - -Final surfaced decision receipt with full provenance. - -| Field | Type | Description | -|-------|------|-------------| -| `graphRevisionId` | string | Graph revision ID this verdict was computed from | -| `findingKey` | object | Finding key (sbomEntryId + vulnerabilityId) | -| `rule` | object | Policy rule that produced this verdict | -| `decision` | object | Decision made by the rule | -| `inputs` | object | Inputs used to compute this verdict | -| `outputs` | object | Outputs/references from this verdict | -| `createdAt` | DateTimeOffset | UTC timestamp when verdict was created | - -#### SBOM Linkage Statement (`sbom-linkage/v1`) - -SBOM-to-component linkage metadata. - -| Field | Type | Description | -|-------|------|-------------| -| `sbom` | object | SBOM descriptor (id, format, specVersion, mediaType, sha256, location) | -| `generator` | object | Generator tool descriptor | -| `generatedAt` | DateTimeOffset | UTC timestamp when linkage was generated | -| `incompleteSubjects` | object[]? | Subjects that could not be fully resolved | -| `tags` | object? | Arbitrary tags for classification or filtering | - -**Reference:** `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Statements/` - -#### Signed Exception Statement (`signed-exception/v1`) - -DSSE-signed exception objects with recheck policy for independent verification and automated re-approval workflows. - -| Field | Type | Description | -|-------|------|-------------| -| `schemaVersion` | string | Schema version (current: "1.0") | -| `exception` | object | The wrapped `BudgetExceptionEntry` | -| `exceptionContentId` | string | Content-addressed ID (sha256:<hash>) for deduplication | -| `signedAt` | DateTimeOffset | UTC timestamp when the exception was signed | -| `recheckPolicy` | object | Recheck policy configuration | -| `environments` | string[]? | Environments this exception applies to (dev, staging, prod) | -| `coveredViolationIds` | string[]? | IDs of violations this exception covers | -| `approvalPolicyDigest` | string? | Digest of the policy bundle that approved this exception | -| `renewsExceptionId` | string? | Previous exception ID for renewal chains | -| `status` | string | Status: `Active`, `PendingRecheck`, `Expired`, `Revoked`, `PendingApproval` | - -##### Recheck Policy Schema - -| Field | Type | Description | -|-------|------|-------------| -| `recheckIntervalDays` | int | Interval in days between rechecks (default: 30) | -| `autoRecheckEnabled` | bool | Whether automatic recheck scheduling is enabled | -| `maxRenewalCount` | int? | Maximum renewals before escalated approval required | -| `renewalCount` | int | Current renewal count | -| `nextRecheckAt` | DateTimeOffset? | Next scheduled recheck timestamp | -| `lastRecheckAt` | DateTimeOffset? | Last completed recheck timestamp | -| `requiresReapprovalOnExpiry` | bool | Whether re-approval is required after expiry | -| `approvalRoles` | string[]? | Roles required for approval | - -##### Exception Signing API - -The exception signing service provides endpoints for signing, verifying, and renewing exceptions: - -| Endpoint | Method | Description | -|----------|--------|-------------| -| `/internal/api/v1/exceptions/sign` | POST | Sign an exception and wrap in DSSE envelope | -| `/internal/api/v1/exceptions/verify` | POST | Verify a signed exception envelope | -| `/internal/api/v1/exceptions/recheck-status` | POST | Check if exception requires recheck | -| `/internal/api/v1/exceptions/renew` | POST | Renew an expired/expiring exception | - -**Reference:** `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Statements/DsseSignedExceptionPayload.cs` - ---- - -## 3) Input contract (from Signer) - -**Attestor accepts only** DSSE envelopes that satisfy all of: - -1. **mTLS** peer certificate maps to `signer` service (CA‑pinned). -2. **Authority** OpTok with `aud=attestor`, `scope=attestor.write`, DPoP or mTLS bound. -3. DSSE envelope is **signed by the Signer’s key** (or includes a **Fulcio‑issued** cert chain) and **chains to configured roots** (Fulcio/KMS). -4. **Predicate type** is one of Stella Ops types (sbom/report/vex‑export) with valid schema. -5. `subject[*].digest.sha256` is present and canonicalized. - -**Wire shape (JSON):** - -```json -{ - "bundle": { "dsse": { "payloadType": "application/vnd.in-toto+json", "payload": "", "signatures": [ ... ] }, - "certificateChain": [ "-----BEGIN CERTIFICATE-----..." ], - "mode": "keyless" }, - "meta": { - "artifact": { "sha256": "", "kind": "sbom|report|vex-export", "imageDigest": "sha256:..." }, - "bundleSha256": "", - "logPreference": "primary", // "primary" | "mirror" | "both" - "archive": true // whether Attestor should archive bundle to S3 - } -} -``` - ---- - -## 4) APIs - -### 4.1 Signing - -`POST /api/v1/attestations:sign` *(mTLS + OpTok required)* - -* **Purpose**: Deterministically wrap Stella Ops payloads in DSSE envelopes before Rekor submission. Reuses the submission rate limiter and honours caller tenancy/audience scopes. -* **Body**: - - ```json - { - "keyId": "signing-key-id", - "payloadType": "application/vnd.in-toto+json", - "payload": "", - "mode": "keyless|keyful|kms", - "certificateChain": ["-----BEGIN CERTIFICATE-----..."], - "artifact": { - "sha256": "", - "kind": "sbom|report|vex-export", - "imageDigest": "sha256:...", - "subjectUri": "oci://..." - }, - "logPreference": "primary|mirror|both", - "archive": true - } - ``` - -* **Behaviour**: - * Resolve the signing key from `attestor.signing.keys[]` (includes algorithm, provider, and optional KMS version). - * Compute DSSE pre‑authentication encoding, sign with the resolved provider (default EC, BouncyCastle Ed25519, or File‑KMS ES256), and add static + request certificate chains. - * Canonicalise the resulting bundle, derive `bundleSha256`, and mirror the request meta shape used by `/api/v1/rekor/entries`. - * Emit `attestor.sign_total{result,algorithm,provider}` and `attestor.sign_latency_seconds{algorithm,provider}` metrics and append an audit row (`action=sign`). -* **Response 200**: - - ```json - { - "bundle": { "dsse": { "payloadType": "...", "payload": "...", "signatures": [{ "keyid": "signing-key-id", "sig": "..." }] }, "certificateChain": ["..."], "mode": "kms" }, - "meta": { "artifact": { "sha256": "...", "kind": "sbom" }, "bundleSha256": "...", "logPreference": "primary", "archive": true }, - "key": { "keyId": "signing-key-id", "algorithm": "ES256", "mode": "kms", "provider": "kms", "signedAt": "2025-11-01T12:34:56Z" } - } - ``` - -* **Errors**: `400 key_not_found`, `400 payload_missing|payload_invalid_base64|artifact_sha_missing`, `400 mode_not_allowed`, `403 client_certificate_required`, `401 invalid_token`, `500 signing_failed`. - -### 4.2 Submission - -`POST /api/v1/rekor/entries` *(mTLS + OpTok required)* - -* **Body**: as above. -* **Behavior**: - - * Verify caller (mTLS + OpTok). - * Validate DSSE bundle (signature, cert chain to Fulcio/KMS; DSSE structure; payloadType allowed). - * Idempotency: compute `bundleSha256`; check `dedupe`. If present, return existing `rekorUuid`. - * Rekor pre-check: call Rekor index lookup (`/api/v2/index/retrieve` with v1 fallback) by bundle hash before submit; if a UUID is found, fetch and reuse existing entry metadata instead of creating a duplicate. - * Submit canonicalized bundle to Rekor v2 (primary or mirror according to `logPreference`). - * Retrieve **inclusion proof** (blocking until inclusion or up to `proofTimeoutMs`); if backend returns promise only, return `status=pending` and retry asynchronously. - * Persist `entries` record; archive DSSE to S3 if `archive=true`. -* **Response 200**: - - ```json - { - "uuid": "…", - "index": 123456, - "proof": { - "checkpoint": { "origin": "rekor@site", "size": 987654, "rootHash": "…", "timestamp": "…" }, - "inclusion": { "leafHash": "…", "path": ["…","…"] } - }, - "logURL": "https://rekor…/api/v2/log/…/entries/…", - "status": "included" - } - ``` -* **Errors**: `401 invalid_token`, `403 not_signer|chain_untrusted`, `409 duplicate_bundle` (with existing `uuid`), `502 rekor_unavailable`, `504 proof_timeout`. - -### 4.3 Proof retrieval - -`GET /api/v1/rekor/entries/{uuid}` - -* Returns `entries` row (refreshes proof from Rekor if stale/missing). -* Accepts `?refresh=true` to force backend query. - -### 4.4 Verification (third‑party or internal) - -`POST /api/v1/rekor/verify` - -* **Body** (one of): - - * `{ "uuid": "…" }` - * `{ "bundle": { …DSSE… } }` - * `{ "artifactSha256": "…" }` *(looks up most recent entry)* - -* **Checks**: - - 1. **Bundle signature** → cert chain to Fulcio/KMS roots configured. - 2. **Inclusion proof** → recompute leaf hash; verify Merkle path against checkpoint root. - 3. Optionally verify **checkpoint** against local trust anchors (if Rekor signs checkpoints). - 4. Confirm **subject.digest** matches caller‑provided hash (when given). - 5. Fetch **transparency witness** statement when enabled; cache results and downgrade status to WARN when endorsements are missing or mismatched. - -* **Response**: - - ```json - { "ok": true, "uuid": "…", "index": 123, "logURL": "…", "checkedAt": "…" } - ``` - -### 4.5 Bulk verification - -`POST /api/v1/rekor/verify:bulk` enqueues a verification job containing up to `quotas.bulk.maxItemsPerJob` items. Each item mirrors the single verification payload (uuid | artifactSha256 | subject+envelopeId, optional policyVersion/refreshProof). The handler persists a PostgreSQL job record (`bulk_jobs` table) and returns `202 Accepted` with a job descriptor and polling URL. - -`GET /api/v1/rekor/verify:bulk/{jobId}` returns progress and per-item results (subject/uuid, status, issues, cached verification report if available). Jobs are tenant- and subject-scoped; only the initiating principal can read their progress. - -**Worker path:** `BulkVerificationWorker` claims queued jobs (`status=queued → running`), executes items sequentially through the cached verification service, updates progress counters, and records metrics: - -- `attestor.bulk_jobs_total{status}` – completed/failed jobs -- `attestor.bulk_job_duration_seconds{status}` – job runtime -- `attestor.bulk_items_total{status}` – per-item outcomes (`succeeded`, `verification_failed`, `exception`) - -The worker honours `bulkVerification.itemDelayMilliseconds` for throttling and reschedules persistence conflicts with optimistic version checks. Results hydrate the verification cache; failed items record the error reason without aborting the overall job. - ---- - -## 5) Rekor v2 driver (backend) - -* **Canonicalization**: DSSE envelopes are **normalized** (stable JSON ordering, no insignificant whitespace) before hashing and submission. -* **Transport**: HTTP/2 with retries (exponential backoff, jitter), budgeted timeouts. -* **Idempotency**: if backend returns “already exists,” map to existing `uuid`. -* **Proof acquisition**: - - * In synchronous mode, poll the log for inclusion up to `proofTimeoutMs`. - * In asynchronous mode, return `pending` and schedule a **proof fetcher** job (PostgreSQL job record + backoff). -* **Mirrors/dual logs**: - - * When `logPreference="both"`, submit to primary and mirror; store **both** UUIDs (primary canonical). - * Optional **cloud endorsement**: POST to the Stella Ops cloud `/attest/endorse` with `{uuid, artifactSha256}`; store returned endorsement id. - ---- - -## 6) Security model - -* **mTLS required** for submission from **Signer** (CA‑pinned). -* **Authority token** with `aud=attestor` and DPoP/mTLS binding must be presented; Attestor verifies both. -* **Bundle acceptance policy**: - - * DSSE signature must chain to the configured **Fulcio** (keyless) or **KMS/HSM** roots. - * SAN (Subject Alternative Name) must match **Signer identity** policy (e.g., `urn:stellaops:signer` or pinned OIDC issuer). - * Predicate `predicateType` must be on allowlist (sbom/report/vex-export). - * `subject.digest.sha256` values must be present and well‑formed (hex). -* **No public submission** path. **Never** accept bundles from untrusted clients. -* **Client certificate allowlists**: optional `security.mtls.allowedSubjects` / `allowedThumbprints` tighten peer identity checks beyond CA pinning. -* **Rate limits**: token-bucket per caller derived from `quotas.perCaller` (QPS/burst) returns `429` + `Retry-After` when exceeded. -* **Scope enforcement**: API separates `attestor.write`, `attestor.verify`, and `attestor.read` policies; verification/list endpoints accept read or verify scopes while submission endpoints remain write-only. -* **Request hygiene**: JSON content-type is mandatory (415 returned otherwise); DSSE payloads are capped (default 2 MiB), certificate chains limited to six entries, and signatures to six per envelope to mitigate parsing abuse. -* **Redaction**: Attestor never logs secret material; DSSE payloads **should** be public by design (SBOMs/reports). If customers require redaction, enforce policy at Signer (predicate minimization) **before** Attestor. - ---- - -## 7) Storage & archival - -* **Entries** in PostgreSQL provide a local ledger keyed by `rekorUuid` and **artifact sha256** for quick reverse lookups. -* **S3 archival** (if enabled): - - ``` - s3://stellaops/attest/ - dsse/.json - proof/.json - bundle/.zip # optional verification bundle - ``` -* **Verification bundles** (zip): - - * DSSE (`*.dsse.json`), proof (`*.proof.json`), `chain.pem` (certs), `README.txt` with verification steps & hashes. - ---- - -## 8) Observability & audit - -**Metrics** (Prometheus): - -* `attestor.sign_total{result,algorithm,provider}` -* `attestor.sign_latency_seconds{algorithm,provider}` -* `attestor.submit_total{result,backend}` -* `attestor.submit_latency_seconds{backend}` -* `attestor.proof_fetch_total{subject,issuer,policy,result,attestor.log.backend}` -* `attestor.verify_total{subject,issuer,policy,result}` -* `attestor.verify_latency_seconds{subject,issuer,policy,result}` -* `attestor.dedupe_hits_total` -* `attestor.errors_total{type}` - -SLO guardrails: - -* `attestor.verify_latency_seconds` P95 ≤ 2 s per policy. -* `attestor.verify_total{result="failed"}` ≤ 1 % of `attestor.verify_total` over 30 min rolling windows. - -**Correlation**: - -* HTTP callers may supply `X-Correlation-Id`; Attestor will echo the header and push `CorrelationId` into the log scope for cross-service tracing. - -**Tracing**: - -* Spans: `attestor.sign`, `validate`, `rekor.submit`, `rekor.poll`, `persist`, `archive`, `attestor.verify`, `attestor.verify.refresh_proof`. - -**Audit**: - -* Immutable `audit` rows (ts, caller, action, hashes, uuid, index, backend, result, latency). - ---- - -## 9) Configuration (YAML) - -```yaml -attestor: - listen: "https://0.0.0.0:8444" - security: - mtls: - caBundle: /etc/ssl/signer-ca.pem - requireClientCert: true - authority: - issuer: "https://authority.internal" - jwksUrl: "https://authority.internal/jwks" - requireSenderConstraint: "dpop" # or "mtls" - signerIdentity: - mode: ["keyless","kms"] - fulcioRoots: ["/etc/fulcio/root.pem"] - allowedSANs: ["urn:stellaops:signer"] - kmsKeys: ["kms://cluster-kms/stellaops-signer"] - submissionLimits: - maxPayloadBytes: 2097152 - maxCertificateChainEntries: 6 - maxSignatures: 6 - signing: - preferredProviders: ["kms","bouncycastle.ed25519","default"] - kms: - enabled: true - rootPath: "/var/lib/stellaops/kms" - password: "${ATTESTOR_KMS_PASSWORD}" - keys: - - keyId: "kms-primary" - algorithm: ES256 - mode: kms - provider: "kms" - providerKeyId: "kms-primary" - kmsVersionId: "v1" - - keyId: "ed25519-offline" - algorithm: Ed25519 - mode: keyful - provider: "bouncycastle.ed25519" - materialFormat: base64 - materialPath: "/etc/stellaops/keys/ed25519.key" - certificateChain: - - "-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----" - rekor: - primary: - url: "https://rekor-v2.internal" - proofTimeoutMs: 15000 - pollIntervalMs: 250 - maxAttempts: 60 - mirror: - enabled: false - url: "https://rekor-v2.mirror" - postgres: - connectionString: "Host=postgres;Port=5432;Database=attestor;Username=stellaops;Password=secret" - s3: - enabled: true - endpoint: "http://rustfs:8080" - bucket: "stellaops" - prefix: "attest/" - objectLock: "governance" - valkey: - url: "valkey://valkey:6379/2" - quotas: - perCaller: - qps: 50 - burst: 100 -``` - -**Notes:** - -* `signing.preferredProviders` defines the resolution order when multiple providers support the requested algorithm. Omit to fall back to registration order. -* File-backed KMS (`signing.kms`) is required when at least one key uses `mode: kms`; the password should be injected via secret store or environment. -* For keyful providers, supply inline `material` or `materialPath` plus `materialFormat` (`pem` (default), `base64`, or `hex`). KMS keys ignore these fields and require `kmsVersionId`. -* `certificateChain` entries are appended to returned bundles so offline verifiers do not need to dereference external stores. - ---- - -## 10) End‑to‑end sequences - -**A) Submit & include (happy path)** - -```mermaid -sequenceDiagram - autonumber - participant SW as Scanner.WebService - participant SG as Signer - participant AT as Attestor - participant RK as Rekor v2 - - SW->>SG: POST /sign/dsse (OpTok+PoE) - SG-->>SW: DSSE bundle (+certs) - SW->>AT: POST /rekor/entries (mTLS + OpTok) - AT->>AT: Validate DSSE (chain to Fulcio/KMS; signer identity) - AT->>RK: submit(bundle) - RK-->>AT: {uuid, index?} - AT->>RK: poll inclusion until proof or timeout - RK-->>AT: inclusion proof (checkpoint + path) - AT-->>SW: {uuid, index, proof, logURL} -``` - -**B) Verify by artifact digest (CLI)** - -```mermaid -sequenceDiagram - autonumber - participant CLI as stellaops verify - participant SW as Scanner.WebService - participant AT as Attestor - - CLI->>SW: GET /catalog/artifacts/{id} - SW-->>CLI: {artifactSha256, rekor: {uuid}} - CLI->>AT: POST /rekor/verify { uuid } - AT-->>CLI: { ok: true, index, logURL } -``` - ---- - -## 11) Failure modes & responses - -| Condition | Return | Details | | | -| ------------------------------------- | ----------------------- | --------------------------------------------------------- | -------- | ------------ | -| mTLS/OpTok invalid | `401 invalid_token` | Include `WWW-Authenticate` DPoP challenge when applicable | | | -| Bundle not signed by trusted identity | `403 chain_untrusted` | DSSE accepted only from Signer identities | | | -| Duplicate bundle | `409 duplicate_bundle` | Return existing `uuid` (idempotent) | | | -| Rekor unreachable/timeout | `502 rekor_unavailable` | Retry with backoff; surface `Retry-After` | | | -| Inclusion proof timeout | `202 accepted` | `status=pending`, background job continues to fetch proof | | | -| Archive failure | `207 multi-status` | Entry recorded; archive will retry asynchronously | | | -| Verification mismatch | `400 verify_failed` | Include reason: chain | leafHash | rootMismatch | - ---- - -## 12) Performance & scale - -* Stateless; scale horizontally. -* **Targets**: - - * Submit+proof P95 ≤ **300 ms** (warm log; local Rekor). - * Verify P95 ≤ **30 ms** from cache; ≤ **120 ms** with live proof fetch. - * 1k submissions/minute per replica sustained. -* **Hot caches**: `dedupe` (bundle hash → uuid), recent `entries` by artifact sha256. - ---- - -## 13) Testing matrix - -* **Happy path**: valid DSSE, inclusion within timeout. -* **Idempotency**: resubmit same `bundleSha256` → same `uuid`. -* **Security**: reject non‑Signer mTLS, wrong `aud`, DPoP replay, untrusted cert chain, forbidden predicateType. -* **Rekor variants**: promise‑then‑proof, proof delayed, mirror dual‑submit, mirror failure. -* **Verification**: corrupt leaf path, wrong root, tampered bundle. -* **Throughput**: soak test with 10k submissions; latency SLOs, zero drops. - ---- - -## 14) Implementation notes - -* Language: **.NET 10** minimal API; `HttpClient` with **sockets handler** tuned for HTTP/2. -* JSON: **canonical writer** for DSSE payload hashing. -* Crypto: use **BouncyCastle**/**System.Security.Cryptography**; PEM parsing for cert chains. -* Rekor client: pluggable driver; treat backend errors as retryable/non‑retryable with granular mapping. -* Safety: size caps on bundles; decompress bombs guarded; strict UTF‑8. -* CLI integration: `stellaops verify attestation ` calls `/rekor/verify`. - ---- - -## 15) Optional features - -* **Dual‑log** write (primary + mirror) and **cross‑log proof** packaging. -* **Cloud endorsement**: send `{uuid, artifactSha256}` to Stella Ops cloud; store returned endorsement id for marketing/chain‑of‑custody. -* **Checkpoint pinning**: periodically pin latest Rekor checkpoints to an external audit store for independent monitoring. - ---- - -## 16) Observability (stub) - -- Runbook + dashboard placeholder for offline import: `operations/observability.md`, `operations/dashboards/attestor-observability.json`. -- Metrics to surface: signing latency p95/p99, verification failure rate, transparency log submission lag, key rotation age, queue backlog, attestation bundle size histogram. -- Health endpoints: `/health/liveness`, `/health/readiness`, `/status`; verification probe `/api/attestations/verify` once demo bundle is available (see runbook). -- Alert hints: signing latency > 1s p99, verification failure spikes, tlog submission lag >10s, key rotation age over policy threshold, backlog above configured threshold. - - ---- - -## 17) Rekor Entry Events - -> Sprint: SPRINT_20260112_007_ATTESTOR_rekor_entry_events - -Attestor emits deterministic events when DSSE bundles are logged to Rekor and inclusion proofs become available. These events drive policy reanalysis. - -### Event Types - -| Event Type | Constant | Description | -|------------|----------|-------------| -| `rekor.entry.logged` | `RekorEventTypes.EntryLogged` | Bundle successfully logged with inclusion proof | -| `rekor.entry.queued` | `RekorEventTypes.EntryQueued` | Bundle queued for logging (async mode) | -| `rekor.entry.inclusion_verified` | `RekorEventTypes.InclusionVerified` | Inclusion proof independently verified | -| `rekor.entry.failed` | `RekorEventTypes.EntryFailed` | Logging or verification failed | - -### RekorEntryEvent Schema - -```jsonc -{ - "eventId": "rekor-evt-sha256:...", - "eventType": "rekor.entry.logged", - "tenant": "default", - "bundleDigest": "sha256:abc123...", - "artifactDigest": "sha256:def456...", - "predicateType": "StellaOps.ScanResults@1", - "rekorEntry": { - "uuid": "24296fb24b8ad77a...", - "logIndex": 123456789, - "logUrl": "https://rekor.sigstore.dev", - "integratedTime": "2026-01-15T10:30:02Z" - }, - "reanalysisHints": { - "cveIds": ["CVE-2026-1234"], - "productKeys": ["pkg:npm/lodash@4.17.21"], - "mayAffectDecision": true, - "reanalysisScope": "immediate" - }, - "occurredAtUtc": "2026-01-15T10:30:05Z" -} -``` - -### Offline Mode Behavior - -When operating in offline/air-gapped mode: -1. Events are not emitted when Rekor is unreachable -2. Bundles are queued locally for later submission -3. Verification uses bundled checkpoints -4. Events are generated when connectivity is restored - -### Snapshot Export/Import for Air-Gap Transfer - -> Sprint: SPRINT_20260208_021_Attestor_snapshot_export_import_for_air_gap - -The Offline library provides snapshot export and import for transferring -attestation state to air-gapped systems via portable archives. - -**Snapshot Levels:** - -| Level | Contents | Use Case | -|-------|----------|----------| -| **A** | Attestation bundles only | Online verification still available | -| **B** | Evidence + verification material (Fulcio roots, Rekor keys) | Standard air-gap transfer | -| **C** | Full state: policies, trust anchors, org keys | Fully disconnected deployment | - -**Key Types:** - -- `SnapshotManifest` — Content-addressed manifest with SHA-256 digests per entry -- `SnapshotManifestEntry` — Individual artifact with `RelativePath`, `Digest`, `SizeBytes`, `Category` -- `ISnapshotExporter` — Produces portable JSON archives at the requested level -- `ISnapshotImporter` — Validates archive integrity and ingests entries into local stores -- `SnapshotExportRequest/Result`, `SnapshotImportRequest/Result` — Request/response models - -**Integrity:** - -- Each entry carries a SHA-256 digest; the manifest digest is computed from - sorted `path:digest` pairs plus the creation timestamp. -- Import verifies all entry digests before ingestion (configurable via `VerifyIntegrity`). -- Existing entries can be skipped during import (`SkipExisting`). - -**DI Registration:** - -```csharp -services.AddAttestorOffline(); // registers ISnapshotExporter, ISnapshotImporter -``` - ---- - -## 18) Identity Watchlist & Monitoring - -> Sprint: SPRINT_0129_001_ATTESTOR_identity_watchlist_alerting - -The Attestor provides proactive monitoring for signing identities appearing in transparency logs. Organizations can define watchlists to receive alerts when specific identities sign artifacts. - -### Purpose - -- **Credential compromise detection**: Alert when your signing identity appears unexpectedly -- **Third-party monitoring**: Watch for specific vendors or dependencies signing artifacts -- **Compliance auditing**: Track all signing activity for specific issuers - -### Watchlist Entry Model - -```jsonc -{ - "id": "uuid", - "tenantId": "tenant-123", - "scope": "tenant", // tenant | global | system - "displayName": "GitHub Actions Signer", - "description": "Watch for GitHub Actions OIDC tokens", - - // Identity fields (at least one required) - "issuer": "https://token.actions.githubusercontent.com", - "subjectAlternativeName": "repo:org/repo:*", // glob pattern - "keyId": null, - - "matchMode": "glob", // exact | prefix | glob | regex - - // Alert configuration - "severity": "warning", // info | warning | critical - "enabled": true, - "channelOverrides": ["slack-security"], - "suppressDuplicatesMinutes": 60, - - "tags": ["github", "ci-cd"], - "createdAt": "2026-01-29T10:00:00Z", - "createdBy": "admin@example.com" -} -``` - -### Matching Modes - -| Mode | Behavior | Example Pattern | Matches | -|------|----------|-----------------|---------| -| `exact` | Case-insensitive equality | `alice@example.com` | `Alice@example.com` | -| `prefix` | Starts-with match | `https://accounts.google.com/` | Any Google OIDC issuer | -| `glob` | Glob pattern (`*`, `?`) | `*@example.com` | `alice@example.com`, `bob@example.com` | -| `regex` | Full regex (with timeout) | `repo:org/(frontend\|backend):.*` | `repo:org/frontend:ref:main` | - -### Scope Hierarchy - +# component_architecture_attestor.md — **Stella Ops Attestor** (2025Q4) + +> Derived from Epic 19 – Attestor Console with provenance hooks aligned to the Export Center bundle workflows scoped in Epic 10. + +> **Scope.** Implementation‑ready architecture for the **Attestor**: the service that **submits** DSSE envelopes to **Rekor v2**, retrieves/validates inclusion proofs, caches results, and exposes verification APIs. It accepts DSSE **only** from the **Signer** over mTLS, enforces chain‑of‑trust to Stella Ops roots, and returns `{uuid, index, proof, logURL}` to calling services (Scanner.WebService for SBOMs; backend for final reports; Excititor exports when configured). + +--- + +## 0) Mission & boundaries + +**Mission.** Turn a signed DSSE envelope from the Signer into a **transparency‑logged, verifiable fact** with a durable, replayable proof (Merkle inclusion + (optional) checkpoint anchoring). Provide **fast verification** for downstream consumers and a stable retrieval interface for UI/CLI. + +**Boundaries.** + +* Attestor **does not sign**; it **must not** accept unsigned or third‑party‑signed bundles. +* Attestor **does not decide PASS/FAIL**; it logs attestations for SBOMs, reports, and export artifacts. +* Rekor v2 backends may be **local** (self‑hosted) or **remote**; Attestor handles both with retries, backoff, and idempotency. + +--- + +## 1) Topology & dependencies + +**Process shape:** single stateless service `stellaops/attestor` behind mTLS. + +**Dependencies:** + +* **Signer** (caller) — authenticated via **mTLS** and **Authority** OpToks. +* **Rekor v2** — tile‑backed transparency log endpoint(s). +* **RustFS (S3-compatible)** — optional archive store for DSSE envelopes & verification bundles. +* **PostgreSQL** — local cache of `{uuid, index, proof, artifactSha256, bundleSha256}`; job state; audit. +* **Valkey** — dedupe/idempotency keys and short‑lived rate‑limit buckets. +* **Licensing Service (optional)** — “endorse” call for cross‑log publishing when customer opts‑in. + +Trust boundary: **Only the Signer** is allowed to call submission endpoints; enforced by **mTLS peer cert allowlist** + `aud=attestor` OpTok. + +--- + +### Roles, identities & scopes +- **Subjects** — immutable digests for artifacts (container images, SBOMs, reports) referenced in DSSE envelopes. +- **Issuers** — authenticated builders/scanners/policy engines signing evidence; tracked with mode (`keyless`, `kms`, `hsm`, `fido2`) and tenant scope. +- **Consumers** — Scanner, Export Center, CLI, Console, Policy Engine that verify proofs using Attestor APIs. +- **Authority scopes** — `attestor.write`, `attestor.verify`, `attestor.read`, and administrative scopes for key management; all calls mTLS/DPoP-bound. + +### Supported predicate types +- `StellaOps.BuildProvenance@1` +- `StellaOps.SBOMAttestation@1` +- `StellaOps.ScanResults@1` +- `StellaOps.PolicyEvaluation@1` +- `StellaOps.VEXAttestation@1` +- `StellaOps.RiskProfileEvidence@1` +- `StellaOps.SignedException@1` + +Each predicate embeds subject digests, issuer metadata, policy context, materials, and optional transparency hints. Unsupported predicates return `422 predicate_unsupported`. + +> **Golden fixtures:** Deterministic JSON statements for each predicate live in `src/Attestor/StellaOps.Attestor.Types/samples`. They are kept stable by the `StellaOps.Attestor.Types.Tests` project so downstream docs and contracts can rely on them without drifting. + +### Envelope & signature model +- DSSE envelopes canonicalised (stable JSON ordering) prior to hashing. +- Signature modes: keyless (Fulcio cert chain), keyful (KMS/HSM), hardware (FIDO2/WebAuthn). Multiple signatures allowed. +- Rekor entry stores bundle hash, certificate chain, and optional witness endorsements. +- Archive CAS retains original envelope plus metadata for offline verification. +- Envelope serializer emits **compact** (canonical, minified) and **expanded** (annotated, indented) JSON variants off the same canonical byte stream so hashing stays deterministic while humans get context. +- Payload handling supports **optional compression** (`gzip`, `brotli`) with compression metadata recorded in the expanded view and digesting always performed over the uncompressed bytes. +- Expanded envelopes surface **detached payload references** (URI, digest, media type, size) so large artifacts can live in CAS/object storage while the canonical payload remains embedded for verification. +- Payload previews auto-render JSON or UTF-8 text in the expanded output to simplify triage in air-gapped and offline review flows. + +### Verification pipeline overview +1. Fetch envelope (from request, cache, or storage) and validate DSSE structure. +2. Verify signature(s) against configured trust roots; evaluate issuer policy. +3. Retrieve or acquire inclusion proof from Rekor (primary + optional mirror). +4. Validate Merkle proof against checkpoint; optionally verify witness endorsement. +5. Return cached verification bundle including policy verdict and timestamps. + +### Rekor Inclusion Proof Verification (SPRINT_3000_0001_0001) + +The Attestor implements RFC 6962-compliant Merkle inclusion proof verification for Rekor transparency log entries: + +**Components:** +- `MerkleProofVerifier` — Verifies Merkle audit paths per RFC 6962 Section 2.1.1 +- `CheckpointSignatureVerifier` — Parses and verifies Rekor checkpoint signatures (ECDSA/Ed25519) +- `RekorVerificationOptions` — Configuration for public keys, offline mode, and checkpoint caching + +**Verification Flow:** +1. Parse checkpoint body (origin, tree size, root hash) +2. Verify checkpoint signature against Rekor public key +3. Compute leaf hash from canonicalized entry +4. Walk Merkle path from leaf to root using RFC 6962 interior node hashing +5. Compare computed root with checkpoint root hash (constant-time) + +**Offline Mode:** +- Bundled checkpoints can be used in air-gapped environments +- `EnableOfflineMode` and `OfflineCheckpointBundlePath` configuration options +- `AllowOfflineWithoutSignature` for fully disconnected scenarios (reduced security) + +**Metrics:** +- `attestor.rekor_inclusion_verify_total` — Verification attempts by result +- `attestor.rekor_checkpoint_verify_total` — Checkpoint signature verifications +- `attestor.rekor_offline_verify_total` — Offline mode verifications +- `attestor.rekor_checkpoint_cache_hits/misses` — Checkpoint cache performance + +### UI & CLI touchpoints +- Console: Evidence browser, verification report, chain-of-custody graph, issuer/key management, attestation workbench, bulk verification views. +- CLI: `stella attest sign|verify|list|fetch|key` with offline verification and export bundle support. +- SDKs expose sign/verify primitives for build pipelines. + +### Performance & observability targets +- Throughput goal: ≥1 000 envelopes/minute per worker with cached verification. +- Metrics: `attestor_submission_total`, `attestor_verify_seconds`, `attestor_rekor_latency_seconds`, `attestor_cache_hit_ratio`. +- Logs include `tenant`, `issuer`, `subjectDigest`, `rekorUuid`, `proofStatus`; traces cover submission → Rekor → cache → response path. + +--- + +## 2) Data model (PostgreSQL) + +Database: `attestor` + +**Tables & schemas** + +* `entries` table + + ```sql + CREATE TABLE attestor.entries ( + id UUID PRIMARY KEY, -- rekor-uuid + artifact_sha256 TEXT NOT NULL, + artifact_kind TEXT NOT NULL, -- sbom|report|vex-export + artifact_image_digest TEXT, + artifact_subject_uri TEXT, + bundle_sha256 TEXT NOT NULL, -- canonicalized DSSE + log_index INTEGER, -- log index/sequence if provided by backend + proof_checkpoint JSONB, -- { origin, size, rootHash, timestamp } + proof_inclusion JSONB, -- { leafHash, path[] } Merkle path (tiles) + log_url TEXT, + log_id TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + status TEXT NOT NULL, -- included|pending|failed + signer_identity JSONB -- { mode, issuer, san?, kid? } + ); + ``` + +* `dedupe` table + + ```sql + CREATE TABLE attestor.dedupe ( + key TEXT PRIMARY KEY, -- bundle: idempotency key + rekor_uuid UUID NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), + ttl_at TIMESTAMPTZ NOT NULL -- for scheduled cleanup + ); + ``` + +* `audit` table + + ```sql + CREATE TABLE attestor.audit ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + ts TIMESTAMPTZ DEFAULT NOW(), + caller_cn TEXT, + caller_mtls_thumbprint TEXT, + caller_sub TEXT, + caller_aud TEXT, + action TEXT NOT NULL, -- submit|verify|fetch + artifact_sha256 TEXT, + bundle_sha256 TEXT, + rekor_uuid UUID, + log_index INTEGER, + result TEXT NOT NULL, + latency_ms INTEGER, + backend TEXT + ); + ``` + +Indexes: + +* `entries`: indexes on `artifact_sha256`, `bundle_sha256`, `created_at`, and composite `(status, created_at DESC)`. +* `dedupe`: unique index on `key`; scheduled job cleans rows where `ttl_at < NOW()` (24–48h retention). +* `audit`: index on `ts` for time‑range queries. + +--- + +## 2.1) Content-Addressed Identifier Formats + +The ProofChain library (`StellaOps.Attestor.ProofChain`) defines canonical content-addressed identifiers for all proof chain components. These IDs ensure determinism, tamper-evidence, and reproducibility. + +### Identifier Types + +| ID Type | Format | Source | Example | +|---------|--------|--------|---------| +| **ArtifactID** | `sha256:<64-hex>` | Container manifest or binary hash | `sha256:a1b2c3d4e5f6...` | +| **SBOMEntryID** | `:[@]` | SBOM hash + component PURL | `sha256:91f2ab3c:pkg:npm/lodash@4.17.21` | +| **EvidenceID** | `sha256:` | Canonical evidence JSON | `sha256:e7f8a9b0c1d2...` | +| **ReasoningID** | `sha256:` | Canonical reasoning JSON | `sha256:f0e1d2c3b4a5...` | +| **VEXVerdictID** | `sha256:` | Canonical VEX verdict JSON | `sha256:d4c5b6a7e8f9...` | +| **ProofBundleID** | `sha256:` | Merkle root of bundle components | `sha256:1a2b3c4d5e6f...` | +| **GraphRevisionID** | `grv_sha256:` | Merkle root of graph state | `grv_sha256:9f8e7d6c5b4a...` | + +### Canonicalization (RFC 8785) + +All JSON-based IDs use RFC 8785 (JCS) canonicalization: +- UTF-8 encoding +- Lexicographically sorted keys +- No whitespace (minified) +- No volatile fields (timestamps, random values excluded) + +**Implementation:** `StellaOps.Attestor.ProofChain.Json.Rfc8785JsonCanonicalizer` + +### Merkle Tree Construction + +ProofBundleID and GraphRevisionID use deterministic binary Merkle trees: +- SHA-256 hash function +- Lexicographically sorted leaf inputs +- Standard binary tree construction (pair-wise hashing) +- Odd leaves promoted to next level + +**Implementation:** `StellaOps.Attestor.ProofChain.Merkle.DeterministicMerkleTreeBuilder` + +### ID Generation Interface + +```csharp +// Core interface for ID generation +public interface IContentAddressedIdGenerator +{ + EvidenceId GenerateEvidenceId(EvidencePredicate predicate); + ReasoningId GenerateReasoningId(ReasoningPredicate predicate); + VexVerdictId GenerateVexVerdictId(VexPredicate predicate); + ProofBundleId GenerateProofBundleId(SbomEntryId sbom, EvidenceId[] evidence, + ReasoningId reasoning, VexVerdictId verdict); + GraphRevisionId GenerateGraphRevisionId(GraphState state); +} +``` + +### Predicate Types + +The ProofChain library defines DSSE predicates for proof chain attestations. All predicates follow the in-toto Statement/v1 format. + +#### Predicate Type Registry + +| Predicate | Type URI | Purpose | Signer Role | +|-----------|----------|---------|-------------| +| **Evidence** | `evidence.stella/v1` | Raw evidence from scanner/ingestor (findings, reachability data) | Scanner/Ingestor key | +| **Reasoning** | `reasoning.stella/v1` | Policy evaluation trace with inputs and intermediate findings | Policy/Authority key | +| **VEX Verdict** | `cdx-vex.stella/v1` | VEX verdict with status, justification, and provenance | VEXer/Vendor key | +| **Proof Spine** | `proofspine.stella/v1` | Merkle-aggregated proof spine linking evidence to verdict | Authority key | +| **Verdict Receipt** | `verdict.stella/v1` | Final surfaced decision receipt with policy rule reference | Authority key | +| **SBOM Linkage** | `https://stella-ops.org/predicates/sbom-linkage/v1` | SBOM-to-component linkage metadata | Generator key | +| **Signed Exception** | `https://stellaops.io/attestation/v1/signed-exception` | DSSE-signed budget exception with recheck policy | Authority key | + +#### Evidence Statement (`evidence.stella/v1`) + +Captures raw evidence collected from scanners or vulnerability feeds. + +| Field | Type | Description | +|-------|------|-------------| +| `source` | string | Scanner or feed name that produced this evidence | +| `sourceVersion` | string | Version of the source tool | +| `collectionTime` | DateTimeOffset | UTC timestamp when evidence was collected | +| `sbomEntryId` | string | Reference to the SBOM entry this evidence relates to | +| `vulnerabilityId` | string? | CVE or vulnerability identifier if applicable | +| `rawFinding` | object | Pointer to or inline representation of raw finding data | +| `evidenceId` | string | Content-addressed ID (sha256:<hash>) | + +#### Reasoning Statement (`reasoning.stella/v1`) + +Captures policy evaluation traces linking evidence to decisions. + +| Field | Type | Description | +|-------|------|-------------| +| `sbomEntryId` | string | SBOM entry this reasoning applies to | +| `evidenceIds` | string[] | Evidence IDs considered in this reasoning | +| `policyVersion` | string | Version of the policy used for evaluation | +| `inputs` | object | Inputs to the reasoning process (evaluation time, thresholds, lattice rules) | +| `intermediateFindings` | object? | Intermediate findings from the evaluation | +| `reasoningId` | string | Content-addressed ID (sha256:<hash>) | + +#### VEX Verdict Statement (`cdx-vex.stella/v1`) + +Captures VEX status determinations with provenance. + +| Field | Type | Description | +|-------|------|-------------| +| `sbomEntryId` | string | SBOM entry this verdict applies to | +| `vulnerabilityId` | string | CVE, GHSA, or other vulnerability identifier | +| `status` | string | VEX status: `not_affected`, `affected`, `fixed`, `under_investigation` | +| `justification` | string | Justification for the VEX status | +| `policyVersion` | string | Version of the policy used | +| `reasoningId` | string | Reference to the reasoning that led to this verdict | +| `vexVerdictId` | string | Content-addressed ID (sha256:<hash>) | + +#### Proof Spine Statement (`proofspine.stella/v1`) + +Merkle-aggregated proof bundle linking all chain components. + +| Field | Type | Description | +|-------|------|-------------| +| `sbomEntryId` | string | SBOM entry this proof spine covers | +| `evidenceIds` | string[] | Sorted list of evidence IDs included in this proof bundle | +| `reasoningId` | string | Reasoning ID linking evidence to verdict | +| `vexVerdictId` | string | VEX verdict ID for this entry | +| `policyVersion` | string | Version of the policy used | +| `proofBundleId` | string | Content-addressed ID (sha256:<merkle_root>) | + +#### Verdict Receipt Statement (`verdict.stella/v1`) + +Final surfaced decision receipt with full provenance. + +| Field | Type | Description | +|-------|------|-------------| +| `graphRevisionId` | string | Graph revision ID this verdict was computed from | +| `findingKey` | object | Finding key (sbomEntryId + vulnerabilityId) | +| `rule` | object | Policy rule that produced this verdict | +| `decision` | object | Decision made by the rule | +| `inputs` | object | Inputs used to compute this verdict | +| `outputs` | object | Outputs/references from this verdict | +| `createdAt` | DateTimeOffset | UTC timestamp when verdict was created | + +#### SBOM Linkage Statement (`sbom-linkage/v1`) + +SBOM-to-component linkage metadata. + +| Field | Type | Description | +|-------|------|-------------| +| `sbom` | object | SBOM descriptor (id, format, specVersion, mediaType, sha256, location) | +| `generator` | object | Generator tool descriptor | +| `generatedAt` | DateTimeOffset | UTC timestamp when linkage was generated | +| `incompleteSubjects` | object[]? | Subjects that could not be fully resolved | +| `tags` | object? | Arbitrary tags for classification or filtering | + +**Reference:** `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Statements/` + +#### Signed Exception Statement (`signed-exception/v1`) + +DSSE-signed exception objects with recheck policy for independent verification and automated re-approval workflows. + +| Field | Type | Description | +|-------|------|-------------| +| `schemaVersion` | string | Schema version (current: "1.0") | +| `exception` | object | The wrapped `BudgetExceptionEntry` | +| `exceptionContentId` | string | Content-addressed ID (sha256:<hash>) for deduplication | +| `signedAt` | DateTimeOffset | UTC timestamp when the exception was signed | +| `recheckPolicy` | object | Recheck policy configuration | +| `environments` | string[]? | Environments this exception applies to (dev, staging, prod) | +| `coveredViolationIds` | string[]? | IDs of violations this exception covers | +| `approvalPolicyDigest` | string? | Digest of the policy bundle that approved this exception | +| `renewsExceptionId` | string? | Previous exception ID for renewal chains | +| `status` | string | Status: `Active`, `PendingRecheck`, `Expired`, `Revoked`, `PendingApproval` | + +##### Recheck Policy Schema + +| Field | Type | Description | +|-------|------|-------------| +| `recheckIntervalDays` | int | Interval in days between rechecks (default: 30) | +| `autoRecheckEnabled` | bool | Whether automatic recheck scheduling is enabled | +| `maxRenewalCount` | int? | Maximum renewals before escalated approval required | +| `renewalCount` | int | Current renewal count | +| `nextRecheckAt` | DateTimeOffset? | Next scheduled recheck timestamp | +| `lastRecheckAt` | DateTimeOffset? | Last completed recheck timestamp | +| `requiresReapprovalOnExpiry` | bool | Whether re-approval is required after expiry | +| `approvalRoles` | string[]? | Roles required for approval | + +##### Exception Signing API + +The exception signing service provides endpoints for signing, verifying, and renewing exceptions: + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/internal/api/v1/exceptions/sign` | POST | Sign an exception and wrap in DSSE envelope | +| `/internal/api/v1/exceptions/verify` | POST | Verify a signed exception envelope | +| `/internal/api/v1/exceptions/recheck-status` | POST | Check if exception requires recheck | +| `/internal/api/v1/exceptions/renew` | POST | Renew an expired/expiring exception | + +**Reference:** `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Statements/DsseSignedExceptionPayload.cs` + +--- + +## 3) Input contract (from Signer) + +**Attestor accepts only** DSSE envelopes that satisfy all of: + +1. **mTLS** peer certificate maps to `signer` service (CA‑pinned). +2. **Authority** OpTok with `aud=attestor`, `scope=attestor.write`, DPoP or mTLS bound. +3. DSSE envelope is **signed by the Signer’s key** (or includes a **Fulcio‑issued** cert chain) and **chains to configured roots** (Fulcio/KMS). +4. **Predicate type** is one of Stella Ops types (sbom/report/vex‑export) with valid schema. +5. `subject[*].digest.sha256` is present and canonicalized. + +**Wire shape (JSON):** + +```json +{ + "bundle": { "dsse": { "payloadType": "application/vnd.in-toto+json", "payload": "", "signatures": [ ... ] }, + "certificateChain": [ "-----BEGIN CERTIFICATE-----..." ], + "mode": "keyless" }, + "meta": { + "artifact": { "sha256": "", "kind": "sbom|report|vex-export", "imageDigest": "sha256:..." }, + "bundleSha256": "", + "logPreference": "primary", // "primary" | "mirror" | "both" + "archive": true // whether Attestor should archive bundle to S3 + } +} +``` + +--- + +## 4) APIs + +### 4.1 Signing + +`POST /api/v1/attestations:sign` *(mTLS + OpTok required)* + +* **Purpose**: Deterministically wrap Stella Ops payloads in DSSE envelopes before Rekor submission. Reuses the submission rate limiter and honours caller tenancy/audience scopes. +* **Body**: + + ```json + { + "keyId": "signing-key-id", + "payloadType": "application/vnd.in-toto+json", + "payload": "", + "mode": "keyless|keyful|kms", + "certificateChain": ["-----BEGIN CERTIFICATE-----..."], + "artifact": { + "sha256": "", + "kind": "sbom|report|vex-export", + "imageDigest": "sha256:...", + "subjectUri": "oci://..." + }, + "logPreference": "primary|mirror|both", + "archive": true + } + ``` + +* **Behaviour**: + * Resolve the signing key from `attestor.signing.keys[]` (includes algorithm, provider, and optional KMS version). + * Compute DSSE pre‑authentication encoding, sign with the resolved provider (default EC, BouncyCastle Ed25519, or File‑KMS ES256), and add static + request certificate chains. + * Canonicalise the resulting bundle, derive `bundleSha256`, and mirror the request meta shape used by `/api/v1/rekor/entries`. + * Emit `attestor.sign_total{result,algorithm,provider}` and `attestor.sign_latency_seconds{algorithm,provider}` metrics and append an audit row (`action=sign`). +* **Response 200**: + + ```json + { + "bundle": { "dsse": { "payloadType": "...", "payload": "...", "signatures": [{ "keyid": "signing-key-id", "sig": "..." }] }, "certificateChain": ["..."], "mode": "kms" }, + "meta": { "artifact": { "sha256": "...", "kind": "sbom" }, "bundleSha256": "...", "logPreference": "primary", "archive": true }, + "key": { "keyId": "signing-key-id", "algorithm": "ES256", "mode": "kms", "provider": "kms", "signedAt": "2025-11-01T12:34:56Z" } + } + ``` + +* **Errors**: `400 key_not_found`, `400 payload_missing|payload_invalid_base64|artifact_sha_missing`, `400 mode_not_allowed`, `403 client_certificate_required`, `401 invalid_token`, `500 signing_failed`. + +### 4.2 Submission + +`POST /api/v1/rekor/entries` *(mTLS + OpTok required)* + +* **Body**: as above. +* **Behavior**: + + * Verify caller (mTLS + OpTok). + * Validate DSSE bundle (signature, cert chain to Fulcio/KMS; DSSE structure; payloadType allowed). + * Idempotency: compute `bundleSha256`; check `dedupe`. If present, return existing `rekorUuid`. + * Rekor pre-check: call Rekor index lookup (`/api/v2/index/retrieve` with v1 fallback) by bundle hash before submit; if a UUID is found, fetch and reuse existing entry metadata instead of creating a duplicate. + * Submit canonicalized bundle to Rekor v2 (primary or mirror according to `logPreference`). + * Retrieve **inclusion proof** (blocking until inclusion or up to `proofTimeoutMs`); if backend returns promise only, return `status=pending` and retry asynchronously. + * Persist `entries` record; archive DSSE to S3 if `archive=true`. +* **Response 200**: + + ```json + { + "uuid": "…", + "index": 123456, + "proof": { + "checkpoint": { "origin": "rekor@site", "size": 987654, "rootHash": "…", "timestamp": "…" }, + "inclusion": { "leafHash": "…", "path": ["…","…"] } + }, + "logURL": "https://rekor…/api/v2/log/…/entries/…", + "status": "included" + } + ``` +* **Errors**: `401 invalid_token`, `403 not_signer|chain_untrusted`, `409 duplicate_bundle` (with existing `uuid`), `502 rekor_unavailable`, `504 proof_timeout`. + +### 4.3 Proof retrieval + +`GET /api/v1/rekor/entries/{uuid}` + +* Returns `entries` row (refreshes proof from Rekor if stale/missing). +* Accepts `?refresh=true` to force backend query. + +### 4.4 Verification (third‑party or internal) + +`POST /api/v1/rekor/verify` + +* **Body** (one of): + + * `{ "uuid": "…" }` + * `{ "bundle": { …DSSE… } }` + * `{ "artifactSha256": "…" }` *(looks up most recent entry)* + +* **Checks**: + + 1. **Bundle signature** → cert chain to Fulcio/KMS roots configured. + 2. **Inclusion proof** → recompute leaf hash; verify Merkle path against checkpoint root. + 3. Optionally verify **checkpoint** against local trust anchors (if Rekor signs checkpoints). + 4. Confirm **subject.digest** matches caller‑provided hash (when given). + 5. Fetch **transparency witness** statement when enabled; cache results and downgrade status to WARN when endorsements are missing or mismatched. + +* **Response**: + + ```json + { "ok": true, "uuid": "…", "index": 123, "logURL": "…", "checkedAt": "…" } + ``` + +### 4.5 Bulk verification + +`POST /api/v1/rekor/verify:bulk` enqueues a verification job containing up to `quotas.bulk.maxItemsPerJob` items. Each item mirrors the single verification payload (uuid | artifactSha256 | subject+envelopeId, optional policyVersion/refreshProof). The handler persists a PostgreSQL job record (`bulk_jobs` table) and returns `202 Accepted` with a job descriptor and polling URL. + +`GET /api/v1/rekor/verify:bulk/{jobId}` returns progress and per-item results (subject/uuid, status, issues, cached verification report if available). Jobs are tenant- and subject-scoped; only the initiating principal can read their progress. + +**Worker path:** `BulkVerificationWorker` claims queued jobs (`status=queued → running`), executes items sequentially through the cached verification service, updates progress counters, and records metrics: + +- `attestor.bulk_jobs_total{status}` – completed/failed jobs +- `attestor.bulk_job_duration_seconds{status}` – job runtime +- `attestor.bulk_items_total{status}` – per-item outcomes (`succeeded`, `verification_failed`, `exception`) + +The worker honours `bulkVerification.itemDelayMilliseconds` for throttling and reschedules persistence conflicts with optimistic version checks. Results hydrate the verification cache; failed items record the error reason without aborting the overall job. + +--- + +## 5) Rekor v2 driver (backend) + +* **Canonicalization**: DSSE envelopes are **normalized** (stable JSON ordering, no insignificant whitespace) before hashing and submission. +* **Transport**: HTTP/2 with retries (exponential backoff, jitter), budgeted timeouts. +* **Idempotency**: if backend returns “already exists,” map to existing `uuid`. +* **Proof acquisition**: + + * In synchronous mode, poll the log for inclusion up to `proofTimeoutMs`. + * In asynchronous mode, return `pending` and schedule a **proof fetcher** job (PostgreSQL job record + backoff). +* **Mirrors/dual logs**: + + * When `logPreference="both"`, submit to primary and mirror; store **both** UUIDs (primary canonical). + * Optional **cloud endorsement**: POST to the Stella Ops cloud `/attest/endorse` with `{uuid, artifactSha256}`; store returned endorsement id. + +--- + +## 6) Security model + +* **mTLS required** for submission from **Signer** (CA‑pinned). +* **Authority token** with `aud=attestor` and DPoP/mTLS binding must be presented; Attestor verifies both. +* **Bundle acceptance policy**: + + * DSSE signature must chain to the configured **Fulcio** (keyless) or **KMS/HSM** roots. + * SAN (Subject Alternative Name) must match **Signer identity** policy (e.g., `urn:stellaops:signer` or pinned OIDC issuer). + * Predicate `predicateType` must be on allowlist (sbom/report/vex-export). + * `subject.digest.sha256` values must be present and well‑formed (hex). +* **No public submission** path. **Never** accept bundles from untrusted clients. +* **Client certificate allowlists**: optional `security.mtls.allowedSubjects` / `allowedThumbprints` tighten peer identity checks beyond CA pinning. +* **Rate limits**: token-bucket per caller derived from `quotas.perCaller` (QPS/burst) returns `429` + `Retry-After` when exceeded. +* **Scope enforcement**: API separates `attestor.write`, `attestor.verify`, and `attestor.read` policies; verification/list endpoints accept read or verify scopes while submission endpoints remain write-only. +* **Request hygiene**: JSON content-type is mandatory (415 returned otherwise); DSSE payloads are capped (default 2 MiB), certificate chains limited to six entries, and signatures to six per envelope to mitigate parsing abuse. +* **Redaction**: Attestor never logs secret material; DSSE payloads **should** be public by design (SBOMs/reports). If customers require redaction, enforce policy at Signer (predicate minimization) **before** Attestor. + +--- + +## 7) Storage & archival + +* **Entries** in PostgreSQL provide a local ledger keyed by `rekorUuid` and **artifact sha256** for quick reverse lookups. +* **S3 archival** (if enabled): + + ``` + s3://stellaops/attest/ + dsse/.json + proof/.json + bundle/.zip # optional verification bundle + ``` +* **Verification bundles** (zip): + + * DSSE (`*.dsse.json`), proof (`*.proof.json`), `chain.pem` (certs), `README.txt` with verification steps & hashes. + +--- + +## 8) Observability & audit + +**Metrics** (Prometheus): + +* `attestor.sign_total{result,algorithm,provider}` +* `attestor.sign_latency_seconds{algorithm,provider}` +* `attestor.submit_total{result,backend}` +* `attestor.submit_latency_seconds{backend}` +* `attestor.proof_fetch_total{subject,issuer,policy,result,attestor.log.backend}` +* `attestor.verify_total{subject,issuer,policy,result}` +* `attestor.verify_latency_seconds{subject,issuer,policy,result}` +* `attestor.dedupe_hits_total` +* `attestor.errors_total{type}` + +SLO guardrails: + +* `attestor.verify_latency_seconds` P95 ≤ 2 s per policy. +* `attestor.verify_total{result="failed"}` ≤ 1 % of `attestor.verify_total` over 30 min rolling windows. + +**Correlation**: + +* HTTP callers may supply `X-Correlation-Id`; Attestor will echo the header and push `CorrelationId` into the log scope for cross-service tracing. + +**Tracing**: + +* Spans: `attestor.sign`, `validate`, `rekor.submit`, `rekor.poll`, `persist`, `archive`, `attestor.verify`, `attestor.verify.refresh_proof`. + +**Audit**: + +* Immutable `audit` rows (ts, caller, action, hashes, uuid, index, backend, result, latency). + +--- + +## 9) Configuration (YAML) + +```yaml +attestor: + listen: "https://0.0.0.0:8444" + security: + mtls: + caBundle: /etc/ssl/signer-ca.pem + requireClientCert: true + authority: + issuer: "https://authority.internal" + jwksUrl: "https://authority.internal/jwks" + requireSenderConstraint: "dpop" # or "mtls" + signerIdentity: + mode: ["keyless","kms"] + fulcioRoots: ["/etc/fulcio/root.pem"] + allowedSANs: ["urn:stellaops:signer"] + kmsKeys: ["kms://cluster-kms/stellaops-signer"] + submissionLimits: + maxPayloadBytes: 2097152 + maxCertificateChainEntries: 6 + maxSignatures: 6 + signing: + preferredProviders: ["kms","bouncycastle.ed25519","default"] + kms: + enabled: true + rootPath: "/var/lib/stellaops/kms" + password: "${ATTESTOR_KMS_PASSWORD}" + keys: + - keyId: "kms-primary" + algorithm: ES256 + mode: kms + provider: "kms" + providerKeyId: "kms-primary" + kmsVersionId: "v1" + - keyId: "ed25519-offline" + algorithm: Ed25519 + mode: keyful + provider: "bouncycastle.ed25519" + materialFormat: base64 + materialPath: "/etc/stellaops/keys/ed25519.key" + certificateChain: + - "-----BEGIN CERTIFICATE-----...-----END CERTIFICATE-----" + rekor: + primary: + url: "https://rekor-v2.internal" + proofTimeoutMs: 15000 + pollIntervalMs: 250 + maxAttempts: 60 + mirror: + enabled: false + url: "https://rekor-v2.mirror" + postgres: + connectionString: "Host=postgres;Port=5432;Database=attestor;Username=stellaops;Password=secret" + s3: + enabled: true + endpoint: "http://rustfs:8080" + bucket: "stellaops" + prefix: "attest/" + objectLock: "governance" + valkey: + url: "valkey://valkey:6379/2" + quotas: + perCaller: + qps: 50 + burst: 100 +``` + +**Notes:** + +* `signing.preferredProviders` defines the resolution order when multiple providers support the requested algorithm. Omit to fall back to registration order. +* File-backed KMS (`signing.kms`) is required when at least one key uses `mode: kms`; the password should be injected via secret store or environment. +* For keyful providers, supply inline `material` or `materialPath` plus `materialFormat` (`pem` (default), `base64`, or `hex`). KMS keys ignore these fields and require `kmsVersionId`. +* `certificateChain` entries are appended to returned bundles so offline verifiers do not need to dereference external stores. + +--- + +## 10) End‑to‑end sequences + +**A) Submit & include (happy path)** + +```mermaid +sequenceDiagram + autonumber + participant SW as Scanner.WebService + participant SG as Signer + participant AT as Attestor + participant RK as Rekor v2 + + SW->>SG: POST /sign/dsse (OpTok+PoE) + SG-->>SW: DSSE bundle (+certs) + SW->>AT: POST /rekor/entries (mTLS + OpTok) + AT->>AT: Validate DSSE (chain to Fulcio/KMS; signer identity) + AT->>RK: submit(bundle) + RK-->>AT: {uuid, index?} + AT->>RK: poll inclusion until proof or timeout + RK-->>AT: inclusion proof (checkpoint + path) + AT-->>SW: {uuid, index, proof, logURL} +``` + +**B) Verify by artifact digest (CLI)** + +```mermaid +sequenceDiagram + autonumber + participant CLI as stellaops verify + participant SW as Scanner.WebService + participant AT as Attestor + + CLI->>SW: GET /catalog/artifacts/{id} + SW-->>CLI: {artifactSha256, rekor: {uuid}} + CLI->>AT: POST /rekor/verify { uuid } + AT-->>CLI: { ok: true, index, logURL } +``` + +--- + +## 11) Failure modes & responses + +| Condition | Return | Details | | | +| ------------------------------------- | ----------------------- | --------------------------------------------------------- | -------- | ------------ | +| mTLS/OpTok invalid | `401 invalid_token` | Include `WWW-Authenticate` DPoP challenge when applicable | | | +| Bundle not signed by trusted identity | `403 chain_untrusted` | DSSE accepted only from Signer identities | | | +| Duplicate bundle | `409 duplicate_bundle` | Return existing `uuid` (idempotent) | | | +| Rekor unreachable/timeout | `502 rekor_unavailable` | Retry with backoff; surface `Retry-After` | | | +| Inclusion proof timeout | `202 accepted` | `status=pending`, background job continues to fetch proof | | | +| Archive failure | `207 multi-status` | Entry recorded; archive will retry asynchronously | | | +| Verification mismatch | `400 verify_failed` | Include reason: chain | leafHash | rootMismatch | + +--- + +## 12) Performance & scale + +* Stateless; scale horizontally. +* **Targets**: + + * Submit+proof P95 ≤ **300 ms** (warm log; local Rekor). + * Verify P95 ≤ **30 ms** from cache; ≤ **120 ms** with live proof fetch. + * 1k submissions/minute per replica sustained. +* **Hot caches**: `dedupe` (bundle hash → uuid), recent `entries` by artifact sha256. + +--- + +## 13) Testing matrix + +* **Happy path**: valid DSSE, inclusion within timeout. +* **Idempotency**: resubmit same `bundleSha256` → same `uuid`. +* **Security**: reject non‑Signer mTLS, wrong `aud`, DPoP replay, untrusted cert chain, forbidden predicateType. +* **Rekor variants**: promise‑then‑proof, proof delayed, mirror dual‑submit, mirror failure. +* **Verification**: corrupt leaf path, wrong root, tampered bundle. +* **Throughput**: soak test with 10k submissions; latency SLOs, zero drops. + +--- + +## 14) Implementation notes + +* Language: **.NET 10** minimal API; `HttpClient` with **sockets handler** tuned for HTTP/2. +* JSON: **canonical writer** for DSSE payload hashing. +* Crypto: use **BouncyCastle**/**System.Security.Cryptography**; PEM parsing for cert chains. +* Rekor client: pluggable driver; treat backend errors as retryable/non‑retryable with granular mapping. +* Safety: size caps on bundles; decompress bombs guarded; strict UTF‑8. +* CLI integration: `stellaops verify attestation ` calls `/rekor/verify`. + +--- + +## 15) Optional features + +* **Dual‑log** write (primary + mirror) and **cross‑log proof** packaging. +* **Cloud endorsement**: send `{uuid, artifactSha256}` to Stella Ops cloud; store returned endorsement id for marketing/chain‑of‑custody. +* **Checkpoint pinning**: periodically pin latest Rekor checkpoints to an external audit store for independent monitoring. + +--- + +## 16) Observability (stub) + +- Runbook + dashboard placeholder for offline import: `operations/observability.md`, `operations/dashboards/attestor-observability.json`. +- Metrics to surface: signing latency p95/p99, verification failure rate, transparency log submission lag, key rotation age, queue backlog, attestation bundle size histogram. +- Health endpoints: `/health/liveness`, `/health/readiness`, `/status`; verification probe `/api/attestations/verify` once demo bundle is available (see runbook). +- Alert hints: signing latency > 1s p99, verification failure spikes, tlog submission lag >10s, key rotation age over policy threshold, backlog above configured threshold. + + +--- + +## 17) Rekor Entry Events + +> Sprint: SPRINT_20260112_007_ATTESTOR_rekor_entry_events + +Attestor emits deterministic events when DSSE bundles are logged to Rekor and inclusion proofs become available. These events drive policy reanalysis. + +### Event Types + +| Event Type | Constant | Description | +|------------|----------|-------------| +| `rekor.entry.logged` | `RekorEventTypes.EntryLogged` | Bundle successfully logged with inclusion proof | +| `rekor.entry.queued` | `RekorEventTypes.EntryQueued` | Bundle queued for logging (async mode) | +| `rekor.entry.inclusion_verified` | `RekorEventTypes.InclusionVerified` | Inclusion proof independently verified | +| `rekor.entry.failed` | `RekorEventTypes.EntryFailed` | Logging or verification failed | + +### RekorEntryEvent Schema + +```jsonc +{ + "eventId": "rekor-evt-sha256:...", + "eventType": "rekor.entry.logged", + "tenant": "default", + "bundleDigest": "sha256:abc123...", + "artifactDigest": "sha256:def456...", + "predicateType": "StellaOps.ScanResults@1", + "rekorEntry": { + "uuid": "24296fb24b8ad77a...", + "logIndex": 123456789, + "logUrl": "https://rekor.sigstore.dev", + "integratedTime": "2026-01-15T10:30:02Z" + }, + "reanalysisHints": { + "cveIds": ["CVE-2026-1234"], + "productKeys": ["pkg:npm/lodash@4.17.21"], + "mayAffectDecision": true, + "reanalysisScope": "immediate" + }, + "occurredAtUtc": "2026-01-15T10:30:05Z" +} +``` + +### Offline Mode Behavior + +When operating in offline/air-gapped mode: +1. Events are not emitted when Rekor is unreachable +2. Bundles are queued locally for later submission +3. Verification uses bundled checkpoints +4. Events are generated when connectivity is restored + +### Snapshot Export/Import for Air-Gap Transfer + +> Sprint: SPRINT_20260208_021_Attestor_snapshot_export_import_for_air_gap + +The Offline library provides snapshot export and import for transferring +attestation state to air-gapped systems via portable archives. + +**Snapshot Levels:** + +| Level | Contents | Use Case | +|-------|----------|----------| +| **A** | Attestation bundles only | Online verification still available | +| **B** | Evidence + verification material (Fulcio roots, Rekor keys) | Standard air-gap transfer | +| **C** | Full state: policies, trust anchors, org keys | Fully disconnected deployment | + +**Key Types:** + +- `SnapshotManifest` — Content-addressed manifest with SHA-256 digests per entry +- `SnapshotManifestEntry` — Individual artifact with `RelativePath`, `Digest`, `SizeBytes`, `Category` +- `ISnapshotExporter` — Produces portable JSON archives at the requested level +- `ISnapshotImporter` — Validates archive integrity and ingests entries into local stores +- `SnapshotExportRequest/Result`, `SnapshotImportRequest/Result` — Request/response models + +**Integrity:** + +- Each entry carries a SHA-256 digest; the manifest digest is computed from + sorted `path:digest` pairs plus the creation timestamp. +- Import verifies all entry digests before ingestion (configurable via `VerifyIntegrity`). +- Existing entries can be skipped during import (`SkipExisting`). + +**DI Registration:** + +```csharp +services.AddAttestorOffline(); // registers ISnapshotExporter, ISnapshotImporter +``` + +--- + +## 18) Identity Watchlist & Monitoring + +> Sprint: SPRINT_0129_001_ATTESTOR_identity_watchlist_alerting + +The Attestor provides proactive monitoring for signing identities appearing in transparency logs. Organizations can define watchlists to receive alerts when specific identities sign artifacts. + +### Purpose + +- **Credential compromise detection**: Alert when your signing identity appears unexpectedly +- **Third-party monitoring**: Watch for specific vendors or dependencies signing artifacts +- **Compliance auditing**: Track all signing activity for specific issuers + +### Watchlist Entry Model + +```jsonc +{ + "id": "uuid", + "tenantId": "tenant-123", + "scope": "tenant", // tenant | global | system + "displayName": "GitHub Actions Signer", + "description": "Watch for GitHub Actions OIDC tokens", + + // Identity fields (at least one required) + "issuer": "https://token.actions.githubusercontent.com", + "subjectAlternativeName": "repo:org/repo:*", // glob pattern + "keyId": null, + + "matchMode": "glob", // exact | prefix | glob | regex + + // Alert configuration + "severity": "warning", // info | warning | critical + "enabled": true, + "channelOverrides": ["slack-security"], + "suppressDuplicatesMinutes": 60, + + "tags": ["github", "ci-cd"], + "createdAt": "2026-01-29T10:00:00Z", + "createdBy": "admin@example.com" +} +``` + +### Matching Modes + +| Mode | Behavior | Example Pattern | Matches | +|------|----------|-----------------|---------| +| `exact` | Case-insensitive equality | `alice@example.com` | `Alice@example.com` | +| `prefix` | Starts-with match | `https://accounts.google.com/` | Any Google OIDC issuer | +| `glob` | Glob pattern (`*`, `?`) | `*@example.com` | `alice@example.com`, `bob@example.com` | +| `regex` | Full regex (with timeout) | `repo:org/(frontend\|backend):.*` | `repo:org/frontend:ref:main` | + +### Scope Hierarchy + | Scope | Visibility | Who Can Create | |-------|------------|----------------| | `tenant` | Owning tenant only | Operators with `trust:write` | @@ -934,1820 +934,1820 @@ The Attestor provides proactive monitoring for signing identities appearing in t | `system` | All tenants (read-only) | System bootstrap | Authorization for the live watchlist surface follows the canonical trust scope family (`trust:read`, `trust:write`, `trust:admin`). The service still accepts legacy `watchlist:*` aliases for backward compatibility, but new clients and UI sessions should rely on the trust scopes. - -### Event Flow - -``` -New AttestorEntry persisted - → SignerIdentityDescriptor extracted - → IIdentityMatcher.MatchAsync() - → For each match: - → Check dedup window (default 60 min) - → Emit attestor.identity.matched event - → Route via Notifier rules → Slack/Email/Webhook -``` - -### Event Schema (IdentityAlertEvent) - -```jsonc -{ - "eventId": "uuid", - "eventKind": "attestor.identity.matched", - "tenantId": "tenant-123", - "watchlistEntryId": "uuid", - "watchlistEntryName": "GitHub Actions Signer", - "matchedIdentity": { - "issuer": "https://token.actions.githubusercontent.com", - "subjectAlternativeName": "repo:org/repo:ref:refs/heads/main", - "keyId": null - }, - "rekorEntry": { - "uuid": "24296fb24b8ad77a...", - "logIndex": 123456789, - "artifactSha256": "sha256:abc123...", - "integratedTimeUtc": "2026-01-29T10:30:00Z" - }, - "severity": "warning", - "occurredAtUtc": "2026-01-29T10:30:05Z", - "suppressedCount": 0 -} -``` - -### API Endpoints - -| Method | Path | Description | -|--------|------|-------------| -| `POST` | `/api/v1/watchlist` | Create watchlist entry | -| `GET` | `/api/v1/watchlist` | List entries (tenant + optional global) | -| `GET` | `/api/v1/watchlist/{id}` | Get single entry | -| `PUT` | `/api/v1/watchlist/{id}` | Update entry | -| `DELETE` | `/api/v1/watchlist/{id}` | Delete entry | -| `POST` | `/api/v1/watchlist/{id}/test` | Test pattern against sample identity | -| `GET` | `/api/v1/watchlist/alerts` | List recent alerts (paginated) | - -### CLI Commands - -```bash -# Add a watchlist entry -stella watchlist add --issuer "https://token.actions.githubusercontent.com" \ - --san "repo:org/*" --match-mode glob --severity warning - -# List entries -stella watchlist list --include-global - -# Test a pattern -stella watchlist test --issuer "https://..." --san "repo:org/repo:ref:main" - -# View recent alerts -stella watchlist alerts --since 24h --severity warning -``` - -### Metrics - -| Metric | Description | -|--------|-------------| -| `attestor.watchlist.entries_scanned_total` | Entries processed by monitor | -| `attestor.watchlist.matches_total{severity}` | Pattern matches by severity | -| `attestor.watchlist.alerts_emitted_total` | Alerts sent to notification system | -| `attestor.watchlist.alerts_suppressed_total` | Alerts deduplicated | -| `attestor.watchlist.scan_latency_seconds` | Per-entry scan duration | - -### Configuration - -```yaml -attestor: - watchlist: - enabled: true - monitorMode: "changefeed" # changefeed | polling - pollingIntervalSeconds: 5 # only for polling mode - maxEventsPerSecond: 100 # rate limit - defaultDedupWindowMinutes: 60 - regexTimeoutMs: 100 # safety limit - maxWatchlistEntriesPerTenant: 1000 -``` - -### Offline Mode - -In air-gapped environments: -- Polling mode used instead of Postgres NOTIFY -- Alerts queued locally if notification channels unavailable -- Alerts delivered when connectivity restored - ---- - -## Unknowns Five-Dimensional Triage Scoring (P/E/U/C/S) - -> Sprint: SPRINT_20260208_022_Attestor_unknowns_five_dimensional_triage_scoring - -### Overview - -The triage scorer extends the existing `IUnknownsAggregator` pipeline with -a five-dimensional scoring model for unknowns, enabling prioritized triage -and temperature-band classification. - -### Scoring Dimensions - -| Dimension | Code | Range | Description | -|-----------|------|-------|-------------| -| Probability | P | [0,1] | Likelihood of exploitability or relevance | -| Exposure | E | [0,1] | Attack surface exposure (internal → internet-facing) | -| Uncertainty | U | [0,1] | Confidence deficit (fully understood → unknown) | -| Consequence | C | [0,1] | Impact severity (negligible → catastrophic) | -| Signal Freshness | S | [0,1] | Recency of intelligence (stale → just reported) | - -### Composite Score - -Composite = Σ(dimension × weight) / Σ(weights), clamped to [0, 1]. - -Default weights: P=0.30, E=0.25, U=0.20, C=0.15, S=0.10 (configurable via `TriageDimensionWeights`). - -### Temperature Bands - -| Band | Threshold | Action | -|------|-----------|--------| -| **Hot** | ≥ 0.70 | Immediate triage required | -| **Warm** | ≥ 0.40 | Scheduled review | -| **Cold** | < 0.40 | Archive / low priority | - -Thresholds are configurable via `TriageBandThresholds`. - -### Key Types - -- `IUnknownsTriageScorer` — Interface: `Score()`, `ComputeComposite()`, `Classify()` -- `UnknownsTriageScorer` — Implementation with OTel counters -- `TriageScore` — Five-dimensional score vector -- `TriageDimensionWeights` — Configurable weights with static `Default` -- `TriageBandThresholds` — Configurable Hot/Warm thresholds with static `Default` -- `TriageScoredItem` — Scored unknown with composite score and band -- `TriageScoringRequest/Result` — Batch scoring request/response - -### OTel Metrics - -| Metric | Description | -|--------|-------------| -| `triage.scored.total` | Total unknowns scored | -| `triage.band.hot.total` | Unknowns classified as Hot | -| `triage.band.warm.total` | Unknowns classified as Warm | -| `triage.band.cold.total` | Unknowns classified as Cold | - -### DI Registration - -```csharp -services.AddAttestorProofChain(); // registers IUnknownsTriageScorer -``` - ---- - -## VEX Findings API with Proof Artifacts - -> Sprint: SPRINT_20260208_023_Attestor_vex_findings_api_with_proof_artifacts - -### Overview - -The VEX Findings API provides a query and resolution service for VEX findings -(CVE + component combinations) with their associated proof artifacts. Each -finding carries DSSE signatures, Rekor receipts, Merkle proofs, and policy -decision attestations that prove how the VEX status was determined. - -### Key Types - -- `VexFinding` — A finding with `FindingId`, `VulnerabilityId`, `ComponentPurl`, - `Status`, `Justification`, `ProofArtifacts`, `DeterminedAt` -- `ProofArtifact` — Proof material: `Kind` (DsseSignature/RekorReceipt/MerkleProof/ - PolicyDecision/VexDelta/ReachabilityWitness), `Digest`, `Payload`, `ProducedAt` -- `VexFindingStatus` — NotAffected | Affected | Fixed | UnderInvestigation -- `IVexFindingsService` — `GetByIdAsync`, `QueryAsync`, `ResolveProofsAsync`, `UpsertAsync` -- `VexFindingQuery` — Filters: VulnerabilityId, ComponentPurlPrefix, Status, TenantId, Limit, Offset - -### Proof Resolution - -`ResolveProofsAsync()` merges new proof artifacts into a finding, deduplicating -by digest. This allows incremental proof collection as new evidence is produced. - -### Finding IDs - -Finding IDs are deterministic: `SHA-256(vulnId:componentPurl)` prefixed with -`finding:`. This ensures the same CVE + component always maps to the same ID. - -### OTel Metrics - -| Metric | Description | -|--------|-------------| -| `findings.get.total` | Findings retrieved by ID | -| `findings.query.total` | Finding queries executed | -| `findings.upsert.total` | Findings upserted | -| `findings.resolve.total` | Proof resolution requests | -| `findings.proofs.total` | Proof artifacts resolved | - -### DI Registration - -```csharp -services.AddAttestorProofChain(); // registers IVexFindingsService -``` - ---- - -## Binary Fingerprint Store & Trust Scoring - -### Overview - -The Binary Fingerprint Store is a content-addressed repository for section-level -binary hashes (ELF `.text`/`.rodata`, PE sections) with golden-set management -and trust scoring. It enables: - -- **Content-addressed lookup**: Fingerprints identified by `fp:sha256:…` computed - from `(format, architecture, sectionHashes)`. -- **Section-level matching**: Find closest match by comparing individual section - hashes with a similarity score. -- **Golden-set management**: Define named sets of known-good fingerprints for - baseline comparison. -- **Trust scoring**: Multi-factor score (0.0–0.99) based on golden membership, - Build-ID, section coverage, evidence, and package provenance. - -Library: `StellaOps.Attestor.ProofChain` -Namespace: `StellaOps.Attestor.ProofChain.FingerprintStore` - -### Models - -| Type | Purpose | -|------|---------| -| `BinaryFingerprintRecord` | Stored fingerprint: ID, format, architecture, file SHA-256, Build-ID, section hashes, package PURL, golden-set flag, trust score, evidence digests, timestamps. | -| `FingerprintRegistration` | Input for `RegisterAsync`: format, architecture, file hash, section hashes, optional PURL/Build-ID/evidence. | -| `FingerprintLookupResult` | Match result: found flag, matched record, golden match, section similarity (0.0–1.0), matched/differing section lists. | -| `TrustScoreBreakdown` | Decomposed score: golden bonus, Build-ID score, section coverage, evidence score, provenance score. | -| `GoldenSet` | Named golden set with count and timestamps. | -| `FingerprintQuery` | Filters: format, architecture, PURL prefix, golden flag, golden set name, min trust score, limit/offset. | - -### Service Interface (`IBinaryFingerprintStore`) - -| Method | Description | -|--------|-------------| -| `RegisterAsync(registration)` | Register fingerprint (idempotent by content-addressed ID). | -| `GetByIdAsync(fingerprintId)` | Look up by content-addressed ID. | -| `GetByFileSha256Async(fileSha256)` | Look up by whole-file hash. | -| `FindBySectionHashesAsync(sectionHashes, minSimilarity)` | Best-match search by section hashes. | -| `ComputeTrustScoreAsync(fingerprintId)` | Detailed trust-score breakdown. | -| `ListAsync(query)` | Filtered + paginated listing. | -| `AddToGoldenSetAsync(fingerprintId, goldenSetName)` | Mark fingerprint as golden (recalculates trust score). | -| `RemoveFromGoldenSetAsync(fingerprintId)` | Remove golden flag. | -| `CreateGoldenSetAsync(name, description)` | Create a named golden set. | -| `ListGoldenSetsAsync()` | List all golden sets. | -| `GetGoldenSetMembersAsync(goldenSetName)` | List members of a golden set. | -| `DeleteAsync(fingerprintId)` | Remove fingerprint from store. | - -### Trust Score Computation - -| Factor | Weight | Raw value | -|--------|--------|-----------| -| Golden-set membership | 0.30 | 1.0 if golden, 0.0 otherwise | -| Build-ID present | 0.20 | 1.0 if Build-ID exists, 0.0 otherwise | -| Section coverage | 0.25 | Ratio of key sections (`.text`, `.rodata`, `.data`, `.bss`) present | -| Evidence count | 0.15 | `min(count/5, 1.0)` | -| Package provenance | 0.10 | 1.0 if PURL present, 0.0 otherwise | - -Final score is capped at 0.99. - -### DI Registration - -`AddProofChainServices()` registers `IBinaryFingerprintStore → BinaryFingerprintStore` (singleton, via `TryAddSingleton`). - -### Observability (OTel Metrics) - -Meter: `StellaOps.Attestor.ProofChain.FingerprintStore` - -| Metric | Type | Description | -|--------|------|-------------| -| `fingerprint.store.registered` | Counter | Fingerprints registered | -| `fingerprint.store.lookups` | Counter | Store lookups performed | -| `fingerprint.store.golden_added` | Counter | Fingerprints added to golden sets | -| `fingerprint.store.deleted` | Counter | Fingerprints deleted | - -### Test Coverage - -30 tests in `StellaOps.Attestor.ProofChain.Tests/FingerprintStore/BinaryFingerprintStoreTests.cs`: -- Registration (new, idempotent, different sections → different IDs, validation) -- Lookup (by ID, by file SHA-256, not-found cases) -- Section-hash matching (exact, partial, below threshold, empty) -- Trust scoring (with/without Build-ID/PURL, minimal, golden bonus, cap at 0.99, determinism) -- Golden-set management (create, add, remove, list members, list sets) -- List/query with filters (format, min trust score) -- Delete (existing, non-existent) -- Content-addressed ID determinism - ---- - -## Content-Addressed Store (CAS) for SBOM/VEX/Attestation Artifacts - -### Overview - -The CAS provides a unified content-addressed storage service for all artifact types -(SBOM, VEX, attestation, proof bundles, evidence packs, binary fingerprints). -All blobs are keyed by SHA-256 digest of their raw content. Puts are idempotent: -storing the same content twice returns the existing record with a dedup flag. - -Library: `StellaOps.Attestor.ProofChain` -Namespace: `StellaOps.Attestor.ProofChain.Cas` - -### Artifact Types - -| Type | Description | -|------|-------------| -| `Sbom` | Software Bill of Materials | -| `Vex` | VEX (Vulnerability Exploitability Exchange) document | -| `Attestation` | DSSE-signed attestation envelope | -| `ProofBundle` | Proof chain bundle | -| `EvidencePack` | Evidence pack manifest | -| `BinaryFingerprint` | Binary fingerprint record | -| `Other` | Generic/other artifact | - -### Models - -| Type | Purpose | -|------|---------| -| `CasArtifact` | Stored artifact metadata: digest, type, media type, size, tags, related digests, timestamps, dedup flag. | -| `CasPutRequest` | Input: raw content bytes, artifact type, media type, optional tags and related digests. | -| `CasPutResult` | Output: stored artifact + dedup flag. | -| `CasGetResult` | Retrieved artifact with content bytes. | -| `CasQuery` | Filters: artifact type, media type, tag key/value, limit/offset. | -| `CasStatistics` | Store metrics: total artifacts, bytes, dedup count, type breakdown. | - -### Service Interface (`IContentAddressedStore`) - -| Method | Description | -|--------|-------------| -| `PutAsync(request)` | Store artifact (idempotent by SHA-256 digest). Returns dedup flag. | -| `GetAsync(digest)` | Retrieve artifact + content by digest. | -| `ExistsAsync(digest)` | Check existence by digest. | -| `DeleteAsync(digest)` | Remove artifact. | -| `ListAsync(query)` | Filtered + paginated listing. | -| `GetStatisticsAsync()` | Total artifacts, bytes, dedup savings, type breakdown. | - -### Deduplication - -When `PutAsync` receives content whose SHA-256 digest already exists in the store: -1. The existing artifact metadata is returned (no duplicate storage). -2. `CasPutResult.Deduplicated` is set to `true`. -3. An OTel counter is incremented for audit. - -### DI Registration - -`AddProofChainServices()` registers `IContentAddressedStore → InMemoryContentAddressedStore` (singleton, via `TryAddSingleton`). - -### Observability (OTel Metrics) - -Meter: `StellaOps.Attestor.ProofChain.Cas` - -| Metric | Type | Description | -|--------|------|-------------| -| `cas.puts` | Counter | CAS put operations | -| `cas.deduplications` | Counter | Deduplicated puts | -| `cas.gets` | Counter | CAS get operations | -| `cas.deletes` | Counter | CAS delete operations | - -### Test Coverage - -24 tests in `StellaOps.Attestor.ProofChain.Tests/Cas/InMemoryContentAddressedStoreTests.cs`: -- Put (new, dedup, different content, validation, tags, related digests) -- Get (existing, non-existent) -- Exists (stored, not stored) -- Delete (existing, non-existent) -- List with filters (artifact type, media type, tags, pagination) -- Statistics (counts, bytes, dedup tracking) -- Digest determinism - ---- - -## Crypto-Sovereign Design (eIDAS/FIPS/GOST/SM/PQC) - -### Overview - -The crypto-sovereign subsystem bridges the Attestor's role-based `SigningKeyProfile` -(Evidence, Reasoning, VexVerdict, Authority, Generator, Exception) to algorithm-specific -crypto profiles governed by regional compliance constraints. This enables a single -Attestor deployment to enforce eIDAS qualified signatures, FIPS-approved algorithms, -GOST, SM2, or Post-Quantum Cryptography depending on the configured region. - -Library: `StellaOps.Attestor.ProofChain` -Namespace: `StellaOps.Attestor.ProofChain.Signing` - -### Algorithm Profiles - -| Profile | Algorithm ID | Standard | -|---------|-------------|----------| -| `Ed25519` | ED25519 | RFC 8032 | -| `EcdsaP256` | ES256 | NIST FIPS 186-4 | -| `EcdsaP384` | ES384 | NIST FIPS 186-4 | -| `RsaPss` | PS256 | PKCS#1 v2.1 | -| `Gost2012_256` | GOST-R34.10-2012-256 | Russian Federation | -| `Gost2012_512` | GOST-R34.10-2012-512 | Russian Federation | -| `Sm2` | SM2 | Chinese GB/T 32918 | -| `Dilithium3` | DILITHIUM3 | NIST FIPS 204 (ML-DSA) | -| `Falcon512` | FALCON512 | NIST PQC Round 3 | -| `EidasRsaSha256` | eIDAS-RSA-SHA256 | EU eIDAS + CAdES | -| `EidasEcdsaSha256` | eIDAS-ECDSA-SHA256 | EU eIDAS + CAdES | - -### Sovereign Regions - -| Region | Default Algorithm | Requirements | -|--------|------------------|--------------| -| `International` | Ed25519 | None | -| `EuEidas` | eIDAS-RSA-SHA256 | Qualified timestamp (Article 42), CAdES-T minimum | -| `UsFips` | ECDSA-P256 | HSM-backed keys | -| `RuGost` | GOST-2012-256 | GOST algorithms only | -| `CnSm` | SM2 | SM national standards only | -| `PostQuantum` | Dilithium3 | PQC finalist algorithms only | - -### Service Interface (`ICryptoProfileResolver`) - -| Method | Description | -|--------|-------------| -| `ResolveAsync(keyProfile)` | Resolve key profile using active region. | -| `ResolveAsync(keyProfile, region)` | Resolve key profile with explicit region override. | -| `ActiveRegion` | Get the configured sovereign region. | -| `GetPolicy(region)` | Get the sovereign policy for a region. | -| `ValidateQualifiedTimestampAsync(...)` | eIDAS Article 42 timestamp validation. | - -### Resolution Flow - -1. `SigningKeyProfile` (role: Evidence/Reasoning/etc.) arrives at `ICryptoProfileResolver` -2. Active `CryptoSovereignRegion` determines the `CryptoSovereignPolicy` -3. Policy's `DefaultAlgorithm` produces a `CryptoProfileBinding` -4. Binding carries: algorithm ID, region, CAdES level, HSM/timestamp requirements -5. Caller (or composition root) uses binding to resolve key material from `ICryptoProviderRegistry` - -### eIDAS Article 42 Qualified Timestamp Validation - -`ValidateQualifiedTimestampAsync` performs structural validation of RFC 3161 timestamp tokens: -- Non-eIDAS regions return `IsQualified = false` immediately -- Empty tokens or signed data are rejected -- ASN.1 SEQUENCE tag (0x30) is verified as structural check -- Full TSA certificate chain and EU Trusted List validation deferred to eIDAS plugin integration - -### CAdES Levels - -| Level | Description | -|-------|-------------| -| `CadesB` | Basic Electronic Signature | -| `CadesT` | With Timestamp (Article 42 minimum) | -| `CadesLT` | With Long-Term validation data | -| `CadesLTA` | With Long-Term Archival validation data | - -### DI Registration - -`AddProofChainServices()` registers `ICryptoProfileResolver → DefaultCryptoProfileResolver` (singleton, via `TryAddSingleton`). -The Attestor Infrastructure layer can pre-register a registry-aware implementation -that bridges `ICryptoProviderRegistry` before this fallback applies. - -### Observability (OTel Metrics) - -Meter: `StellaOps.Attestor.ProofChain.CryptoSovereign` - -| Metric | Type | Description | -|--------|------|-------------| -| `crypto_sovereign.resolves` | Counter | Profile resolution operations (tagged by region) | -| `crypto_sovereign.timestamp_validations` | Counter | Qualified timestamp validations | - -### Test Coverage - -27 tests in `StellaOps.Attestor.ProofChain.Tests/Signing/DefaultCryptoProfileResolverTests.cs`: -- Region-based resolution (International/eIDAS/FIPS/GOST/SM/PQC default algorithms) -- Explicit region override -- All key profiles resolve for all regions -- Active region property -- Policy access and validation (all regions, eIDAS timestamp requirement, FIPS HSM requirement) -- Algorithm ID mapping (all 11 profiles) -- Qualified timestamp validation (non-eIDAS, empty token, empty data, invalid ASN.1, valid structure) -- Cancellation handling -- Determinism (same inputs → identical bindings) -- Policy consistency (default in allowed list, non-empty allowed lists) - ---- - -## DSSE Envelope Size Management (Guardrails, Chunking, Gateway Awareness) - -### Overview - -Pre-submission size guard for DSSE envelopes submitted to Rekor transparency logs. -Validates envelope size against a configurable policy and determines the submission mode: -full envelope (under soft limit), hash-only fallback, chunked with manifest, or rejected. - -Library: `StellaOps.Attestor.ProofChain` -Namespace: `StellaOps.Attestor.ProofChain.Rekor` - -### Submission Modes - -| Mode | Trigger | Behavior | -|------|---------|----------| -| `FullEnvelope` | Size ≤ soft limit | Envelope submitted to Rekor as-is | -| `HashOnly` | Soft limit < size ≤ hard limit, hash-only enabled | Only SHA-256 payload digest submitted | -| `Chunked` | Soft limit < size ≤ hard limit, chunking enabled | Envelope split into chunks with manifest | -| `Rejected` | Size > hard limit, or no fallback available | Submission blocked | - -### Size Policy (`DsseEnvelopeSizePolicy`) - -| Property | Default | Description | -|----------|---------|-------------| -| `SoftLimitBytes` | 102,400 (100 KB) | Threshold for hash-only/chunked fallback | -| `HardLimitBytes` | 1,048,576 (1 MB) | Absolute rejection threshold | -| `ChunkSizeBytes` | 65,536 (64 KB) | Maximum size per chunk | -| `EnableHashOnlyFallback` | `true` | Allow hash-only submission for oversized envelopes | -| `EnableChunking` | `false` | Allow chunked submission (takes priority over hash-only) | -| `HashAlgorithm` | "SHA-256" | Hash algorithm for digest computation | - -### Service Interface (`IDsseEnvelopeSizeGuard`) - -| Method | Description | -|--------|-------------| -| `ValidateAsync(DsseEnvelope)` | Validate a typed DSSE envelope against size policy | -| `ValidateAsync(ReadOnlyMemory)` | Validate raw serialized envelope bytes | -| `Policy` | Get the active size policy | - -### Chunk Manifest - -When chunking is enabled and an envelope exceeds the soft limit, the guard produces -an `EnvelopeChunkManifest` containing: -- `TotalSizeBytes`: original envelope size -- `ChunkCount`: number of chunks -- `OriginalDigest`: SHA-256 digest of the complete original envelope -- `Chunks`: ordered array of `ChunkDescriptor` (index, size, digest, offset) - -Each chunk is content-addressed by its SHA-256 digest for integrity verification. - -### DI Registration - -`AddProofChainServices()` registers `IDsseEnvelopeSizeGuard → DsseEnvelopeSizeGuard` -(singleton, via `TryAddSingleton`). Default policy uses 100 KB soft / 1 MB hard limits. - -### Observability (OTel Metrics) - -Meter: `StellaOps.Attestor.ProofChain.EnvelopeSize` - -| Metric | Type | Description | -|--------|------|-------------| -| `envelope_size.validations` | Counter | Total envelope size validations | -| `envelope_size.hash_only_fallbacks` | Counter | Hash-only fallback activations | -| `envelope_size.chunked` | Counter | Chunked submission activations | -| `envelope_size.rejections` | Counter | Envelope rejections | - -### Test Coverage - -28 tests in `StellaOps.Attestor.ProofChain.Tests/Rekor/DsseEnvelopeSizeGuardTests.cs`: -- Full envelope (small, exact soft limit) -- Hash-only fallback (activation, digest determinism) -- Chunked mode (activation, correct chunk count, priority over hash-only) -- Hard limit rejection -- Both fallbacks disabled rejection -- Raw bytes validation (under limit, empty rejection) -- Policy validation (negative soft, hard < soft, zero chunk size, defaults) -- Cancellation handling -- Digest determinism (same/different input) -- Chunk manifest determinism -- Size tracking - ---- - -## DSSE-Wrapped Reach-Maps - -### Purpose - -Reach-maps are standalone in-toto attestation artifacts that capture the full reachability graph for a scanned artifact. Unlike micro-witnesses (which capture individual vulnerability reachability paths), a reach-map aggregates the entire graph — all nodes, edges, findings, and analysis metadata — into a single DSSE-wrapped statement that can be stored, transmitted, and verified independently. - -### Predicate Type - -URI: `reach-map.stella/v1` - -The reach-map predicate follows **Pattern B** (predicate model in `Predicates/`, statement delegates `PredicateType`). - -### Data Model - -#### ReachMapPredicate - -Top-level predicate record containing: - -| Field | Type | Description | -|---|---|---| -| `SchemaVersion` | string | Always "1.0.0" | -| `GraphDigest` | string | Deterministic SHA-256 digest of sorted graph content | -| `GraphCasUri` | string? | Optional CAS URI for externalized graph storage | -| `ScanId` | string | Identifier of the originating scan | -| `ArtifactRef` | string | Package URL or image reference of the scanned artifact | -| `Nodes` | ImmutableArray\ | All nodes in the reachability graph | -| `Edges` | ImmutableArray\ | All edges (call relationships) | -| `Findings` | ImmutableArray\ | Vulnerability findings with reachability status | -| `AggregatedWitnessIds` | ImmutableArray\ | Deduplicated witness IDs from findings + explicit additions | -| `Analysis` | ReachMapAnalysis | Analyzer metadata (tool, version, confidence, completeness) | -| `Summary` | ReachMapSummary | Computed statistics (counts of nodes, edges, entry points, sinks) | - -#### ReachMapNode - -| Field | Type | Description | -|---|---|---| -| `NodeId` | string | Unique identifier for the node | -| `QualifiedName` | string | Fully qualified name (e.g., class.method) | -| `Module` | string | Module or assembly containing the node | -| `IsEntryPoint` | bool | Whether this node is a graph entry point | -| `IsSink` | bool | Whether this node is a vulnerability sink | -| `ReachabilityState` | string | One of the 8-state lattice values | - -#### ReachMapEdge - -| Field | Type | Description | -|---|---|---| -| `SourceNodeId` | string | Origin node of the call edge | -| `TargetNodeId` | string | Destination node of the call edge | -| `CallType` | string | Edge type (direct, virtual, reflection, etc.) | -| `Confidence` | double | Edge confidence score (0.0–1.0), default 1.0 | - -#### ReachMapFinding - -| Field | Type | Description | -|---|---|---| -| `VulnId` | string | Vulnerability identifier | -| `CveId` | string? | Optional CVE identifier | -| `Purl` | string? | Optional package URL | -| `IsReachable` | bool | Whether the vulnerability is reachable | -| `ConfidenceScore` | double | Reachability confidence (0.0–1.0) | -| `SinkNodeIds` | ImmutableArray\ | Nodes where the vulnerability manifests | -| `ReachableEntryPointIds` | ImmutableArray\ | Entry points that can reach sinks | -| `WitnessId` | string? | Optional micro-witness identifier | - -### ReachMapBuilder (Fluent API) - -`ReachMapBuilder` provides a fluent interface for constructing reach-map predicates: - -```csharp -var predicate = new ReachMapBuilder() - .WithScanId("scan-001") - .WithArtifactRef("pkg:docker/myapp@sha256:abc123") - .WithAnalyzer("stella-reach", "2.0.0", 0.95, "full") - .WithGeneratedAt(DateTimeOffset.UtcNow) - .AddNodes(nodes) - .AddEdges(edges) - .AddFindings(findings) - .Build(); -``` - -#### Deterministic Graph Digest - -The builder computes a deterministic SHA-256 digest over the graph content: - -1. Nodes are sorted by `NodeId`, each contributing `NodeId|QualifiedName|ReachabilityState` -2. Edges are sorted by `SourceNodeId` then `TargetNodeId`, each contributing `Source→Target|CallType` -3. Findings are sorted by `VulnId`, each contributing `VulnId|IsReachable|ConfidenceScore` -4. All contributions are concatenated with newlines and hashed - -This ensures identical graphs always produce the same digest regardless of insertion order. - -#### Witness Aggregation - -Witness IDs are collected from two sources: -- `WitnessId` fields on individual `ReachMapFinding` records -- Explicit `AddWitnessId()` calls on the builder - -All witness IDs are deduplicated in the final predicate. - -### Schema Validation - -The reach-map predicate type is registered in `PredicateSchemaValidator`: -- `HasSchema("reach-map.stella/v1")` → `true` -- `ValidateByPredicateType` routes to `ValidateReachMapPredicate` -- Required JSON properties: `graph_digest`, `scan_id`, `artifact_ref`, `nodes`, `edges`, `analysis`, `summary` - -### Statement Integration - -`ReachMapStatement` extends `InTotoStatement` with: -- `PredicateType` → `"reach-map.stella/v1"` (from `ReachMapPredicate.PredicateTypeUri`) -- `Type` → `"https://in-toto.io/Statement/v1"` (inherited) - -### Source Files - -- Predicate: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Predicates/ReachMapPredicate.cs` -- Statement: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Statements/ReachMapStatement.cs` -- Builder: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Rekor/ReachMapBuilder.cs` -- Validator: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Json/PredicateSchemaValidator.DeltaValidators.cs` - -### Test Coverage (25 tests) - -- Build validation (missing ScanId, ArtifactRef, Analyzer) -- Minimal build, full build with summary statistics -- Graph digest determinism (same input, different order, different content) -- Witness aggregation (from findings, explicit, deduplication) -- Bulk add operations (AddNodes, AddEdges, AddFindings) -- CAS URI inclusion -- Statement integration (predicate type, statement type) -- Null argument protection (5 tests) - ---- - -## Evidence Coverage Score for AI Gating - -### Purpose - -The Evidence Coverage Scorer provides a deterministic, multi-dimensional assessment of how thoroughly an artifact's evidence base covers the key verification axes. This score directly gates AI auto-processing decisions: AI-generated artifacts (explanations, remediation plans, VEX drafts, policy drafts) can only be promoted to verdicts when evidence coverage meets a configurable threshold. - -### Evidence Dimensions - -The scorer evaluates five independent dimensions: - -| Dimension | Default Weight | Description | -|---|---|---| -| **Reachability** | 0.25 | Call graph analysis, micro-witnesses, reach-maps | -| **BinaryAnalysis** | 0.20 | Binary fingerprints, build-id verification, section hashes | -| **SbomCompleteness** | 0.25 | Component inventory, dependency resolution completeness | -| **VexCoverage** | 0.20 | Vulnerability status decisions (affected/not_affected/fixed) | -| **Provenance** | 0.10 | Build provenance, source attestation, supply chain evidence | - -### Scoring Algorithm - -1. For each dimension, the scorer receives a list of evidence identifiers -2. Each identifier is checked against an **evidence resolver** (`Func`) — the same pattern used by `AIAuthorityClassifier` -3. Dimension score = (resolvable count) / (total count), producing a 0.0–1.0 value -4. Overall score = weighted average across all dimensions (normalized by total weight) -5. Missing dimensions receive a score of 0.0 - -### Coverage Levels (Badge Rendering) - -| Level | Threshold | Meaning | -|---|---|---| -| **Green** | >= 80% (configurable) | Full evidence coverage, auto-processing eligible | -| **Yellow** | >= 50% (configurable) | Partial coverage, manual review recommended | -| **Red** | < 50% | Insufficient evidence, gating blocks promotion | - -### AI Gating Policy - -The `EvidenceCoveragePolicy` record controls: -- Per-dimension weights (must be non-negative) -- AI gating threshold (default 0.80) — minimum overall score for auto-processing -- Green/yellow badge thresholds - -When `MeetsAiGatingThreshold` is `false`, the `AIAuthorityClassifier`'s `CanAutoProcess` path should be blocked. - -### DI Registration - -Registered via `ProofChainServiceCollectionExtensions.AddProofChainServices()`: -- `IEvidenceCoverageScorer` -> `EvidenceCoverageScorer` (TryAddSingleton) -- Default evidence resolver returns `false` (no evidence resolvable) — Infrastructure layer overrides with a persistence-backed resolver - -### OTel Metrics - -Meter: `StellaOps.Attestor.ProofChain.EvidenceCoverage` - -| Counter | Description | -|---|---| -| `coverage.evaluations` | Total coverage evaluations performed | -| `coverage.gating.pass` | Evaluations that met AI gating threshold | -| `coverage.gating.fail` | Evaluations that failed AI gating threshold | - -### Source Files - -- Models: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Predicates/AI/EvidenceCoverageModels.cs` -- Interface: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Predicates/AI/IEvidenceCoverageScorer.cs` -- Implementation: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Predicates/AI/EvidenceCoverageScorer.cs` -- DI: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/ProofChainServiceCollectionExtensions.cs` - -### Test Coverage (24 tests) - -- Full coverage (all dimensions resolvable, Green level) -- No evidence (empty inputs, Red, zero score) -- Partial coverage (weighted score calculation) -- Per-dimension breakdown (counts, reasons) -- Missing dimensions (zero score) -- Gating threshold (at threshold, below threshold) -- Custom thresholds (coverage level boundaries) -- Policy validation (negative weight, invalid threshold, green < yellow) -- Null argument protection (policy, resolver, meter factory, subject ref, inputs, result) -- Cancellation handling -- Determinism (same inputs produce same results) -- Default policy values -- Reason text verification - ---- - -## Evidence Subgraph UI Visualization - -### Purpose - -The Subgraph Visualization Service renders proof graph subgraphs into multiple visualization formats suitable for interactive frontend rendering. It bridges the existing `IProofGraphService.GetArtifactSubgraphAsync()` BFS traversal with UI-ready output in Mermaid, Graphviz DOT, and structured JSON formats. - -### Render Formats - -| Format | Use Case | Output | -|---|---|---| -| **Mermaid** | Browser-side rendering via Mermaid.js | `graph TD` markup with class definitions | -| **Dot** | Static/server-side rendering via Graphviz | `digraph` markup with color/shape attributes | -| **Json** | Custom frontend rendering (D3.js, Cytoscape.js) | Structured `{nodes, edges}` JSON | - -### Visualization Models - -#### VisualizationNode - -| Field | Type | Description | -|---|---|---| -| `Id` | string | Unique node identifier | -| `Label` | string | Formatted display label (type + truncated digest) | -| `Type` | string | Node type string for icon/color selection | -| `ContentDigest` | string? | Full content digest for provenance verification | -| `IsRoot` | bool | Whether this is the subgraph root | -| `Depth` | int | BFS depth from root (for layout layering) | -| `Metadata` | ImmutableDictionary? | Optional key-value pairs for tooltips | - -#### VisualizationEdge - -| Field | Type | Description | -|---|---|---| -| `Source` | string | Source node ID | -| `Target` | string | Target node ID | -| `Label` | string | Human-readable edge type label | -| `Type` | string | Edge type string for styling | - -### Depth Computation - -The service computes BFS depth from the root node bidirectionally through all edges, enabling hierarchical layout rendering. Unreachable nodes receive the maximum depth value. - -### Node Type Styling - -| Node Type | Mermaid Shape | DOT Color | -|---|---|---| -| Artifact / Subject | `[box]` | #4CAF50 (green) | -| SbomDocument | `([stadium])` | #2196F3 (blue) | -| InTotoStatement / DsseEnvelope | `[[subroutine]]` | #FF9800 (orange) | -| VexStatement | `([stadium])` | #9C27B0 (purple) | -| RekorEntry | `[(cylinder)]` | #795548 (brown) | -| SigningKey / TrustAnchor | `((circle))` | #607D8B (blue-grey) | - -### DI Registration - -`ISubgraphVisualizationService` -> `SubgraphVisualizationService` (TryAddSingleton) - -### Source Files - -- Models: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Graph/SubgraphVisualizationModels.cs` -- Interface: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Graph/ISubgraphVisualizationService.cs` -- Implementation: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Graph/SubgraphVisualizationService.cs` - -### Test Coverage (22 tests) - -- Empty subgraph rendering -- Single node with root detection and depth -- Multi-node depth computation (root=0, child=1, grandchild=2) -- Mermaid format (graph directive, node/edge content, class definitions) -- DOT format (digraph directive, node colors) -- JSON format (valid JSON output) -- Edge type labels (5 inline data tests) -- Node type preservation (4 inline data tests) -- Content digest truncation in labels -- Cancellation handling -- Null argument protection -- Determinism (same input produces same output) -- All three formats produce non-empty content (3 inline data tests) - ---- - -## Field-Level Ownership Map for Receipts and Bundles - -### Purpose - -The Field-Level Ownership Map provides a machine-readable and human-readable document that maps each field in `VerificationReceipt` and `VerificationCheck` to the responsible module. This enables automated validation that fields are populated by their designated owner module, supporting audit trails and cross-module accountability. - -### Owner Modules - -| Module | Responsibility | -|---|---| -| **Core** | Fundamental identifiers, timestamps, versions, tool digests | -| **Signing** | Key identifiers and signature-related fields | -| **Rekor** | Transparency log indices and inclusion proofs | -| **Verification** | Trust anchors, verification results, check details | -| **SbomVex** | SBOM/VEX document references | -| **Provenance** | Provenance and build attestation fields | -| **Policy** | Policy evaluation results | -| **External** | Fields populated by external integrations | - -### Ownership Map Structure - -The `FieldOwnershipMap` record contains: -- `DocumentType` — the document being mapped (e.g., "VerificationReceipt") -- `SchemaVersion` — version of the ownership schema (default "1.0") -- `Entries` — immutable list of `FieldOwnershipEntry` records - -Each `FieldOwnershipEntry` declares: -- `FieldPath` — dot-path or array-path (e.g., `proofBundleId`, `checks[].keyId`) -- `Owner` — the `OwnerModule` responsible for populating the field -- `IsRequired` — whether the field must be populated for validity -- `Description` — human-readable purpose of the field - -### Default Receipt Ownership Map (14 entries) - -| Field Path | Owner | Required | -|---|---|---| -| `proofBundleId` | Core | Yes | -| `verifiedAt` | Core | Yes | -| `verifierVersion` | Core | Yes | -| `anchorId` | Verification | Yes | -| `result` | Verification | Yes | -| `checks` | Verification | Yes | -| `checks[].check` | Verification | Yes | -| `checks[].status` | Verification | Yes | -| `checks[].keyId` | Signing | No | -| `checks[].logIndex` | Rekor | No | -| `checks[].expected` | Verification | No | -| `checks[].actual` | Verification | No | -| `checks[].details` | Verification | No | -| `toolDigests` | Core | No | - -### Validation - -`ValidateReceiptOwnershipAsync` checks a `VerificationReceipt` against the ownership map: -1. Iterates top-level fields, recording population status -2. Expands per-check fields for each `VerificationCheck` entry -3. Counts missing required fields -4. Returns `FieldOwnershipValidationResult` with computed properties: - - `IsValid` — true when `MissingRequiredCount == 0` - - `TotalFields` — total field population records - - `PopulatedCount` — fields that have values - - `ValidCount` — fields with valid ownership - -### DI Registration - -`IFieldOwnershipValidator` -> `FieldOwnershipValidator` (TryAddSingleton) - -### Source Files - -- Models: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Receipts/FieldOwnershipModels.cs` -- Interface: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Receipts/IFieldOwnershipValidator.cs` -- Implementation: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Receipts/FieldOwnershipValidator.cs` - -### Test Coverage (24 tests) - -- Ownership map structure (document type, entry count, top-level fields, check fields) -- Owner assignment theories (7 top-level + 4 check-level field-to-owner mappings) -- Description completeness (all entries have descriptions) -- Full receipt validation (valid, all populated, correct counts) -- Minimal receipt validation (valid, optional fields not populated) -- Empty checks validation (missing required → invalid) -- Multi-check field expansion (fields per check entry) -- Ownership validity (all fields valid in static map) -- ValidatedAt propagation -- Null receipt protection -- Cancellation token handling -- Determinism (same inputs produce same results) -- Static map required/optional field markers -- Computed property correctness - ---- - -## Idempotent SBOM/Attestation APIs - -### Purpose - -The Idempotent Ingest Service provides content-hash-based deduplication for SBOM ingest and attestation verification operations. Duplicate submissions return the original result without creating duplicate records, ensuring safe retries and deterministic outcomes. - -### Architecture - -The service builds on the existing `IContentAddressedStore` (CAS), which already provides SHA-256-based deduplication at the storage layer. The idempotent service adds: - -1. **SBOM Ingest** — wraps CAS `PutAsync` with SBOM-specific metadata (media type, tags, artifact type) and returns a typed `SbomEntryId` -2. **Attestation Verify** — stores attestation in CAS, performs verification checks, and caches results by content hash in a `ConcurrentDictionary` -3. **Idempotency Key Support** — optional client-provided keys that map to content digests, enabling safe retries even when content bytes differ - -### Idempotency Guarantees - -| Scenario | Behavior | -|---|---| -| Same content, no key | CAS deduplicates by SHA-256 hash, returns `Deduplicated = true` | -| Same content, same key | Returns cached result via key lookup | -| Different content, same key | Returns original result mapped to the key | -| Same content, different key | Both keys map to the same digest | - -### Verification Checks - -The baseline attestation verification performs three deterministic checks: - -| Check | Description | -|---|---| -| `content_present` | Content is non-empty | -| `digest_format` | Valid SHA-256 digest format (71 chars) | -| `json_structure` | Content starts with `{` and ends with `}` | - -Infrastructure layer may override with full DSSE/Rekor verification. - -### Models - -| Type | Description | -|---|---| -| `SbomIngestRequest` | Content, MediaType, Tags, optional IdempotencyKey | -| `SbomIngestResult` | Digest, Deduplicated, Artifact, SbomEntryId | -| `AttestationVerifyRequest` | Content, MediaType, optional IdempotencyKey | -| `AttestationVerifyResult` | Digest, CacheHit, Verified, Summary, Checks, VerifiedAt | -| `AttestationCheckResult` | Check, Passed, Details | -| `IdempotencyKeyEntry` | Key, Digest, CreatedAt, OperationType | - -### DI Registration - -`IIdempotentIngestService` -> `IdempotentIngestService` (TryAddSingleton factory) -- Resolves: `IContentAddressedStore`, optional `TimeProvider`, `IMeterFactory` - -### OTel Metrics - -Meter: `StellaOps.Attestor.ProofChain.Idempotency` - -| Counter | Description | -|---|---| -| `idempotent.sbom.ingests` | Total SBOM ingest operations | -| `idempotent.sbom.deduplications` | SBOM submissions that were deduplicated | -| `idempotent.attest.verifications` | Total attestation verifications (non-cached) | -| `idempotent.attest.cache_hits` | Attestation verifications served from cache | -| `idempotent.key.hits` | Idempotency key lookups that found existing entries | - -### Source Files - -- Models: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Idempotency/IdempotentIngestModels.cs` -- Interface: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Idempotency/IIdempotentIngestService.cs` -- Implementation: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Idempotency/IdempotentIngestService.cs` - -### Test Coverage (30 tests) - -- SBOM ingest: first submission, duplicate dedup, different content, tags, idempotency key retry, empty content, empty media type, null request, cancellation, artifact type -- Attestation verify: first submission, duplicate cache hit, JSON structure pass/fail, content check, digest check, idempotency key, null request, empty content, cancellation, determinism, summary text -- Idempotency key lookup: unknown key, after ingest, after verify, null key -- Constructor validation: null store, null meter factory, null time provider - ---- - -## Regulatory Compliance Report Generator (NIS2/DORA/ISO-27001/EU CRA) - -### Purpose - -The Compliance Report Generator provides a static registry of regulatory controls and maps evidence artifacts to regulatory requirements. It generates compliance reports that identify which controls are satisfied by available evidence and which have gaps, enabling auditable regulatory alignment for release decisions. - -### Supported Frameworks - -| Framework | Controls | Description | -|---|---|---| -| **NIS2** | 5 | EU Network and Information Security Directive 2 | -| **DORA** | 5 | EU Digital Operational Resilience Act | -| **ISO-27001** | 6 | ISO/IEC 27001 Information Security Management | -| **EU CRA** | 4 | EU Cyber Resilience Act | - -### Evidence Artifact Types - -| Type | Description | -|---|---| -| `Sbom` | Software Bill of Materials | -| `VexStatement` | Vulnerability Exploitability Exchange statement | -| `SignedAttestation` | Signed attestation envelope | -| `TransparencyLogEntry` | Rekor transparency log entry | -| `VerificationReceipt` | Proof of verification | -| `ProofBundle` | Bundled evidence pack | -| `ReachabilityAnalysis` | Binary fingerprint or reachability analysis | -| `PolicyEvaluation` | Policy evaluation result | -| `ProvenanceAttestation` | Build origin proof | -| `IncidentReport` | Incident response documentation | - -### Control Registry (20 controls) - -#### NIS2 Controls -| ID | Category | Satisfied By | -|---|---|---| -| NIS2-Art21.2d | Supply Chain Security | SBOM, VEX, Provenance | -| NIS2-Art21.2e | Supply Chain Security | VEX, Reachability | -| NIS2-Art21.2a | Risk Management | Policy, Attestation | -| NIS2-Art21.2g | Risk Management | Receipt, ProofBundle | -| NIS2-Art23 | Incident Management | Incident, Transparency | - -#### DORA Controls -| ID | Category | Satisfied By | -|---|---|---| -| DORA-Art6.1 | ICT Risk Management | Policy, Attestation | -| DORA-Art9.1 | ICT Risk Management | Attestation, Receipt, ProofBundle | -| DORA-Art17 | Incident Classification | Incident, VEX | -| DORA-Art28 | Third-Party Risk | SBOM, Provenance, Reachability | -| DORA-Art11 | ICT Risk Management (optional) | ProofBundle, Transparency | - -#### ISO-27001 Controls -| ID | Category | Satisfied By | -|---|---|---| -| A.8.28 | Application Security | SBOM, Reachability, Provenance | -| A.8.9 | Configuration Management | Policy, Attestation | -| A.8.8 | Vulnerability Management | VEX, Reachability, SBOM | -| A.5.23 | Cloud Security (optional) | Provenance, ProofBundle | -| A.5.37 | Operations Security | Receipt, Transparency | -| A.5.21 | Supply Chain Security | SBOM, VEX, Provenance | - -#### EU CRA Controls -| ID | Category | Satisfied By | -|---|---|---| -| CRA-AnnexI.2.1 | Product Security | SBOM | -| CRA-AnnexI.2.5 | Vulnerability Management | VEX, Reachability | -| CRA-Art11 | Vulnerability Management | VEX, Incident, Transparency | -| CRA-AnnexI.1.2 | Product Security | Policy, Attestation, Receipt | - -### Report Structure - -`ComplianceReport` computed properties: -- `CompliancePercentage` — ratio of satisfied to total controls -- `MandatoryGapCount` — mandatory controls not satisfied -- `MeetsMinimumCompliance` — true when all mandatory controls satisfied - -### DI Registration - -`IComplianceReportGenerator` -> `ComplianceReportGenerator` (TryAddSingleton factory) - -### OTel Metrics - -Meter: `StellaOps.Attestor.ProofChain.Compliance` - -| Counter | Description | -|---|---| -| `compliance.reports.generated` | Total compliance reports generated | -| `compliance.controls.evaluated` | Total individual controls evaluated | - -### Source Files - -- Models: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Compliance/RegulatoryComplianceModels.cs` -- Interface: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Compliance/IComplianceReportGenerator.cs` -- Implementation: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Compliance/ComplianceReportGenerator.cs` - -### Test Coverage (26 tests) - -- Supported frameworks (count and membership) -- Control counts per framework (4 theories) -- Control ID presence per framework (4 theories) -- Framework assignment and required field validation -- Full evidence → 100% compliance (4 theories) -- No evidence → 0% compliance (4 theories) -- Partial evidence → partial compliance -- Subject ref and framework recording -- Generated timestamp -- Artifact ref tracing -- Gap descriptions (present for unsatisfied, absent for satisfied) -- Null subject/evidence protection -- Cancellation token -- Determinism -- Constructor validation -- Mandatory vs optional controls -- NIS2 control categories (5 theories) - - ---- - -## In-toto Link Attestation Capture (Sprint 015) - -The **LinkCapture** subsystem provides in-toto link attestation capture for supply chain step recording. It captures materials (inputs) and products (outputs) with content-addressed deduplication, enabling CI pipeline step evidence collection. - -### Domain Model - -| Record | Purpose | -|---|---| -| `CapturedMaterial` | Input artifact (URI + digest map) | -| `CapturedProduct` | Output artifact (URI + digest map) | -| `CapturedEnvironment` | Execution context (hostname, OS, variables) | -| `LinkCaptureRequest` | Capture request with step, functionary, command, materials, products, env, byproducts, pipeline/step IDs | -| `LinkCaptureResult` | Result with content-addressed digest, dedup flag, stored record | -| `CapturedLinkRecord` | Stored link with all fields + CapturedAt timestamp | -| `LinkCaptureQuery` | Query filter: step name, functionary, pipeline ID, limit | - -### Deduplication - -Content-addressed deduplication uses canonical hashing: -- Canonical form: step name + functionary + command + sorted materials + sorted products -- Environment and byproducts are **excluded** from the digest to ensure deterministic deduplication across different execution contexts -- SHA-256 digest with `sha256:` prefix -- Materials and products sorted by URI (ordinal) before hashing - -### Service Interface - -`ILinkCaptureService`: -- `CaptureAsync(LinkCaptureRequest)` → `LinkCaptureResult` — idempotent capture -- `GetByDigestAsync(string digest)` → `CapturedLinkRecord?` — lookup by content digest -- `QueryAsync(LinkCaptureQuery)` → `ImmutableArray` — filtered query (case-insensitive, ordered by descending timestamp) - -### DI Registration - -`ILinkCaptureService` -> `LinkCaptureService` (TryAddSingleton factory) - -### OTel Metrics - -Meter: `StellaOps.Attestor.ProofChain.LinkCapture` - -| Counter | Description | -|---|---| -| `link.captures` | Total new link attestations captured | -| `link.deduplications` | Total deduplicated captures | -| `link.queries` | Total query operations | - -### Source Files - -- Models: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/LinkCapture/LinkCaptureModels.cs` -- Interface: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/LinkCapture/ILinkCaptureService.cs` -- Implementation: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/LinkCapture/LinkCaptureService.cs` - -### Test Coverage (30 tests) - -- Basic capture with digest, step, functionary verification -- Timestamp from TimeProvider -- Materials and products recording -- Environment and byproducts recording -- Pipeline/step ID recording -- Deduplication (same request returns deduplicated=true) -- Different step/functionary/materials produce different digests -- Deterministic digest (material order invariance) -- Environment excluded from digest -- Null/empty validation (request, step, functionary) -- Cancellation token handling -- GetByDigest (found, not found, null, cancelled) -- Query by step name, functionary, pipeline ID -- Case-insensitive query filtering -- Empty store query -- No-filter returns all -- Limit enforcement -- Descending timestamp ordering -- Constructor validation ---- - -## Monthly Bundle Rotation and Re-Signing (Sprint 016) - -The **BundleRotation** subsystem provides scheduled key rotation for DSSE-signed bundles. It verifies bundles with the old key, re-signs them with a new key, and records a transition attestation for audit trail. - -### Domain Model - -| Record | Purpose | -|---|---| -| `RotationStatus` | Enum: Pending, Verified, ReSigned, Completed, Failed, Skipped | -| `RotationCadence` | Enum: Monthly, Quarterly, OnDemand | -| `KeyTransition` | Old/new key IDs, algorithm, effective date, grace period | -| `BundleRotationRequest` | Rotation cycle request with transition, bundle digests, cadence, tenant | -| `BundleRotationEntry` | Per-bundle result (original/new digest, status, error) | -| `BundleRotationResult` | Full cycle result with computed SuccessCount/FailureCount/SkippedCount | -| `TransitionAttestation` | Audit record: attestation ID, rotation ID, result digest, counts | -| `RotationScheduleEntry` | Schedule config: cadence, next/last rotation, current key, enabled | -| `RotationHistoryQuery` | Query filter: tenant, key ID, status, limit | - -### Re-Signing Workflow - -1. Validate request (rotation ID, key IDs, bundle digests) -2. Verify old key and new key exist in `IProofChainKeyStore` -3. For each bundle: verify with old key → compute re-signed digest → record entry -4. Determine overall status from individual entries -5. Create `TransitionAttestation` with result digest for integrity verification -6. Store in rotation history - -### Service Interface - -`IBundleRotationService`: -- `RotateAsync(BundleRotationRequest)` → `BundleRotationResult` — execute rotation cycle -- `GetTransitionAttestationAsync(string rotationId)` → `TransitionAttestation?` — get audit attestation -- `QueryHistoryAsync(RotationHistoryQuery)` → `ImmutableArray` — query history -- `ComputeNextRotationDate(RotationCadence, DateTimeOffset?)` → `DateTimeOffset` — schedule computation - -### DI Registration - -`IBundleRotationService` -> `BundleRotationService` (TryAddSingleton factory, requires `IProofChainKeyStore`) - -### OTel Metrics - -Meter: `StellaOps.Attestor.ProofChain.Signing.Rotation` - -| Counter | Description | -|---|---| -| `rotation.cycles.started` | Total rotation cycles initiated | -| `rotation.cycles.completed` | Total rotation cycles completed | -| `rotation.bundles.resigned` | Total bundles successfully re-signed | -| `rotation.bundles.skipped` | Total bundles skipped | -| `rotation.bundles.failed` | Total bundles that failed rotation | - -### Source Files - -- Models: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Signing/BundleRotationModels.cs` -- Interface: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Signing/IBundleRotationService.cs` -- Implementation: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Signing/BundleRotationService.cs` - -### Test Coverage (35 tests) - -- Basic rotation (completed result, success count, new digests, transition, timestamps) -- Key validation (old key missing, new key missing → all fail) -- Empty bundle digest → entry fails -- Argument validation (null request, empty rotation ID, empty bundles, empty key IDs, cancellation) -- Transition attestation (created after rotation, has result digest, records transition, not found for unknown, null/cancel) -- Query history (empty, after rotation, filter by key ID, filter by status, limit, null/cancel) -- Schedule computation (monthly +1 month, quarterly +3 months, on-demand immediate, null last uses current time) -- Determinism (same inputs → same re-signed digests) -- Constructor validation (null key store, null meter factory, null time provider OK) - ---- - -## Noise Ledger — Audit Log of Suppressions (Sprint 017) - -The **NoiseLedger** subsystem provides an auditable, queryable log of all suppression decisions in the attestation pipeline. It records VEX overrides, alert deduplications, policy-based suppressions, operator acknowledgments, and false positive determinations. - -### Domain Model - -| Type | Purpose | -|---|---| -| `SuppressionCategory` | Enum: VexOverride, AlertDedup, PolicyRule, OperatorAck, SeverityFilter, ComponentExclusion, FalsePositive | -| `FindingSeverity` | Enum: None, Low, Medium, High, Critical | -| `NoiseLedgerEntry` | Immutable record with digest, finding, category, severity, component, justification, suppressor, timestamps, expiry, evidence | -| `RecordSuppressionRequest` | Request to log a suppression | -| `RecordSuppressionResult` | Result with digest, dedup flag, entry | -| `NoiseLedgerQuery` | Query filter: finding, category, severity, component, suppressor, tenant, active-only, limit | -| `SuppressionStatistics` | Aggregated counts by category, severity, active/expired | - -### Deduplication - -Content-addressed using SHA-256 of canonical form: findingId + category + severity + componentRef + suppressedBy + justification. - -### Service Interface - -`INoiseLedgerService`: -- `RecordAsync(RecordSuppressionRequest)` → `RecordSuppressionResult` — idempotent record -- `GetByDigestAsync(string)` → `NoiseLedgerEntry?` — lookup by digest -- `QueryAsync(NoiseLedgerQuery)` → `ImmutableArray` — filtered query -- `GetStatisticsAsync(string? tenantId)` → `SuppressionStatistics` — aggregated stats - -### DI Registration - -`INoiseLedgerService` -> `NoiseLedgerService` (TryAddSingleton factory) - -### OTel Metrics - -Meter: `StellaOps.Attestor.ProofChain.Audit.NoiseLedger` - -| Counter | Description | -|---|---| -| `noise.suppressions.recorded` | New suppression entries | -| `noise.suppressions.deduplicated` | Deduplicated entries | -| `noise.queries.executed` | Query operations | -| `noise.statistics.computed` | Statistics computations | - -### Source Files - -- Models: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Audit/NoiseLedgerModels.cs` -- Interface: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Audit/INoiseLedgerService.cs` -- Implementation: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Audit/NoiseLedgerService.cs` - -### Test Coverage (34 tests) - -- Basic recording (digest, timestamp, all fields, evidence, correlation) -- Deduplication (same request, different finding/category) -- Validation (null, empty findingId/componentRef/justification/suppressedBy, cancellation) -- GetByDigest (found, not found, null) -- Query by findingId, category, severity, componentRef, active-only -- No-filter returns all, limit enforcement -- Statistics: empty, by category, by severity, active/expired tracking -- IsExpired model method (expired, no expiration) -- Constructor validation -- Determinism (same inputs → same digest) - ---- - -## PostgreSQL Persistence Layer — Schema Isolation, RLS, Temporal Tables - -Sprint: `SPRINT_20260208_018_Attestor_postgresql_persistence_layer` - -### Purpose - -Manages per-module PostgreSQL schema isolation, Row-Level Security (RLS) policy -scaffolding, and temporal table configuration for Attestor persistence modules. -Generates SQL statements for schema provisioning, tenant isolation, and history -tracking without modifying existing `ProofChainDbContext` or entity classes. - -### Schema Registry - -Five schema assignments covering all Attestor persistence modules: - -| Schema | PostgreSQL Name | Tables | -|---|---|---| -| ProofChain | `proofchain` | sbom_entries, dsse_envelopes, spines, trust_anchors, rekor_entries, audit_log | -| Attestor | `attestor` | rekor_submission_queue, submission_state | -| Verdict | `verdict` | verdict_ledger, verdict_policies | -| Watchlist | `watchlist` | watched_identities, identity_alerts, alert_dedup | -| Audit | `audit` | noise_ledger, hash_audit_log, suppression_stats | - -### RLS Policy Coverage - -Tenant isolation policies are defined for schemas that contain tenant-scoped data: - -- **Verdict**: verdict_ledger, verdict_policies -- **Watchlist**: watched_identities, identity_alerts -- **Attestor**: rekor_submission_queue -- **Audit**: noise_ledger -- **ProofChain**: No RLS (shared read-only reference data) - -All policies use `tenant_id` column with `current_setting('app.current_tenant')` expression. - -### Temporal Table Configuration - -Three tables configured for system-versioned history tracking: - -| Table | History Table | Retention | -|---|---|---| -| verdict.verdict_ledger | verdict.verdict_ledger_history | 7 years | -| watchlist.watched_identities | watchlist.watched_identities_history | 1 year | -| audit.noise_ledger | audit.noise_ledger_history | 7 years | - -Temporal tables use PostgreSQL trigger-based versioning with `sys_period_start`/`sys_period_end` period columns. - -### SQL Generation (Not Execution) - -The service generates SQL statements for operators to review and execute: - -- **Provisioning**: `CREATE SCHEMA IF NOT EXISTS`, `GRANT USAGE`, default privileges, documentation comments -- **RLS**: `ENABLE ROW LEVEL SECURITY`, `FORCE ROW LEVEL SECURITY`, `CREATE POLICY` with tenant isolation -- **Temporal**: Period column addition, history table creation, trigger functions, trigger attachment - -### DI Registration - -`PersistenceServiceCollectionExtensions.AddAttestorPersistence()` registers `ISchemaIsolationService` as a singleton with `TimeProvider` and `IMeterFactory`. - -### OTel Metrics - -Meter: `StellaOps.Attestor.Persistence.SchemaIsolation` - -| Counter | Description | -|---|---| -| `schema.provisioning.operations` | Schema provisioning SQL generations | -| `schema.rls.operations` | RLS policy SQL generations | -| `schema.temporal.operations` | Temporal table SQL generations | - -### Source Files - -- Models: `src/Attestor/__Libraries/StellaOps.Attestor.Persistence/SchemaIsolationModels.cs` -- Interface: `src/Attestor/__Libraries/StellaOps.Attestor.Persistence/ISchemaIsolationService.cs` -- Implementation: `src/Attestor/__Libraries/StellaOps.Attestor.Persistence/SchemaIsolationService.cs` -- DI: `src/Attestor/__Libraries/StellaOps.Attestor.Persistence/PersistenceServiceCollectionExtensions.cs` - -### Test Coverage (40 tests) - -- GetAssignment per schema (5 schemas, correct names, table counts) -- Invalid schema throws ArgumentException -- GetAllAssignments returns all five, all have tables -- Provisioning SQL: CREATE SCHEMA, GRANT, default privileges, comment, timestamp, statement count -- RLS policies per schema (Verdict has policies, ProofChain empty, all have tenant_id, UsingExpression) -- RLS SQL: ENABLE/FORCE/CREATE POLICY, permissive mode, empty for ProofChain, multiple for Watchlist -- Temporal tables: count, retention values per table, history table names -- Temporal SQL: period columns, history table, trigger function, trigger, retention comment, statement count -- GetSummary: complete data, ProvisionedCount, RlsEnabledCount, timestamp -- Constructor validation (null TimeProvider fallback, null MeterFactory throws) -- Cross-schema consistency (RLS references valid schemas, temporal references valid schemas) -- Determinism (provisioning, RLS, temporal SQL produce identical output) - ---- - -## S3/MinIO/GCS Object Storage for Tiles - -Sprint: `SPRINT_20260208_019_Attestor_s3_minio_gcs_object_storage_for_tiles` - -### Purpose - -Provides a pluggable object storage abstraction for the Content-Addressed Store (CAS), -enabling durable blob storage via S3-compatible backends (AWS S3, MinIO, Wasabi), Google -Cloud Storage, or local filesystem. The existing `InMemoryContentAddressedStore` is -complemented by `ObjectStorageContentAddressedStore` which delegates to an -`IObjectStorageProvider` for persistence. - -### Architecture - -``` -IContentAddressedStore (existing interface) -├── InMemoryContentAddressedStore (existing, for tests) -└── ObjectStorageContentAddressedStore (new, durable) - └── delegates to IObjectStorageProvider - ├── FileSystemObjectStorageProvider (offline/air-gap) - ├── S3-compatible (AWS/MinIO/Wasabi) — future - └── GCS — future -``` - -### Provider Interface - -`IObjectStorageProvider` defines five low-level operations: -- `PutAsync` — Store a blob by key, idempotent with write-once support -- `GetAsync` — Retrieve blob content and metadata by key -- `ExistsAsync` — Check blob existence -- `DeleteAsync` — Remove a blob (blocked in WORM mode) -- `ListAsync` — List blobs with prefix filtering and pagination - -### Storage Layout - -Content blobs: `blobs/sha256:` — raw content -Metadata sidecars: `meta/sha256:.json` — JSON with artifact type, tags, timestamps - -### Configuration - -`ObjectStorageConfig` selects the backend and connection details: - -| Property | Description | -|---|---| -| `Provider` | `FileSystem`, `S3Compatible`, or `Gcs` | -| `RootPath` | Root directory (FileSystem only) | -| `BucketName` | S3/GCS bucket name | -| `EndpointUrl` | Custom endpoint (MinIO, localstack) | -| `Region` | AWS/GCS region | -| `Prefix` | Key prefix for namespace isolation | -| `EnforceWriteOnce` | WORM mode (prevents deletes and overwrites) | - -### FileSystem Provider - -- Atomic writes via temp file + rename -- Metadata stored as `.meta` sidecar files -- WORM enforcement: skips overwrite, blocks delete -- Offset-based pagination for listing - -### DI Registration - -`IObjectStorageProvider` → `FileSystemObjectStorageProvider` registered via TryAddSingleton -in `ProofChainServiceCollectionExtensions`. Override with S3/GCS provider for cloud deployments. - -### OTel Metrics - -Meter: `StellaOps.Attestor.ProofChain.Cas.FileSystem` - -| Counter | Description | -|---|---| -| `objectstorage.fs.puts` | Filesystem put operations | -| `objectstorage.fs.gets` | Filesystem get operations | -| `objectstorage.fs.deletes` | Filesystem delete operations | - -Meter: `StellaOps.Attestor.ProofChain.Cas.ObjectStorage` - -| Counter | Description | -|---|---| -| `cas.objectstorage.puts` | CAS put via object storage | -| `cas.objectstorage.deduplications` | Deduplicated puts | -| `cas.objectstorage.gets` | CAS get via object storage | -| `cas.objectstorage.deletes` | CAS delete via object storage | - -### Source Files - -- Models: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Cas/ObjectStorageModels.cs` -- Provider interface: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Cas/IObjectStorageProvider.cs` -- Filesystem provider: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Cas/FileSystemObjectStorageProvider.cs` -- CAS bridge: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Cas/ObjectStorageContentAddressedStore.cs` - -### Test Coverage (42 tests) - -**ObjectStorageContentAddressedStore (27 tests):** -- Put: store, dedup, null/empty-media-type throws, tags, related digests, timestamp -- Get: retrieves, missing returns null, null/empty throws -- Exists: true for stored, false for missing -- Delete: removes, false for missing -- List: returns all, filters by type, respects limit -- Statistics: accurate counts, dedup tracking -- Constructor validation (null provider/meterFactory, null timeProvider fallback) -- Determinism: same content → same digest - -**FileSystemObjectStorageProvider (13 tests):** -- Put: store and retrieve, write-once enforcement -- Exists: true/false -- Delete: removes, false for missing, blocked in WORM mode -- List: returns stored, empty directory -- Metadata preservation -- Constructor validation (null config, empty root, null meterFactory) - -**ObjectStorageModels (5 tests):** -- Default values for config, put request, get result, list query -- Provider kind enum count -- Determinism (provisioning, RLS, temporal SQL produce identical output) - ---- - -## Score Replay and Verification - -Sprint: `SPRINT_20260208_020_Attestor_score_replay_and_verification` - -### Purpose - -Enables deterministic replay of verdict scores by re-executing scoring computations -with captured inputs, comparing original and replayed scores to quantify divergence, -and producing DSSE-ready attestations with payload type `application/vnd.stella.score+json`. - -### Architecture - -The score replay service sits alongside the existing AI artifact replay infrastructure -in `ProofChain/Replay/` and provides: - -1. **Score Replay** — Re-executes deterministic scoring from captured inputs (policy weights, - coverage data, severity), computing a replayed score and determinism hash -2. **Score Comparison** — Compares two replay results, quantifying divergence and - identifying specific differences (score, hash, status) -3. **DSSE Attestation** — Produces JSON-encoded attestation payloads ready for - DSSE signing with `application/vnd.stella.score+json` payload type - -### Deterministic Scoring - -- Inputs sorted by key (ordinal) for canonical ordering -- Weighted average of numeric values, normalized to [0, 1] -- Weight inputs identified by key containing "weight" -- Non-numeric inputs silently ignored -- Determinism hash computed from canonical key=value\n format - -### Models - -| Type | Description | -|---|---| -| `ScoreReplayRequest` | Replay request with verdict ID, original score, scoring inputs | -| `ScoreReplayResult` | Result with replay digest, status, replayed/original scores, divergence, determinism hash | -| `ScoreReplayStatus` | Matched, Diverged, FailedMissingInputs, FailedError | -| `ScoreComparisonRequest` | Request to compare two replays by digest | -| `ScoreComparisonResult` | Comparison with divergence, determinism flag, difference details | -| `ScoreReplayAttestation` | DSSE-ready attestation with JSON payload and signing key slot | -| `ScoreReplayQuery` | Query with verdict ID, tenant, status, limit filters | - -### DI Registration - -`IScoreReplayService` → `ScoreReplayService` registered via TryAddSingleton -in `ProofChainServiceCollectionExtensions`. - -### OTel Metrics - -Meter: `StellaOps.Attestor.ProofChain.Replay.Score` - -| Counter | Description | -|---|---| -| `score.replays.executed` | Total replay executions | -| `score.replays.matched` | Replays matching original score | -| `score.replays.diverged` | Replays diverging from original | -| `score.comparisons.executed` | Comparison operations | -| `score.attestations.created` | Attestation productions | - -### Source Files - -- Models: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Replay/ScoreReplayModels.cs` -- Interface: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Replay/IScoreReplayService.cs` -- Implementation: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Replay/ScoreReplayService.cs` - -### Test Coverage (37 tests) - -- ReplayAsync: produces digest, matched/diverged status, duration, determinism hash match/mismatch, null original hash, empty inputs, validation (null request, empty verdictId, cancellation) -- CompareAsync: identical results deterministic, divergent reports differences, null validation -- CreateAttestationAsync: payload type, valid JSON, null signing key, null validation -- GetByDigestAsync: stored result, missing returns null, null throws -- QueryAsync: no filter, verdict ID filter, status filter, limit enforcement, null throws -- ComputeScore: empty inputs, non-numeric ignored, deterministic, clamped [0,1] -- ComputeDeterminismHash: same inputs same hash, different inputs different hash -- Constructor validation (null meterFactory throws, null timeProvider fallback) - ---- - -## VEX Receipt Sidebar - -Converts `VerificationReceipt` domain objects into sidebar-ready DTOs for the UI, -providing a formatted view of DSSE signature verification, Rekor inclusion proofs, -and per-check results. - -### Architecture - -1. **FormatReceipt** — Converts `VerificationReceipt` → `ReceiptSidebarDetail`: - maps `ProofBundleId.Digest` → string, `TrustAnchorId.Value` → string, - iterates checks to build `ReceiptCheckDetail` list, derives overall - `ReceiptVerificationStatus` from pass/fail counts, sets `DsseVerified` and - `RekorInclusionVerified` by scanning check names for DSSE/Rekor keywords -2. **GetDetailAsync** — Looks up registered receipt by bundle ID, returns - `ReceiptSidebarDetail` with optional check and tool digest exclusion -3. **GetContextAsync** — Returns `VexReceiptSidebarContext` combining receipt - detail with VEX decision, justification, evidence refs, and finding metadata; - falls back to receipt-only context when no explicit context is registered - -### Verification Status Derivation - -| Condition | Status | -|---|---| -| No checks present | `Unverified` | -| All checks pass | `Verified` | -| Some pass, some fail | `PartiallyVerified` | -| All checks fail | `Failed` | - -### Models - -| Type | Description | -|---|---| -| `ReceiptVerificationStatus` | Verified, PartiallyVerified, Unverified, Failed | -| `ReceiptCheckDetail` | Single check formatted for sidebar (Name, Passed, KeyId?, LogIndex?, Detail?) | -| `ReceiptSidebarDetail` | Full receipt DTO with computed TotalChecks/PassedChecks/FailedChecks, DsseVerified, RekorInclusionVerified | -| `VexReceiptSidebarContext` | Receipt + Decision + Justification + EvidenceRefs + finding metadata | -| `ReceiptSidebarRequest` | Query by BundleId with IncludeChecks/IncludeToolDigests flags | - -### DI Registration - -`IReceiptSidebarService` → `ReceiptSidebarService` registered via TryAddSingleton -in `ProofChainServiceCollectionExtensions`. - -### OTel Metrics - -Meter: `StellaOps.Attestor.ProofChain.Receipts.Sidebar` - -| Counter | Description | -|---|---| -| `sidebar.detail.total` | Sidebar detail requests | -| `sidebar.context.total` | Sidebar context requests | -| `sidebar.format.total` | Receipts formatted for sidebar | - -### Source Files - -- Models: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Receipts/ReceiptSidebarModels.cs` -- Interface: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Receipts/IReceiptSidebarService.cs` -- Implementation: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Receipts/ReceiptSidebarService.cs` - -### Test Coverage (35 tests) - -- ReceiptVerificationStatus: 4 enum values -- ReceiptCheckDetail: property roundtrips, optional defaults -- ReceiptSidebarDetail: computed check counts, empty checks -- VexReceiptSidebarContext: defaults, full roundtrip -- ReceiptSidebarRequest: defaults -- FormatReceipt: bundle/anchor/version mapping, all-pass/mixed/all-fail/no-checks status, - DSSE verified/not-verified, Rekor verified/absent, check detail mapping, - expected/actual formatting, tool digests mapping, null tool digests, null throws -- GetDetailAsync: unknown returns null, registered returns detail, exclude checks, - exclude tool digests, null throws -- GetContextAsync: unknown returns null, registered context, fallback receipt-only, - null/empty/whitespace throws -- DeriveVerificationStatus: single pass, single fail -- Register: null throws -- RegisterContext: null/empty/whitespace bundleId throws - -## Advisory Commitments (2026-02-26 Batch) - -- `SPRINT_20260226_225_Attestor_signature_trust_and_verdict_api_hardening` governs: - - DSSE signature verifier trust behavior (including deterministic failure reasons). - - authority roster validation for verdict creation. - - authenticated tenant context enforcement over header-only spoofable inputs. - - deterministic verdict retrieval APIs for hash-based lookup. - -- Rekor/tile verification commitments from `Deterministic tile verification with Rekor v2` are coordinated with Symbols sprint `SPRINT_20260226_226_Symbols_dsse_rekor_merkle_and_hash_integrity`. - ---- - -## Trust Domain Model (Sprint 204 -- 2026-03-04) - -### Overview - -As of Sprint 204, the Attestor module directory (`src/Attestor/`) is the trust domain owner for three runtime services and their supporting libraries: - -1. **Attestor** -- transparency log submission, inclusion proof verification, evidence caching -2. **Signer** -- DSSE envelope creation, cryptographic signing (keyless/keyful/HSM), entitlement enforcement -3. **Provenance** -- SLSA/DSSE attestation generation, Merkle tree construction, verification tooling - -Source consolidation places all trust-domain code under a single directory for ownership clarity, while preserving runtime service identities and security boundaries. - -### Trust Data Classification - -| Data Category | Owner Service | Storage | Sensitivity | -|---|---|---|---| -| Attestation evidence (proofchain, inclusion proofs, Rekor entries) | Attestor | `attestor` PostgreSQL schema | High -- tamper-evident, integrity-critical | -| Provenance evidence (SLSA predicates, build attestations, Merkle trees) | Provenance (library) | Consumed by Attestor/EvidenceLocker | High -- deterministic, reproducible | -| Signer metadata (audit events, signing ceremony state, rate limits) | Signer | `signer` PostgreSQL schema | High -- operational security | -| Signer key material (KMS/HSM refs, Fulcio certs, trust anchors, rotation state) | Signer (KeyManagement) | `key_management` PostgreSQL schema | Critical -- cryptographic trust root | - -### PostgreSQL Schema Ownership - -Each trust-domain service retains its own DbContext and dedicated PostgreSQL schema: - -- **`attestor` schema** -- Owned by the Attestor service. Contains `entries`, `dedupe`, `audit` tables for transparency log state. -- **`signer` schema** -- Owned by the Signer service. Contains signing ceremony audit, rate limit state, and operational metadata. -- **`key_management` schema** -- Owned by the Signer KeyManagement library. Contains key rotation records, trust anchor configurations, and HSM/KMS binding metadata. - -There is **no cross-schema merge**. Each service connects with its own connection string scoped to its own schema. - -### Security Boundary: No-Merge Decision (ADR) - -**Decision:** Signer key-material isolation from attestation evidence is a deliberate security boundary. The schemas will NOT be merged into a unified DbContext. - -**Rationale:** -- A merged DbContext would require a single connection string with access to both key material (signing keys, HSM/KMS bindings, trust anchors) and evidence stores (proofchain entries, Rekor logs). -- This widens the blast radius of any credential compromise: an attacker gaining the Attestor database credential would also gain access to key rotation state and trust anchor configurations. -- Schema isolation is a defense-in-depth measure. Each service authenticates to PostgreSQL independently, with schema-level `GRANT` restrictions. -- The Signer's KeyManagement database contains material that, if compromised, could allow forging of signatures. This material must be isolated from the higher-volume, lower-privilege evidence store. - -**Implications:** -- No shared EF Core DbContext across trust services. -- Each service manages its own migrations independently (`src/Attestor/__Libraries/StellaOps.Attestor.Persistence/` for Attestor; `src/Attestor/__Libraries/StellaOps.Signer.KeyManagement/` for Signer key management). -- Cross-service queries (e.g., "find the signing identity for a given attestation entry") use API calls, not database joins. - -### Source Layout (post-Sprint 204) - -``` -src/Attestor/ - StellaOps.Attestation/ # DSSE envelope model library - StellaOps.Attestation.Tests/ - StellaOps.Attestor/ # Attestor service (Core, Infrastructure, WebService, Tests) - StellaOps.Attestor.Envelope/ # Envelope serialization - StellaOps.Attestor.TileProxy/ # Rekor tile proxy - StellaOps.Attestor.Types/ # Shared predicate types - StellaOps.Attestor.Verify/ # Verification pipeline - StellaOps.Signer/ # Signer service (Core, Infrastructure, WebService, Tests) - StellaOps.Provenance.Attestation/ # Provenance attestation library - StellaOps.Provenance.Attestation.Tool/ # Forensic verification CLI tool - __Libraries/ - StellaOps.Attestor.*/ # Attestor domain libraries - StellaOps.Signer.KeyManagement/ # Key rotation and trust anchor management - StellaOps.Signer.Keyless/ # Keyless (Fulcio/Sigstore) signing support - __Tests/ - StellaOps.Attestor.*/ # Attestor test projects - StellaOps.Provenance.Attestation.Tests/ # Provenance test project -``` - -### What Did NOT Change - -- **Namespaces** -- All `StellaOps.Signer.*` and `StellaOps.Provenance.*` namespaces are preserved. -- **Runtime service identities** -- Docker image names (`stellaops/signer`), container names, network aliases, and API base paths (`/api/v1/signer/`) are unchanged. -- **Database schemas** -- No schema changes, no migrations, no data movement. -- **API contracts** -- All endpoints including `/api/v1/signer/sign/dsse` remain stable. + +### Event Flow + +``` +New AttestorEntry persisted + → SignerIdentityDescriptor extracted + → IIdentityMatcher.MatchAsync() + → For each match: + → Check dedup window (default 60 min) + → Emit attestor.identity.matched event + → Route via Notifier rules → Slack/Email/Webhook +``` + +### Event Schema (IdentityAlertEvent) + +```jsonc +{ + "eventId": "uuid", + "eventKind": "attestor.identity.matched", + "tenantId": "tenant-123", + "watchlistEntryId": "uuid", + "watchlistEntryName": "GitHub Actions Signer", + "matchedIdentity": { + "issuer": "https://token.actions.githubusercontent.com", + "subjectAlternativeName": "repo:org/repo:ref:refs/heads/main", + "keyId": null + }, + "rekorEntry": { + "uuid": "24296fb24b8ad77a...", + "logIndex": 123456789, + "artifactSha256": "sha256:abc123...", + "integratedTimeUtc": "2026-01-29T10:30:00Z" + }, + "severity": "warning", + "occurredAtUtc": "2026-01-29T10:30:05Z", + "suppressedCount": 0 +} +``` + +### API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| `POST` | `/api/v1/watchlist` | Create watchlist entry | +| `GET` | `/api/v1/watchlist` | List entries (tenant + optional global) | +| `GET` | `/api/v1/watchlist/{id}` | Get single entry | +| `PUT` | `/api/v1/watchlist/{id}` | Update entry | +| `DELETE` | `/api/v1/watchlist/{id}` | Delete entry | +| `POST` | `/api/v1/watchlist/{id}/test` | Test pattern against sample identity | +| `GET` | `/api/v1/watchlist/alerts` | List recent alerts (paginated) | + +### CLI Commands + +```bash +# Add a watchlist entry +stella watchlist add --issuer "https://token.actions.githubusercontent.com" \ + --san "repo:org/*" --match-mode glob --severity warning + +# List entries +stella watchlist list --include-global + +# Test a pattern +stella watchlist test --issuer "https://..." --san "repo:org/repo:ref:main" + +# View recent alerts +stella watchlist alerts --since 24h --severity warning +``` + +### Metrics + +| Metric | Description | +|--------|-------------| +| `attestor.watchlist.entries_scanned_total` | Entries processed by monitor | +| `attestor.watchlist.matches_total{severity}` | Pattern matches by severity | +| `attestor.watchlist.alerts_emitted_total` | Alerts sent to notification system | +| `attestor.watchlist.alerts_suppressed_total` | Alerts deduplicated | +| `attestor.watchlist.scan_latency_seconds` | Per-entry scan duration | + +### Configuration + +```yaml +attestor: + watchlist: + enabled: true + monitorMode: "changefeed" # changefeed | polling + pollingIntervalSeconds: 5 # only for polling mode + maxEventsPerSecond: 100 # rate limit + defaultDedupWindowMinutes: 60 + regexTimeoutMs: 100 # safety limit + maxWatchlistEntriesPerTenant: 1000 +``` + +### Offline Mode + +In air-gapped environments: +- Polling mode used instead of Postgres NOTIFY +- Alerts queued locally if notification channels unavailable +- Alerts delivered when connectivity restored + +--- + +## Unknowns Five-Dimensional Triage Scoring (P/E/U/C/S) + +> Sprint: SPRINT_20260208_022_Attestor_unknowns_five_dimensional_triage_scoring + +### Overview + +The triage scorer extends the existing `IUnknownsAggregator` pipeline with +a five-dimensional scoring model for unknowns, enabling prioritized triage +and temperature-band classification. + +### Scoring Dimensions + +| Dimension | Code | Range | Description | +|-----------|------|-------|-------------| +| Probability | P | [0,1] | Likelihood of exploitability or relevance | +| Exposure | E | [0,1] | Attack surface exposure (internal → internet-facing) | +| Uncertainty | U | [0,1] | Confidence deficit (fully understood → unknown) | +| Consequence | C | [0,1] | Impact severity (negligible → catastrophic) | +| Signal Freshness | S | [0,1] | Recency of intelligence (stale → just reported) | + +### Composite Score + +Composite = Σ(dimension × weight) / Σ(weights), clamped to [0, 1]. + +Default weights: P=0.30, E=0.25, U=0.20, C=0.15, S=0.10 (configurable via `TriageDimensionWeights`). + +### Temperature Bands + +| Band | Threshold | Action | +|------|-----------|--------| +| **Hot** | ≥ 0.70 | Immediate triage required | +| **Warm** | ≥ 0.40 | Scheduled review | +| **Cold** | < 0.40 | Archive / low priority | + +Thresholds are configurable via `TriageBandThresholds`. + +### Key Types + +- `IUnknownsTriageScorer` — Interface: `Score()`, `ComputeComposite()`, `Classify()` +- `UnknownsTriageScorer` — Implementation with OTel counters +- `TriageScore` — Five-dimensional score vector +- `TriageDimensionWeights` — Configurable weights with static `Default` +- `TriageBandThresholds` — Configurable Hot/Warm thresholds with static `Default` +- `TriageScoredItem` — Scored unknown with composite score and band +- `TriageScoringRequest/Result` — Batch scoring request/response + +### OTel Metrics + +| Metric | Description | +|--------|-------------| +| `triage.scored.total` | Total unknowns scored | +| `triage.band.hot.total` | Unknowns classified as Hot | +| `triage.band.warm.total` | Unknowns classified as Warm | +| `triage.band.cold.total` | Unknowns classified as Cold | + +### DI Registration + +```csharp +services.AddAttestorProofChain(); // registers IUnknownsTriageScorer +``` + +--- + +## VEX Findings API with Proof Artifacts + +> Sprint: SPRINT_20260208_023_Attestor_vex_findings_api_with_proof_artifacts + +### Overview + +The VEX Findings API provides a query and resolution service for VEX findings +(CVE + component combinations) with their associated proof artifacts. Each +finding carries DSSE signatures, Rekor receipts, Merkle proofs, and policy +decision attestations that prove how the VEX status was determined. + +### Key Types + +- `VexFinding` — A finding with `FindingId`, `VulnerabilityId`, `ComponentPurl`, + `Status`, `Justification`, `ProofArtifacts`, `DeterminedAt` +- `ProofArtifact` — Proof material: `Kind` (DsseSignature/RekorReceipt/MerkleProof/ + PolicyDecision/VexDelta/ReachabilityWitness), `Digest`, `Payload`, `ProducedAt` +- `VexFindingStatus` — NotAffected | Affected | Fixed | UnderInvestigation +- `IVexFindingsService` — `GetByIdAsync`, `QueryAsync`, `ResolveProofsAsync`, `UpsertAsync` +- `VexFindingQuery` — Filters: VulnerabilityId, ComponentPurlPrefix, Status, TenantId, Limit, Offset + +### Proof Resolution + +`ResolveProofsAsync()` merges new proof artifacts into a finding, deduplicating +by digest. This allows incremental proof collection as new evidence is produced. + +### Finding IDs + +Finding IDs are deterministic: `SHA-256(vulnId:componentPurl)` prefixed with +`finding:`. This ensures the same CVE + component always maps to the same ID. + +### OTel Metrics + +| Metric | Description | +|--------|-------------| +| `findings.get.total` | Findings retrieved by ID | +| `findings.query.total` | Finding queries executed | +| `findings.upsert.total` | Findings upserted | +| `findings.resolve.total` | Proof resolution requests | +| `findings.proofs.total` | Proof artifacts resolved | + +### DI Registration + +```csharp +services.AddAttestorProofChain(); // registers IVexFindingsService +``` + +--- + +## Binary Fingerprint Store & Trust Scoring + +### Overview + +The Binary Fingerprint Store is a content-addressed repository for section-level +binary hashes (ELF `.text`/`.rodata`, PE sections) with golden-set management +and trust scoring. It enables: + +- **Content-addressed lookup**: Fingerprints identified by `fp:sha256:…` computed + from `(format, architecture, sectionHashes)`. +- **Section-level matching**: Find closest match by comparing individual section + hashes with a similarity score. +- **Golden-set management**: Define named sets of known-good fingerprints for + baseline comparison. +- **Trust scoring**: Multi-factor score (0.0–0.99) based on golden membership, + Build-ID, section coverage, evidence, and package provenance. + +Library: `StellaOps.Attestor.ProofChain` +Namespace: `StellaOps.Attestor.ProofChain.FingerprintStore` + +### Models + +| Type | Purpose | +|------|---------| +| `BinaryFingerprintRecord` | Stored fingerprint: ID, format, architecture, file SHA-256, Build-ID, section hashes, package PURL, golden-set flag, trust score, evidence digests, timestamps. | +| `FingerprintRegistration` | Input for `RegisterAsync`: format, architecture, file hash, section hashes, optional PURL/Build-ID/evidence. | +| `FingerprintLookupResult` | Match result: found flag, matched record, golden match, section similarity (0.0–1.0), matched/differing section lists. | +| `TrustScoreBreakdown` | Decomposed score: golden bonus, Build-ID score, section coverage, evidence score, provenance score. | +| `GoldenSet` | Named golden set with count and timestamps. | +| `FingerprintQuery` | Filters: format, architecture, PURL prefix, golden flag, golden set name, min trust score, limit/offset. | + +### Service Interface (`IBinaryFingerprintStore`) + +| Method | Description | +|--------|-------------| +| `RegisterAsync(registration)` | Register fingerprint (idempotent by content-addressed ID). | +| `GetByIdAsync(fingerprintId)` | Look up by content-addressed ID. | +| `GetByFileSha256Async(fileSha256)` | Look up by whole-file hash. | +| `FindBySectionHashesAsync(sectionHashes, minSimilarity)` | Best-match search by section hashes. | +| `ComputeTrustScoreAsync(fingerprintId)` | Detailed trust-score breakdown. | +| `ListAsync(query)` | Filtered + paginated listing. | +| `AddToGoldenSetAsync(fingerprintId, goldenSetName)` | Mark fingerprint as golden (recalculates trust score). | +| `RemoveFromGoldenSetAsync(fingerprintId)` | Remove golden flag. | +| `CreateGoldenSetAsync(name, description)` | Create a named golden set. | +| `ListGoldenSetsAsync()` | List all golden sets. | +| `GetGoldenSetMembersAsync(goldenSetName)` | List members of a golden set. | +| `DeleteAsync(fingerprintId)` | Remove fingerprint from store. | + +### Trust Score Computation + +| Factor | Weight | Raw value | +|--------|--------|-----------| +| Golden-set membership | 0.30 | 1.0 if golden, 0.0 otherwise | +| Build-ID present | 0.20 | 1.0 if Build-ID exists, 0.0 otherwise | +| Section coverage | 0.25 | Ratio of key sections (`.text`, `.rodata`, `.data`, `.bss`) present | +| Evidence count | 0.15 | `min(count/5, 1.0)` | +| Package provenance | 0.10 | 1.0 if PURL present, 0.0 otherwise | + +Final score is capped at 0.99. + +### DI Registration + +`AddProofChainServices()` registers `IBinaryFingerprintStore → BinaryFingerprintStore` (singleton, via `TryAddSingleton`). + +### Observability (OTel Metrics) + +Meter: `StellaOps.Attestor.ProofChain.FingerprintStore` + +| Metric | Type | Description | +|--------|------|-------------| +| `fingerprint.store.registered` | Counter | Fingerprints registered | +| `fingerprint.store.lookups` | Counter | Store lookups performed | +| `fingerprint.store.golden_added` | Counter | Fingerprints added to golden sets | +| `fingerprint.store.deleted` | Counter | Fingerprints deleted | + +### Test Coverage + +30 tests in `StellaOps.Attestor.ProofChain.Tests/FingerprintStore/BinaryFingerprintStoreTests.cs`: +- Registration (new, idempotent, different sections → different IDs, validation) +- Lookup (by ID, by file SHA-256, not-found cases) +- Section-hash matching (exact, partial, below threshold, empty) +- Trust scoring (with/without Build-ID/PURL, minimal, golden bonus, cap at 0.99, determinism) +- Golden-set management (create, add, remove, list members, list sets) +- List/query with filters (format, min trust score) +- Delete (existing, non-existent) +- Content-addressed ID determinism + +--- + +## Content-Addressed Store (CAS) for SBOM/VEX/Attestation Artifacts + +### Overview + +The CAS provides a unified content-addressed storage service for all artifact types +(SBOM, VEX, attestation, proof bundles, evidence packs, binary fingerprints). +All blobs are keyed by SHA-256 digest of their raw content. Puts are idempotent: +storing the same content twice returns the existing record with a dedup flag. + +Library: `StellaOps.Attestor.ProofChain` +Namespace: `StellaOps.Attestor.ProofChain.Cas` + +### Artifact Types + +| Type | Description | +|------|-------------| +| `Sbom` | Software Bill of Materials | +| `Vex` | VEX (Vulnerability Exploitability Exchange) document | +| `Attestation` | DSSE-signed attestation envelope | +| `ProofBundle` | Proof chain bundle | +| `EvidencePack` | Evidence pack manifest | +| `BinaryFingerprint` | Binary fingerprint record | +| `Other` | Generic/other artifact | + +### Models + +| Type | Purpose | +|------|---------| +| `CasArtifact` | Stored artifact metadata: digest, type, media type, size, tags, related digests, timestamps, dedup flag. | +| `CasPutRequest` | Input: raw content bytes, artifact type, media type, optional tags and related digests. | +| `CasPutResult` | Output: stored artifact + dedup flag. | +| `CasGetResult` | Retrieved artifact with content bytes. | +| `CasQuery` | Filters: artifact type, media type, tag key/value, limit/offset. | +| `CasStatistics` | Store metrics: total artifacts, bytes, dedup count, type breakdown. | + +### Service Interface (`IContentAddressedStore`) + +| Method | Description | +|--------|-------------| +| `PutAsync(request)` | Store artifact (idempotent by SHA-256 digest). Returns dedup flag. | +| `GetAsync(digest)` | Retrieve artifact + content by digest. | +| `ExistsAsync(digest)` | Check existence by digest. | +| `DeleteAsync(digest)` | Remove artifact. | +| `ListAsync(query)` | Filtered + paginated listing. | +| `GetStatisticsAsync()` | Total artifacts, bytes, dedup savings, type breakdown. | + +### Deduplication + +When `PutAsync` receives content whose SHA-256 digest already exists in the store: +1. The existing artifact metadata is returned (no duplicate storage). +2. `CasPutResult.Deduplicated` is set to `true`. +3. An OTel counter is incremented for audit. + +### DI Registration + +`AddProofChainServices()` registers `IContentAddressedStore → InMemoryContentAddressedStore` (singleton, via `TryAddSingleton`). + +### Observability (OTel Metrics) + +Meter: `StellaOps.Attestor.ProofChain.Cas` + +| Metric | Type | Description | +|--------|------|-------------| +| `cas.puts` | Counter | CAS put operations | +| `cas.deduplications` | Counter | Deduplicated puts | +| `cas.gets` | Counter | CAS get operations | +| `cas.deletes` | Counter | CAS delete operations | + +### Test Coverage + +24 tests in `StellaOps.Attestor.ProofChain.Tests/Cas/InMemoryContentAddressedStoreTests.cs`: +- Put (new, dedup, different content, validation, tags, related digests) +- Get (existing, non-existent) +- Exists (stored, not stored) +- Delete (existing, non-existent) +- List with filters (artifact type, media type, tags, pagination) +- Statistics (counts, bytes, dedup tracking) +- Digest determinism + +--- + +## Crypto-Sovereign Design (eIDAS/FIPS/GOST/SM/PQC) + +### Overview + +The crypto-sovereign subsystem bridges the Attestor's role-based `SigningKeyProfile` +(Evidence, Reasoning, VexVerdict, Authority, Generator, Exception) to algorithm-specific +crypto profiles governed by regional compliance constraints. This enables a single +Attestor deployment to enforce eIDAS qualified signatures, FIPS-approved algorithms, +GOST, SM2, or Post-Quantum Cryptography depending on the configured region. + +Library: `StellaOps.Attestor.ProofChain` +Namespace: `StellaOps.Attestor.ProofChain.Signing` + +### Algorithm Profiles + +| Profile | Algorithm ID | Standard | +|---------|-------------|----------| +| `Ed25519` | ED25519 | RFC 8032 | +| `EcdsaP256` | ES256 | NIST FIPS 186-4 | +| `EcdsaP384` | ES384 | NIST FIPS 186-4 | +| `RsaPss` | PS256 | PKCS#1 v2.1 | +| `Gost2012_256` | GOST-R34.10-2012-256 | Russian Federation | +| `Gost2012_512` | GOST-R34.10-2012-512 | Russian Federation | +| `Sm2` | SM2 | Chinese GB/T 32918 | +| `Dilithium3` | DILITHIUM3 | NIST FIPS 204 (ML-DSA) | +| `Falcon512` | FALCON512 | NIST PQC Round 3 | +| `EidasRsaSha256` | eIDAS-RSA-SHA256 | EU eIDAS + CAdES | +| `EidasEcdsaSha256` | eIDAS-ECDSA-SHA256 | EU eIDAS + CAdES | + +### Sovereign Regions + +| Region | Default Algorithm | Requirements | +|--------|------------------|--------------| +| `International` | Ed25519 | None | +| `EuEidas` | eIDAS-RSA-SHA256 | Qualified timestamp (Article 42), CAdES-T minimum | +| `UsFips` | ECDSA-P256 | HSM-backed keys | +| `RuGost` | GOST-2012-256 | GOST algorithms only | +| `CnSm` | SM2 | SM national standards only | +| `PostQuantum` | Dilithium3 | PQC finalist algorithms only | + +### Service Interface (`ICryptoProfileResolver`) + +| Method | Description | +|--------|-------------| +| `ResolveAsync(keyProfile)` | Resolve key profile using active region. | +| `ResolveAsync(keyProfile, region)` | Resolve key profile with explicit region override. | +| `ActiveRegion` | Get the configured sovereign region. | +| `GetPolicy(region)` | Get the sovereign policy for a region. | +| `ValidateQualifiedTimestampAsync(...)` | eIDAS Article 42 timestamp validation. | + +### Resolution Flow + +1. `SigningKeyProfile` (role: Evidence/Reasoning/etc.) arrives at `ICryptoProfileResolver` +2. Active `CryptoSovereignRegion` determines the `CryptoSovereignPolicy` +3. Policy's `DefaultAlgorithm` produces a `CryptoProfileBinding` +4. Binding carries: algorithm ID, region, CAdES level, HSM/timestamp requirements +5. Caller (or composition root) uses binding to resolve key material from `ICryptoProviderRegistry` + +### eIDAS Article 42 Qualified Timestamp Validation + +`ValidateQualifiedTimestampAsync` performs structural validation of RFC 3161 timestamp tokens: +- Non-eIDAS regions return `IsQualified = false` immediately +- Empty tokens or signed data are rejected +- ASN.1 SEQUENCE tag (0x30) is verified as structural check +- Full TSA certificate chain and EU Trusted List validation deferred to eIDAS plugin integration + +### CAdES Levels + +| Level | Description | +|-------|-------------| +| `CadesB` | Basic Electronic Signature | +| `CadesT` | With Timestamp (Article 42 minimum) | +| `CadesLT` | With Long-Term validation data | +| `CadesLTA` | With Long-Term Archival validation data | + +### DI Registration + +`AddProofChainServices()` registers `ICryptoProfileResolver → DefaultCryptoProfileResolver` (singleton, via `TryAddSingleton`). +The Attestor Infrastructure layer can pre-register a registry-aware implementation +that bridges `ICryptoProviderRegistry` before this fallback applies. + +### Observability (OTel Metrics) + +Meter: `StellaOps.Attestor.ProofChain.CryptoSovereign` + +| Metric | Type | Description | +|--------|------|-------------| +| `crypto_sovereign.resolves` | Counter | Profile resolution operations (tagged by region) | +| `crypto_sovereign.timestamp_validations` | Counter | Qualified timestamp validations | + +### Test Coverage + +27 tests in `StellaOps.Attestor.ProofChain.Tests/Signing/DefaultCryptoProfileResolverTests.cs`: +- Region-based resolution (International/eIDAS/FIPS/GOST/SM/PQC default algorithms) +- Explicit region override +- All key profiles resolve for all regions +- Active region property +- Policy access and validation (all regions, eIDAS timestamp requirement, FIPS HSM requirement) +- Algorithm ID mapping (all 11 profiles) +- Qualified timestamp validation (non-eIDAS, empty token, empty data, invalid ASN.1, valid structure) +- Cancellation handling +- Determinism (same inputs → identical bindings) +- Policy consistency (default in allowed list, non-empty allowed lists) + +--- + +## DSSE Envelope Size Management (Guardrails, Chunking, Gateway Awareness) + +### Overview + +Pre-submission size guard for DSSE envelopes submitted to Rekor transparency logs. +Validates envelope size against a configurable policy and determines the submission mode: +full envelope (under soft limit), hash-only fallback, chunked with manifest, or rejected. + +Library: `StellaOps.Attestor.ProofChain` +Namespace: `StellaOps.Attestor.ProofChain.Rekor` + +### Submission Modes + +| Mode | Trigger | Behavior | +|------|---------|----------| +| `FullEnvelope` | Size ≤ soft limit | Envelope submitted to Rekor as-is | +| `HashOnly` | Soft limit < size ≤ hard limit, hash-only enabled | Only SHA-256 payload digest submitted | +| `Chunked` | Soft limit < size ≤ hard limit, chunking enabled | Envelope split into chunks with manifest | +| `Rejected` | Size > hard limit, or no fallback available | Submission blocked | + +### Size Policy (`DsseEnvelopeSizePolicy`) + +| Property | Default | Description | +|----------|---------|-------------| +| `SoftLimitBytes` | 102,400 (100 KB) | Threshold for hash-only/chunked fallback | +| `HardLimitBytes` | 1,048,576 (1 MB) | Absolute rejection threshold | +| `ChunkSizeBytes` | 65,536 (64 KB) | Maximum size per chunk | +| `EnableHashOnlyFallback` | `true` | Allow hash-only submission for oversized envelopes | +| `EnableChunking` | `false` | Allow chunked submission (takes priority over hash-only) | +| `HashAlgorithm` | "SHA-256" | Hash algorithm for digest computation | + +### Service Interface (`IDsseEnvelopeSizeGuard`) + +| Method | Description | +|--------|-------------| +| `ValidateAsync(DsseEnvelope)` | Validate a typed DSSE envelope against size policy | +| `ValidateAsync(ReadOnlyMemory)` | Validate raw serialized envelope bytes | +| `Policy` | Get the active size policy | + +### Chunk Manifest + +When chunking is enabled and an envelope exceeds the soft limit, the guard produces +an `EnvelopeChunkManifest` containing: +- `TotalSizeBytes`: original envelope size +- `ChunkCount`: number of chunks +- `OriginalDigest`: SHA-256 digest of the complete original envelope +- `Chunks`: ordered array of `ChunkDescriptor` (index, size, digest, offset) + +Each chunk is content-addressed by its SHA-256 digest for integrity verification. + +### DI Registration + +`AddProofChainServices()` registers `IDsseEnvelopeSizeGuard → DsseEnvelopeSizeGuard` +(singleton, via `TryAddSingleton`). Default policy uses 100 KB soft / 1 MB hard limits. + +### Observability (OTel Metrics) + +Meter: `StellaOps.Attestor.ProofChain.EnvelopeSize` + +| Metric | Type | Description | +|--------|------|-------------| +| `envelope_size.validations` | Counter | Total envelope size validations | +| `envelope_size.hash_only_fallbacks` | Counter | Hash-only fallback activations | +| `envelope_size.chunked` | Counter | Chunked submission activations | +| `envelope_size.rejections` | Counter | Envelope rejections | + +### Test Coverage + +28 tests in `StellaOps.Attestor.ProofChain.Tests/Rekor/DsseEnvelopeSizeGuardTests.cs`: +- Full envelope (small, exact soft limit) +- Hash-only fallback (activation, digest determinism) +- Chunked mode (activation, correct chunk count, priority over hash-only) +- Hard limit rejection +- Both fallbacks disabled rejection +- Raw bytes validation (under limit, empty rejection) +- Policy validation (negative soft, hard < soft, zero chunk size, defaults) +- Cancellation handling +- Digest determinism (same/different input) +- Chunk manifest determinism +- Size tracking + +--- + +## DSSE-Wrapped Reach-Maps + +### Purpose + +Reach-maps are standalone in-toto attestation artifacts that capture the full reachability graph for a scanned artifact. Unlike micro-witnesses (which capture individual vulnerability reachability paths), a reach-map aggregates the entire graph — all nodes, edges, findings, and analysis metadata — into a single DSSE-wrapped statement that can be stored, transmitted, and verified independently. + +### Predicate Type + +URI: `reach-map.stella/v1` + +The reach-map predicate follows **Pattern B** (predicate model in `Predicates/`, statement delegates `PredicateType`). + +### Data Model + +#### ReachMapPredicate + +Top-level predicate record containing: + +| Field | Type | Description | +|---|---|---| +| `SchemaVersion` | string | Always "1.0.0" | +| `GraphDigest` | string | Deterministic SHA-256 digest of sorted graph content | +| `GraphCasUri` | string? | Optional CAS URI for externalized graph storage | +| `ScanId` | string | Identifier of the originating scan | +| `ArtifactRef` | string | Package URL or image reference of the scanned artifact | +| `Nodes` | ImmutableArray\ | All nodes in the reachability graph | +| `Edges` | ImmutableArray\ | All edges (call relationships) | +| `Findings` | ImmutableArray\ | Vulnerability findings with reachability status | +| `AggregatedWitnessIds` | ImmutableArray\ | Deduplicated witness IDs from findings + explicit additions | +| `Analysis` | ReachMapAnalysis | Analyzer metadata (tool, version, confidence, completeness) | +| `Summary` | ReachMapSummary | Computed statistics (counts of nodes, edges, entry points, sinks) | + +#### ReachMapNode + +| Field | Type | Description | +|---|---|---| +| `NodeId` | string | Unique identifier for the node | +| `QualifiedName` | string | Fully qualified name (e.g., class.method) | +| `Module` | string | Module or assembly containing the node | +| `IsEntryPoint` | bool | Whether this node is a graph entry point | +| `IsSink` | bool | Whether this node is a vulnerability sink | +| `ReachabilityState` | string | One of the 8-state lattice values | + +#### ReachMapEdge + +| Field | Type | Description | +|---|---|---| +| `SourceNodeId` | string | Origin node of the call edge | +| `TargetNodeId` | string | Destination node of the call edge | +| `CallType` | string | Edge type (direct, virtual, reflection, etc.) | +| `Confidence` | double | Edge confidence score (0.0–1.0), default 1.0 | + +#### ReachMapFinding + +| Field | Type | Description | +|---|---|---| +| `VulnId` | string | Vulnerability identifier | +| `CveId` | string? | Optional CVE identifier | +| `Purl` | string? | Optional package URL | +| `IsReachable` | bool | Whether the vulnerability is reachable | +| `ConfidenceScore` | double | Reachability confidence (0.0–1.0) | +| `SinkNodeIds` | ImmutableArray\ | Nodes where the vulnerability manifests | +| `ReachableEntryPointIds` | ImmutableArray\ | Entry points that can reach sinks | +| `WitnessId` | string? | Optional micro-witness identifier | + +### ReachMapBuilder (Fluent API) + +`ReachMapBuilder` provides a fluent interface for constructing reach-map predicates: + +```csharp +var predicate = new ReachMapBuilder() + .WithScanId("scan-001") + .WithArtifactRef("pkg:docker/myapp@sha256:abc123") + .WithAnalyzer("stella-reach", "2.0.0", 0.95, "full") + .WithGeneratedAt(DateTimeOffset.UtcNow) + .AddNodes(nodes) + .AddEdges(edges) + .AddFindings(findings) + .Build(); +``` + +#### Deterministic Graph Digest + +The builder computes a deterministic SHA-256 digest over the graph content: + +1. Nodes are sorted by `NodeId`, each contributing `NodeId|QualifiedName|ReachabilityState` +2. Edges are sorted by `SourceNodeId` then `TargetNodeId`, each contributing `Source→Target|CallType` +3. Findings are sorted by `VulnId`, each contributing `VulnId|IsReachable|ConfidenceScore` +4. All contributions are concatenated with newlines and hashed + +This ensures identical graphs always produce the same digest regardless of insertion order. + +#### Witness Aggregation + +Witness IDs are collected from two sources: +- `WitnessId` fields on individual `ReachMapFinding` records +- Explicit `AddWitnessId()` calls on the builder + +All witness IDs are deduplicated in the final predicate. + +### Schema Validation + +The reach-map predicate type is registered in `PredicateSchemaValidator`: +- `HasSchema("reach-map.stella/v1")` → `true` +- `ValidateByPredicateType` routes to `ValidateReachMapPredicate` +- Required JSON properties: `graph_digest`, `scan_id`, `artifact_ref`, `nodes`, `edges`, `analysis`, `summary` + +### Statement Integration + +`ReachMapStatement` extends `InTotoStatement` with: +- `PredicateType` → `"reach-map.stella/v1"` (from `ReachMapPredicate.PredicateTypeUri`) +- `Type` → `"https://in-toto.io/Statement/v1"` (inherited) + +### Source Files + +- Predicate: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Predicates/ReachMapPredicate.cs` +- Statement: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Statements/ReachMapStatement.cs` +- Builder: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Rekor/ReachMapBuilder.cs` +- Validator: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Json/PredicateSchemaValidator.DeltaValidators.cs` + +### Test Coverage (25 tests) + +- Build validation (missing ScanId, ArtifactRef, Analyzer) +- Minimal build, full build with summary statistics +- Graph digest determinism (same input, different order, different content) +- Witness aggregation (from findings, explicit, deduplication) +- Bulk add operations (AddNodes, AddEdges, AddFindings) +- CAS URI inclusion +- Statement integration (predicate type, statement type) +- Null argument protection (5 tests) + +--- + +## Evidence Coverage Score for AI Gating + +### Purpose + +The Evidence Coverage Scorer provides a deterministic, multi-dimensional assessment of how thoroughly an artifact's evidence base covers the key verification axes. This score directly gates AI auto-processing decisions: AI-generated artifacts (explanations, remediation plans, VEX drafts, policy drafts) can only be promoted to verdicts when evidence coverage meets a configurable threshold. + +### Evidence Dimensions + +The scorer evaluates five independent dimensions: + +| Dimension | Default Weight | Description | +|---|---|---| +| **Reachability** | 0.25 | Call graph analysis, micro-witnesses, reach-maps | +| **BinaryAnalysis** | 0.20 | Binary fingerprints, build-id verification, section hashes | +| **SbomCompleteness** | 0.25 | Component inventory, dependency resolution completeness | +| **VexCoverage** | 0.20 | Vulnerability status decisions (affected/not_affected/fixed) | +| **Provenance** | 0.10 | Build provenance, source attestation, supply chain evidence | + +### Scoring Algorithm + +1. For each dimension, the scorer receives a list of evidence identifiers +2. Each identifier is checked against an **evidence resolver** (`Func`) — the same pattern used by `AIAuthorityClassifier` +3. Dimension score = (resolvable count) / (total count), producing a 0.0–1.0 value +4. Overall score = weighted average across all dimensions (normalized by total weight) +5. Missing dimensions receive a score of 0.0 + +### Coverage Levels (Badge Rendering) + +| Level | Threshold | Meaning | +|---|---|---| +| **Green** | >= 80% (configurable) | Full evidence coverage, auto-processing eligible | +| **Yellow** | >= 50% (configurable) | Partial coverage, manual review recommended | +| **Red** | < 50% | Insufficient evidence, gating blocks promotion | + +### AI Gating Policy + +The `EvidenceCoveragePolicy` record controls: +- Per-dimension weights (must be non-negative) +- AI gating threshold (default 0.80) — minimum overall score for auto-processing +- Green/yellow badge thresholds + +When `MeetsAiGatingThreshold` is `false`, the `AIAuthorityClassifier`'s `CanAutoProcess` path should be blocked. + +### DI Registration + +Registered via `ProofChainServiceCollectionExtensions.AddProofChainServices()`: +- `IEvidenceCoverageScorer` -> `EvidenceCoverageScorer` (TryAddSingleton) +- Default evidence resolver returns `false` (no evidence resolvable) — Infrastructure layer overrides with a persistence-backed resolver + +### OTel Metrics + +Meter: `StellaOps.Attestor.ProofChain.EvidenceCoverage` + +| Counter | Description | +|---|---| +| `coverage.evaluations` | Total coverage evaluations performed | +| `coverage.gating.pass` | Evaluations that met AI gating threshold | +| `coverage.gating.fail` | Evaluations that failed AI gating threshold | + +### Source Files + +- Models: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Predicates/AI/EvidenceCoverageModels.cs` +- Interface: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Predicates/AI/IEvidenceCoverageScorer.cs` +- Implementation: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Predicates/AI/EvidenceCoverageScorer.cs` +- DI: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/ProofChainServiceCollectionExtensions.cs` + +### Test Coverage (24 tests) + +- Full coverage (all dimensions resolvable, Green level) +- No evidence (empty inputs, Red, zero score) +- Partial coverage (weighted score calculation) +- Per-dimension breakdown (counts, reasons) +- Missing dimensions (zero score) +- Gating threshold (at threshold, below threshold) +- Custom thresholds (coverage level boundaries) +- Policy validation (negative weight, invalid threshold, green < yellow) +- Null argument protection (policy, resolver, meter factory, subject ref, inputs, result) +- Cancellation handling +- Determinism (same inputs produce same results) +- Default policy values +- Reason text verification + +--- + +## Evidence Subgraph UI Visualization + +### Purpose + +The Subgraph Visualization Service renders proof graph subgraphs into multiple visualization formats suitable for interactive frontend rendering. It bridges the existing `IProofGraphService.GetArtifactSubgraphAsync()` BFS traversal with UI-ready output in Mermaid, Graphviz DOT, and structured JSON formats. + +### Render Formats + +| Format | Use Case | Output | +|---|---|---| +| **Mermaid** | Browser-side rendering via Mermaid.js | `graph TD` markup with class definitions | +| **Dot** | Static/server-side rendering via Graphviz | `digraph` markup with color/shape attributes | +| **Json** | Custom frontend rendering (D3.js, Cytoscape.js) | Structured `{nodes, edges}` JSON | + +### Visualization Models + +#### VisualizationNode + +| Field | Type | Description | +|---|---|---| +| `Id` | string | Unique node identifier | +| `Label` | string | Formatted display label (type + truncated digest) | +| `Type` | string | Node type string for icon/color selection | +| `ContentDigest` | string? | Full content digest for provenance verification | +| `IsRoot` | bool | Whether this is the subgraph root | +| `Depth` | int | BFS depth from root (for layout layering) | +| `Metadata` | ImmutableDictionary? | Optional key-value pairs for tooltips | + +#### VisualizationEdge + +| Field | Type | Description | +|---|---|---| +| `Source` | string | Source node ID | +| `Target` | string | Target node ID | +| `Label` | string | Human-readable edge type label | +| `Type` | string | Edge type string for styling | + +### Depth Computation + +The service computes BFS depth from the root node bidirectionally through all edges, enabling hierarchical layout rendering. Unreachable nodes receive the maximum depth value. + +### Node Type Styling + +| Node Type | Mermaid Shape | DOT Color | +|---|---|---| +| Artifact / Subject | `[box]` | #4CAF50 (green) | +| SbomDocument | `([stadium])` | #2196F3 (blue) | +| InTotoStatement / DsseEnvelope | `[[subroutine]]` | #FF9800 (orange) | +| VexStatement | `([stadium])` | #9C27B0 (purple) | +| RekorEntry | `[(cylinder)]` | #795548 (brown) | +| SigningKey / TrustAnchor | `((circle))` | #607D8B (blue-grey) | + +### DI Registration + +`ISubgraphVisualizationService` -> `SubgraphVisualizationService` (TryAddSingleton) + +### Source Files + +- Models: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Graph/SubgraphVisualizationModels.cs` +- Interface: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Graph/ISubgraphVisualizationService.cs` +- Implementation: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Graph/SubgraphVisualizationService.cs` + +### Test Coverage (22 tests) + +- Empty subgraph rendering +- Single node with root detection and depth +- Multi-node depth computation (root=0, child=1, grandchild=2) +- Mermaid format (graph directive, node/edge content, class definitions) +- DOT format (digraph directive, node colors) +- JSON format (valid JSON output) +- Edge type labels (5 inline data tests) +- Node type preservation (4 inline data tests) +- Content digest truncation in labels +- Cancellation handling +- Null argument protection +- Determinism (same input produces same output) +- All three formats produce non-empty content (3 inline data tests) + +--- + +## Field-Level Ownership Map for Receipts and Bundles + +### Purpose + +The Field-Level Ownership Map provides a machine-readable and human-readable document that maps each field in `VerificationReceipt` and `VerificationCheck` to the responsible module. This enables automated validation that fields are populated by their designated owner module, supporting audit trails and cross-module accountability. + +### Owner Modules + +| Module | Responsibility | +|---|---| +| **Core** | Fundamental identifiers, timestamps, versions, tool digests | +| **Signing** | Key identifiers and signature-related fields | +| **Rekor** | Transparency log indices and inclusion proofs | +| **Verification** | Trust anchors, verification results, check details | +| **SbomVex** | SBOM/VEX document references | +| **Provenance** | Provenance and build attestation fields | +| **Policy** | Policy evaluation results | +| **External** | Fields populated by external integrations | + +### Ownership Map Structure + +The `FieldOwnershipMap` record contains: +- `DocumentType` — the document being mapped (e.g., "VerificationReceipt") +- `SchemaVersion` — version of the ownership schema (default "1.0") +- `Entries` — immutable list of `FieldOwnershipEntry` records + +Each `FieldOwnershipEntry` declares: +- `FieldPath` — dot-path or array-path (e.g., `proofBundleId`, `checks[].keyId`) +- `Owner` — the `OwnerModule` responsible for populating the field +- `IsRequired` — whether the field must be populated for validity +- `Description` — human-readable purpose of the field + +### Default Receipt Ownership Map (14 entries) + +| Field Path | Owner | Required | +|---|---|---| +| `proofBundleId` | Core | Yes | +| `verifiedAt` | Core | Yes | +| `verifierVersion` | Core | Yes | +| `anchorId` | Verification | Yes | +| `result` | Verification | Yes | +| `checks` | Verification | Yes | +| `checks[].check` | Verification | Yes | +| `checks[].status` | Verification | Yes | +| `checks[].keyId` | Signing | No | +| `checks[].logIndex` | Rekor | No | +| `checks[].expected` | Verification | No | +| `checks[].actual` | Verification | No | +| `checks[].details` | Verification | No | +| `toolDigests` | Core | No | + +### Validation + +`ValidateReceiptOwnershipAsync` checks a `VerificationReceipt` against the ownership map: +1. Iterates top-level fields, recording population status +2. Expands per-check fields for each `VerificationCheck` entry +3. Counts missing required fields +4. Returns `FieldOwnershipValidationResult` with computed properties: + - `IsValid` — true when `MissingRequiredCount == 0` + - `TotalFields` — total field population records + - `PopulatedCount` — fields that have values + - `ValidCount` — fields with valid ownership + +### DI Registration + +`IFieldOwnershipValidator` -> `FieldOwnershipValidator` (TryAddSingleton) + +### Source Files + +- Models: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Receipts/FieldOwnershipModels.cs` +- Interface: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Receipts/IFieldOwnershipValidator.cs` +- Implementation: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Receipts/FieldOwnershipValidator.cs` + +### Test Coverage (24 tests) + +- Ownership map structure (document type, entry count, top-level fields, check fields) +- Owner assignment theories (7 top-level + 4 check-level field-to-owner mappings) +- Description completeness (all entries have descriptions) +- Full receipt validation (valid, all populated, correct counts) +- Minimal receipt validation (valid, optional fields not populated) +- Empty checks validation (missing required → invalid) +- Multi-check field expansion (fields per check entry) +- Ownership validity (all fields valid in static map) +- ValidatedAt propagation +- Null receipt protection +- Cancellation token handling +- Determinism (same inputs produce same results) +- Static map required/optional field markers +- Computed property correctness + +--- + +## Idempotent SBOM/Attestation APIs + +### Purpose + +The Idempotent Ingest Service provides content-hash-based deduplication for SBOM ingest and attestation verification operations. Duplicate submissions return the original result without creating duplicate records, ensuring safe retries and deterministic outcomes. + +### Architecture + +The service builds on the existing `IContentAddressedStore` (CAS), which already provides SHA-256-based deduplication at the storage layer. The idempotent service adds: + +1. **SBOM Ingest** — wraps CAS `PutAsync` with SBOM-specific metadata (media type, tags, artifact type) and returns a typed `SbomEntryId` +2. **Attestation Verify** — stores attestation in CAS, performs verification checks, and caches results by content hash in a `ConcurrentDictionary` +3. **Idempotency Key Support** — optional client-provided keys that map to content digests, enabling safe retries even when content bytes differ + +### Idempotency Guarantees + +| Scenario | Behavior | +|---|---| +| Same content, no key | CAS deduplicates by SHA-256 hash, returns `Deduplicated = true` | +| Same content, same key | Returns cached result via key lookup | +| Different content, same key | Returns original result mapped to the key | +| Same content, different key | Both keys map to the same digest | + +### Verification Checks + +The baseline attestation verification performs three deterministic checks: + +| Check | Description | +|---|---| +| `content_present` | Content is non-empty | +| `digest_format` | Valid SHA-256 digest format (71 chars) | +| `json_structure` | Content starts with `{` and ends with `}` | + +Infrastructure layer may override with full DSSE/Rekor verification. + +### Models + +| Type | Description | +|---|---| +| `SbomIngestRequest` | Content, MediaType, Tags, optional IdempotencyKey | +| `SbomIngestResult` | Digest, Deduplicated, Artifact, SbomEntryId | +| `AttestationVerifyRequest` | Content, MediaType, optional IdempotencyKey | +| `AttestationVerifyResult` | Digest, CacheHit, Verified, Summary, Checks, VerifiedAt | +| `AttestationCheckResult` | Check, Passed, Details | +| `IdempotencyKeyEntry` | Key, Digest, CreatedAt, OperationType | + +### DI Registration + +`IIdempotentIngestService` -> `IdempotentIngestService` (TryAddSingleton factory) +- Resolves: `IContentAddressedStore`, optional `TimeProvider`, `IMeterFactory` + +### OTel Metrics + +Meter: `StellaOps.Attestor.ProofChain.Idempotency` + +| Counter | Description | +|---|---| +| `idempotent.sbom.ingests` | Total SBOM ingest operations | +| `idempotent.sbom.deduplications` | SBOM submissions that were deduplicated | +| `idempotent.attest.verifications` | Total attestation verifications (non-cached) | +| `idempotent.attest.cache_hits` | Attestation verifications served from cache | +| `idempotent.key.hits` | Idempotency key lookups that found existing entries | + +### Source Files + +- Models: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Idempotency/IdempotentIngestModels.cs` +- Interface: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Idempotency/IIdempotentIngestService.cs` +- Implementation: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Idempotency/IdempotentIngestService.cs` + +### Test Coverage (30 tests) + +- SBOM ingest: first submission, duplicate dedup, different content, tags, idempotency key retry, empty content, empty media type, null request, cancellation, artifact type +- Attestation verify: first submission, duplicate cache hit, JSON structure pass/fail, content check, digest check, idempotency key, null request, empty content, cancellation, determinism, summary text +- Idempotency key lookup: unknown key, after ingest, after verify, null key +- Constructor validation: null store, null meter factory, null time provider + +--- + +## Regulatory Compliance Report Generator (NIS2/DORA/ISO-27001/EU CRA) + +### Purpose + +The Compliance Report Generator provides a static registry of regulatory controls and maps evidence artifacts to regulatory requirements. It generates compliance reports that identify which controls are satisfied by available evidence and which have gaps, enabling auditable regulatory alignment for release decisions. + +### Supported Frameworks + +| Framework | Controls | Description | +|---|---|---| +| **NIS2** | 5 | EU Network and Information Security Directive 2 | +| **DORA** | 5 | EU Digital Operational Resilience Act | +| **ISO-27001** | 6 | ISO/IEC 27001 Information Security Management | +| **EU CRA** | 4 | EU Cyber Resilience Act | + +### Evidence Artifact Types + +| Type | Description | +|---|---| +| `Sbom` | Software Bill of Materials | +| `VexStatement` | Vulnerability Exploitability Exchange statement | +| `SignedAttestation` | Signed attestation envelope | +| `TransparencyLogEntry` | Rekor transparency log entry | +| `VerificationReceipt` | Proof of verification | +| `ProofBundle` | Bundled evidence pack | +| `ReachabilityAnalysis` | Binary fingerprint or reachability analysis | +| `PolicyEvaluation` | Policy evaluation result | +| `ProvenanceAttestation` | Build origin proof | +| `IncidentReport` | Incident response documentation | + +### Control Registry (20 controls) + +#### NIS2 Controls +| ID | Category | Satisfied By | +|---|---|---| +| NIS2-Art21.2d | Supply Chain Security | SBOM, VEX, Provenance | +| NIS2-Art21.2e | Supply Chain Security | VEX, Reachability | +| NIS2-Art21.2a | Risk Management | Policy, Attestation | +| NIS2-Art21.2g | Risk Management | Receipt, ProofBundle | +| NIS2-Art23 | Incident Management | Incident, Transparency | + +#### DORA Controls +| ID | Category | Satisfied By | +|---|---|---| +| DORA-Art6.1 | ICT Risk Management | Policy, Attestation | +| DORA-Art9.1 | ICT Risk Management | Attestation, Receipt, ProofBundle | +| DORA-Art17 | Incident Classification | Incident, VEX | +| DORA-Art28 | Third-Party Risk | SBOM, Provenance, Reachability | +| DORA-Art11 | ICT Risk Management (optional) | ProofBundle, Transparency | + +#### ISO-27001 Controls +| ID | Category | Satisfied By | +|---|---|---| +| A.8.28 | Application Security | SBOM, Reachability, Provenance | +| A.8.9 | Configuration Management | Policy, Attestation | +| A.8.8 | Vulnerability Management | VEX, Reachability, SBOM | +| A.5.23 | Cloud Security (optional) | Provenance, ProofBundle | +| A.5.37 | Operations Security | Receipt, Transparency | +| A.5.21 | Supply Chain Security | SBOM, VEX, Provenance | + +#### EU CRA Controls +| ID | Category | Satisfied By | +|---|---|---| +| CRA-AnnexI.2.1 | Product Security | SBOM | +| CRA-AnnexI.2.5 | Vulnerability Management | VEX, Reachability | +| CRA-Art11 | Vulnerability Management | VEX, Incident, Transparency | +| CRA-AnnexI.1.2 | Product Security | Policy, Attestation, Receipt | + +### Report Structure + +`ComplianceReport` computed properties: +- `CompliancePercentage` — ratio of satisfied to total controls +- `MandatoryGapCount` — mandatory controls not satisfied +- `MeetsMinimumCompliance` — true when all mandatory controls satisfied + +### DI Registration + +`IComplianceReportGenerator` -> `ComplianceReportGenerator` (TryAddSingleton factory) + +### OTel Metrics + +Meter: `StellaOps.Attestor.ProofChain.Compliance` + +| Counter | Description | +|---|---| +| `compliance.reports.generated` | Total compliance reports generated | +| `compliance.controls.evaluated` | Total individual controls evaluated | + +### Source Files + +- Models: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Compliance/RegulatoryComplianceModels.cs` +- Interface: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Compliance/IComplianceReportGenerator.cs` +- Implementation: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Compliance/ComplianceReportGenerator.cs` + +### Test Coverage (26 tests) + +- Supported frameworks (count and membership) +- Control counts per framework (4 theories) +- Control ID presence per framework (4 theories) +- Framework assignment and required field validation +- Full evidence → 100% compliance (4 theories) +- No evidence → 0% compliance (4 theories) +- Partial evidence → partial compliance +- Subject ref and framework recording +- Generated timestamp +- Artifact ref tracing +- Gap descriptions (present for unsatisfied, absent for satisfied) +- Null subject/evidence protection +- Cancellation token +- Determinism +- Constructor validation +- Mandatory vs optional controls +- NIS2 control categories (5 theories) + + +--- + +## In-toto Link Attestation Capture (Sprint 015) + +The **LinkCapture** subsystem provides in-toto link attestation capture for supply chain step recording. It captures materials (inputs) and products (outputs) with content-addressed deduplication, enabling CI pipeline step evidence collection. + +### Domain Model + +| Record | Purpose | +|---|---| +| `CapturedMaterial` | Input artifact (URI + digest map) | +| `CapturedProduct` | Output artifact (URI + digest map) | +| `CapturedEnvironment` | Execution context (hostname, OS, variables) | +| `LinkCaptureRequest` | Capture request with step, functionary, command, materials, products, env, byproducts, pipeline/step IDs | +| `LinkCaptureResult` | Result with content-addressed digest, dedup flag, stored record | +| `CapturedLinkRecord` | Stored link with all fields + CapturedAt timestamp | +| `LinkCaptureQuery` | Query filter: step name, functionary, pipeline ID, limit | + +### Deduplication + +Content-addressed deduplication uses canonical hashing: +- Canonical form: step name + functionary + command + sorted materials + sorted products +- Environment and byproducts are **excluded** from the digest to ensure deterministic deduplication across different execution contexts +- SHA-256 digest with `sha256:` prefix +- Materials and products sorted by URI (ordinal) before hashing + +### Service Interface + +`ILinkCaptureService`: +- `CaptureAsync(LinkCaptureRequest)` → `LinkCaptureResult` — idempotent capture +- `GetByDigestAsync(string digest)` → `CapturedLinkRecord?` — lookup by content digest +- `QueryAsync(LinkCaptureQuery)` → `ImmutableArray` — filtered query (case-insensitive, ordered by descending timestamp) + +### DI Registration + +`ILinkCaptureService` -> `LinkCaptureService` (TryAddSingleton factory) + +### OTel Metrics + +Meter: `StellaOps.Attestor.ProofChain.LinkCapture` + +| Counter | Description | +|---|---| +| `link.captures` | Total new link attestations captured | +| `link.deduplications` | Total deduplicated captures | +| `link.queries` | Total query operations | + +### Source Files + +- Models: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/LinkCapture/LinkCaptureModels.cs` +- Interface: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/LinkCapture/ILinkCaptureService.cs` +- Implementation: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/LinkCapture/LinkCaptureService.cs` + +### Test Coverage (30 tests) + +- Basic capture with digest, step, functionary verification +- Timestamp from TimeProvider +- Materials and products recording +- Environment and byproducts recording +- Pipeline/step ID recording +- Deduplication (same request returns deduplicated=true) +- Different step/functionary/materials produce different digests +- Deterministic digest (material order invariance) +- Environment excluded from digest +- Null/empty validation (request, step, functionary) +- Cancellation token handling +- GetByDigest (found, not found, null, cancelled) +- Query by step name, functionary, pipeline ID +- Case-insensitive query filtering +- Empty store query +- No-filter returns all +- Limit enforcement +- Descending timestamp ordering +- Constructor validation +--- + +## Monthly Bundle Rotation and Re-Signing (Sprint 016) + +The **BundleRotation** subsystem provides scheduled key rotation for DSSE-signed bundles. It verifies bundles with the old key, re-signs them with a new key, and records a transition attestation for audit trail. + +### Domain Model + +| Record | Purpose | +|---|---| +| `RotationStatus` | Enum: Pending, Verified, ReSigned, Completed, Failed, Skipped | +| `RotationCadence` | Enum: Monthly, Quarterly, OnDemand | +| `KeyTransition` | Old/new key IDs, algorithm, effective date, grace period | +| `BundleRotationRequest` | Rotation cycle request with transition, bundle digests, cadence, tenant | +| `BundleRotationEntry` | Per-bundle result (original/new digest, status, error) | +| `BundleRotationResult` | Full cycle result with computed SuccessCount/FailureCount/SkippedCount | +| `TransitionAttestation` | Audit record: attestation ID, rotation ID, result digest, counts | +| `RotationScheduleEntry` | Schedule config: cadence, next/last rotation, current key, enabled | +| `RotationHistoryQuery` | Query filter: tenant, key ID, status, limit | + +### Re-Signing Workflow + +1. Validate request (rotation ID, key IDs, bundle digests) +2. Verify old key and new key exist in `IProofChainKeyStore` +3. For each bundle: verify with old key → compute re-signed digest → record entry +4. Determine overall status from individual entries +5. Create `TransitionAttestation` with result digest for integrity verification +6. Store in rotation history + +### Service Interface + +`IBundleRotationService`: +- `RotateAsync(BundleRotationRequest)` → `BundleRotationResult` — execute rotation cycle +- `GetTransitionAttestationAsync(string rotationId)` → `TransitionAttestation?` — get audit attestation +- `QueryHistoryAsync(RotationHistoryQuery)` → `ImmutableArray` — query history +- `ComputeNextRotationDate(RotationCadence, DateTimeOffset?)` → `DateTimeOffset` — schedule computation + +### DI Registration + +`IBundleRotationService` -> `BundleRotationService` (TryAddSingleton factory, requires `IProofChainKeyStore`) + +### OTel Metrics + +Meter: `StellaOps.Attestor.ProofChain.Signing.Rotation` + +| Counter | Description | +|---|---| +| `rotation.cycles.started` | Total rotation cycles initiated | +| `rotation.cycles.completed` | Total rotation cycles completed | +| `rotation.bundles.resigned` | Total bundles successfully re-signed | +| `rotation.bundles.skipped` | Total bundles skipped | +| `rotation.bundles.failed` | Total bundles that failed rotation | + +### Source Files + +- Models: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Signing/BundleRotationModels.cs` +- Interface: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Signing/IBundleRotationService.cs` +- Implementation: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Signing/BundleRotationService.cs` + +### Test Coverage (35 tests) + +- Basic rotation (completed result, success count, new digests, transition, timestamps) +- Key validation (old key missing, new key missing → all fail) +- Empty bundle digest → entry fails +- Argument validation (null request, empty rotation ID, empty bundles, empty key IDs, cancellation) +- Transition attestation (created after rotation, has result digest, records transition, not found for unknown, null/cancel) +- Query history (empty, after rotation, filter by key ID, filter by status, limit, null/cancel) +- Schedule computation (monthly +1 month, quarterly +3 months, on-demand immediate, null last uses current time) +- Determinism (same inputs → same re-signed digests) +- Constructor validation (null key store, null meter factory, null time provider OK) + +--- + +## Noise Ledger — Audit Log of Suppressions (Sprint 017) + +The **NoiseLedger** subsystem provides an auditable, queryable log of all suppression decisions in the attestation pipeline. It records VEX overrides, alert deduplications, policy-based suppressions, operator acknowledgments, and false positive determinations. + +### Domain Model + +| Type | Purpose | +|---|---| +| `SuppressionCategory` | Enum: VexOverride, AlertDedup, PolicyRule, OperatorAck, SeverityFilter, ComponentExclusion, FalsePositive | +| `FindingSeverity` | Enum: None, Low, Medium, High, Critical | +| `NoiseLedgerEntry` | Immutable record with digest, finding, category, severity, component, justification, suppressor, timestamps, expiry, evidence | +| `RecordSuppressionRequest` | Request to log a suppression | +| `RecordSuppressionResult` | Result with digest, dedup flag, entry | +| `NoiseLedgerQuery` | Query filter: finding, category, severity, component, suppressor, tenant, active-only, limit | +| `SuppressionStatistics` | Aggregated counts by category, severity, active/expired | + +### Deduplication + +Content-addressed using SHA-256 of canonical form: findingId + category + severity + componentRef + suppressedBy + justification. + +### Service Interface + +`INoiseLedgerService`: +- `RecordAsync(RecordSuppressionRequest)` → `RecordSuppressionResult` — idempotent record +- `GetByDigestAsync(string)` → `NoiseLedgerEntry?` — lookup by digest +- `QueryAsync(NoiseLedgerQuery)` → `ImmutableArray` — filtered query +- `GetStatisticsAsync(string? tenantId)` → `SuppressionStatistics` — aggregated stats + +### DI Registration + +`INoiseLedgerService` -> `NoiseLedgerService` (TryAddSingleton factory) + +### OTel Metrics + +Meter: `StellaOps.Attestor.ProofChain.Audit.NoiseLedger` + +| Counter | Description | +|---|---| +| `noise.suppressions.recorded` | New suppression entries | +| `noise.suppressions.deduplicated` | Deduplicated entries | +| `noise.queries.executed` | Query operations | +| `noise.statistics.computed` | Statistics computations | + +### Source Files + +- Models: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Audit/NoiseLedgerModels.cs` +- Interface: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Audit/INoiseLedgerService.cs` +- Implementation: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Audit/NoiseLedgerService.cs` + +### Test Coverage (34 tests) + +- Basic recording (digest, timestamp, all fields, evidence, correlation) +- Deduplication (same request, different finding/category) +- Validation (null, empty findingId/componentRef/justification/suppressedBy, cancellation) +- GetByDigest (found, not found, null) +- Query by findingId, category, severity, componentRef, active-only +- No-filter returns all, limit enforcement +- Statistics: empty, by category, by severity, active/expired tracking +- IsExpired model method (expired, no expiration) +- Constructor validation +- Determinism (same inputs → same digest) + +--- + +## PostgreSQL Persistence Layer — Schema Isolation, RLS, Temporal Tables + +Sprint: `SPRINT_20260208_018_Attestor_postgresql_persistence_layer` + +### Purpose + +Manages per-module PostgreSQL schema isolation, Row-Level Security (RLS) policy +scaffolding, and temporal table configuration for Attestor persistence modules. +Generates SQL statements for schema provisioning, tenant isolation, and history +tracking without modifying existing `ProofChainDbContext` or entity classes. + +### Schema Registry + +Five schema assignments covering all Attestor persistence modules: + +| Schema | PostgreSQL Name | Tables | +|---|---|---| +| ProofChain | `proofchain` | sbom_entries, dsse_envelopes, spines, trust_anchors, rekor_entries, audit_log | +| Attestor | `attestor` | rekor_submission_queue, submission_state | +| Verdict | `verdict` | verdict_ledger, verdict_policies | +| Watchlist | `watchlist` | watched_identities, identity_alerts, alert_dedup | +| Audit | `audit` | noise_ledger, hash_audit_log, suppression_stats | + +### RLS Policy Coverage + +Tenant isolation policies are defined for schemas that contain tenant-scoped data: + +- **Verdict**: verdict_ledger, verdict_policies +- **Watchlist**: watched_identities, identity_alerts +- **Attestor**: rekor_submission_queue +- **Audit**: noise_ledger +- **ProofChain**: No RLS (shared read-only reference data) + +All policies use `tenant_id` column with `current_setting('app.current_tenant')` expression. + +### Temporal Table Configuration + +Three tables configured for system-versioned history tracking: + +| Table | History Table | Retention | +|---|---|---| +| verdict.verdict_ledger | verdict.verdict_ledger_history | 7 years | +| watchlist.watched_identities | watchlist.watched_identities_history | 1 year | +| audit.noise_ledger | audit.noise_ledger_history | 7 years | + +Temporal tables use PostgreSQL trigger-based versioning with `sys_period_start`/`sys_period_end` period columns. + +### SQL Generation (Not Execution) + +The service generates SQL statements for operators to review and execute: + +- **Provisioning**: `CREATE SCHEMA IF NOT EXISTS`, `GRANT USAGE`, default privileges, documentation comments +- **RLS**: `ENABLE ROW LEVEL SECURITY`, `FORCE ROW LEVEL SECURITY`, `CREATE POLICY` with tenant isolation +- **Temporal**: Period column addition, history table creation, trigger functions, trigger attachment + +### DI Registration + +`PersistenceServiceCollectionExtensions.AddAttestorPersistence()` registers `ISchemaIsolationService` as a singleton with `TimeProvider` and `IMeterFactory`. + +### OTel Metrics + +Meter: `StellaOps.Attestor.Persistence.SchemaIsolation` + +| Counter | Description | +|---|---| +| `schema.provisioning.operations` | Schema provisioning SQL generations | +| `schema.rls.operations` | RLS policy SQL generations | +| `schema.temporal.operations` | Temporal table SQL generations | + +### Source Files + +- Models: `src/Attestor/__Libraries/StellaOps.Attestor.Persistence/SchemaIsolationModels.cs` +- Interface: `src/Attestor/__Libraries/StellaOps.Attestor.Persistence/ISchemaIsolationService.cs` +- Implementation: `src/Attestor/__Libraries/StellaOps.Attestor.Persistence/SchemaIsolationService.cs` +- DI: `src/Attestor/__Libraries/StellaOps.Attestor.Persistence/PersistenceServiceCollectionExtensions.cs` + +### Test Coverage (40 tests) + +- GetAssignment per schema (5 schemas, correct names, table counts) +- Invalid schema throws ArgumentException +- GetAllAssignments returns all five, all have tables +- Provisioning SQL: CREATE SCHEMA, GRANT, default privileges, comment, timestamp, statement count +- RLS policies per schema (Verdict has policies, ProofChain empty, all have tenant_id, UsingExpression) +- RLS SQL: ENABLE/FORCE/CREATE POLICY, permissive mode, empty for ProofChain, multiple for Watchlist +- Temporal tables: count, retention values per table, history table names +- Temporal SQL: period columns, history table, trigger function, trigger, retention comment, statement count +- GetSummary: complete data, ProvisionedCount, RlsEnabledCount, timestamp +- Constructor validation (null TimeProvider fallback, null MeterFactory throws) +- Cross-schema consistency (RLS references valid schemas, temporal references valid schemas) +- Determinism (provisioning, RLS, temporal SQL produce identical output) + +--- + +## S3/MinIO/GCS Object Storage for Tiles + +Sprint: `SPRINT_20260208_019_Attestor_s3_minio_gcs_object_storage_for_tiles` + +### Purpose + +Provides a pluggable object storage abstraction for the Content-Addressed Store (CAS), +enabling durable blob storage via S3-compatible backends (AWS S3, MinIO, Wasabi), Google +Cloud Storage, or local filesystem. The existing `InMemoryContentAddressedStore` is +complemented by `ObjectStorageContentAddressedStore` which delegates to an +`IObjectStorageProvider` for persistence. + +### Architecture + +``` +IContentAddressedStore (existing interface) +├── InMemoryContentAddressedStore (existing, for tests) +└── ObjectStorageContentAddressedStore (new, durable) + └── delegates to IObjectStorageProvider + ├── FileSystemObjectStorageProvider (offline/air-gap) + ├── S3-compatible (AWS/MinIO/Wasabi) — future + └── GCS — future +``` + +### Provider Interface + +`IObjectStorageProvider` defines five low-level operations: +- `PutAsync` — Store a blob by key, idempotent with write-once support +- `GetAsync` — Retrieve blob content and metadata by key +- `ExistsAsync` — Check blob existence +- `DeleteAsync` — Remove a blob (blocked in WORM mode) +- `ListAsync` — List blobs with prefix filtering and pagination + +### Storage Layout + +Content blobs: `blobs/sha256:` — raw content +Metadata sidecars: `meta/sha256:.json` — JSON with artifact type, tags, timestamps + +### Configuration + +`ObjectStorageConfig` selects the backend and connection details: + +| Property | Description | +|---|---| +| `Provider` | `FileSystem`, `S3Compatible`, or `Gcs` | +| `RootPath` | Root directory (FileSystem only) | +| `BucketName` | S3/GCS bucket name | +| `EndpointUrl` | Custom endpoint (MinIO, localstack) | +| `Region` | AWS/GCS region | +| `Prefix` | Key prefix for namespace isolation | +| `EnforceWriteOnce` | WORM mode (prevents deletes and overwrites) | + +### FileSystem Provider + +- Atomic writes via temp file + rename +- Metadata stored as `.meta` sidecar files +- WORM enforcement: skips overwrite, blocks delete +- Offset-based pagination for listing + +### DI Registration + +`IObjectStorageProvider` → `FileSystemObjectStorageProvider` registered via TryAddSingleton +in `ProofChainServiceCollectionExtensions`. Override with S3/GCS provider for cloud deployments. + +### OTel Metrics + +Meter: `StellaOps.Attestor.ProofChain.Cas.FileSystem` + +| Counter | Description | +|---|---| +| `objectstorage.fs.puts` | Filesystem put operations | +| `objectstorage.fs.gets` | Filesystem get operations | +| `objectstorage.fs.deletes` | Filesystem delete operations | + +Meter: `StellaOps.Attestor.ProofChain.Cas.ObjectStorage` + +| Counter | Description | +|---|---| +| `cas.objectstorage.puts` | CAS put via object storage | +| `cas.objectstorage.deduplications` | Deduplicated puts | +| `cas.objectstorage.gets` | CAS get via object storage | +| `cas.objectstorage.deletes` | CAS delete via object storage | + +### Source Files + +- Models: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Cas/ObjectStorageModels.cs` +- Provider interface: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Cas/IObjectStorageProvider.cs` +- Filesystem provider: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Cas/FileSystemObjectStorageProvider.cs` +- CAS bridge: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Cas/ObjectStorageContentAddressedStore.cs` + +### Test Coverage (42 tests) + +**ObjectStorageContentAddressedStore (27 tests):** +- Put: store, dedup, null/empty-media-type throws, tags, related digests, timestamp +- Get: retrieves, missing returns null, null/empty throws +- Exists: true for stored, false for missing +- Delete: removes, false for missing +- List: returns all, filters by type, respects limit +- Statistics: accurate counts, dedup tracking +- Constructor validation (null provider/meterFactory, null timeProvider fallback) +- Determinism: same content → same digest + +**FileSystemObjectStorageProvider (13 tests):** +- Put: store and retrieve, write-once enforcement +- Exists: true/false +- Delete: removes, false for missing, blocked in WORM mode +- List: returns stored, empty directory +- Metadata preservation +- Constructor validation (null config, empty root, null meterFactory) + +**ObjectStorageModels (5 tests):** +- Default values for config, put request, get result, list query +- Provider kind enum count +- Determinism (provisioning, RLS, temporal SQL produce identical output) + +--- + +## Score Replay and Verification + +Sprint: `SPRINT_20260208_020_Attestor_score_replay_and_verification` + +### Purpose + +Enables deterministic replay of verdict scores by re-executing scoring computations +with captured inputs, comparing original and replayed scores to quantify divergence, +and producing DSSE-ready attestations with payload type `application/vnd.stella.score+json`. + +### Architecture + +The score replay service sits alongside the existing AI artifact replay infrastructure +in `ProofChain/Replay/` and provides: + +1. **Score Replay** — Re-executes deterministic scoring from captured inputs (policy weights, + coverage data, severity), computing a replayed score and determinism hash +2. **Score Comparison** — Compares two replay results, quantifying divergence and + identifying specific differences (score, hash, status) +3. **DSSE Attestation** — Produces JSON-encoded attestation payloads ready for + DSSE signing with `application/vnd.stella.score+json` payload type + +### Deterministic Scoring + +- Inputs sorted by key (ordinal) for canonical ordering +- Weighted average of numeric values, normalized to [0, 1] +- Weight inputs identified by key containing "weight" +- Non-numeric inputs silently ignored +- Determinism hash computed from canonical key=value\n format + +### Models + +| Type | Description | +|---|---| +| `ScoreReplayRequest` | Replay request with verdict ID, original score, scoring inputs | +| `ScoreReplayResult` | Result with replay digest, status, replayed/original scores, divergence, determinism hash | +| `ScoreReplayStatus` | Matched, Diverged, FailedMissingInputs, FailedError | +| `ScoreComparisonRequest` | Request to compare two replays by digest | +| `ScoreComparisonResult` | Comparison with divergence, determinism flag, difference details | +| `ScoreReplayAttestation` | DSSE-ready attestation with JSON payload and signing key slot | +| `ScoreReplayQuery` | Query with verdict ID, tenant, status, limit filters | + +### DI Registration + +`IScoreReplayService` → `ScoreReplayService` registered via TryAddSingleton +in `ProofChainServiceCollectionExtensions`. + +### OTel Metrics + +Meter: `StellaOps.Attestor.ProofChain.Replay.Score` + +| Counter | Description | +|---|---| +| `score.replays.executed` | Total replay executions | +| `score.replays.matched` | Replays matching original score | +| `score.replays.diverged` | Replays diverging from original | +| `score.comparisons.executed` | Comparison operations | +| `score.attestations.created` | Attestation productions | + +### Source Files + +- Models: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Replay/ScoreReplayModels.cs` +- Interface: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Replay/IScoreReplayService.cs` +- Implementation: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Replay/ScoreReplayService.cs` + +### Test Coverage (37 tests) + +- ReplayAsync: produces digest, matched/diverged status, duration, determinism hash match/mismatch, null original hash, empty inputs, validation (null request, empty verdictId, cancellation) +- CompareAsync: identical results deterministic, divergent reports differences, null validation +- CreateAttestationAsync: payload type, valid JSON, null signing key, null validation +- GetByDigestAsync: stored result, missing returns null, null throws +- QueryAsync: no filter, verdict ID filter, status filter, limit enforcement, null throws +- ComputeScore: empty inputs, non-numeric ignored, deterministic, clamped [0,1] +- ComputeDeterminismHash: same inputs same hash, different inputs different hash +- Constructor validation (null meterFactory throws, null timeProvider fallback) + +--- + +## VEX Receipt Sidebar + +Converts `VerificationReceipt` domain objects into sidebar-ready DTOs for the UI, +providing a formatted view of DSSE signature verification, Rekor inclusion proofs, +and per-check results. + +### Architecture + +1. **FormatReceipt** — Converts `VerificationReceipt` → `ReceiptSidebarDetail`: + maps `ProofBundleId.Digest` → string, `TrustAnchorId.Value` → string, + iterates checks to build `ReceiptCheckDetail` list, derives overall + `ReceiptVerificationStatus` from pass/fail counts, sets `DsseVerified` and + `RekorInclusionVerified` by scanning check names for DSSE/Rekor keywords +2. **GetDetailAsync** — Looks up registered receipt by bundle ID, returns + `ReceiptSidebarDetail` with optional check and tool digest exclusion +3. **GetContextAsync** — Returns `VexReceiptSidebarContext` combining receipt + detail with VEX decision, justification, evidence refs, and finding metadata; + falls back to receipt-only context when no explicit context is registered + +### Verification Status Derivation + +| Condition | Status | +|---|---| +| No checks present | `Unverified` | +| All checks pass | `Verified` | +| Some pass, some fail | `PartiallyVerified` | +| All checks fail | `Failed` | + +### Models + +| Type | Description | +|---|---| +| `ReceiptVerificationStatus` | Verified, PartiallyVerified, Unverified, Failed | +| `ReceiptCheckDetail` | Single check formatted for sidebar (Name, Passed, KeyId?, LogIndex?, Detail?) | +| `ReceiptSidebarDetail` | Full receipt DTO with computed TotalChecks/PassedChecks/FailedChecks, DsseVerified, RekorInclusionVerified | +| `VexReceiptSidebarContext` | Receipt + Decision + Justification + EvidenceRefs + finding metadata | +| `ReceiptSidebarRequest` | Query by BundleId with IncludeChecks/IncludeToolDigests flags | + +### DI Registration + +`IReceiptSidebarService` → `ReceiptSidebarService` registered via TryAddSingleton +in `ProofChainServiceCollectionExtensions`. + +### OTel Metrics + +Meter: `StellaOps.Attestor.ProofChain.Receipts.Sidebar` + +| Counter | Description | +|---|---| +| `sidebar.detail.total` | Sidebar detail requests | +| `sidebar.context.total` | Sidebar context requests | +| `sidebar.format.total` | Receipts formatted for sidebar | + +### Source Files + +- Models: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Receipts/ReceiptSidebarModels.cs` +- Interface: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Receipts/IReceiptSidebarService.cs` +- Implementation: `src/Attestor/__Libraries/StellaOps.Attestor.ProofChain/Receipts/ReceiptSidebarService.cs` + +### Test Coverage (35 tests) + +- ReceiptVerificationStatus: 4 enum values +- ReceiptCheckDetail: property roundtrips, optional defaults +- ReceiptSidebarDetail: computed check counts, empty checks +- VexReceiptSidebarContext: defaults, full roundtrip +- ReceiptSidebarRequest: defaults +- FormatReceipt: bundle/anchor/version mapping, all-pass/mixed/all-fail/no-checks status, + DSSE verified/not-verified, Rekor verified/absent, check detail mapping, + expected/actual formatting, tool digests mapping, null tool digests, null throws +- GetDetailAsync: unknown returns null, registered returns detail, exclude checks, + exclude tool digests, null throws +- GetContextAsync: unknown returns null, registered context, fallback receipt-only, + null/empty/whitespace throws +- DeriveVerificationStatus: single pass, single fail +- Register: null throws +- RegisterContext: null/empty/whitespace bundleId throws + +## Advisory Commitments (2026-02-26 Batch) + +- `SPRINT_20260226_225_Attestor_signature_trust_and_verdict_api_hardening` governs: + - DSSE signature verifier trust behavior (including deterministic failure reasons). + - authority roster validation for verdict creation. + - authenticated tenant context enforcement over header-only spoofable inputs. + - deterministic verdict retrieval APIs for hash-based lookup. + +- Rekor/tile verification commitments from `Deterministic tile verification with Rekor v2` are coordinated with Symbols sprint `SPRINT_20260226_226_Symbols_dsse_rekor_merkle_and_hash_integrity`. + +--- + +## Trust Domain Model (Sprint 204 -- 2026-03-04) + +### Overview + +As of Sprint 204, the Attestor module directory (`src/Attestor/`) is the trust domain owner for three runtime services and their supporting libraries: + +1. **Attestor** -- transparency log submission, inclusion proof verification, evidence caching +2. **Signer** -- DSSE envelope creation, cryptographic signing (keyless/keyful/HSM), entitlement enforcement +3. **Provenance** -- SLSA/DSSE attestation generation, Merkle tree construction, verification tooling + +Source consolidation places all trust-domain code under a single directory for ownership clarity, while preserving runtime service identities and security boundaries. + +### Trust Data Classification + +| Data Category | Owner Service | Storage | Sensitivity | +|---|---|---|---| +| Attestation evidence (proofchain, inclusion proofs, Rekor entries) | Attestor | `attestor` PostgreSQL schema | High -- tamper-evident, integrity-critical | +| Provenance evidence (SLSA predicates, build attestations, Merkle trees) | Provenance (library) | Consumed by Attestor/EvidenceLocker | High -- deterministic, reproducible | +| Signer metadata (audit events, signing ceremony state, rate limits) | Signer | `signer` PostgreSQL schema | High -- operational security | +| Signer key material (KMS/HSM refs, Fulcio certs, trust anchors, rotation state) | Signer (KeyManagement) | `key_management` PostgreSQL schema | Critical -- cryptographic trust root | + +### PostgreSQL Schema Ownership + +Each trust-domain service retains its own DbContext and dedicated PostgreSQL schema: + +- **`attestor` schema** -- Owned by the Attestor service. Contains `entries`, `dedupe`, `audit` tables for transparency log state. +- **`signer` schema** -- Owned by the Signer service. Contains signing ceremony audit, rate limit state, and operational metadata. +- **`key_management` schema** -- Owned by the Signer KeyManagement library. Contains key rotation records, trust anchor configurations, and HSM/KMS binding metadata. + +There is **no cross-schema merge**. Each service connects with its own connection string scoped to its own schema. + +### Security Boundary: No-Merge Decision (ADR) + +**Decision:** Signer key-material isolation from attestation evidence is a deliberate security boundary. The schemas will NOT be merged into a unified DbContext. + +**Rationale:** +- A merged DbContext would require a single connection string with access to both key material (signing keys, HSM/KMS bindings, trust anchors) and evidence stores (proofchain entries, Rekor logs). +- This widens the blast radius of any credential compromise: an attacker gaining the Attestor database credential would also gain access to key rotation state and trust anchor configurations. +- Schema isolation is a defense-in-depth measure. Each service authenticates to PostgreSQL independently, with schema-level `GRANT` restrictions. +- The Signer's KeyManagement database contains material that, if compromised, could allow forging of signatures. This material must be isolated from the higher-volume, lower-privilege evidence store. + +**Implications:** +- No shared EF Core DbContext across trust services. +- Each service manages its own migrations independently (`src/Attestor/__Libraries/StellaOps.Attestor.Persistence/` for Attestor; `src/Attestor/__Libraries/StellaOps.Signer.KeyManagement/` for Signer key management). +- Cross-service queries (e.g., "find the signing identity for a given attestation entry") use API calls, not database joins. + +### Source Layout (post-Sprint 204) + +``` +src/Attestor/ + StellaOps.Attestation/ # DSSE envelope model library + StellaOps.Attestation.Tests/ + StellaOps.Attestor/ # Attestor service (Core, Infrastructure, WebService, Tests) + StellaOps.Attestor.Envelope/ # Envelope serialization + StellaOps.Attestor.TileProxy/ # Rekor tile proxy + StellaOps.Attestor.Types/ # Shared predicate types + StellaOps.Attestor.Verify/ # Verification pipeline + StellaOps.Signer/ # Signer service (Core, Infrastructure, WebService, Tests) + StellaOps.Provenance.Attestation/ # Provenance attestation library + StellaOps.Provenance.Attestation.Tool/ # Forensic verification CLI tool + __Libraries/ + StellaOps.Attestor.*/ # Attestor domain libraries + StellaOps.Signer.KeyManagement/ # Key rotation and trust anchor management + StellaOps.Signer.Keyless/ # Keyless (Fulcio/Sigstore) signing support + __Tests/ + StellaOps.Attestor.*/ # Attestor test projects + StellaOps.Provenance.Attestation.Tests/ # Provenance test project +``` + +### What Did NOT Change + +- **Namespaces** -- All `StellaOps.Signer.*` and `StellaOps.Provenance.*` namespaces are preserved. +- **Runtime service identities** -- Docker image names (`stellaops/signer`), container names, network aliases, and API base paths (`/api/v1/signer/`) are unchanged. +- **Database schemas** -- No schema changes, no migrations, no data movement. +- **API contracts** -- All endpoints including `/api/v1/signer/sign/dsse` remain stable. diff --git a/docs/modules/authority/architecture.md b/docs/modules/authority/architecture.md index 1f5553ad8..33fec468b 100644 --- a/docs/modules/authority/architecture.md +++ b/docs/modules/authority/architecture.md @@ -1,4 +1,4 @@ -# component_architecture_authority.md — **Stella Ops Authority** (2025Q4) +# component_architecture_authority.md — **Stella Ops Authority** (2025Q4) > **Current tenant-selection ADR:** `docs/architecture/decisions/ADR-002-multi-tenant-same-api-key-selection.md` > **Service impact ledger:** `docs/technical/architecture/multi-tenant-service-impact-ledger.md` @@ -8,18 +8,18 @@ > Consolidates identity and tenancy requirements documented across the AOC, Policy, and Platform guides, along with the dedicated Authority implementation plan. -> **Scope.** Implementation‑ready architecture for **Stella Ops Authority**: the on‑prem **OIDC/OAuth2** service that issues **short‑lived, sender‑constrained operational tokens (OpToks)** to first‑party services and tools. Covers protocols (DPoP & mTLS binding), token shapes, endpoints, storage, rotation, HA, RBAC, audit, and testing. This component is the trust anchor for *who* is calling inside a Stella Ops installation. (Entitlement is proven separately by **PoE** from the cloud Licensing Service; Authority does not issue PoE.) +> **Scope.** Implementation‑ready architecture for **Stella Ops Authority**: the on‑prem **OIDC/OAuth2** service that issues **short‑lived, sender‑constrained operational tokens (OpToks)** to first‑party services and tools. Covers protocols (DPoP & mTLS binding), token shapes, endpoints, storage, rotation, HA, RBAC, audit, and testing. This component is the trust anchor for *who* is calling inside a Stella Ops installation. (Entitlement is proven separately by **PoE** from the cloud Licensing Service; Authority does not issue PoE.) --- ## 0) Mission & boundaries -**Mission.** Provide **fast, local, verifiable** authentication for Stella Ops microservices and tools by minting **very short‑lived** OAuth2/OIDC tokens that are **sender‑constrained** (DPoP or mTLS‑bound). Support RBAC scopes, multi‑tenant claims, and deterministic validation for APIs (Scanner, Signer, Attestor, Excititor, Concelier, UI, CLI, Zastava). +**Mission.** Provide **fast, local, verifiable** authentication for Stella Ops microservices and tools by minting **very short‑lived** OAuth2/OIDC tokens that are **sender‑constrained** (DPoP or mTLS‑bound). Support RBAC scopes, multi‑tenant claims, and deterministic validation for APIs (Scanner, Signer, Attestor, Excititor, Concelier, UI, CLI, Zastava). **Boundaries.** -* Authority **does not** validate entitlements/licensing. That’s enforced by **Signer** using **PoE** with the cloud Licensing Service. -* Authority tokens are **operational only** (2–5 min TTL) and must not be embedded in long‑lived artifacts or stored in SBOMs. +* Authority **does not** validate entitlements/licensing. That’s enforced by **Signer** using **PoE** with the cloud Licensing Service. +* Authority tokens are **operational only** (2–5 min TTL) and must not be embedded in long‑lived artifacts or stored in SBOMs. * Authority is **stateless for validation** (JWT) and **optional introspection** for services that prefer online checks. --- @@ -29,16 +29,16 @@ * **OIDC Discovery**: `/.well-known/openid-configuration` * **OAuth2** grant types: - * **Client Credentials** (service↔service, with mTLS or private_key_jwt) + * **Client Credentials** (service↔service, with mTLS or private_key_jwt) * **Device Code** (CLI login on headless agents; optional) * **Authorization Code + PKCE** (browser login for UI; optional) * **Sender constraint options** (choose per caller or per audience): - * **DPoP** (Demonstration of Proof‑of‑Possession): proof JWT on each HTTP request, bound to the access token via `cnf.jkt`. - * **OAuth 2.0 mTLS** (certificate‑bound tokens): token bound to client certificate thumbprint via `cnf.x5t#S256`. -* **Signing algorithms**: **EdDSA (Ed25519)** preferred; fallback **ES256 (P‑256)**. Rotation is supported via **kid** in JWKS. + * **DPoP** (Demonstration of Proof‑of‑Possession): proof JWT on each HTTP request, bound to the access token via `cnf.jkt`. + * **OAuth 2.0 mTLS** (certificate‑bound tokens): token bound to client certificate thumbprint via `cnf.x5t#S256`. +* **Signing algorithms**: **EdDSA (Ed25519)** preferred; fallback **ES256 (P‑256)**. Rotation is supported via **kid** in JWKS. * **Token format**: **JWT** access tokens (compact), optionally opaque reference tokens for services that insist on introspection. -* **Clock skew tolerance**: ±60 s; issue `nbf`, `iat`, `exp` accordingly. +* **Clock skew tolerance**: ±60 s; issue `nbf`, `iat`, `exp` accordingly. --- @@ -47,7 +47,7 @@ * **Incident mode tokens** require the `obs:incident` scope, a human-supplied `incident_reason`, and remain valid only while `auth_time` stays within a five-minute freshness window. Resource servers enforce the same window and persist `incident.reason`, `incident.auth_time`, and the fresh-auth verdict in `authority.resource.authorize` events. Authority exposes `/authority/audit/incident` so auditors can review recent activations. -### 2.1 Access token (OpTok) — short‑lived (120–300 s) +### 2.1 Access token (OpTok) — short‑lived (120–300 s) **Registered claims** @@ -62,7 +62,7 @@ jti = scope = "scanner.scan scanner.export signer.sign ..." ``` -**Sender‑constraint (`cnf`)** +**Sender‑constraint (`cnf`)** * **DPoP**: @@ -84,11 +84,11 @@ roles = [ "svc.scanner", "svc.signer", "ui.admin", ... ] plan? = // optional hint for UIs; not used for enforcement ``` -> **Note**: Do **not** copy PoE claims into OpTok; OpTok ≠ entitlement. Only **Signer** checks PoE. +> **Note**: Do **not** copy PoE claims into OpTok; OpTok ≠ entitlement. Only **Signer** checks PoE. ### 2.2 Refresh tokens (optional) -* Default **disabled**. If enabled (for UI interactive logins), pair with **DPoP‑bound** refresh tokens or **mTLS** client sessions; short TTL (≤ 8 h), rotating on use (replay‑safe). +* Default **disabled**. If enabled (for UI interactive logins), pair with **DPoP‑bound** refresh tokens or **mTLS** client sessions; short TTL (≤ 8 h), rotating on use (replay‑safe). ### 2.3 ID tokens (optional) @@ -100,8 +100,8 @@ plan? = // optional hint for UIs; not used for e ### 3.1 OIDC discovery & keys -* `GET /.well-known/openid-configuration` → endpoints, algs, jwks_uri -* `GET /jwks` → JSON Web Key Set (rotating, at least 2 active keys during transition) +* `GET /.well-known/openid-configuration` → endpoints, algs, jwks_uri +* `GET /jwks` → JSON Web Key Set (rotating, at least 2 active keys during transition) > **KMS-backed keys.** When the signing provider is `kms`, Authority fetches only the public coordinates (`Qx`, `Qy`) and version identifiers from the backing KMS. Private scalars never leave the provider; JWKS entries are produced by re-exporting the public material via the `kms.version` metadata attached to each key. Retired keys keep the same `kms.version` metadata so audits can trace which cloud KMS version produced a token. @@ -111,12 +111,12 @@ plan? = // optional hint for UIs; not used for e > Legacy aliases under `/oauth/token` are deprecated as of 1 November 2025 and now emit `Deprecation/Sunset/Warning` headers. See [`docs/api/authority-legacy-auth-endpoints.md`](../../api/authority-legacy-auth-endpoints.md) for timelines and migration guidance. - * **Client Credentials** (service→service): + * **Client Credentials** (service→service): - * **mTLS**: mutual TLS + `client_id` → bound token (`cnf.x5t#S256`) + * **mTLS**: mutual TLS + `client_id` → bound token (`cnf.x5t#S256`) * `security.senderConstraints.mtls.enforceForAudiences` forces the mTLS path when requested `aud`/`resource` values intersect high-value audiences (defaults include `signer`). Authority rejects clients attempting to use DPoP/basic secrets for these audiences. * Stored `certificateBindings` are authoritative: thumbprint, subject, issuer, serial number, and SAN values are matched against the presented certificate, with rotation grace applied to activation windows. Failures surface deterministic error codes (e.g. `certificate_binding_subject_mismatch`). - * **private_key_jwt**: JWT‑based client auth + **DPoP** header (preferred for tools and CLI) + * **private_key_jwt**: JWT‑based client auth + **DPoP** header (preferred for tools and CLI) * **Device Code** (CLI): `POST /oauth/device/code` + `POST /oauth/token` poll * **Authorization Code + PKCE** (UI): standard @@ -134,7 +134,7 @@ plan? = // optional hint for UIs; not used for e signed with the DPoP private key; header carries JWK. 3. Authority validates proof; issues access token with `cnf.jkt=`. -4. Client uses the same DPoP key to sign **every subsequent API request** to services (Signer, Scanner, …). +4. Client uses the same DPoP key to sign **every subsequent API request** to services (Signer, Scanner, …). **mTLS flow** @@ -142,11 +142,11 @@ plan? = // optional hint for UIs; not used for e ### 3.3 Introspection & revocation (optional) -* `POST /introspect` → `{ active, sub, scope, aud, exp, cnf, ... }` -* `POST /revoke` → revokes refresh tokens or opaque access tokens. +* `POST /introspect` → `{ active, sub, scope, aud, exp, cnf, ... }` +* `POST /revoke` → revokes refresh tokens or opaque access tokens. > Requests targeting the legacy `/oauth/{introspect|revoke}` paths receive deprecation headers and are scheduled for removal after 1 May 2026. -* **Replay prevention**: maintain **DPoP `jti` cache** (TTL ≤ 10 min) to reject duplicate proofs when services supply DPoP nonces (Signer requires nonce for high‑value operations). +* **Replay prevention**: maintain **DPoP `jti` cache** (TTL ≤ 10 min) to reject duplicate proofs when services supply DPoP nonces (Signer requires nonce for high‑value operations). ### 3.4 UserInfo (optional for UI) @@ -156,19 +156,19 @@ plan? = // optional hint for UIs; not used for e ### 3.5 Vuln Explorer workflow safeguards -* **Anti-forgery flow** — Vuln Explorer’s mutation verbs call +* **Anti-forgery flow** — Vuln Explorer’s mutation verbs call * `POST /vuln/workflow/anti-forgery/issue` * `POST /vuln/workflow/anti-forgery/verify` - Callers must hold `vuln:operate` scopes. Issued tokens embed the actor, tenant, whitelisted actions, ABAC selectors (environment/owner/business tier), and optional context key/value pairs. Tokens are EdDSA/ES256 signed via the primary Authority signing key and default to a 10‑minute TTL (cap: 30 minutes). Verification enforces nonce reuse prevention, tenant match, and action membership before forwarding the request to Vuln Explorer. + Callers must hold `vuln:operate` scopes. Issued tokens embed the actor, tenant, whitelisted actions, ABAC selectors (environment/owner/business tier), and optional context key/value pairs. Tokens are EdDSA/ES256 signed via the primary Authority signing key and default to a 10‑minute TTL (cap: 30 minutes). Verification enforces nonce reuse prevention, tenant match, and action membership before forwarding the request to Vuln Explorer. -* **Attachment access** — Evidence bundles and attachments reference a ledger hash. Vuln Explorer obtains a scoped download token through: +* **Attachment access** — Evidence bundles and attachments reference a ledger hash. Vuln Explorer obtains a scoped download token through: * `POST /vuln/attachments/tokens/issue` * `POST /vuln/attachments/tokens/verify` - These tokens bind the ledger event hash, attachment identifier, optional finding/content metadata, and the actor. They default to a 30‑minute TTL (cap: 4 hours) and require `vuln:investigate`. + These tokens bind the ledger event hash, attachment identifier, optional finding/content metadata, and the actor. They default to a 30‑minute TTL (cap: 4 hours) and require `vuln:investigate`. -* **Audit trail** — Both flows emit `vuln.workflow.csrf.*` and `vuln.attachment.token.*` audit records with tenant, actor, ledger hash, nonce, and filtered context metadata so Offline Kit operators can reconcile actions against ledger entries. +* **Audit trail** — Both flows emit `vuln.workflow.csrf.*` and `vuln.attachment.token.*` audit records with tenant, actor, ledger hash, nonce, and filtered context metadata so Offline Kit operators can reconcile actions against ledger entries. * **Configuration** @@ -200,7 +200,7 @@ plan? = // optional hint for UIs; not used for e ### 4.1 Audiences -* `signer` — only the **Signer** service should accept tokens with `aud=signer`. +* `signer` — only the **Signer** service should accept tokens with `aud=signer`. * `attestor`, `scanner`, `concelier`, `excititor`, `ui`, `zastava` similarly. Services **must** verify `aud` and **sender constraint** (DPoP/mTLS) per their policy. @@ -227,13 +227,13 @@ Services **must** verify `aud` and **sender constraint** (DPoP/mTLS) per their p | `authority:branding.read` / `authority:branding.write` | Authority | Branding admin | | `zastava.emit` / `zastava.enforce` | Scanner/Zastava | Runtime events / admission | -**Roles → scopes mapping** is configured centrally (Authority policy) and pushed during token issuance. +**Roles → scopes mapping** is configured centrally (Authority policy) and pushed during token issuance. --- ## 5) Storage & state -* **Configuration DB** (PostgreSQL/MySQL): clients, audiences, role→scope maps, tenant/installation registry, device code grants, persistent consents (if any). +* **Configuration DB** (PostgreSQL/MySQL): clients, audiences, role→scope maps, tenant/installation registry, device code grants, persistent consents (if any). * **Cache** (Valkey): * DPoP **jti** replay cache (short TTL) @@ -247,20 +247,20 @@ Services **must** verify `aud` and **sender constraint** (DPoP/mTLS) per their p * Maintain **at least 2 signing keys** active during rotation; tokens carry `kid`. * Prefer **Ed25519** for compact tokens; maintain **ES256** fallback for FIPS contexts. -* Rotation cadence: 30–90 days; emergency rotation supported. -* Publish new JWKS **before** issuing tokens with the new `kid` to avoid cold‑start validation misses. +* Rotation cadence: 30–90 days; emergency rotation supported. +* Publish new JWKS **before** issuing tokens with the new `kid` to avoid cold‑start validation misses. * Keep **old keys** available **at least** for max token TTL + 5 minutes. --- ## 7) HA & performance -* **Stateless issuance** (except device codes/refresh) → scale horizontally behind a load‑balancer. -* **DB** only for client metadata and optional flows; token checks are JWT‑local; introspection endpoints hit cache/DB minimally. +* **Stateless issuance** (except device codes/refresh) → scale horizontally behind a load‑balancer. +* **DB** only for client metadata and optional flows; token checks are JWT‑local; introspection endpoints hit cache/DB minimally. * **Targets**: - * Token issuance P95 ≤ **20 ms** under warm cache. - * DPoP proof validation ≤ **1 ms** extra per request at resource servers (Signer/Scanner). + * Token issuance P95 ≤ **20 ms** under warm cache. + * DPoP proof validation ≤ **1 ms** extra per request at resource servers (Signer/Scanner). * 99.9% uptime; HPA on CPU/latency. --- @@ -269,7 +269,7 @@ Services **must** verify `aud` and **sender constraint** (DPoP/mTLS) per their p * **Strict TLS** (1.3 preferred); HSTS; modern cipher suites. * **mTLS** enabled where required (Signer/Attestor paths). -* **Replay protection**: DPoP `jti` cache, nonce support for **Signer** (add `DPoP-Nonce` header on 401; clients re‑sign). +* **Replay protection**: DPoP `jti` cache, nonce support for **Signer** (add `DPoP-Nonce` header on 401; clients re‑sign). * **Rate limits** per client & per IP; exponential backoff on failures. * **Secrets**: clients use **private_key_jwt** or **mTLS**; never basic secrets over the wire. * **CSP/CSRF** hardening on UI flows; `SameSite=Lax` cookies; PKCE enforced. @@ -277,10 +277,10 @@ Services **must** verify `aud` and **sender constraint** (DPoP/mTLS) per their p --- -## 9) Multi‑tenancy & installations +## 9) Multi‑tenancy & installations * **Tenant (`tid`)** and **Installation (`inst`)** registries define which audiences/scopes a client can request. -* Cross‑tenant isolation enforced at issuance (disallow rogue `aud`), and resource servers **must** check that `tid` matches their configured tenant. +* Cross‑tenant isolation enforced at issuance (disallow rogue `aud`), and resource servers **must** check that `tid` matches their configured tenant. --- @@ -293,7 +293,7 @@ Authority exposes two admin tiers: ``` POST /admin/clients # create/update client (confidential/public) POST /admin/audiences # register audience resource URIs -POST /admin/roles # define role→scope mappings +POST /admin/roles # define role→scope mappings POST /admin/tenants # create tenant/install entries POST /admin/keys/rotate # rotate signing key (zero-downtime) GET /admin/metrics # Prometheus exposition (token issue rates, errors) @@ -306,10 +306,10 @@ Declared client `audiences` flow through to the issued JWT `aud` claim and the t ## 11) Integration hard lines (what resource servers must enforce) -Every Stella Ops service that consumes Authority tokens **must**: +Every Stella Ops service that consumes Authority tokens **must**: 1. Verify JWT signature (`kid` in JWKS), `iss`, `aud`, `exp`, `nbf`. -2. Enforce **sender‑constraint**: +2. Enforce **sender‑constraint**: * **DPoP**: validate DPoP proof (`htu`, `htm`, `iat`, `jti`) and match `cnf.jkt`; cache `jti` for replay defense; honor nonce challenges. * **mTLS**: match presented client cert thumbprint to token `cnf.x5t#S256`. @@ -322,8 +322,8 @@ Every Stella Ops service that consumes Authority tokens **must**: ## 12) Error surfaces & UX * Token endpoint errors follow OAuth2 (`invalid_client`, `invalid_grant`, `invalid_scope`, `unauthorized_client`). -* Resource servers use RFC 6750 style (`WWW-Authenticate: DPoP error="invalid_token", error_description="…", dpop_nonce="…" `). -* For DPoP nonce challenges, clients retry with the server‑supplied nonce once. +* Resource servers use RFC 6750 style (`WWW-Authenticate: DPoP error="invalid_token", error_description="…", dpop_nonce="…" `). +* For DPoP nonce challenges, clients retry with the server‑supplied nonce once. --- @@ -431,8 +431,8 @@ authority: * **JWT validation**: wrong `aud`, expired `exp`, skewed `nbf`, stale `kid`. * **DPoP**: invalid `htu`/`htm`, replayed `jti`, stale `iat`, wrong `jkt`, nonce dance. * **mTLS**: wrong client cert, wrong CA, thumbprint mismatch. -* **RBAC**: scope enforcement per audience; over‑privileged client denied. -* **Rotation**: JWKS rotation while load‑testing; zero‑downtime verification. +* **RBAC**: scope enforcement per audience; over‑privileged client denied. +* **Rotation**: JWKS rotation while load‑testing; zero‑downtime verification. * **HA**: kill one Authority instance; verify issuance continues; JWKS served by peers. * **Performance**: 1k token issuance/sec on 2 cores with Valkey enabled for jti caching. @@ -442,18 +442,18 @@ authority: | Threat | Vector | Mitigation | | ------------------- | ---------------- | ------------------------------------------------------------------------------------------ | -| Token theft | Copy of JWT | **Short TTL**, **sender‑constraint** (DPoP/mTLS); replay blocked by `jti` cache and nonces | +| Token theft | Copy of JWT | **Short TTL**, **sender‑constraint** (DPoP/mTLS); replay blocked by `jti` cache and nonces | | Replay across hosts | Reuse DPoP proof | Enforce `htu`/`htm`, `iat` freshness, `jti` uniqueness; services may require **nonce** | | Impersonation | Fake client | mTLS or `private_key_jwt` with pinned JWK; client registration & rotation | | Key compromise | Signing key leak | HSM/KMS storage, key rotation, audit; emergency key revoke path; narrow token TTL | -| Cross‑tenant abuse | Scope elevation | Enforce `aud`, `tid`, `inst` at issuance and resource servers | +| Cross‑tenant abuse | Scope elevation | Enforce `aud`, `tid`, `inst` at issuance and resource servers | | Downgrade to bearer | Strip DPoP | Resource servers require DPoP/mTLS based on `aud`; reject bearer without `cnf` | --- ## 17) Deployment & HA -* **Stateless** microservice, containerized; run ≥ 2 replicas behind LB. +* **Stateless** microservice, containerized; run ≥ 2 replicas behind LB. * **DB**: HA Postgres (or MySQL) for clients/roles; **Valkey** for device codes, DPoP nonces/jtis. * **Secrets**: mount client JWKs via K8s Secrets/HashiCorp Vault; signing keys via KMS. * **Backups**: DB daily; Valkey not critical (ephemeral). @@ -470,7 +470,7 @@ authority: --- -## 19) Quick reference — wire examples +## 19) Quick reference — wire examples **Access token (payload excerpt)** @@ -507,10 +507,10 @@ Signer validates that `hash(JWK)` in the proof matches `cnf.jkt` in the token. ## 20) Rollout plan -1. **MVP**: Client Credentials (private_key_jwt + DPoP), JWKS, short OpToks, per‑audience scopes. -2. **Add**: mTLS‑bound tokens for Signer/Attestor; device code for CLI; optional introspection. +1. **MVP**: Client Credentials (private_key_jwt + DPoP), JWKS, short OpToks, per‑audience scopes. +2. **Add**: mTLS‑bound tokens for Signer/Attestor; device code for CLI; optional introspection. 3. **Hardening**: DPoP nonce support; full audit pipeline; HA tuning. -4. **UX**: Tenant/installation admin UI; role→scope editors; client bootstrap wizards. +4. **UX**: Tenant/installation admin UI; role→scope editors; client bootstrap wizards. --- diff --git a/docs/modules/cli/architecture.md b/docs/modules/cli/architecture.md index 06fff991d..f9997ceca 100644 --- a/docs/modules/cli/architecture.md +++ b/docs/modules/cli/architecture.md @@ -1,245 +1,245 @@ -# component_architecture_cli.md — **Stella Ops CLI** (2025Q4) - -> Consolidates requirements captured in the Policy Engine, Policy Studio, Vulnerability Explorer, Export Center, and Notifications implementation plans and module guides. - -> **Scope.** Implementation‑ready architecture for **Stella Ops CLI**: command surface, process model, auth (Authority/DPoP), integration with Scanner/Excititor/Concelier/Signer/Attestor, Buildx plug‑in management, offline kit behavior, packaging, observability, security posture, and CI ergonomics. - ---- - -## 0) Mission & boundaries - -**Mission.** Provide a **fast, deterministic, CI‑friendly** command‑line interface to drive Stella Ops workflows: - -* Build‑time SBOM generation via **Buildx generator** orchestration. -* Post‑build **scan/compose/diff/export** against **Scanner.WebService**. -* **Policy** operations and **VEX/Vuln** data pulls (operator tasks). -* **Verification** (attestation, referrers, signatures) for audits. -* Air‑gapped/offline **kit** administration. - -**Boundaries.** - -* CLI **never** signs; it only calls **Signer**/**Attestor** via backend APIs when needed (e.g., `report --attest`). -* CLI **does not** store long‑lived credentials beyond OS keychain; tokens are **short** (Authority OpToks). -* Heavy work (scanning, merging, policy) is executed **server‑side** (Scanner/Excititor/Concelier). - ---- - -## 1) Solution layout & runtime form - -``` -src/ - ├─ StellaOps.Cli/ # net10.0 (Native AOT) single binary - ├─ StellaOps.Cli.Core/ # verb plumbing, config, HTTP, auth - ├─ StellaOps.Cli.Plugins/ # optional verbs packaged as plugins - ├─ StellaOps.Cli.Tests/ # unit + golden-output tests - └─ packaging/ - ├─ msix / msi / deb / rpm / brew formula - └─ scoop manifest / winget manifest -``` - -**Language/runtime**: .NET 10 **Native AOT** for speed/startup; Linux builds use **musl** static when possible. - -**Plug-in verbs.** Non-core verbs (Excititor, runtime helpers, future integrations) ship as restart-time plug-ins under `plugins/cli/**` with manifest descriptors. The launcher loads plug-ins on startup; hot reloading is intentionally unsupported. The inaugural bundle, `StellaOps.Cli.Plugins.NonCore`, packages the Excititor, runtime, and offline-kit command groups and publishes its manifest at `plugins/cli/StellaOps.Cli.Plugins.NonCore/`. - -**OS targets**: linuxâ€'x64/arm64, windowsâ€'x64/arm64, macOSâ€'x64/arm64. - ---- - -## 1.1) Command Routing Infrastructure (v2.x→v3.0 Migration) - -> Sprint: SPRINT_20260118_010_CLI_consolidation_foundation - -The CLI includes a **command routing infrastructure** to support backward-compatible command migration. This enables consolidating 81+ top-level commands into ~18 organized command groups while maintaining backward compatibility. - -### Routing Components - -``` -src/Cli/StellaOps.Cli/Infrastructure/ -├── ICommandRouter.cs # Router interface -├── CommandRouter.cs # Route registration and lookup -├── CommandRoute.cs # Route model (old→new path mapping) -├── CommandGroupBuilder.cs # Fluent builder for command groups -├── DeprecationWarningService.cs # Warning display on stderr -├── RouteMappingConfiguration.cs # JSON config model + loader - -src/Cli/StellaOps.Cli/ -└── cli-routes.json # Embedded route mappings (60+ entries) -``` - -### How Routing Works - -1. **At startup**, `CommandFactory.RegisterDeprecatedAliases()` loads `cli-routes.json` (embedded resource) -2. **For each deprecated route**, creates a hidden alias command that: - - Delegates to the canonical command - - Shows a deprecation warning on stderr (once per session) -3. **Warnings** include the old path, new path, removal version, and suppression instructions - -### Route Configuration Schema - -```json -{ - "version": "1.0", - "mappings": [ - { - "old": "scangraph", - "new": "scan graph", - "type": "deprecated", - "removeIn": "3.0", - "reason": "Consolidated under scan command" - } - ] -} -``` - -### Deprecation Warning Format - -``` -WARNING: 'stella scangraph' is deprecated and will be removed in v3.0. - Use 'stella scan graph' instead. - Set STELLA_SUPPRESS_DEPRECATION_WARNINGS=1 to hide this message. -``` - -### Timeline - -- **v2.x**: Both old and new command paths work; old paths show deprecation warnings -- **v3.0**: Old command paths removed - -### Migration Guide - -See [migration-v3.md](./guides/migration-v3.md) for user-facing migration instructions and command mappings. - ---- - -## 2) Command surface (verbs) - -> All verbs default to **JSON** output when `--json` is set (CI mode). Human output is concise, deterministic. - -### 2.1 Auth & profile - -* `auth login` - - * Modes: **device‑code** (default), **client‑credentials** (service principal). - * Produces **Authority** access token (OpTok) + stores **DPoP** keypair in OS keychain. -* `auth status` — show current issuer, subject, audiences, expiry. -* `auth logout` — wipe cached tokens/keys. - -### 2.2 Build‑time SBOM (Buildx) - -* `buildx install` — install/update the **StellaOps.Scanner.Sbomer.BuildXPlugin** on the host. -* `buildx verify` — ensure generator is usable. -* `buildx build` — thin wrapper around `docker buildx build --attest=type=sbom,generator=stellaops/sbom-indexer` with convenience flags: - - * `--attest` (request Signer/Attestor via backend post‑push) - * `--provenance` pass‑through (optional) - -### 2.3 Scanning & artifacts - -* `scan image ` - - * Options: `--force`, `--wait`, `--view=inventory|usage|both`, `--format=cdx-json|cdx-pb|spdx-json`, `--attest` (ask backend to sign/log). - * Streams progress; exits early unless `--wait`. -* `diff image --old --new [--view ...]` — show layer‑attributed changes. -* `export sbom [--view ... --format ... --out file]` — download artifact. -* `sbom upload --file --artifact [--format cyclonedx|spdx]` - BYOS upload into the scanner analysis pipeline (ledger join uses the SBOM digest). -* `report final [--policy-revision ... --attest]` — request PASS/FAIL report from backend (policy+vex) and optional attestation. -### 2.3.1 Compare Commands & Baseline Selection (SPRINT_20260208_029) - -The `compare` command group supports diffing scan snapshots with automatic baseline resolution. - -#### Commands - -* `compare diff --base --target ` â€" Full comparison showing detailed diff. -* `compare summary --base --target ` â€" Quick summary of changes. -* `compare can-ship --base --target ` â€" Check if target passes policy (exit code: 0=pass, 1=fail). -* `compare vulns --base --target ` â€" List vulnerability changes only. - -#### Baseline Selection Strategies - -All compare commands support `--baseline-strategy` for automatic baseline resolution: - -| Strategy | Description | Requirements | -|----------|-------------|--------------| -| `explicit` (default) | Uses the digest provided via `--base` | `--base` required | -| `last-green` | Selects most recent passing snapshot | `--artifact` required | -| `previous-release` | Selects previous release tag from registry metadata | `--artifact` required | - -**Options:** - -* `--baseline-strategy ` â€" Strategy for baseline selection -* `--artifact ` â€" Artifact identifier for auto-resolution strategies -* `--current-version ` â€" Current version (helps `previous-release` find older releases) -* `--verification-report ` - Attach `bundle verify --output json` checks to compare output (hash/signature overlay) -* `--reverify-bundle ` - Recompute artifact hash and DSSE-sidecar status from local evidence bundle for live re-verification -* `--determinism-manifest ` - Attach determinism manifest score/threshold summary to compare output - -**Examples:** - -```bash -# Explicit baseline (traditional) -stella compare can-ship --base sha256:abc123 --target sha256:def456 - -# Auto-select last green baseline -stella compare can-ship --target sha256:def456 \ - --baseline-strategy last-green \ - --artifact pkg:oci/myapp - -# Use previous release as baseline -stella compare can-ship --target sha256:def456 \ - --baseline-strategy previous-release \ - --artifact pkg:oci/myapp \ - --current-version v2.0.0 - -# Compare diff with inline verification overlay and determinism context -stella compare diff --base sha256:abc123 --target sha256:def456 \ - --verification-report ./verify-report.json \ - --reverify-bundle ./evidence-bundle \ - --determinism-manifest ./determinism.json -``` - -**Resolution Behavior:** - -* `last-green`: Queries forensic snapshot store for latest artifact snapshot with `verdict:pass` tag. -* `previous-release`: Queries for release-tagged snapshots, excludes `--current-version`, returns most recent. -* Both strategies show suggestions when resolution fails. -* Verification overlay: `compare diff` can now include per-artifact `hash`/`signature` status plus determinism score context in table and JSON outputs. - -**Service Interface:** - -```csharp -public interface IBaselineResolver -{ - Task ResolveAsync(BaselineResolutionRequest request, CancellationToken ct); - Task> GetSuggestionsAsync(string artifactId, CancellationToken ct); -} -``` -### 2.4 Policy & data - -* `policy get/set/apply` — fetch active policy, apply staged policy, compute digest. -* `concelier export` — trigger/export canonical JSON or Trivy DB (admin). -* `excititor export` — trigger/export consensus/raw claims (admin). - -### 2.5 Verification - -* `verify attestation --uuid | --artifact | --bundle ` — call **Attestor /verify** and print proof summary. -* `verify referrers ` — ask **Signer /verify/referrers** (is image Stella‑signed?). -* `verify image-signature ` — standalone cosign verification (optional, local). -* `verify release [--sbom ] [--vex ] [--trust-root ] [--checkpoint ]` — run promotion bundle verification and fail if source/build/rekor/signature links cannot be validated. - -### 2.6 Runtime (Zastava helper) - -* `runtime policy test --image/-i [--file --ns --label key=value --json]` — ask backend `/policy/runtime` like the webhook would (accepts multiple `--image`, comma/space lists, or stdin pipelines). - -### 2.7 Offline kit - -* `offline kit pull` — fetch latest **Concelier JSON + Trivy DB + Excititor exports** as a tarball from a mirror. -* `offline kit import ` — upload the kit to on‑prem services (Concelier/Excititor). -* `offline kit status` — list current seed versions. - +# component_architecture_cli.md — **Stella Ops CLI** (2025Q4) + +> Consolidates requirements captured in the Policy Engine, Policy Studio, Vulnerability Explorer, Export Center, and Notifications implementation plans and module guides. + +> **Scope.** Implementation‑ready architecture for **Stella Ops CLI**: command surface, process model, auth (Authority/DPoP), integration with Scanner/Excititor/Concelier/Signer/Attestor, Buildx plug‑in management, offline kit behavior, packaging, observability, security posture, and CI ergonomics. + +--- + +## 0) Mission & boundaries + +**Mission.** Provide a **fast, deterministic, CI‑friendly** command‑line interface to drive Stella Ops workflows: + +* Build‑time SBOM generation via **Buildx generator** orchestration. +* Post‑build **scan/compose/diff/export** against **Scanner.WebService**. +* **Policy** operations and **VEX/Vuln** data pulls (operator tasks). +* **Verification** (attestation, referrers, signatures) for audits. +* Air‑gapped/offline **kit** administration. + +**Boundaries.** + +* CLI **never** signs; it only calls **Signer**/**Attestor** via backend APIs when needed (e.g., `report --attest`). +* CLI **does not** store long‑lived credentials beyond OS keychain; tokens are **short** (Authority OpToks). +* Heavy work (scanning, merging, policy) is executed **server‑side** (Scanner/Excititor/Concelier). + +--- + +## 1) Solution layout & runtime form + +``` +src/ + ├─ StellaOps.Cli/ # net10.0 (Native AOT) single binary + ├─ StellaOps.Cli.Core/ # verb plumbing, config, HTTP, auth + ├─ StellaOps.Cli.Plugins/ # optional verbs packaged as plugins + ├─ StellaOps.Cli.Tests/ # unit + golden-output tests + └─ packaging/ + ├─ msix / msi / deb / rpm / brew formula + └─ scoop manifest / winget manifest +``` + +**Language/runtime**: .NET 10 **Native AOT** for speed/startup; Linux builds use **musl** static when possible. + +**Plug-in verbs.** Non-core verbs (Excititor, runtime helpers, future integrations) ship as restart-time plug-ins under `plugins/cli/**` with manifest descriptors. The launcher loads plug-ins on startup; hot reloading is intentionally unsupported. The inaugural bundle, `StellaOps.Cli.Plugins.NonCore`, packages the Excititor, runtime, and offline-kit command groups and publishes its manifest at `plugins/cli/StellaOps.Cli.Plugins.NonCore/`. + +**OS targets**: linux‑x64/arm64, windows‑x64/arm64, macOS‑x64/arm64. + +--- + +## 1.1) Command Routing Infrastructure (v2.x→v3.0 Migration) + +> Sprint: SPRINT_20260118_010_CLI_consolidation_foundation + +The CLI includes a **command routing infrastructure** to support backward-compatible command migration. This enables consolidating 81+ top-level commands into ~18 organized command groups while maintaining backward compatibility. + +### Routing Components + +``` +src/Cli/StellaOps.Cli/Infrastructure/ +├── ICommandRouter.cs # Router interface +├── CommandRouter.cs # Route registration and lookup +├── CommandRoute.cs # Route model (old→new path mapping) +├── CommandGroupBuilder.cs # Fluent builder for command groups +├── DeprecationWarningService.cs # Warning display on stderr +├── RouteMappingConfiguration.cs # JSON config model + loader + +src/Cli/StellaOps.Cli/ +└── cli-routes.json # Embedded route mappings (60+ entries) +``` + +### How Routing Works + +1. **At startup**, `CommandFactory.RegisterDeprecatedAliases()` loads `cli-routes.json` (embedded resource) +2. **For each deprecated route**, creates a hidden alias command that: + - Delegates to the canonical command + - Shows a deprecation warning on stderr (once per session) +3. **Warnings** include the old path, new path, removal version, and suppression instructions + +### Route Configuration Schema + +```json +{ + "version": "1.0", + "mappings": [ + { + "old": "scangraph", + "new": "scan graph", + "type": "deprecated", + "removeIn": "3.0", + "reason": "Consolidated under scan command" + } + ] +} +``` + +### Deprecation Warning Format + +``` +WARNING: 'stella scangraph' is deprecated and will be removed in v3.0. + Use 'stella scan graph' instead. + Set STELLA_SUPPRESS_DEPRECATION_WARNINGS=1 to hide this message. +``` + +### Timeline + +- **v2.x**: Both old and new command paths work; old paths show deprecation warnings +- **v3.0**: Old command paths removed + +### Migration Guide + +See [migration-v3.md](./guides/migration-v3.md) for user-facing migration instructions and command mappings. + +--- + +## 2) Command surface (verbs) + +> All verbs default to **JSON** output when `--json` is set (CI mode). Human output is concise, deterministic. + +### 2.1 Auth & profile + +* `auth login` + + * Modes: **device‑code** (default), **client‑credentials** (service principal). + * Produces **Authority** access token (OpTok) + stores **DPoP** keypair in OS keychain. +* `auth status` — show current issuer, subject, audiences, expiry. +* `auth logout` — wipe cached tokens/keys. + +### 2.2 Build‑time SBOM (Buildx) + +* `buildx install` — install/update the **StellaOps.Scanner.Sbomer.BuildXPlugin** on the host. +* `buildx verify` — ensure generator is usable. +* `buildx build` — thin wrapper around `docker buildx build --attest=type=sbom,generator=stellaops/sbom-indexer` with convenience flags: + + * `--attest` (request Signer/Attestor via backend post‑push) + * `--provenance` pass‑through (optional) + +### 2.3 Scanning & artifacts + +* `scan image ` + + * Options: `--force`, `--wait`, `--view=inventory|usage|both`, `--format=cdx-json|cdx-pb|spdx-json`, `--attest` (ask backend to sign/log). + * Streams progress; exits early unless `--wait`. +* `diff image --old --new [--view ...]` — show layer‑attributed changes. +* `export sbom [--view ... --format ... --out file]` — download artifact. +* `sbom upload --file --artifact [--format cyclonedx|spdx]` - BYOS upload into the scanner analysis pipeline (ledger join uses the SBOM digest). +* `report final [--policy-revision ... --attest]` — request PASS/FAIL report from backend (policy+vex) and optional attestation. +### 2.3.1 Compare Commands & Baseline Selection (SPRINT_20260208_029) + +The `compare` command group supports diffing scan snapshots with automatic baseline resolution. + +#### Commands + +* `compare diff --base --target ` — Full comparison showing detailed diff. +* `compare summary --base --target ` — Quick summary of changes. +* `compare can-ship --base --target ` — Check if target passes policy (exit code: 0=pass, 1=fail). +* `compare vulns --base --target ` — List vulnerability changes only. + +#### Baseline Selection Strategies + +All compare commands support `--baseline-strategy` for automatic baseline resolution: + +| Strategy | Description | Requirements | +|----------|-------------|--------------| +| `explicit` (default) | Uses the digest provided via `--base` | `--base` required | +| `last-green` | Selects most recent passing snapshot | `--artifact` required | +| `previous-release` | Selects previous release tag from registry metadata | `--artifact` required | + +**Options:** + +* `--baseline-strategy ` — Strategy for baseline selection +* `--artifact ` — Artifact identifier for auto-resolution strategies +* `--current-version ` — Current version (helps `previous-release` find older releases) +* `--verification-report ` - Attach `bundle verify --output json` checks to compare output (hash/signature overlay) +* `--reverify-bundle ` - Recompute artifact hash and DSSE-sidecar status from local evidence bundle for live re-verification +* `--determinism-manifest ` - Attach determinism manifest score/threshold summary to compare output + +**Examples:** + +```bash +# Explicit baseline (traditional) +stella compare can-ship --base sha256:abc123 --target sha256:def456 + +# Auto-select last green baseline +stella compare can-ship --target sha256:def456 \ + --baseline-strategy last-green \ + --artifact pkg:oci/myapp + +# Use previous release as baseline +stella compare can-ship --target sha256:def456 \ + --baseline-strategy previous-release \ + --artifact pkg:oci/myapp \ + --current-version v2.0.0 + +# Compare diff with inline verification overlay and determinism context +stella compare diff --base sha256:abc123 --target sha256:def456 \ + --verification-report ./verify-report.json \ + --reverify-bundle ./evidence-bundle \ + --determinism-manifest ./determinism.json +``` + +**Resolution Behavior:** + +* `last-green`: Queries forensic snapshot store for latest artifact snapshot with `verdict:pass` tag. +* `previous-release`: Queries for release-tagged snapshots, excludes `--current-version`, returns most recent. +* Both strategies show suggestions when resolution fails. +* Verification overlay: `compare diff` can now include per-artifact `hash`/`signature` status plus determinism score context in table and JSON outputs. + +**Service Interface:** + +```csharp +public interface IBaselineResolver +{ + Task ResolveAsync(BaselineResolutionRequest request, CancellationToken ct); + Task> GetSuggestionsAsync(string artifactId, CancellationToken ct); +} +``` +### 2.4 Policy & data + +* `policy get/set/apply` — fetch active policy, apply staged policy, compute digest. +* `concelier export` — trigger/export canonical JSON or Trivy DB (admin). +* `excititor export` — trigger/export consensus/raw claims (admin). + +### 2.5 Verification + +* `verify attestation --uuid | --artifact | --bundle ` — call **Attestor /verify** and print proof summary. +* `verify referrers ` — ask **Signer /verify/referrers** (is image Stella‑signed?). +* `verify image-signature ` — standalone cosign verification (optional, local). +* `verify release [--sbom ] [--vex ] [--trust-root ] [--checkpoint ]` — run promotion bundle verification and fail if source/build/rekor/signature links cannot be validated. + +### 2.6 Runtime (Zastava helper) + +* `runtime policy test --image/-i [--file --ns --label key=value --json]` — ask backend `/policy/runtime` like the webhook would (accepts multiple `--image`, comma/space lists, or stdin pipelines). + +### 2.7 Offline kit + +* `offline kit pull` — fetch latest **Concelier JSON + Trivy DB + Excititor exports** as a tarball from a mirror. +* `offline kit import ` — upload the kit to on‑prem services (Concelier/Excititor). +* `offline kit status` — list current seed versions. + ### 2.8 Utilities -* `config set/get` — endpoint & defaults. -* `whoami` — short auth display. -* `version` — CLI + protocol versions; release channel. +* `config set/get` — endpoint & defaults. +* `whoami` — short auth display. +* `version` — CLI + protocol versions; release channel. * `tools policy-dsl-validate [--strict] [--json]` * `tools policy-schema-export [--output ] [--repo-root ]` * `tools policy-simulation-smoke [--scenario-root ] [--output ] [--repo-root ] [--fixed-time ]` @@ -265,435 +265,435 @@ public interface IBaselineResolver * This command shares the same preference record consumed by the Web shell locale selector so locale choice follows the user across Web and CLI sessions. ### 2.9 Aggregation-only guard helpers - -* `sources ingest --dry-run --source --input [--tenant ... --format table|json --output file]` - - * Normalises documents (handles gzip/base64), posts them to the backend `aoc/ingest/dry-run` route, and exits non-zero when guard violations are detected. - * Defaults to table output with ANSI colour; `--json`/`--output` produce deterministic JSON for CI pipelines. - -* `aoc verify [--since ] [--limit ] [--sources list] [--codes list] [--format table|json] [--export file] [--tenant id] [--no-color]` - - * Replays guard checks against stored raw documents. Maps backend `ERR_AOC_00x` codes onto deterministic exit codes so CI can block regressions. - * Supports pagination hints (`--limit`, `--since`), tenant scoping via `--tenant` or `STELLA_TENANT`, and JSON exports for evidence lockers. - -### 2.10 Key management (file KMS support) - -* `kms export --key-id --output [--version ] [--force]` - - * Decrypts the file-backed KMS store (passphrase supplied via `--passphrase`, `STELLAOPS_KMS_PASSPHRASE`, or interactive prompt) and writes a portable JSON bundle (`KmsKeyMaterial`) with key metadata and coordinates for offline escrow or replication. - -* `kms import --key-id --input [--version ]` - - * Imports a previously exported bundle into the local KMS root (`kms/` by default), promotes the imported version to `Active`, and preserves existing versions by marking them `PendingRotation`. Prompts for the passphrase when not provided to keep automation password-safe. - -### 2.11 CI Template Generation (Sprint 015) - -* `ci init --platform [--template ] [--mode ] [--output ] [--force] [--offline] [--scanner-image ]` - - * Generates ready-to-run CI workflow templates for the specified platform(s). - * Template types: - * `gate` - PR gating workflow that blocks merges on policy violations. - * `scan` - Scheduled/push scan workflow for container images. - * `verify` - Verification workflow for attestations and signatures. - * `full` - All templates combined. - * Modes control attestation behavior: - * `scan-only` - Scan without attestation. - * `scan-attest` - Scan and create attestations (default). - * `scan-vex` - Scan with VEX document generation. - * `--offline` generates templates with pinned digests for air-gapped environments. - -* `ci list` - - * Lists available template types and supported platforms. - -* `ci validate ` - - * Validates a generated workflow file for correctness. - * Checks integration IDs, registry endpoints, and AuthRef references. - -**Generated files:** -- GitHub: `.github/workflows/stellaops-{gate,scan,verify}.yml` -- GitLab: `.gitlab-ci.yml` or `.gitlab/stellaops-{scan,verify}.yml` -- Gitea: `.gitea/workflows/stellaops-{gate,scan,verify}.yml` - -**Implementation:** `CiCommandGroup.cs`, `CiTemplates.cs` in `src/Cli/StellaOps.Cli/Commands/`. - -Both subcommands honour offline-first expectations (no network access) and normalise relative roots via `--root` when operators mirror the credential store. - -### 2.11 Advisory AI (RAG summaries) - -* `advise run --advisory-key [--artifact-id id] [--artifact-purl purl] [--policy-version v] [--profile profile] [--section name] [--force-refresh] [--timeout seconds]` - - * Calls the Advisory AI service (`/v1/advisory-ai/pipeline/{task}` + `/outputs/{cacheKey}`) to materialise a deterministic plan, queue execution, and poll for the generated brief. - * Renders plan metadata (cache key, prompt template, token budgets), guardrail results, provenance hashes/signatures, and citation list. Exit code is non-zero if guardrails block or the command times out. - * Uses `STELLAOPS_ADVISORYAI_URL` when configured; otherwise it reuses the backend base address and adds `X-StellaOps-Scopes` (`advisory:run` + task scope) per request. - * `--timeout 0` performs a single cache lookup (for CI flows that only want cached artefacts). - -* `advise ask "" [--evidence] [--no-action] [--conversation-id ] [--context ]` - - * Calls advisory chat endpoints, returns a cited answer with evidence refs. - * `--no-action` disables action proposals; `--evidence` forces evidence chips in output. - * `--file ` processes newline-delimited JSON batch requests (`{"query":"..."}` or JSON string lines) and emits deterministic per-line results in `json|table|markdown` format. - -* `advise export [--conversation-id ] [--tenant ] [--user ] [--limit ] [--format ] [--output ]` - - * Exports advisory conversation history through the existing AdvisoryAI conversation endpoints (`/v1/advisory-ai/conversations`). - * When no `--conversation-id` is provided, the CLI lists conversations for the scope and fetches each conversation deterministically by `conversationId` before rendering. - -### 2.12 Decision evidence (new) - -- `decision export` - - * Parameters: `--cve`, `--product `, `--scan-id `, `--output-dir`. - * Pulls `decision.openvex.json`, `decision.dsse.json`, `rekor.txt`, and evidence metadata from Policy Engine and writes them into the `bench/findings//` layout defined in [docs/benchmarks/vex-evidence-playbook.md](../benchmarks/vex-evidence-playbook.md). - * When `--sync` is set, uploads the bundle to Git (bench repo) with deterministic commit messages. - -- `decision verify` - - * Offline verifier that wraps `tools/verify.sh`/`verify.py` from the bench repo. Checks DSSE signature, optional Rekor inclusion, and recomputes digests for reachability/SBOM artifacts. - * Supports `--from bench` (local path) and `--remote` (fetch via API). Exit codes align with `verify.sh` (0 success, 3 signature failure, 18 truncated evidence). - -- `decision compare` - - * Executes the benchmark harness against baseline scanners (Trivy/Syft/Grype/Snyk/Xray), capturing false-positive reduction, mean-time-to-decision, and reproducibility metrics into `results/summary.csv`. - * Flags regressions when Stella Ops produces more false positives or slower MTTD than the configured target. - -All verbs require scopes `policy.findings:read`, `signer.verify`, and (for Rekor lookups) `attestor.read`. They honour sealed-mode rules by falling back to offline verification only when Rekor/Signer endpoints are unreachable. - -### 2.13 Air-gap guard - -- CLI outbound HTTP flows (Authority auth, backend APIs, advisory downloads) route through `StellaOps.AirGap.Policy`. When sealed mode is active the CLI refuses commands that would require external egress and surfaces the shared `AIRGAP_EGRESS_BLOCKED` remediation guidance instead of attempting the request. - -### 2.14 Unknowns export artifacts - -- `unknowns export [--band ] [--format ] [--schema-version ] [--output ]` - - * `json` now emits a deterministic export envelope with `schemaVersion`, `exportedAt`, `itemCount`, and sorted `items`. - * `csv` prepends a schema metadata comment (`schema_version`, `exported_at`, `item_count`) before the column header. - * `ndjson` emits a metadata header line followed by schema-scoped item lines. - * The formal contract artifact for the JSON envelope is at `src/Cli/StellaOps.Cli/Commands/Schemas/unknowns-export.schema.json`. - ---- - -## 3) AuthN: Authority + DPoP - -### 3.1 Token acquisition - -* **Device‑code**: the CLI opens an OIDC device code flow against **Authority**; the browser login is optional for service principals. -* **Client‑credentials**: service principals use **private_key_jwt** or **mTLS** to get tokens. - -### 3.2 DPoP key management - -* On first login, the CLI generates an **ephemeral JWK** (Ed25519) and stores it in the **OS keychain** (Keychain/DPAPI/KWallet/Gnome Keyring). -* Every request to backend services includes a **DPoP proof**; CLI refreshes tokens as needed. - -### 3.3 Multi‑audience & scopes - -* CLI requests **audiences** as needed per verb: - - * `scanner` for scan/export/report/diff - * `signer` (indirect; usually backend calls Signer) - * `attestor` for verify (requires `attestor.verify` scope; read-only verbs fall back to `attestor.read`) - * `concelier`/`excititor` for admin verbs - -CLI rejects verbs if required scopes are missing. - ---- - -## 4) Process model & reliability - -### 4.1 HTTP client - -* Single **http2** client with connection pooling, DNS pinning, retry/backoff (idempotent GET/POST marked safe). -* **DPoP nonce** handling: on `401` with nonce challenge, CLI replays once. - -### 4.2 Streaming - -* `scan` and `report` support **server‑sent JSON lines** (progress events). -* `--json` prints machine events; human mode shows compact spinners and crucial updates only. - -### 4.3 Exit codes (CI‑safe) - -| Code | Meaning | -| ---- | ------------------------------------------- | -| 0 | Success | -| 2 | Policy fail (final report verdict=fail) | -| 3 | Verification failed (attestation/signature) | -| 4 | Auth error (invalid/missing token/DPoP) | -| 5 | Resource not found (image/SBOM) | -| 6 | Rate limited / quota exceeded | -| 7 | Backend unavailable (retryable) | -| 9 | Invalid arguments | -| 11–17 | Aggregation-only guard violation (`ERR_AOC_00x`) | -| 18 | Verification truncated (increase `--limit`) | -| 70 | Transport/authentication failure | -| 71 | CLI usage error (missing tenant, invalid cursor) | - ---- - -## 5) Configuration model - -**Precedence:** CLI flags → env vars → config file → defaults. - -**Config file**: `${XDG_CONFIG_HOME}/stellaops/config.yaml` (Windows: `%APPDATA%\StellaOps\config.yaml`) - -```yaml -cli: - authority: "https://authority.internal" - backend: - scanner: "https://scanner-web.internal" - attestor: "https://attestor.internal" - concelier: "https://concelier-web.internal" - excititor: "https://excititor-web.internal" - auth: - audienceDefault: "scanner" - deviceCode: true - output: - json: false - color: auto - tls: - caBundle: "/etc/ssl/certs/ca-bundle.crt" - offline: - kitMirror: "s3://mirror/stellaops-kit" -``` - -Environment variables: `STELLAOPS_AUTHORITY`, `STELLAOPS_SCANNER_URL`, etc. - ---- - -## 6) Buildx generator orchestration - -* `buildx install` locates the Docker root directory, writes the **generator** plugin manifest, and pulls `stellaops/sbom-indexer` image (pinned digest). -* `buildx build` wrapper injects: - - * `--attest=type=sbom,generator=stellaops/sbom-indexer` - * `--label org.stellaops.request=sbom` -* Post‑build: CLI optionally calls **Scanner.WebService** to **verify referrers**, **compose** image SBOMs, and **attest** via Signer/Attestor. - -**Detection**: If Buildx or generator unavailable, CLI falls back to **post‑build scan** with a warning. - ---- - -## 7) Artifact handling - -* **Downloads** (`export sbom`, `report final`): stream to file; compute sha256 on the fly; write sidecar `.sha256` and optional **verification bundle** (if `--bundle`). -* **Uploads** (`offline kit import`): chunked upload; retry on transient errors; show progress bar (unless `--json`). - ---- - -## 8) Security posture - -* **DPoP private keys** stored in **OS keychain**; metadata cached in config. -* **No plaintext tokens** on disk; short‑lived **OpToks** held in memory. -* **TLS**: verify backend certificates; allow custom CA bundle for on‑prem. -* **Redaction**: CLI logs remove `Authorization`, DPoP headers, PoE tokens. -* **Supply chain**: CLI distribution binaries are **cosign‑signed**; `stellaops version --verify` checks its own signature. - ---- - -## 9) Observability - -* `--verbose` adds request IDs, timings, and retry traces. -* **Metrics** (optional, disabled by default): Prometheus text file exporter for local monitoring in long‑running agents. -* **Structured logs** (`--json`): per‑event JSON lines with `ts`, `verb`, `status`, `latencyMs`. - ---- - -## 10) Performance targets - -* Startup ≤ **20 ms** (AOT). -* `scan image` request/response overhead ≤ **5 ms** (excluding server work). -* Buildx wrapper overhead negligible (<1 ms). -* Large artifact download (100 MB) sustained ≥ **80 MB/s** on local networks. - ---- - -## 11) Tests & golden outputs - -* **Unit tests**: argument parsing, config precedence, URL resolution, DPoP proof creation. -* **Integration tests** (Testcontainers): mock Authority/Scanner/Attestor; CI pipeline with fake registry. -* **Golden outputs**: verb snapshots for `--json` across OSes; kept in `tests/golden/…`. -* **Contract tests**: ensure API shapes match service OpenAPI; fail build if incompatible. - ---- - -## 12) Error envelopes (human + JSON) - -**Human:** - -``` -✖ Policy FAIL: 3 high, 1 critical (VEX suppressed 12) - - pkg:rpm/openssl (CVE-2025-12345) — affected (vendor) — fixed in 3.0.14 - - pkg:npm/lodash (GHSA-xxxx) — affected — no fix - See: https://ui.internal/scans/sha256:... -Exit code: 2 -``` - -**JSON (`--json`):** - -```json -{ "event":"report", "status":"fail", "critical":1, "high":3, "url":"https://ui..." } -``` - ---- - -## 13) Admin & advanced flags - -* `--authority`, `--scanner`, `--attestor`, `--concelier`, `--excititor` override config URLs. -* `--no-color`, `--quiet`, `--json`. -* `--timeout`, `--retries`, `--retry-backoff-ms`. -* `--ca-bundle`, `--insecure` (dev only; prints warning). -* `--trace` (dump HTTP traces to file; scrubbed). - ---- - -## 14) Interop with other tools - -* Emits **CycloneDX Protobuf** directly to stdout when `export sbom --format cdx-pb --out -`. -* Pipes to `jq`/`yq` cleanly in JSON mode. -* Can act as a **credential helper** for scripts: `stellaops auth token --aud scanner` prints a one‑shot token for curl. - ---- - -## 15) Packaging & distribution - -* **Installers**: deb/rpm (postinst registers completions), Homebrew, Scoop, Winget, MSI/MSIX. -* **Shell completions**: bash/zsh/fish/pwsh. -* **Update channel**: `stellaops self-update` (optional) fetches cosign‑signed release manifest; corporate environments can disable. - ---- - -## 16) Security hard lines - -* Refuse to print token values; redact Authorization headers in verbose output. -* Disallow `--insecure` unless `STELLAOPS_CLI_ALLOW_INSECURE=1` set (double opt‑in). -* Enforce **short token TTL**; refresh proactively when <30 s left. -* Device‑code cache binding to **machine** and **user** (protect against copy to other machines). - ---- - -## 17) Wire sequences - -**A) Scan & wait with attestation** - -```mermaid -sequenceDiagram - autonumber - participant CLI - participant Auth as Authority - participant SW as Scanner.WebService - participant SG as Signer - participant AT as Attestor - - CLI->>Auth: device code flow (DPoP) - Auth-->>CLI: OpTok (aud=scanner) - - CLI->>SW: POST /scans { imageRef, attest:true } - SW-->>CLI: { scanId } - CLI->>SW: GET /scans/{id} (poll) - SW-->>CLI: { status: completed, artifacts, rekor? } # if attested - - alt attestation pending - SW->>SG: POST /sign/dsse (server-side) - SG-->>SW: DSSE - SW->>AT: POST /rekor/entries - AT-->>SW: { uuid, proof } - end - - CLI->>SW: GET /sboms/?format=cdx-pb&view=usage - SW-->>CLI: bytes -``` - -**B) Verify attestation by artifact** - -```mermaid -sequenceDiagram - autonumber - participant CLI - participant AT as Attestor - - CLI->>AT: POST /rekor/verify { artifactSha256 } - AT-->>CLI: { ok:true, uuid, index, logURL } -``` - ---- - -## 18) Roadmap (CLI) - -* `scan fs ` (local filesystem tree) → upload to backend for analysis. -* `policy test --sbom ` (simulate policy results offline using local policy bundle). -* `runtime capture` (developer mode) — capture small `/proc//maps` for troubleshooting. -* Pluggable output renderers for SARIF/HTML (admin‑controlled). - ---- - -## 19) Example CI snippets - -**GitHub Actions (post‑build)** - -```yaml -- name: Login (device code w/ OIDC broker) - run: stellaops auth login --json --authority ${{ secrets.AUTHORITY_URL }} - -- name: Scan - run: stellaops scan image ${{ steps.build.outputs.digest }} --wait --json - -- name: Export (usage view, protobuf) - run: stellaops export sbom ${{ steps.build.outputs.digest }} --view usage --format cdx-pb --out sbom.pb - -- name: Verify attestation - run: stellaops verify attestation --artifact $(sha256sum sbom.pb | cut -d' ' -f1) --json -``` - -**GitLab (buildx generator)** - -```yaml -script: - - stellaops buildx install - - docker buildx build --attest=type=sbom,generator=stellaops/sbom-indexer -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA . - - stellaops scan image $CI_REGISTRY_IMAGE@$IMAGE_DIGEST --wait --json -``` - ---- - -## 20) Test matrix (OS/arch) - -* Linux: ubuntu‑20.04/22.04/24.04 (x64, arm64), alpine (musl). -* macOS: 13–15 (x64, arm64). -* Windows: 10/11, Server 2019/2022 (x64, arm64). -* Docker engines: Docker Desktop, containerd‑based runners. - -## 21) OCI Referrers for Evidence Storage - -### 21.1 Overview - -Two new evidence sub-commands enable native OCI Referrers API integration: - -| Command | Purpose | -| --- | --- | -| `stella evidence push-referrer` | Push an evidence artifact as an OCI referrer attached to a subject digest | -| `stella evidence list-referrers` | List all OCI referrers for a given artifact, with optional artifact-type filter | - -### 21.2 Push Referrer - -Options: `--image` (required), `--artifact-type` (required), `--file` (required), `--annotation` (repeatable), `--offline`. - -Builds an OCI image manifest v2 with `subject` field pointing to the target -digest. The evidence file becomes a single layer. Config is the OCI empty -descriptor. Annotations are passed through to the manifest. - -`--offline` mode simulates the push locally without network, producing the -manifest JSON on stdout for auditing. - -### 21.3 List Referrers - -Options: `--image` (required), `--digest` (optional), `--artifact-type` (filter), `--format` (table|json), `--offline`. - -Uses `IOciRegistryClient.GetReferrersAsync()` (already implemented) to query -the registry's Referrers API. `--offline` returns simulated data for testing. - -### 21.4 Implementation - + +* `sources ingest --dry-run --source --input [--tenant ... --format table|json --output file]` + + * Normalises documents (handles gzip/base64), posts them to the backend `aoc/ingest/dry-run` route, and exits non-zero when guard violations are detected. + * Defaults to table output with ANSI colour; `--json`/`--output` produce deterministic JSON for CI pipelines. + +* `aoc verify [--since ] [--limit ] [--sources list] [--codes list] [--format table|json] [--export file] [--tenant id] [--no-color]` + + * Replays guard checks against stored raw documents. Maps backend `ERR_AOC_00x` codes onto deterministic exit codes so CI can block regressions. + * Supports pagination hints (`--limit`, `--since`), tenant scoping via `--tenant` or `STELLA_TENANT`, and JSON exports for evidence lockers. + +### 2.10 Key management (file KMS support) + +* `kms export --key-id --output [--version ] [--force]` + + * Decrypts the file-backed KMS store (passphrase supplied via `--passphrase`, `STELLAOPS_KMS_PASSPHRASE`, or interactive prompt) and writes a portable JSON bundle (`KmsKeyMaterial`) with key metadata and coordinates for offline escrow or replication. + +* `kms import --key-id --input [--version ]` + + * Imports a previously exported bundle into the local KMS root (`kms/` by default), promotes the imported version to `Active`, and preserves existing versions by marking them `PendingRotation`. Prompts for the passphrase when not provided to keep automation password-safe. + +### 2.11 CI Template Generation (Sprint 015) + +* `ci init --platform [--template ] [--mode ] [--output ] [--force] [--offline] [--scanner-image ]` + + * Generates ready-to-run CI workflow templates for the specified platform(s). + * Template types: + * `gate` - PR gating workflow that blocks merges on policy violations. + * `scan` - Scheduled/push scan workflow for container images. + * `verify` - Verification workflow for attestations and signatures. + * `full` - All templates combined. + * Modes control attestation behavior: + * `scan-only` - Scan without attestation. + * `scan-attest` - Scan and create attestations (default). + * `scan-vex` - Scan with VEX document generation. + * `--offline` generates templates with pinned digests for air-gapped environments. + +* `ci list` + + * Lists available template types and supported platforms. + +* `ci validate ` + + * Validates a generated workflow file for correctness. + * Checks integration IDs, registry endpoints, and AuthRef references. + +**Generated files:** +- GitHub: `.github/workflows/stellaops-{gate,scan,verify}.yml` +- GitLab: `.gitlab-ci.yml` or `.gitlab/stellaops-{scan,verify}.yml` +- Gitea: `.gitea/workflows/stellaops-{gate,scan,verify}.yml` + +**Implementation:** `CiCommandGroup.cs`, `CiTemplates.cs` in `src/Cli/StellaOps.Cli/Commands/`. + +Both subcommands honour offline-first expectations (no network access) and normalise relative roots via `--root` when operators mirror the credential store. + +### 2.11 Advisory AI (RAG summaries) + +* `advise run --advisory-key [--artifact-id id] [--artifact-purl purl] [--policy-version v] [--profile profile] [--section name] [--force-refresh] [--timeout seconds]` + + * Calls the Advisory AI service (`/v1/advisory-ai/pipeline/{task}` + `/outputs/{cacheKey}`) to materialise a deterministic plan, queue execution, and poll for the generated brief. + * Renders plan metadata (cache key, prompt template, token budgets), guardrail results, provenance hashes/signatures, and citation list. Exit code is non-zero if guardrails block or the command times out. + * Uses `STELLAOPS_ADVISORYAI_URL` when configured; otherwise it reuses the backend base address and adds `X-StellaOps-Scopes` (`advisory:run` + task scope) per request. + * `--timeout 0` performs a single cache lookup (for CI flows that only want cached artefacts). + +* `advise ask "" [--evidence] [--no-action] [--conversation-id ] [--context ]` + + * Calls advisory chat endpoints, returns a cited answer with evidence refs. + * `--no-action` disables action proposals; `--evidence` forces evidence chips in output. + * `--file ` processes newline-delimited JSON batch requests (`{"query":"..."}` or JSON string lines) and emits deterministic per-line results in `json|table|markdown` format. + +* `advise export [--conversation-id ] [--tenant ] [--user ] [--limit ] [--format ] [--output ]` + + * Exports advisory conversation history through the existing AdvisoryAI conversation endpoints (`/v1/advisory-ai/conversations`). + * When no `--conversation-id` is provided, the CLI lists conversations for the scope and fetches each conversation deterministically by `conversationId` before rendering. + +### 2.12 Decision evidence (new) + +- `decision export` + + * Parameters: `--cve`, `--product `, `--scan-id `, `--output-dir`. + * Pulls `decision.openvex.json`, `decision.dsse.json`, `rekor.txt`, and evidence metadata from Policy Engine and writes them into the `bench/findings//` layout defined in [docs/benchmarks/vex-evidence-playbook.md](../benchmarks/vex-evidence-playbook.md). + * When `--sync` is set, uploads the bundle to Git (bench repo) with deterministic commit messages. + +- `decision verify` + + * Offline verifier that wraps `tools/verify.sh`/`verify.py` from the bench repo. Checks DSSE signature, optional Rekor inclusion, and recomputes digests for reachability/SBOM artifacts. + * Supports `--from bench` (local path) and `--remote` (fetch via API). Exit codes align with `verify.sh` (0 success, 3 signature failure, 18 truncated evidence). + +- `decision compare` + + * Executes the benchmark harness against baseline scanners (Trivy/Syft/Grype/Snyk/Xray), capturing false-positive reduction, mean-time-to-decision, and reproducibility metrics into `results/summary.csv`. + * Flags regressions when Stella Ops produces more false positives or slower MTTD than the configured target. + +All verbs require scopes `policy.findings:read`, `signer.verify`, and (for Rekor lookups) `attestor.read`. They honour sealed-mode rules by falling back to offline verification only when Rekor/Signer endpoints are unreachable. + +### 2.13 Air-gap guard + +- CLI outbound HTTP flows (Authority auth, backend APIs, advisory downloads) route through `StellaOps.AirGap.Policy`. When sealed mode is active the CLI refuses commands that would require external egress and surfaces the shared `AIRGAP_EGRESS_BLOCKED` remediation guidance instead of attempting the request. + +### 2.14 Unknowns export artifacts + +- `unknowns export [--band ] [--format ] [--schema-version ] [--output ]` + + * `json` now emits a deterministic export envelope with `schemaVersion`, `exportedAt`, `itemCount`, and sorted `items`. + * `csv` prepends a schema metadata comment (`schema_version`, `exported_at`, `item_count`) before the column header. + * `ndjson` emits a metadata header line followed by schema-scoped item lines. + * The formal contract artifact for the JSON envelope is at `src/Cli/StellaOps.Cli/Commands/Schemas/unknowns-export.schema.json`. + +--- + +## 3) AuthN: Authority + DPoP + +### 3.1 Token acquisition + +* **Device‑code**: the CLI opens an OIDC device code flow against **Authority**; the browser login is optional for service principals. +* **Client‑credentials**: service principals use **private_key_jwt** or **mTLS** to get tokens. + +### 3.2 DPoP key management + +* On first login, the CLI generates an **ephemeral JWK** (Ed25519) and stores it in the **OS keychain** (Keychain/DPAPI/KWallet/Gnome Keyring). +* Every request to backend services includes a **DPoP proof**; CLI refreshes tokens as needed. + +### 3.3 Multi‑audience & scopes + +* CLI requests **audiences** as needed per verb: + + * `scanner` for scan/export/report/diff + * `signer` (indirect; usually backend calls Signer) + * `attestor` for verify (requires `attestor.verify` scope; read-only verbs fall back to `attestor.read`) + * `concelier`/`excititor` for admin verbs + +CLI rejects verbs if required scopes are missing. + +--- + +## 4) Process model & reliability + +### 4.1 HTTP client + +* Single **http2** client with connection pooling, DNS pinning, retry/backoff (idempotent GET/POST marked safe). +* **DPoP nonce** handling: on `401` with nonce challenge, CLI replays once. + +### 4.2 Streaming + +* `scan` and `report` support **server‑sent JSON lines** (progress events). +* `--json` prints machine events; human mode shows compact spinners and crucial updates only. + +### 4.3 Exit codes (CI‑safe) + +| Code | Meaning | +| ---- | ------------------------------------------- | +| 0 | Success | +| 2 | Policy fail (final report verdict=fail) | +| 3 | Verification failed (attestation/signature) | +| 4 | Auth error (invalid/missing token/DPoP) | +| 5 | Resource not found (image/SBOM) | +| 6 | Rate limited / quota exceeded | +| 7 | Backend unavailable (retryable) | +| 9 | Invalid arguments | +| 11–17 | Aggregation-only guard violation (`ERR_AOC_00x`) | +| 18 | Verification truncated (increase `--limit`) | +| 70 | Transport/authentication failure | +| 71 | CLI usage error (missing tenant, invalid cursor) | + +--- + +## 5) Configuration model + +**Precedence:** CLI flags → env vars → config file → defaults. + +**Config file**: `${XDG_CONFIG_HOME}/stellaops/config.yaml` (Windows: `%APPDATA%\StellaOps\config.yaml`) + +```yaml +cli: + authority: "https://authority.internal" + backend: + scanner: "https://scanner-web.internal" + attestor: "https://attestor.internal" + concelier: "https://concelier-web.internal" + excititor: "https://excititor-web.internal" + auth: + audienceDefault: "scanner" + deviceCode: true + output: + json: false + color: auto + tls: + caBundle: "/etc/ssl/certs/ca-bundle.crt" + offline: + kitMirror: "s3://mirror/stellaops-kit" +``` + +Environment variables: `STELLAOPS_AUTHORITY`, `STELLAOPS_SCANNER_URL`, etc. + +--- + +## 6) Buildx generator orchestration + +* `buildx install` locates the Docker root directory, writes the **generator** plugin manifest, and pulls `stellaops/sbom-indexer` image (pinned digest). +* `buildx build` wrapper injects: + + * `--attest=type=sbom,generator=stellaops/sbom-indexer` + * `--label org.stellaops.request=sbom` +* Post‑build: CLI optionally calls **Scanner.WebService** to **verify referrers**, **compose** image SBOMs, and **attest** via Signer/Attestor. + +**Detection**: If Buildx or generator unavailable, CLI falls back to **post‑build scan** with a warning. + +--- + +## 7) Artifact handling + +* **Downloads** (`export sbom`, `report final`): stream to file; compute sha256 on the fly; write sidecar `.sha256` and optional **verification bundle** (if `--bundle`). +* **Uploads** (`offline kit import`): chunked upload; retry on transient errors; show progress bar (unless `--json`). + +--- + +## 8) Security posture + +* **DPoP private keys** stored in **OS keychain**; metadata cached in config. +* **No plaintext tokens** on disk; short‑lived **OpToks** held in memory. +* **TLS**: verify backend certificates; allow custom CA bundle for on‑prem. +* **Redaction**: CLI logs remove `Authorization`, DPoP headers, PoE tokens. +* **Supply chain**: CLI distribution binaries are **cosign‑signed**; `stellaops version --verify` checks its own signature. + +--- + +## 9) Observability + +* `--verbose` adds request IDs, timings, and retry traces. +* **Metrics** (optional, disabled by default): Prometheus text file exporter for local monitoring in long‑running agents. +* **Structured logs** (`--json`): per‑event JSON lines with `ts`, `verb`, `status`, `latencyMs`. + +--- + +## 10) Performance targets + +* Startup ≤ **20 ms** (AOT). +* `scan image` request/response overhead ≤ **5 ms** (excluding server work). +* Buildx wrapper overhead negligible (<1 ms). +* Large artifact download (100 MB) sustained ≥ **80 MB/s** on local networks. + +--- + +## 11) Tests & golden outputs + +* **Unit tests**: argument parsing, config precedence, URL resolution, DPoP proof creation. +* **Integration tests** (Testcontainers): mock Authority/Scanner/Attestor; CI pipeline with fake registry. +* **Golden outputs**: verb snapshots for `--json` across OSes; kept in `tests/golden/…`. +* **Contract tests**: ensure API shapes match service OpenAPI; fail build if incompatible. + +--- + +## 12) Error envelopes (human + JSON) + +**Human:** + +``` +✖ Policy FAIL: 3 high, 1 critical (VEX suppressed 12) + - pkg:rpm/openssl (CVE-2025-12345) — affected (vendor) — fixed in 3.0.14 + - pkg:npm/lodash (GHSA-xxxx) — affected — no fix + See: https://ui.internal/scans/sha256:... +Exit code: 2 +``` + +**JSON (`--json`):** + +```json +{ "event":"report", "status":"fail", "critical":1, "high":3, "url":"https://ui..." } +``` + +--- + +## 13) Admin & advanced flags + +* `--authority`, `--scanner`, `--attestor`, `--concelier`, `--excititor` override config URLs. +* `--no-color`, `--quiet`, `--json`. +* `--timeout`, `--retries`, `--retry-backoff-ms`. +* `--ca-bundle`, `--insecure` (dev only; prints warning). +* `--trace` (dump HTTP traces to file; scrubbed). + +--- + +## 14) Interop with other tools + +* Emits **CycloneDX Protobuf** directly to stdout when `export sbom --format cdx-pb --out -`. +* Pipes to `jq`/`yq` cleanly in JSON mode. +* Can act as a **credential helper** for scripts: `stellaops auth token --aud scanner` prints a one‑shot token for curl. + +--- + +## 15) Packaging & distribution + +* **Installers**: deb/rpm (postinst registers completions), Homebrew, Scoop, Winget, MSI/MSIX. +* **Shell completions**: bash/zsh/fish/pwsh. +* **Update channel**: `stellaops self-update` (optional) fetches cosign‑signed release manifest; corporate environments can disable. + +--- + +## 16) Security hard lines + +* Refuse to print token values; redact Authorization headers in verbose output. +* Disallow `--insecure` unless `STELLAOPS_CLI_ALLOW_INSECURE=1` set (double opt‑in). +* Enforce **short token TTL**; refresh proactively when <30 s left. +* Device‑code cache binding to **machine** and **user** (protect against copy to other machines). + +--- + +## 17) Wire sequences + +**A) Scan & wait with attestation** + +```mermaid +sequenceDiagram + autonumber + participant CLI + participant Auth as Authority + participant SW as Scanner.WebService + participant SG as Signer + participant AT as Attestor + + CLI->>Auth: device code flow (DPoP) + Auth-->>CLI: OpTok (aud=scanner) + + CLI->>SW: POST /scans { imageRef, attest:true } + SW-->>CLI: { scanId } + CLI->>SW: GET /scans/{id} (poll) + SW-->>CLI: { status: completed, artifacts, rekor? } # if attested + + alt attestation pending + SW->>SG: POST /sign/dsse (server-side) + SG-->>SW: DSSE + SW->>AT: POST /rekor/entries + AT-->>SW: { uuid, proof } + end + + CLI->>SW: GET /sboms/?format=cdx-pb&view=usage + SW-->>CLI: bytes +``` + +**B) Verify attestation by artifact** + +```mermaid +sequenceDiagram + autonumber + participant CLI + participant AT as Attestor + + CLI->>AT: POST /rekor/verify { artifactSha256 } + AT-->>CLI: { ok:true, uuid, index, logURL } +``` + +--- + +## 18) Roadmap (CLI) + +* `scan fs ` (local filesystem tree) → upload to backend for analysis. +* `policy test --sbom ` (simulate policy results offline using local policy bundle). +* `runtime capture` (developer mode) — capture small `/proc//maps` for troubleshooting. +* Pluggable output renderers for SARIF/HTML (admin‑controlled). + +--- + +## 19) Example CI snippets + +**GitHub Actions (post‑build)** + +```yaml +- name: Login (device code w/ OIDC broker) + run: stellaops auth login --json --authority ${{ secrets.AUTHORITY_URL }} + +- name: Scan + run: stellaops scan image ${{ steps.build.outputs.digest }} --wait --json + +- name: Export (usage view, protobuf) + run: stellaops export sbom ${{ steps.build.outputs.digest }} --view usage --format cdx-pb --out sbom.pb + +- name: Verify attestation + run: stellaops verify attestation --artifact $(sha256sum sbom.pb | cut -d' ' -f1) --json +``` + +**GitLab (buildx generator)** + +```yaml +script: + - stellaops buildx install + - docker buildx build --attest=type=sbom,generator=stellaops/sbom-indexer -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA . + - stellaops scan image $CI_REGISTRY_IMAGE@$IMAGE_DIGEST --wait --json +``` + +--- + +## 20) Test matrix (OS/arch) + +* Linux: ubuntu‑20.04/22.04/24.04 (x64, arm64), alpine (musl). +* macOS: 13–15 (x64, arm64). +* Windows: 10/11, Server 2019/2022 (x64, arm64). +* Docker engines: Docker Desktop, containerd‑based runners. + +## 21) OCI Referrers for Evidence Storage + +### 21.1 Overview + +Two new evidence sub-commands enable native OCI Referrers API integration: + +| Command | Purpose | +| --- | --- | +| `stella evidence push-referrer` | Push an evidence artifact as an OCI referrer attached to a subject digest | +| `stella evidence list-referrers` | List all OCI referrers for a given artifact, with optional artifact-type filter | + +### 21.2 Push Referrer + +Options: `--image` (required), `--artifact-type` (required), `--file` (required), `--annotation` (repeatable), `--offline`. + +Builds an OCI image manifest v2 with `subject` field pointing to the target +digest. The evidence file becomes a single layer. Config is the OCI empty +descriptor. Annotations are passed through to the manifest. + +`--offline` mode simulates the push locally without network, producing the +manifest JSON on stdout for auditing. + +### 21.3 List Referrers + +Options: `--image` (required), `--digest` (optional), `--artifact-type` (filter), `--format` (table|json), `--offline`. + +Uses `IOciRegistryClient.GetReferrersAsync()` (already implemented) to query +the registry's Referrers API. `--offline` returns simulated data for testing. + +### 21.4 Implementation + - `EvidenceReferrerCommands.cs` — static command builder class following existing pattern - Wired into `EvidenceCommandGroup.BuildEvidenceCommand()` alongside existing sub-commands - Reuses `IOciRegistryClient` and OCI models from `StellaOps.Cli.Services` diff --git a/docs/modules/graph/architecture.md b/docs/modules/graph/architecture.md index 1f152a6e7..3c5213081 100644 --- a/docs/modules/graph/architecture.md +++ b/docs/modules/graph/architecture.md @@ -1,40 +1,40 @@ -# Graph architecture - -> Derived from Epic 5 – SBOM Graph Explorer; this section captures the core model, pipeline, and API expectations. Extend with diagrams as implementation matures. - -## 1) Core model - -- **Nodes:** - - `Artifact` (application/image digest) with metadata (tenant, environment, labels). - - `SBOM` (sbom digest, format, version/sequence, chain id). - - `Component` (package/version, purl, ecosystem). - - `File`/`Path` (source files, binary paths) with hash/time metadata. - - `License` nodes linked to components and SBOM attestations. - - `Advisory` and `VEXStatement` nodes linking to Concelier/Excititor records via digests. - - `PolicyVersion` nodes representing signed policy packs. -- **Edges:** directed, timestamped relationships such as `DEPENDS_ON`, `BUILT_FROM`, `DECLARED_IN`, `AFFECTED_BY`, `VEX_EXEMPTS`, `GOVERNS_WITH`, `OBSERVED_RUNTIME`, `SBOM_VERSION_OF`, and `SBOM_LINEAGE_*`. Each edge carries provenance (SRM hash, SBOM digest, policy run ID). -- **Overlays:** computed index tables providing fast access to reachability, blast radius, and differential views (e.g., `graph_overlay/vuln/{tenant}/{advisoryKey}`). Runtime endpoints emit overlays inline (`policy.overlay.v1`, `openvex.v1`) with deterministic overlay IDs (`sha256(tenant|nodeId|overlayKind)`) and sampled explain traces on policy overlays. - -## 2) Pipelines - -1. **Ingestion:** Cartographer/SBOM Service emit SBOM snapshots (`sbom_snapshot` events) captured by the Graph Indexer. Ledger lineage references become `SBOM_VERSION_OF` + `SBOM_LINEAGE_*` edges. Advisories/VEX from Concelier/Excititor generate edge updates, policy runs attach overlay metadata. -2. **ETL:** Normalises nodes/edges into canonical IDs, deduplicates, enforces tenant partitions, and writes to the graph store (planned: Neo4j-compatible or PostgreSQL adjacency lists). -3. **Overlay computation:** Batch workers build materialised views for frequently used queries (impact lists, saved queries, policy overlays) and store as immutable blobs for Offline Kit exports. -4. **Diffing:** `graph_diff` jobs compare two snapshots (e.g., pre/post deploy) and generate signed diff manifests for UI/CLI consumption. -5. **Analytics (Runtime & Signals 140.A):** background workers run Louvain-style clustering + degree/betweenness approximations on ingested graphs, emitting overlays per tenant/snapshot and writing cluster ids back to nodes when enabled. - -## 3) APIs - -- `POST /graph/search` — NDJSON node tiles with cursor paging, tenant + scope guards. -- `POST /graph/query` — NDJSON nodes/edges/stats/cursor with budgets (tiles/nodes/edges) and optional inline overlays (`includeOverlays=true`) emitting `policy.overlay.v1` and `openvex.v1` payloads; overlay IDs are `sha256(tenant|nodeId|overlayKind)`; policy overlay may include a sampled `explainTrace`. -- `POST /graph/paths` — bounded BFS (depth ≤6) returning path nodes/edges/stats; honours budgets and overlays. -- `POST /graph/diff` — compares `snapshotA` vs `snapshotB`, streaming node/edge added/removed/changed tiles plus stats; budget enforcement mirrors `/graph/query`. -- `POST /graph/export` — async job producing deterministic manifests (`sha256`, size, format) for `ndjson/csv/graphml/png/svg`; download via `/graph/export/{jobId}`. -- `POST /graph/lineage` - returns SBOM lineage nodes/edges anchored by `artifactDigest` or `sbomDigest`, with optional relationship filters and depth limits. -- **Edge Metadata API** (added 2025-01): - - `POST /graph/edges/metadata` — batch query for edge explanations; request contains `EdgeIds[]`, response includes `EdgeTileWithMetadata[]` with full provenance. - - `GET /graph/edges/{edgeId}/metadata` — single edge metadata with explanation, via, provenance, and evidence references. - - `GET /graph/edges/path/{sourceNodeId}/{targetNodeId}` — returns all edges on the shortest path between two nodes, each with metadata. +# Graph architecture + +> Derived from Epic 5 – SBOM Graph Explorer; this section captures the core model, pipeline, and API expectations. Extend with diagrams as implementation matures. + +## 1) Core model + +- **Nodes:** + - `Artifact` (application/image digest) with metadata (tenant, environment, labels). + - `SBOM` (sbom digest, format, version/sequence, chain id). + - `Component` (package/version, purl, ecosystem). + - `File`/`Path` (source files, binary paths) with hash/time metadata. + - `License` nodes linked to components and SBOM attestations. + - `Advisory` and `VEXStatement` nodes linking to Concelier/Excititor records via digests. + - `PolicyVersion` nodes representing signed policy packs. +- **Edges:** directed, timestamped relationships such as `DEPENDS_ON`, `BUILT_FROM`, `DECLARED_IN`, `AFFECTED_BY`, `VEX_EXEMPTS`, `GOVERNS_WITH`, `OBSERVED_RUNTIME`, `SBOM_VERSION_OF`, and `SBOM_LINEAGE_*`. Each edge carries provenance (SRM hash, SBOM digest, policy run ID). +- **Overlays:** computed index tables providing fast access to reachability, blast radius, and differential views (e.g., `graph_overlay/vuln/{tenant}/{advisoryKey}`). Runtime endpoints emit overlays inline (`policy.overlay.v1`, `openvex.v1`) with deterministic overlay IDs (`sha256(tenant|nodeId|overlayKind)`) and sampled explain traces on policy overlays. + +## 2) Pipelines + +1. **Ingestion:** Cartographer/SBOM Service emit SBOM snapshots (`sbom_snapshot` events) captured by the Graph Indexer. Ledger lineage references become `SBOM_VERSION_OF` + `SBOM_LINEAGE_*` edges. Advisories/VEX from Concelier/Excititor generate edge updates, policy runs attach overlay metadata. +2. **ETL:** Normalises nodes/edges into canonical IDs, deduplicates, enforces tenant partitions, and writes to the graph store (planned: Neo4j-compatible or PostgreSQL adjacency lists). +3. **Overlay computation:** Batch workers build materialised views for frequently used queries (impact lists, saved queries, policy overlays) and store as immutable blobs for Offline Kit exports. +4. **Diffing:** `graph_diff` jobs compare two snapshots (e.g., pre/post deploy) and generate signed diff manifests for UI/CLI consumption. +5. **Analytics (Runtime & Signals 140.A):** background workers run Louvain-style clustering + degree/betweenness approximations on ingested graphs, emitting overlays per tenant/snapshot and writing cluster ids back to nodes when enabled. + +## 3) APIs + +- `POST /graph/search` — NDJSON node tiles with cursor paging, tenant + scope guards. +- `POST /graph/query` — NDJSON nodes/edges/stats/cursor with budgets (tiles/nodes/edges) and optional inline overlays (`includeOverlays=true`) emitting `policy.overlay.v1` and `openvex.v1` payloads; overlay IDs are `sha256(tenant|nodeId|overlayKind)`; policy overlay may include a sampled `explainTrace`. +- `POST /graph/paths` — bounded BFS (depth ≤6) returning path nodes/edges/stats; honours budgets and overlays. +- `POST /graph/diff` — compares `snapshotA` vs `snapshotB`, streaming node/edge added/removed/changed tiles plus stats; budget enforcement mirrors `/graph/query`. +- `POST /graph/export` — async job producing deterministic manifests (`sha256`, size, format) for `ndjson/csv/graphml/png/svg`; download via `/graph/export/{jobId}`. +- `POST /graph/lineage` - returns SBOM lineage nodes/edges anchored by `artifactDigest` or `sbomDigest`, with optional relationship filters and depth limits. +- **Edge Metadata API** (added 2025-01): + - `POST /graph/edges/metadata` — batch query for edge explanations; request contains `EdgeIds[]`, response includes `EdgeTileWithMetadata[]` with full provenance. + - `GET /graph/edges/{edgeId}/metadata` — single edge metadata with explanation, via, provenance, and evidence references. + - `GET /graph/edges/path/{sourceNodeId}/{targetNodeId}` — returns all edges on the shortest path between two nodes, each with metadata. - `GET /graph/edges/by-reason/{reason}` — query edges by `EdgeReason` enum (e.g., `SbomDependency`, `AdvisoryAffects`, `VexStatement`, `RuntimeTrace`). - `GET /graph/edges/by-evidence?evidenceType=&evidenceRef=` — query edges by evidence reference. - Legacy: `GET /graph/nodes/{id}`, `POST /graph/query/saved`, `GET /graph/impact/{advisoryKey}`, `POST /graph/overlay/policy` remain in spec but should align to the NDJSON surfaces above as they are brought forward. @@ -54,11 +54,11 @@ - Rate limiting and audit logging use the resolved tenant context; authenticated flows no longer collapse to ambiguous `"unknown"` tenant keys. ### 3.2) Edge Metadata Contracts - -The edge metadata system provides explainability for graph relationships: - -- **EdgeReason** enum: `Unknown`, `SbomDependency`, `StaticSymbol`, `RuntimeTrace`, `PackageManifest`, `Lockfile`, `BuildArtifact`, `ImageLayer`, `AdvisoryAffects`, `VexStatement`, `PolicyOverlay`, `AttestationRef`, `OperatorAnnotation`, `TransitiveInference`, `Provenance`. -- **EdgeVia** record: Describes how the edge was discovered (method, version, timestamp, confidence in basis points, evidence reference). + +The edge metadata system provides explainability for graph relationships: + +- **EdgeReason** enum: `Unknown`, `SbomDependency`, `StaticSymbol`, `RuntimeTrace`, `PackageManifest`, `Lockfile`, `BuildArtifact`, `ImageLayer`, `AdvisoryAffects`, `VexStatement`, `PolicyOverlay`, `AttestationRef`, `OperatorAnnotation`, `TransitiveInference`, `Provenance`. +- **EdgeVia** record: Describes how the edge was discovered (method, version, timestamp, confidence in basis points, evidence reference). - **EdgeExplanationPayload** record: Full explanation including reason, via, human-readable summary, evidence list, provenance reference, and tags. - **EdgeProvenanceRef** record: Source system, collection timestamp, SBOM digest, scan digest, attestation ID, event offset. - **EdgeTileWithMetadata** record: Extends `EdgeTile` with `Explanation` property containing the full metadata. @@ -72,34 +72,34 @@ The edge metadata system provides explainability for graph relationships: - This rollout localizes selected error paths (for example, edge/export not found, invalid reason, and tenant/auth validation text) for `en-US` and `de-DE`. ## 4) Storage considerations - -- Backed by either: - - **Relational + adjacency** (PostgreSQL tables `graph_nodes`, `graph_edges`, `graph_overlays`) with deterministic ordering and streaming exports. - - Or **Graph DB** (e.g., Neo4j/Cosmos Gremlin) behind an abstraction layer; choice depends on deployment footprint. -- All storages require tenant partitioning, append-only change logs, and export manifests for Offline Kits. - -## 5) Offline & export - -- Each snapshot packages `nodes.jsonl`, `edges.jsonl`, `overlays/` plus manifest with hash, counts, and provenance. Export Center consumes these artefacts for graph-specific bundles. -- Saved queries and overlays include deterministic IDs so Offline Kit consumers can import and replay results. -- Runtime hosts register the SBOM ingest pipeline via `services.AddSbomIngestPipeline(...)`. Snapshot exports default to `./artifacts/graph-snapshots` but can be redirected with `STELLAOPS_GRAPH_SNAPSHOT_DIR` or the `SbomIngestOptions.SnapshotRootDirectory` callback. -- Analytics overlays are exported as NDJSON (`overlays/clusters.ndjson`, `overlays/centrality.ndjson`) ordered by node id; `overlays/manifest.json` mirrors snapshot id and counts for offline parity. - -## 6) Observability - -- Metrics: ingestion lag (`graph_ingest_lag_seconds`), node/edge counts, query latency per saved query, overlay generation duration. -- New analytics metrics: `graph_analytics_runs_total`, `graph_analytics_failures_total`, `graph_analytics_clusters_total`, `graph_analytics_centrality_total`, plus change-stream/backfill counters (`graph_changes_total`, `graph_backfill_total`, `graph_change_failures_total`, `graph_change_lag_seconds`). -- Logs: structured events for ETL stages and query execution (with trace IDs). -- Traces: ETL pipeline spans, query engine spans. - -## 7) Rollout notes - -- Phase 1: ingest SBOM + advisories, deliver impact queries. -- Phase 2: add VEX overlays, policy overlays, diff tooling. -- Phase 3: expose runtime/Zastava edges and AI-assisted recommendations (future). - -### Local testing note - -Set `STELLAOPS_TEST_POSTGRES_CONNECTION` to a reachable PostgreSQL instance before running `tests/Graph/StellaOps.Graph.Indexer.Tests`. The test harness falls back to `Host=127.0.0.1;Port=5432;Database=stellaops_test`, then Testcontainers for PostgreSQL, but the CI workflow requires the environment variable to be present to ensure upsert coverage runs against a managed database. Use `STELLAOPS_GRAPH_SNAPSHOT_DIR` (or the `AddSbomIngestPipeline` options callback) to control where graph snapshot artefacts land during local runs. - -Refer to the module README and implementation plan for immediate context, and update this document once component boundaries and data flows are finalised. + +- Backed by either: + - **Relational + adjacency** (PostgreSQL tables `graph_nodes`, `graph_edges`, `graph_overlays`) with deterministic ordering and streaming exports. + - Or **Graph DB** (e.g., Neo4j/Cosmos Gremlin) behind an abstraction layer; choice depends on deployment footprint. +- All storages require tenant partitioning, append-only change logs, and export manifests for Offline Kits. + +## 5) Offline & export + +- Each snapshot packages `nodes.jsonl`, `edges.jsonl`, `overlays/` plus manifest with hash, counts, and provenance. Export Center consumes these artefacts for graph-specific bundles. +- Saved queries and overlays include deterministic IDs so Offline Kit consumers can import and replay results. +- Runtime hosts register the SBOM ingest pipeline via `services.AddSbomIngestPipeline(...)`. Snapshot exports default to `./artifacts/graph-snapshots` but can be redirected with `STELLAOPS_GRAPH_SNAPSHOT_DIR` or the `SbomIngestOptions.SnapshotRootDirectory` callback. +- Analytics overlays are exported as NDJSON (`overlays/clusters.ndjson`, `overlays/centrality.ndjson`) ordered by node id; `overlays/manifest.json` mirrors snapshot id and counts for offline parity. + +## 6) Observability + +- Metrics: ingestion lag (`graph_ingest_lag_seconds`), node/edge counts, query latency per saved query, overlay generation duration. +- New analytics metrics: `graph_analytics_runs_total`, `graph_analytics_failures_total`, `graph_analytics_clusters_total`, `graph_analytics_centrality_total`, plus change-stream/backfill counters (`graph_changes_total`, `graph_backfill_total`, `graph_change_failures_total`, `graph_change_lag_seconds`). +- Logs: structured events for ETL stages and query execution (with trace IDs). +- Traces: ETL pipeline spans, query engine spans. + +## 7) Rollout notes + +- Phase 1: ingest SBOM + advisories, deliver impact queries. +- Phase 2: add VEX overlays, policy overlays, diff tooling. +- Phase 3: expose runtime/Zastava edges and AI-assisted recommendations (future). + +### Local testing note + +Set `STELLAOPS_TEST_POSTGRES_CONNECTION` to a reachable PostgreSQL instance before running `tests/Graph/StellaOps.Graph.Indexer.Tests`. The test harness falls back to `Host=127.0.0.1;Port=5432;Database=stellaops_test`, then Testcontainers for PostgreSQL, but the CI workflow requires the environment variable to be present to ensure upsert coverage runs against a managed database. Use `STELLAOPS_GRAPH_SNAPSHOT_DIR` (or the `AddSbomIngestPipeline` options callback) to control where graph snapshot artefacts land during local runs. + +Refer to the module README and implementation plan for immediate context, and update this document once component boundaries and data flows are finalised. diff --git a/docs/modules/policy/guides/assistant-parameters.md b/docs/modules/policy/guides/assistant-parameters.md index 83b6d2ec8..6c299dc55 100644 --- a/docs/modules/policy/guides/assistant-parameters.md +++ b/docs/modules/policy/guides/assistant-parameters.md @@ -1,16 +1,16 @@ -# Advisory AI Assistant Parameters +# Advisory AI Assistant Parameters -_Primary audience: platform operators & policy authors • Updated: 2026-01-13_ +_Primary audience: platform operators & policy authors • Updated: 2026-01-13_ -This note centralises the tunable knobs that control Advisory AI’s planner, retrieval stack, inference clients, and guardrails. All options live under the `AdvisoryAI` configuration section and can be set via `appsettings.*` files or environment variables using ASP.NET Core’s double-underscore convention (`ADVISORYAI__Inference__Mode`, etc.). Chat quotas and tool allowlists can also be overridden per tenant/user via the chat settings endpoints; appsettings/env values are defaults. +This note centralises the tunable knobs that control Advisory AI’s planner, retrieval stack, inference clients, and guardrails. All options live under the `AdvisoryAI` configuration section and can be set via `appsettings.*` files or environment variables using ASP.NET Core’s double-underscore convention (`ADVISORYAI__Inference__Mode`, etc.). Chat quotas and tool allowlists can also be overridden per tenant/user via the chat settings endpoints; appsettings/env values are defaults. -**Policy/version pin** — For Sprint 0111, use the policy bundle hash shipped on 2025-11-19 (same drop as `CLI-VULN-29-001` / `CLI-VEX-30-001`). Set `AdvisoryAI:PolicyVersion` or `ADVISORYAI__POLICYVERSION=2025.11.19` in deployments; include the hash in DSSE metadata for Offline Kits. +**Policy/version pin** — For Sprint 0111, use the policy bundle hash shipped on 2025-11-19 (same drop as `CLI-VULN-29-001` / `CLI-VEX-30-001`). Set `AdvisoryAI:PolicyVersion` or `ADVISORYAI__POLICYVERSION=2025.11.19` in deployments; include the hash in DSSE metadata for Offline Kits. | Area | Key(s) | Environment variable | Default | Notes | | --- | --- | --- | --- | --- | | Inference mode | `AdvisoryAI:Inference:Mode` | `ADVISORYAI__INFERENCE__MODE` | `Local` | `Local` runs the deterministic pipeline only; `Remote` posts sanitized prompts to `Remote.BaseAddress`. | -| Remote base URI | `AdvisoryAI:Inference:Remote:BaseAddress` | `ADVISORYAI__INFERENCE__REMOTE__BASEADDRESS` | — | Required when `Mode=Remote`. HTTPS strongly recommended. | -| Remote API key | `AdvisoryAI:Inference:Remote:ApiKey` | `ADVISORYAI__INFERENCE__REMOTE__APIKEY` | — | Injected as `Authorization: Bearer ` when present. | +| Remote base URI | `AdvisoryAI:Inference:Remote:BaseAddress` | `ADVISORYAI__INFERENCE__REMOTE__BASEADDRESS` | — | Required when `Mode=Remote`. HTTPS strongly recommended. | +| Remote API key | `AdvisoryAI:Inference:Remote:ApiKey` | `ADVISORYAI__INFERENCE__REMOTE__APIKEY` | — | Injected as `Authorization: Bearer ` when present. | | Remote timeout | `AdvisoryAI:Inference:Remote:TimeoutSeconds` | `ADVISORYAI__INFERENCE__REMOTE__TIMEOUTSECONDS` | `30` | Failing requests fall back to the sanitized prompt with `inference.fallback_reason=remote_timeout`. | | Guardrail prompt cap | `AdvisoryAI:Guardrails:MaxPromptLength` | `ADVISORYAI__GUARDRAILS__MAXPROMPTLENGTH` | `16000` | Prompts longer than the cap are blocked with `prompt_too_long`. | | Guardrail citations | `AdvisoryAI:Guardrails:RequireCitations` | `ADVISORYAI__GUARDRAILS__REQUIRECITATIONS` | `true` | When `true`, at least one citation must accompany every prompt. | @@ -39,12 +39,12 @@ This note centralises the tunable knobs that control Advisory AI’s planne --- -## 1. Inference knobs & “temperature” +## 1. Inference knobs & “temperature” Advisory AI supports two inference modes: -- **Local (default)** – The orchestrator emits deterministic prompts and the worker returns the sanitized prompt verbatim. This mode is offline-friendly and does **not** call any external LLMs. There is no stochastic “temperature” here—the pipeline is purely rule-based. -- **Remote** – Sanitized prompts, citations, and metadata are POSTed to `Remote.BaseAddress + Remote.Endpoint` (default `/v1/inference`). Remote providers control sampling temperature on their side. StellaOps treats remote responses deterministically: we record the provider’s `modelId`, token usage, and any metadata they return. If your remote tier exposes a temperature knob, set it there; Advisory AI simply forwards the prompt. +- **Local (default)** – The orchestrator emits deterministic prompts and the worker returns the sanitized prompt verbatim. This mode is offline-friendly and does **not** call any external LLMs. There is no stochastic “temperature” here—the pipeline is purely rule-based. +- **Remote** – Sanitized prompts, citations, and metadata are POSTed to `Remote.BaseAddress + Remote.Endpoint` (default `/v1/inference`). Remote providers control sampling temperature on their side. StellaOps treats remote responses deterministically: we record the provider’s `modelId`, token usage, and any metadata they return. If your remote tier exposes a temperature knob, set it there; Advisory AI simply forwards the prompt. ### Remote inference quick sample @@ -68,14 +68,14 @@ Advisory AI supports two inference modes: | Setting | Default | Explanation | | --- | --- | --- | -| `MaxPromptLength` | 16000 chars | Upper bound enforced after redaction. Increase cautiously—remote providers typically cap prompts at 32k tokens. | +| `MaxPromptLength` | 16000 chars | Upper bound enforced after redaction. Increase cautiously—remote providers typically cap prompts at 32k tokens. | | `RequireCitations` | `true` | Forces each prompt to include at least one citation. Disable only when testing synthetic prompts. | | `BlockedPhrases[]` | `ignore previous instructions`, `disregard earlier instructions`, `you are now the system`, `override the system prompt`, `please jailbreak` | Inline list merged with the optional file. Comparisons are case-insensitive. | -| `BlockedPhraseFile` | — | Points to a newline-delimited list. Relative paths resolve against the content root (`AdvisoryAI.Hosting` sticks to AppContext base). | +| `BlockedPhraseFile` | — | Points to a newline-delimited list. Relative paths resolve against the content root (`AdvisoryAI.Hosting` sticks to AppContext base). | | `EntropyThreshold` | `3.5` | Shannon entropy threshold for high-risk token redaction. Set to `0` to disable entropy checks. | | `EntropyMinLength` | `20` | Minimum token length evaluated by the entropy scrubber. | | `AllowlistPatterns` | Defaults (sha256/sha1/sha384/sha512) | Regex patterns that bypass entropy redaction for known-safe identifiers. | -| `AllowlistFile` | — | Optional allowlist file (JSON array or newline-delimited). Paths resolve against the content root. | +| `AllowlistFile` | — | Optional allowlist file (JSON array or newline-delimited). Paths resolve against the content root. | Violations surface in the response metadata (`guardrail.violations[*]`) and increment `advisory_ai_guardrail_blocks_total`. Console consumes the same payload for its ribbon state. @@ -95,9 +95,9 @@ Each task type (Summary, Conflict, Remediation) inherits the defaults below. Ove | Task | `StructuredMaxChunks` | `VectorTopK` | `VectorQueries` (default) | `SbomMaxTimelineEntries` | `SbomMaxDependencyPaths` | `IncludeBlastRadius` | | --- | --- | --- | --- | --- | --- | --- | -| Summary | 25 | 5 | `Summarize key facts`, `What is impacted?` | 10 | 20 | ✔ | -| Conflict | 30 | 6 | `Highlight conflicting statements`, `Where do sources disagree?` | 8 | 15 | ✖ | -| Remediation | 35 | 6 | `Provide remediation steps`, `Outline mitigations and fixes` | 12 | 25 | ✔ | +| Summary | 25 | 5 | `Summarize key facts`, `What is impacted?` | 10 | 20 | ✔ | +| Conflict | 30 | 6 | `Highlight conflicting statements`, `Where do sources disagree?` | 8 | 15 | ✖ | +| Remediation | 35 | 6 | `Provide remediation steps`, `Outline mitigations and fixes` | 12 | 25 | ✔ | These knobs act as weighting levers: lower `VectorTopK` emphasises deterministic evidence; higher values favor breadth. `StructuredMaxChunks` bounds how many CSAF/OSV/VEX chunks reach the prompt, keeping token budgets predictable. @@ -107,17 +107,17 @@ These knobs act as weighting levers: lower `VectorTopK` emphasises deterministic | Task | Prompt tokens | Completion tokens | | --- | --- | --- | -| Summary | 2 048 | 512 | -| Conflict | 2 048 | 512 | -| Remediation | 2 048 | 640 | +| Summary | 2 048 | 512 | +| Conflict | 2 048 | 512 | +| Remediation | 2 048 | 640 | Overwrite via `AdvisoryAI:Tasks:Summary:Budget:PromptTokens`, etc. The worker records actual consumption in the response metadata (`inference.prompt_tokens`, `inference.completion_tokens`). ## 5. Cache TTLs & queue directories -- **Plan cache TTLs** – In-memory and file-system caches honour `AdvisoryAI:PlanCache:DefaultTimeToLive` (default 10 minutes) and `CleanupInterval` (default 5 minutes). Shorten the TTL to reduce stale plans or increase it to favour offline reuse. Both values accept ISO 8601 or `hh:mm:ss` time spans. -- **Queue & storage paths** – `AdvisoryAI:Queue:DirectoryPath`, `AdvisoryAI:Storage:PlanCacheDirectory`, and `AdvisoryAI:Storage:OutputDirectory` default to `data/advisory-ai/{queue,plans,outputs}` under the content root; override these when mounting RWX volumes in sovereign clusters. -- **Output TTLs** – Output artefacts inherit the host file-system retention policies. Combine `DefaultTimeToLive` with a cron or systemd timer to prune `outputs/` periodically when operating in remote-inference-heavy environments. +- **Plan cache TTLs** – In-memory and file-system caches honour `AdvisoryAI:PlanCache:DefaultTimeToLive` (default 10 minutes) and `CleanupInterval` (default 5 minutes). Shorten the TTL to reduce stale plans or increase it to favour offline reuse. Both values accept ISO 8601 or `hh:mm:ss` time spans. +- **Queue & storage paths** – `AdvisoryAI:Queue:DirectoryPath`, `AdvisoryAI:Storage:PlanCacheDirectory`, and `AdvisoryAI:Storage:OutputDirectory` default to `data/advisory-ai/{queue,plans,outputs}` under the content root; override these when mounting RWX volumes in sovereign clusters. +- **Output TTLs** – Output artefacts inherit the host file-system retention policies. Combine `DefaultTimeToLive` with a cron or systemd timer to prune `outputs/` periodically when operating in remote-inference-heavy environments. ### Example: raised TTL & custom queue path @@ -138,6 +138,6 @@ Overwrite via `AdvisoryAI:Tasks:Summary:Budget:PromptTokens`, etc. The worker re ## 6. Operational notes - Updating **guardrail phrases** triggers only on host reload. When distributing blocked-phrase files via Offline Kits, keep filenames stable and version them through Git so QA can diff changes. -- **Temperature / sampling** remains a remote-provider concern. StellaOps records the provider’s `modelId` and exposes fallback metadata so policy authors can audit when sanitized prompts were returned instead of model output. +- **Temperature / sampling** remains a remote-provider concern. StellaOps records the provider’s `modelId` and exposes fallback metadata so policy authors can audit when sanitized prompts were returned instead of model output. - Always track changes in `docs/implplan/SPRINT_0111_0001_0001_advisoryai.md` (task `DOCS-AIAI-31-006`) when promoting this document so the guild can trace which parameters were added per sprint. diff --git a/docs/modules/scanner/architecture.md b/docs/modules/scanner/architecture.md index ca817e8e2..5c8a705c8 100644 --- a/docs/modules/scanner/architecture.md +++ b/docs/modules/scanner/architecture.md @@ -1,56 +1,56 @@ -# component_architecture_scanner.md — **Stella Ops Scanner** (2025Q4) - -> Aligned with Epic 6 – Vulnerability Explorer and Epic 10 – Export Center. - -> **Scope.** Implementation‑ready architecture for the **Scanner** subsystem: WebService, Workers, analyzers, SBOM assembly (inventory & usage), per‑layer caching, three‑way diffs, artifact catalog (RustFS default + PostgreSQL, S3-compatible fallback), attestation hand‑off, and scale/security posture. This document is the contract between the scanning plane and everything else (Policy, Excititor, Concelier, UI, CLI). +# component_architecture_scanner.md — **Stella Ops Scanner** (2025Q4) + +> Aligned with Epic 6 – Vulnerability Explorer and Epic 10 – Export Center. + +> **Scope.** Implementation‑ready architecture for the **Scanner** subsystem: WebService, Workers, analyzers, SBOM assembly (inventory & usage), per‑layer caching, three‑way diffs, artifact catalog (RustFS default + PostgreSQL, S3-compatible fallback), attestation hand‑off, and scale/security posture. This document is the contract between the scanning plane and everything else (Policy, Excititor, Concelier, UI, CLI). > **Related:** `docs/modules/scanner/operations/ai-code-guard.md` > **Storage profile:** `docs/modules/scanner/sbom-attestation-hot-lookup-profile.md` - ---- - -## 0) Mission & boundaries - -**Mission.** Produce **deterministic**, **explainable** SBOMs and diffs for container images and filesystems, quickly and repeatedly, without guessing. Emit two views: **Inventory** (everything present) and **Usage** (entrypoint closure + actually linked libs). Attach attestations through **Signer→Attestor→Rekor v2**. - -**Boundaries.** - -* Scanner **does not** produce PASS/FAIL. The backend (Policy + Excititor + Concelier) decides presentation and verdicts. -* Scanner **does not** keep third‑party SBOM warehouses. It may **bind** to existing attestations for exact hashes. -* Core analyzers are **deterministic** (no fuzzy identity). Optional heuristic plug‑ins (e.g., patch‑presence) run under explicit flags and never contaminate the core SBOM. - -SBOM dependency reachability inference uses dependency graphs to reduce false positives and -apply reachability-aware severity adjustments. See `src/Scanner/docs/sbom-reachability-filtering.md` -for policy configuration and reporting expectations. - ---- - -## 1) Solution & project layout - -``` -src/ - ├─ StellaOps.Scanner.WebService/ # REST control plane, catalog, diff, exports - ├─ StellaOps.Scanner.Worker/ # queue consumer; executes analyzers - ├─ StellaOps.Scanner.Models/ # DTOs, evidence, graph nodes, CDX/SPDX adapters - ├─ StellaOps.Scanner.Storage/ # PostgreSQL repositories; RustFS object client (default) + S3 fallback; ILM/GC - ├─ StellaOps.Scanner.Queue/ # queue abstraction (Valkey/NATS/RabbitMQ) - ├─ StellaOps.Scanner.Cache/ # layer cache; file CAS; bloom/bitmap indexes - ├─ StellaOps.Scanner.EntryTrace/ # ENTRYPOINT/CMD → terminal program resolver (shell AST) - ├─ StellaOps.Scanner.Analyzers.OS.[Apk|Dpkg|Rpm]/ - ├─ StellaOps.Scanner.Analyzers.Lang.[Java|Node|Bun|Python|Go|DotNet|Rust|Ruby|Php]/ - ├─ StellaOps.Scanner.Analyzers.Native.[ELF|PE|MachO]/ # PE/Mach-O planned (M2) - ├─ StellaOps.Scanner.Analyzers.Secrets/ # Secret leak detection (2026.01) - ├─ StellaOps.Scanner.Symbols.Native/ # NEW – native symbol reader/demangler (Sprint 401) - ├─ StellaOps.Scanner.CallGraph.Native/ # NEW – function/call-edge builder + CAS emitter - ├─ StellaOps.Scanner.Emit.CDX/ # CycloneDX (JSON + Protobuf) - ├─ StellaOps.Scanner.Emit.SPDX/ # SPDX 3.0.1 JSON - ├─ StellaOps.Scanner.Diff/ # image→layer→component three‑way diff - ├─ StellaOps.Scanner.Index/ # BOM‑Index sidecar (purls + roaring bitmaps) - ├─ StellaOps.Scanner.Tests.* # unit/integration/e2e fixtures - └─ Tools/ - ├─ StellaOps.Scanner.Sbomer.BuildXPlugin/ # BuildKit generator (image referrer SBOMs) - └─ StellaOps.Scanner.Sbomer.DockerImage/ # CLI‑driven scanner container -``` - + +--- + +## 0) Mission & boundaries + +**Mission.** Produce **deterministic**, **explainable** SBOMs and diffs for container images and filesystems, quickly and repeatedly, without guessing. Emit two views: **Inventory** (everything present) and **Usage** (entrypoint closure + actually linked libs). Attach attestations through **Signer→Attestor→Rekor v2**. + +**Boundaries.** + +* Scanner **does not** produce PASS/FAIL. The backend (Policy + Excititor + Concelier) decides presentation and verdicts. +* Scanner **does not** keep third‑party SBOM warehouses. It may **bind** to existing attestations for exact hashes. +* Core analyzers are **deterministic** (no fuzzy identity). Optional heuristic plug‑ins (e.g., patch‑presence) run under explicit flags and never contaminate the core SBOM. + +SBOM dependency reachability inference uses dependency graphs to reduce false positives and +apply reachability-aware severity adjustments. See `src/Scanner/docs/sbom-reachability-filtering.md` +for policy configuration and reporting expectations. + +--- + +## 1) Solution & project layout + +``` +src/ + ├─ StellaOps.Scanner.WebService/ # REST control plane, catalog, diff, exports + ├─ StellaOps.Scanner.Worker/ # queue consumer; executes analyzers + ├─ StellaOps.Scanner.Models/ # DTOs, evidence, graph nodes, CDX/SPDX adapters + ├─ StellaOps.Scanner.Storage/ # PostgreSQL repositories; RustFS object client (default) + S3 fallback; ILM/GC + ├─ StellaOps.Scanner.Queue/ # queue abstraction (Valkey/NATS/RabbitMQ) + ├─ StellaOps.Scanner.Cache/ # layer cache; file CAS; bloom/bitmap indexes + ├─ StellaOps.Scanner.EntryTrace/ # ENTRYPOINT/CMD → terminal program resolver (shell AST) + ├─ StellaOps.Scanner.Analyzers.OS.[Apk|Dpkg|Rpm]/ + ├─ StellaOps.Scanner.Analyzers.Lang.[Java|Node|Bun|Python|Go|DotNet|Rust|Ruby|Php]/ + ├─ StellaOps.Scanner.Analyzers.Native.[ELF|PE|MachO]/ # PE/Mach-O planned (M2) + ├─ StellaOps.Scanner.Analyzers.Secrets/ # Secret leak detection (2026.01) + ├─ StellaOps.Scanner.Symbols.Native/ # NEW – native symbol reader/demangler (Sprint 401) + ├─ StellaOps.Scanner.CallGraph.Native/ # NEW – function/call-edge builder + CAS emitter + ├─ StellaOps.Scanner.Emit.CDX/ # CycloneDX (JSON + Protobuf) + ├─ StellaOps.Scanner.Emit.SPDX/ # SPDX 3.0.1 JSON + ├─ StellaOps.Scanner.Diff/ # image→layer→component three‑way diff + ├─ StellaOps.Scanner.Index/ # BOM‑Index sidecar (purls + roaring bitmaps) + ├─ StellaOps.Scanner.Tests.* # unit/integration/e2e fixtures + └─ Tools/ + ├─ StellaOps.Scanner.Sbomer.BuildXPlugin/ # BuildKit generator (image referrer SBOMs) + └─ StellaOps.Scanner.Sbomer.DockerImage/ # CLI‑driven scanner container +``` + ### 1.0 Cartographer Ownership (Sprint 201) - Cartographer is owned by Scanner and implemented at `src/Scanner/StellaOps.Scanner.Cartographer/`. @@ -58,176 +58,176 @@ src/ - Legacy `src/Cartographer/` paths are retired; operational and build references now resolve through Scanner-owned solution/project paths. Per-analyzer notes (language analyzers): -- `docs/modules/scanner/analyzers-java.md` — Java/Kotlin (Maven, Gradle, fat archives) -- `docs/modules/scanner/dotnet-analyzer.md` — .NET (deps.json, NuGet, packages.lock.json, declared-only) -- `docs/modules/scanner/analyzers-python.md` — Python (pip, Poetry, pipenv, conda, editables, vendored) -- `docs/modules/scanner/analyzers-node.md` — Node.js (npm, Yarn, pnpm, multi-version locks) -- `docs/modules/scanner/analyzers-bun.md` — Bun (bun.lock v1, dev classification, patches) -- `docs/modules/scanner/analyzers-go.md` — Go (build info, modules) - -Cross-analyzer contract (identity safety, evidence locators, container layout): -- `docs/modules/scanner/language-analyzers-contract.md` — PURL vs explicit-key rules, evidence formats, bounded scanning - -Semantic entrypoint analysis (Sprint 0411): -- `docs/modules/scanner/semantic-entrypoint-schema.md` — Schema for intent, capabilities, threat vectors, and data boundaries - -Analyzer assemblies and buildx generators are packaged as **restart-time plug-ins** under `plugins/scanner/**` with manifests; services must restart to activate new plug-ins. - -### 1.3 Semantic Entrypoint Engine (Sprint 0411) - -The **Semantic Entrypoint Engine** enriches scan results with application-level understanding: - -- **Intent Classification** — Infers application type (WebServer, Worker, CliTool, Serverless, etc.) from framework detection and entrypoint analysis -- **Capability Detection** — Identifies system resource access patterns (network, filesystem, database, crypto) -- **Threat Vector Inference** — Maps capabilities to potential attack vectors with CWE/OWASP references -- **Data Boundary Mapping** — Tracks data flow boundaries with sensitivity classification - -Components: -- `StellaOps.Scanner.EntryTrace/Semantic/` — Core semantic types and orchestrator -- `StellaOps.Scanner.EntryTrace/Semantic/Adapters/` — Language-specific adapters (Python, Java, Node, .NET, Go) -- `StellaOps.Scanner.EntryTrace/Semantic/Analysis/` — Capability detection, threat inference, boundary mapping - -Integration points: -- `LanguageComponentRecord` includes semantic fields (`intent`, `capabilities[]`, `threatVectors[]`) -- `richgraph-v1` nodes carry semantic attributes via `semantic_*` keys -- CycloneDX/SPDX SBOMs include `stellaops:semantic.*` property extensions - -CLI usage: `stella scan --semantic ` enables semantic analysis in output. - -### 1.2 Native reachability upgrades (Nov 2026) - -- **Stripped-binary pipeline**: native analyzers must recover functions even without symbols (prolog patterns, xrefs, PLT/GOT, vtables). Emit a tool-agnostic neutral JSON (NJIF) with functions, CFG/CG, and evidence tags. Keep heuristics deterministic and record toolchain hashes in the scan manifest. -- **Synthetic roots**: treat `.preinit_array`, `.init_array`, legacy `.ctors`, and `_init` as graph entrypoints; add roots for constructors in each `DT_NEEDED` dependency. Tag edges from these roots with `phase=load` for explainers. -- **Build-id capture**: read `.note.gnu.build-id` for every ELF, store hex build-id alongside soname/path, propagate into `SymbolID`/`code_id`, and expose it to SBOM + runtime joiners. If missing, fall back to file hash and mark source accordingly. -- **PURL-resolved edges**: annotate call edges with the callee purl and `symbol_digest` so graphs merge with SBOM components. See `docs/modules/reach-graph/guides/purl-resolved-edges.md` for schema rules and acceptance tests. -- **Symbol hints in evidence**: reachability union and richgraph payloads emit `symbol {mangled,demangled,source,confidence}` plus optional `code_block_hash` for stripped/heuristic functions; serializers clamp confidence to [0,1] and uppercase `source` (`DWARF|PDB|SYM|NONE`) for determinism. -- **Unknowns emission**: when symbol -> purl mapping or edge targets remain unresolved, emit structured Unknowns to Signals (see `docs/modules/signals/guides/unknowns-registry.md`) instead of dropping evidence. -- **Hybrid attestation**: emit **graph-level DSSE** for every `richgraph-v1` (mandatory) and optional **edge-bundle DSSE** (≤512 edges) for runtime/init-root/contested edges or third-party provenance. Publish graph DSSE digests to Rekor by default; edge-bundle Rekor publish is policy-driven. CAS layout: `cas://reachability/graphs/{blake3}` for graph body, `.../{blake3}.dsse` for envelope, and `cas://reachability/edges/{graph_hash}/{bundle_id}[.dsse]` for bundles. Deterministic ordering before hashing/signing is required. -- **Deterministic call-graph manifest**: capture analyzer versions, feed hashes, toolchain digests, and flags in a manifest stored alongside `richgraph-v1`; replaying with the same manifest MUST yield identical node/edge sets and hashes (see `docs/modules/reach-graph/guides/lead.md`). - -### 1.1 Queue backbone (Valkey Streams) - -`StellaOps.Scanner.Queue` exposes a transport-agnostic contract (`IScanQueue`/`IScanQueueLease`) used by the WebService producer and Worker consumers. - -**Valkey Streams** is the standard transport. Uses consumer groups, deterministic idempotency keys (`scanner:jobs:idemp:*`), and supports lease claim (`XCLAIM`), renewal, exponential-backoff retries, and a `scanner:jobs:dead` stream for exhausted attempts. - -Metrics are emitted via `Meter` counters (`scanner_queue_enqueued_total`, `scanner_queue_retry_total`, `scanner_queue_deadletter_total`), and `ScannerQueueHealthCheck` pings the Valkey backend. Configuration is bound from `scanner.queue`: - -```yaml -scanner: - queue: - kind: valkey - valkey: - connectionString: "valkey://valkey:6379/0" - streamName: "scanner:jobs" - maxDeliveryAttempts: 5 - retryInitialBackoff: 00:00:05 - retryMaxBackoff: 00:02:00 -``` - -The DI extension (`AddScannerQueue`) wires the transport. - -**Runtime form‑factor:** two deployables - -* **Scanner.WebService** (stateless REST) -* **Scanner.Worker** (N replicas; queue‑driven) - ---- - -## 2) External dependencies - -* **OCI registry** with **Referrers API** (discover attached SBOMs/signatures). -* **RustFS** (default, offline-first) for SBOM artifacts; S3-compatible interface with **Object Lock** semantics emulated via retention headers; **ILM** for TTL. -* **PostgreSQL** for catalog, job state, diffs, ILM rules. -* **Queue** (Valkey Streams/NATS/RabbitMQ). -* **Authority** (on‑prem OIDC) for **OpToks** (DPoP/mTLS). -* **Signer** + **Attestor** (+ **Fulcio/KMS** + **Rekor v2**) for DSSE + transparency. - ---- - -## 3) Contracts & data model - -### 3.1 Evidence‑first component model - -**Nodes** - -* `Image`, `Layer`, `File` -* `Component` (`purl?`, `name`, `version?`, `type`, `id` — may be `bin:{sha256}`) -* `Executable` (ELF/PE/Mach‑O), `Library` (native or managed), `EntryScript` (shell/launcher) - -**Edges** (all carry **Evidence**) - -* `contains(Image|Layer → File)` -* `installs(PackageDB → Component)` (OS database row) -* `declares(InstalledMetadata → Component)` (dist‑info, pom.properties, deps.json…) -* `links_to(Executable → Library)` (ELF `DT_NEEDED`, PE imports) -* `calls(EntryScript → Program)` (file:line from shell AST) -* `attests(Rekor → Component|Image)` (SBOM/predicate binding) -* `bound_from_attestation(Component_attested → Component_observed)` (hash equality proof) - -**Evidence** - -``` -{ source: enum, locator: (path|offset|line), sha256?, method: enum, timestamp } -``` - -No confidences. Either a fact is proven with listed mechanisms, or it is not claimed. - -### 3.2 Catalog schema (PostgreSQL) - -* `artifacts` - - ``` - { _id, type: layer-bom|image-bom|diff|index, - format: cdx-json|cdx-pb|spdx-json, - bytesSha256, size, rekor: { uuid,index,url }?, - ttlClass, immutable, refCount, createdAt } - ``` -* `images { imageDigest, repo, tag?, arch, createdAt, lastSeen }` -* `layers { layerDigest, mediaType, size, createdAt, lastSeen }` -* `links { fromType, fromDigest, artifactId }` // image/layer -> artifact -* `jobs { _id, kind, args, state, startedAt, heartbeatAt, endedAt, error }` -* `lifecycleRules { ruleId, scope, ttlDays, retainIfReferenced, immutable }` -* `ruby.packages { _id: scanId, imageDigest, generatedAtUtc, packages[] }` // decoded `RubyPackageInventory` rows for CLI/Policy reuse -* `bun.packages { _id: scanId, imageDigest, generatedAtUtc, packages[] }` // decoded `BunPackageInventory` rows for CLI/Policy reuse - -### 3.3 Object store layout (RustFS) - -``` -layers//sbom.cdx.json.zst -layers//sbom.spdx.json.zst -images//inventory.cdx.pb # CycloneDX Protobuf -images//usage.cdx.pb -indexes//bom-index.bin # purls + roaring bitmaps -diffs/_/diff.json.zst -attest/.dsse.json # DSSE bundle (cert chain + Rekor proof) -``` - -RustFS exposes a deterministic HTTP API (`PUT|GET|DELETE /api/v1/buckets/{bucket}/objects/{key}`). -Scanner clients tag immutable uploads with `X-RustFS-Immutable: true` and, when retention applies, -`X-RustFS-Retain-Seconds: `. Additional headers can be injected via -`scanner.artifactStore.headers` to support custom auth or proxy requirements. RustFS provides the standard S3-compatible interface for all artifact storage. - ---- - -## 4) REST API (Scanner.WebService) - -All under `/api/v1/scanner`. Auth: **OpTok** (DPoP/mTLS); RBAC scopes. - -``` -POST /scans { imageRef|digest, force?:bool } → { scanId } -GET /scans/{id} → { status, imageDigest, artifacts[], rekor? } -GET /sboms/{imageDigest} ?format=cdx-json|cdx-pb|spdx-json&view=inventory|usage → bytes -POST /sbom/upload { artifactRef, sbom|sbomBase64, format?, source? } -> { sbomId, analysisJobId } -GET /sbom/uploads/{sbomId} -> upload record + provenance -GET /scans/{id}/ruby-packages → { scanId, imageDigest, generatedAt, packages[] } -GET /scans/{id}/bun-packages → { scanId, imageDigest, generatedAt, packages[] } -GET /diff?old=&new=&view=inventory|usage → diff.json -POST /exports { imageDigest, format, view, attest?:bool } → { artifactId, rekor? } -POST /reports { imageDigest, policyRevision? } → { reportId, rekor? } # delegates to backend policy+vex -GET /catalog/artifacts/{id} → { meta } -GET /healthz | /readyz | /metrics -``` +- `docs/modules/scanner/analyzers-java.md` — Java/Kotlin (Maven, Gradle, fat archives) +- `docs/modules/scanner/dotnet-analyzer.md` — .NET (deps.json, NuGet, packages.lock.json, declared-only) +- `docs/modules/scanner/analyzers-python.md` — Python (pip, Poetry, pipenv, conda, editables, vendored) +- `docs/modules/scanner/analyzers-node.md` — Node.js (npm, Yarn, pnpm, multi-version locks) +- `docs/modules/scanner/analyzers-bun.md` — Bun (bun.lock v1, dev classification, patches) +- `docs/modules/scanner/analyzers-go.md` — Go (build info, modules) + +Cross-analyzer contract (identity safety, evidence locators, container layout): +- `docs/modules/scanner/language-analyzers-contract.md` — PURL vs explicit-key rules, evidence formats, bounded scanning + +Semantic entrypoint analysis (Sprint 0411): +- `docs/modules/scanner/semantic-entrypoint-schema.md` — Schema for intent, capabilities, threat vectors, and data boundaries + +Analyzer assemblies and buildx generators are packaged as **restart-time plug-ins** under `plugins/scanner/**` with manifests; services must restart to activate new plug-ins. + +### 1.3 Semantic Entrypoint Engine (Sprint 0411) + +The **Semantic Entrypoint Engine** enriches scan results with application-level understanding: + +- **Intent Classification** — Infers application type (WebServer, Worker, CliTool, Serverless, etc.) from framework detection and entrypoint analysis +- **Capability Detection** — Identifies system resource access patterns (network, filesystem, database, crypto) +- **Threat Vector Inference** — Maps capabilities to potential attack vectors with CWE/OWASP references +- **Data Boundary Mapping** — Tracks data flow boundaries with sensitivity classification + +Components: +- `StellaOps.Scanner.EntryTrace/Semantic/` — Core semantic types and orchestrator +- `StellaOps.Scanner.EntryTrace/Semantic/Adapters/` — Language-specific adapters (Python, Java, Node, .NET, Go) +- `StellaOps.Scanner.EntryTrace/Semantic/Analysis/` — Capability detection, threat inference, boundary mapping + +Integration points: +- `LanguageComponentRecord` includes semantic fields (`intent`, `capabilities[]`, `threatVectors[]`) +- `richgraph-v1` nodes carry semantic attributes via `semantic_*` keys +- CycloneDX/SPDX SBOMs include `stellaops:semantic.*` property extensions + +CLI usage: `stella scan --semantic ` enables semantic analysis in output. + +### 1.2 Native reachability upgrades (Nov 2026) + +- **Stripped-binary pipeline**: native analyzers must recover functions even without symbols (prolog patterns, xrefs, PLT/GOT, vtables). Emit a tool-agnostic neutral JSON (NJIF) with functions, CFG/CG, and evidence tags. Keep heuristics deterministic and record toolchain hashes in the scan manifest. +- **Synthetic roots**: treat `.preinit_array`, `.init_array`, legacy `.ctors`, and `_init` as graph entrypoints; add roots for constructors in each `DT_NEEDED` dependency. Tag edges from these roots with `phase=load` for explainers. +- **Build-id capture**: read `.note.gnu.build-id` for every ELF, store hex build-id alongside soname/path, propagate into `SymbolID`/`code_id`, and expose it to SBOM + runtime joiners. If missing, fall back to file hash and mark source accordingly. +- **PURL-resolved edges**: annotate call edges with the callee purl and `symbol_digest` so graphs merge with SBOM components. See `docs/modules/reach-graph/guides/purl-resolved-edges.md` for schema rules and acceptance tests. +- **Symbol hints in evidence**: reachability union and richgraph payloads emit `symbol {mangled,demangled,source,confidence}` plus optional `code_block_hash` for stripped/heuristic functions; serializers clamp confidence to [0,1] and uppercase `source` (`DWARF|PDB|SYM|NONE`) for determinism. +- **Unknowns emission**: when symbol -> purl mapping or edge targets remain unresolved, emit structured Unknowns to Signals (see `docs/modules/signals/guides/unknowns-registry.md`) instead of dropping evidence. +- **Hybrid attestation**: emit **graph-level DSSE** for every `richgraph-v1` (mandatory) and optional **edge-bundle DSSE** (≤512 edges) for runtime/init-root/contested edges or third-party provenance. Publish graph DSSE digests to Rekor by default; edge-bundle Rekor publish is policy-driven. CAS layout: `cas://reachability/graphs/{blake3}` for graph body, `.../{blake3}.dsse` for envelope, and `cas://reachability/edges/{graph_hash}/{bundle_id}[.dsse]` for bundles. Deterministic ordering before hashing/signing is required. +- **Deterministic call-graph manifest**: capture analyzer versions, feed hashes, toolchain digests, and flags in a manifest stored alongside `richgraph-v1`; replaying with the same manifest MUST yield identical node/edge sets and hashes (see `docs/modules/reach-graph/guides/lead.md`). + +### 1.1 Queue backbone (Valkey Streams) + +`StellaOps.Scanner.Queue` exposes a transport-agnostic contract (`IScanQueue`/`IScanQueueLease`) used by the WebService producer and Worker consumers. + +**Valkey Streams** is the standard transport. Uses consumer groups, deterministic idempotency keys (`scanner:jobs:idemp:*`), and supports lease claim (`XCLAIM`), renewal, exponential-backoff retries, and a `scanner:jobs:dead` stream for exhausted attempts. + +Metrics are emitted via `Meter` counters (`scanner_queue_enqueued_total`, `scanner_queue_retry_total`, `scanner_queue_deadletter_total`), and `ScannerQueueHealthCheck` pings the Valkey backend. Configuration is bound from `scanner.queue`: + +```yaml +scanner: + queue: + kind: valkey + valkey: + connectionString: "valkey://valkey:6379/0" + streamName: "scanner:jobs" + maxDeliveryAttempts: 5 + retryInitialBackoff: 00:00:05 + retryMaxBackoff: 00:02:00 +``` + +The DI extension (`AddScannerQueue`) wires the transport. + +**Runtime form‑factor:** two deployables + +* **Scanner.WebService** (stateless REST) +* **Scanner.Worker** (N replicas; queue‑driven) + +--- + +## 2) External dependencies + +* **OCI registry** with **Referrers API** (discover attached SBOMs/signatures). +* **RustFS** (default, offline-first) for SBOM artifacts; S3-compatible interface with **Object Lock** semantics emulated via retention headers; **ILM** for TTL. +* **PostgreSQL** for catalog, job state, diffs, ILM rules. +* **Queue** (Valkey Streams/NATS/RabbitMQ). +* **Authority** (on‑prem OIDC) for **OpToks** (DPoP/mTLS). +* **Signer** + **Attestor** (+ **Fulcio/KMS** + **Rekor v2**) for DSSE + transparency. + +--- + +## 3) Contracts & data model + +### 3.1 Evidence‑first component model + +**Nodes** + +* `Image`, `Layer`, `File` +* `Component` (`purl?`, `name`, `version?`, `type`, `id` — may be `bin:{sha256}`) +* `Executable` (ELF/PE/Mach‑O), `Library` (native or managed), `EntryScript` (shell/launcher) + +**Edges** (all carry **Evidence**) + +* `contains(Image|Layer → File)` +* `installs(PackageDB → Component)` (OS database row) +* `declares(InstalledMetadata → Component)` (dist‑info, pom.properties, deps.json…) +* `links_to(Executable → Library)` (ELF `DT_NEEDED`, PE imports) +* `calls(EntryScript → Program)` (file:line from shell AST) +* `attests(Rekor → Component|Image)` (SBOM/predicate binding) +* `bound_from_attestation(Component_attested → Component_observed)` (hash equality proof) + +**Evidence** + +``` +{ source: enum, locator: (path|offset|line), sha256?, method: enum, timestamp } +``` + +No confidences. Either a fact is proven with listed mechanisms, or it is not claimed. + +### 3.2 Catalog schema (PostgreSQL) + +* `artifacts` + + ``` + { _id, type: layer-bom|image-bom|diff|index, + format: cdx-json|cdx-pb|spdx-json, + bytesSha256, size, rekor: { uuid,index,url }?, + ttlClass, immutable, refCount, createdAt } + ``` +* `images { imageDigest, repo, tag?, arch, createdAt, lastSeen }` +* `layers { layerDigest, mediaType, size, createdAt, lastSeen }` +* `links { fromType, fromDigest, artifactId }` // image/layer -> artifact +* `jobs { _id, kind, args, state, startedAt, heartbeatAt, endedAt, error }` +* `lifecycleRules { ruleId, scope, ttlDays, retainIfReferenced, immutable }` +* `ruby.packages { _id: scanId, imageDigest, generatedAtUtc, packages[] }` // decoded `RubyPackageInventory` rows for CLI/Policy reuse +* `bun.packages { _id: scanId, imageDigest, generatedAtUtc, packages[] }` // decoded `BunPackageInventory` rows for CLI/Policy reuse + +### 3.3 Object store layout (RustFS) + +``` +layers//sbom.cdx.json.zst +layers//sbom.spdx.json.zst +images//inventory.cdx.pb # CycloneDX Protobuf +images//usage.cdx.pb +indexes//bom-index.bin # purls + roaring bitmaps +diffs/_/diff.json.zst +attest/.dsse.json # DSSE bundle (cert chain + Rekor proof) +``` + +RustFS exposes a deterministic HTTP API (`PUT|GET|DELETE /api/v1/buckets/{bucket}/objects/{key}`). +Scanner clients tag immutable uploads with `X-RustFS-Immutable: true` and, when retention applies, +`X-RustFS-Retain-Seconds: `. Additional headers can be injected via +`scanner.artifactStore.headers` to support custom auth or proxy requirements. RustFS provides the standard S3-compatible interface for all artifact storage. + +--- + +## 4) REST API (Scanner.WebService) + +All under `/api/v1/scanner`. Auth: **OpTok** (DPoP/mTLS); RBAC scopes. + +``` +POST /scans { imageRef|digest, force?:bool } → { scanId } +GET /scans/{id} → { status, imageDigest, artifacts[], rekor? } +GET /sboms/{imageDigest} ?format=cdx-json|cdx-pb|spdx-json&view=inventory|usage → bytes +POST /sbom/upload { artifactRef, sbom|sbomBase64, format?, source? } -> { sbomId, analysisJobId } +GET /sbom/uploads/{sbomId} -> upload record + provenance +GET /scans/{id}/ruby-packages → { scanId, imageDigest, generatedAt, packages[] } +GET /scans/{id}/bun-packages → { scanId, imageDigest, generatedAt, packages[] } +GET /diff?old=&new=&view=inventory|usage → diff.json +POST /exports { imageDigest, format, view, attest?:bool } → { artifactId, rekor? } +POST /reports { imageDigest, policyRevision? } → { reportId, rekor? } # delegates to backend policy+vex +GET /catalog/artifacts/{id} → { meta } +GET /healthz | /readyz | /metrics +``` See docs/modules/scanner/byos-ingestion.md for BYOS workflow, formats, and troubleshooting. ### 4.1 Localization runtime contract (Sprint 20260224_002) @@ -238,34 +238,34 @@ See docs/modules/scanner/byos-ingestion.md for BYOS workflow, formats, and troub - Current localized API responses in this rollout are provided for `en-US` and `de-DE` (for example, slice query validation and not-found responses). ### Report events - -When `scanner.events.enabled = true`, the WebService serialises the signed report (canonical JSON + DSSE envelope) with `NotifyCanonicalJsonSerializer` and publishes two Redis Stream entries (`scanner.report.ready`, `scanner.scan.completed`) to the configured stream (default `stella.events`). The stream fields carry the whole envelope plus lightweight headers (`kind`, `tenant`, `ts`) so Notify and UI timelines can consume the event bus without recomputing signatures. Publish timeouts and bounded stream length are controlled via `scanner:events:publishTimeoutSeconds` and `scanner:events:maxStreamLength`. If the queue driver is already Redis and no explicit events DSN is provided, the host reuses the queue connection and auto-enables event emission so deployments get live envelopes without extra wiring. Compose/Helm bundles expose the same knobs via the `SCANNER__EVENTS__*` environment variables for quick tuning. - ---- - -## 5) Execution flow (Worker) - -### 5.1 Acquire & verify - -1. **Resolve image** (prefer `repo@sha256:…`). -2. **(Optional) verify image signature** per policy (cosign). -3. **Pull blobs**, compute layer digests; record metadata. - -### 5.2 Layer union FS - -* Apply whiteouts; materialize final filesystem; map **file → first introducing layer**. -* Windows layers (MSI/SxS/GAC) planned in **M2**. - -### 5.3 Evidence harvest (parallel analyzers; deterministic only) - -**A) OS packages** - -* **apk**: `/lib/apk/db/installed` -* **dpkg**: `/var/lib/dpkg/status`, `/var/lib/dpkg/info/*.list` -* **rpm**: `/var/lib/rpm/Packages` (via librpm or parser) -* Record `name`, `version` (epoch/revision), `arch`, source package where present, and **declared file lists**. - -> **Data flow note:** Each OS analyzer now writes its canonical output into the shared `ScanAnalysisStore` under + +When `scanner.events.enabled = true`, the WebService serialises the signed report (canonical JSON + DSSE envelope) with `NotifyCanonicalJsonSerializer` and publishes two Redis Stream entries (`scanner.report.ready`, `scanner.scan.completed`) to the configured stream (default `stella.events`). The stream fields carry the whole envelope plus lightweight headers (`kind`, `tenant`, `ts`) so Notify and UI timelines can consume the event bus without recomputing signatures. Publish timeouts and bounded stream length are controlled via `scanner:events:publishTimeoutSeconds` and `scanner:events:maxStreamLength`. If the queue driver is already Redis and no explicit events DSN is provided, the host reuses the queue connection and auto-enables event emission so deployments get live envelopes without extra wiring. Compose/Helm bundles expose the same knobs via the `SCANNER__EVENTS__*` environment variables for quick tuning. + +--- + +## 5) Execution flow (Worker) + +### 5.1 Acquire & verify + +1. **Resolve image** (prefer `repo@sha256:…`). +2. **(Optional) verify image signature** per policy (cosign). +3. **Pull blobs**, compute layer digests; record metadata. + +### 5.2 Layer union FS + +* Apply whiteouts; materialize final filesystem; map **file → first introducing layer**. +* Windows layers (MSI/SxS/GAC) planned in **M2**. + +### 5.3 Evidence harvest (parallel analyzers; deterministic only) + +**A) OS packages** + +* **apk**: `/lib/apk/db/installed` +* **dpkg**: `/var/lib/dpkg/status`, `/var/lib/dpkg/info/*.list` +* **rpm**: `/var/lib/rpm/Packages` (via librpm or parser) +* Record `name`, `version` (epoch/revision), `arch`, source package where present, and **declared file lists**. + +> **Data flow note:** Each OS analyzer now writes its canonical output into the shared `ScanAnalysisStore` under > `analysis.os.packages` (raw results), `analysis.os.fragments` (per-analyzer layer fragments), and contributes to > `analysis.layers.fragments` (the aggregated view consumed by emit/diff pipelines). Helpers in > `ScanAnalysisCompositionBuilder` convert these fragments into SBOM composition requests and component graphs so the @@ -273,169 +273,169 @@ When `scanner.events.enabled = true`, the WebService serialises the signed repor > RPM and DPKG changelog evidence now also emits deterministic vendor metadata keys > `vendor.changelogBugRefs` and `vendor.changelogBugToCves` for Debian `Closes`, `RHBZ#`, and Launchpad `LP` bug-to-CVE > correlation traces used during backport triage. - -**B) Language ecosystems (installed state only)** - -* **Java**: `META-INF/maven/*/pom.properties`, MANIFEST → `pkg:maven/...` -* **Node**: `node_modules/**/package.json` → `pkg:npm/...` -* **Bun**: `bun.lock` (JSONC text) + `node_modules/**/package.json` + `node_modules/.bun/**/package.json` (isolated linker) → `pkg:npm/...`; `bun.lockb` (binary) emits remediation guidance -* **Python**: `*.dist-info/{METADATA,RECORD}` → `pkg:pypi/...` -* **Go**: Go **buildinfo** in binaries → `pkg:golang/...` -* **.NET**: `*.deps.json` + assembly metadata → `pkg:nuget/...` -* **Rust**: crates only when **explicitly present** (embedded metadata or cargo/registry traces); otherwise binaries reported as `bin:{sha256}`. - -> **Rule:** We only report components proven **on disk** with authoritative metadata. Lockfiles are evidence only. - -**C) Native link graph** - -* **ELF**: parse `PT_INTERP`, `DT_NEEDED`, RPATH/RUNPATH, **GNU symbol versions**; map **SONAMEs** to file paths; link executables → libs. -* **PE/Mach‑O** (planned M2): import table, delay‑imports; version resources; code signatures. -* Map libs back to **OS packages** if possible (via file lists); else emit `bin:{sha256}` components. -* The exported metadata (`stellaops.os.*` properties, license list, source package) feeds policy scoring and export pipelines - directly – Policy evaluates quiet rules against package provenance while Exporters forward the enriched fields into - downstream JSON/Trivy payloads. -* **Reachability lattice**: analyzers + runtime probes emit `Evidence`/`Mitigation` records (see `docs/modules/reach-graph/guides/lattice.md`). The lattice engine joins static path evidence, runtime hits (EventPipe/JFR), taint flows, environment gates, and mitigations into `ReachDecision` documents that feed VEX gating and event graph storage. -* Sprint 401 introduces `StellaOps.Scanner.Symbols.Native` (DWARF/PDB reader + demangler) and `StellaOps.Scanner.CallGraph.Native` - (function boundary detector + call-edge builder). These libraries feed `FuncNode`/`CallEdge` CAS bundles and enrich reachability - graphs with `{code_id, confidence, evidence}` so Signals/Policy/UI can cite function-level justifications. - -**D) EntryTrace (ENTRYPOINT/CMD → terminal program)** - -* Read image config; parse shell (POSIX/Bash subset) with AST: `source`/`.` includes; `case/if`; `exec`/`command`; `run‑parts`. -* Resolve commands via **PATH** within the **built rootfs**; follow language launchers (Java/Node/Python) to identify the terminal program (ELF/JAR/venv script). -* Record **file:line** and choices for each hop; output chain graph. -* Unresolvable dynamic constructs are recorded as **unknown** edges with reasons (e.g., `$FOO` unresolved). - -**D.1) Semantic Entrypoint Analysis (Sprint 0411)** - -Post-resolution, the `SemanticEntrypointOrchestrator` enriches entry trace results with semantic understanding: - -* **Application Intent** — Infers the purpose (WebServer, CliTool, Worker, Serverless, BatchJob, etc.) from framework detection and command patterns. -* **Capability Classes** — Detects capabilities (NetworkListen, DatabaseSql, ProcessSpawn, SecretAccess, etc.) via import/dependency analysis and framework signatures. -* **Attack Surface** — Maps capabilities to potential threat vectors (SqlInjection, Xss, Ssrf, Rce, PathTraversal) with CWE IDs and OWASP Top 10 categories. -* **Data Boundaries** — Traces I/O edges (HttpRequest, DatabaseQuery, FileInput, EnvironmentVar) with direction and sensitivity classification. -* **Confidence Scoring** — Each inference carries a score (0.0–1.0), tier (Definitive/High/Medium/Low/Unknown), and reasoning chain. - -Language-specific adapters (`PythonSemanticAdapter`, `JavaSemanticAdapter`, `NodeSemanticAdapter`, `DotNetSemanticAdapter`, `GoSemanticAdapter`) recognize framework patterns: -* **Python**: Django, Flask, FastAPI, Celery, Click/Typer, Lambda handlers -* **Java**: Spring Boot, Quarkus, Micronaut, Kafka Streams -* **Node**: Express, NestJS, Fastify, CLI bin entries -* **.NET**: ASP.NET Core, Worker services, Azure Functions -* **Go**: net/http, Cobra, gRPC - -Semantic data flows into: -* **RichGraph nodes** via `semantic_intent`, `semantic_capabilities`, `semantic_threats` attributes -* **CycloneDX properties** via `stellaops:semantic.*` namespace -* **LanguageComponentRecord** metadata for reachability scoring - -See `docs/modules/scanner/operations/entrypoint-semantic.md` for full schema reference. - -**E) Binary Vulnerability Lookup (Sprint 20251226_014_BINIDX)** - -The **BinaryLookupStageExecutor** enriches scan results with binary-level vulnerability evidence: - -* **Identity Extraction**: For each ELF/PE/Mach-O binary, extract Build-ID, file SHA256, and architecture. Generate a `binary_key` for catalog lookups. -* **Build-ID Catalog Lookup**: Query the BinaryIndex known-build catalog using Build-ID as primary key. Returns CVE matches with high confidence (>=0.95) when the exact binary version is indexed. -* **Fingerprint Matching**: For binaries not in the catalog, compute position-independent fingerprints (basic-block, CFG, string-refs) and match against the vulnerability corpus. Returns similarity scores and confidence. + +**B) Language ecosystems (installed state only)** + +* **Java**: `META-INF/maven/*/pom.properties`, MANIFEST → `pkg:maven/...` +* **Node**: `node_modules/**/package.json` → `pkg:npm/...` +* **Bun**: `bun.lock` (JSONC text) + `node_modules/**/package.json` + `node_modules/.bun/**/package.json` (isolated linker) → `pkg:npm/...`; `bun.lockb` (binary) emits remediation guidance +* **Python**: `*.dist-info/{METADATA,RECORD}` → `pkg:pypi/...` +* **Go**: Go **buildinfo** in binaries → `pkg:golang/...` +* **.NET**: `*.deps.json` + assembly metadata → `pkg:nuget/...` +* **Rust**: crates only when **explicitly present** (embedded metadata or cargo/registry traces); otherwise binaries reported as `bin:{sha256}`. + +> **Rule:** We only report components proven **on disk** with authoritative metadata. Lockfiles are evidence only. + +**C) Native link graph** + +* **ELF**: parse `PT_INTERP`, `DT_NEEDED`, RPATH/RUNPATH, **GNU symbol versions**; map **SONAMEs** to file paths; link executables → libs. +* **PE/Mach‑O** (planned M2): import table, delay‑imports; version resources; code signatures. +* Map libs back to **OS packages** if possible (via file lists); else emit `bin:{sha256}` components. +* The exported metadata (`stellaops.os.*` properties, license list, source package) feeds policy scoring and export pipelines + directly – Policy evaluates quiet rules against package provenance while Exporters forward the enriched fields into + downstream JSON/Trivy payloads. +* **Reachability lattice**: analyzers + runtime probes emit `Evidence`/`Mitigation` records (see `docs/modules/reach-graph/guides/lattice.md`). The lattice engine joins static path evidence, runtime hits (EventPipe/JFR), taint flows, environment gates, and mitigations into `ReachDecision` documents that feed VEX gating and event graph storage. +* Sprint 401 introduces `StellaOps.Scanner.Symbols.Native` (DWARF/PDB reader + demangler) and `StellaOps.Scanner.CallGraph.Native` + (function boundary detector + call-edge builder). These libraries feed `FuncNode`/`CallEdge` CAS bundles and enrich reachability + graphs with `{code_id, confidence, evidence}` so Signals/Policy/UI can cite function-level justifications. + +**D) EntryTrace (ENTRYPOINT/CMD → terminal program)** + +* Read image config; parse shell (POSIX/Bash subset) with AST: `source`/`.` includes; `case/if`; `exec`/`command`; `run‑parts`. +* Resolve commands via **PATH** within the **built rootfs**; follow language launchers (Java/Node/Python) to identify the terminal program (ELF/JAR/venv script). +* Record **file:line** and choices for each hop; output chain graph. +* Unresolvable dynamic constructs are recorded as **unknown** edges with reasons (e.g., `$FOO` unresolved). + +**D.1) Semantic Entrypoint Analysis (Sprint 0411)** + +Post-resolution, the `SemanticEntrypointOrchestrator` enriches entry trace results with semantic understanding: + +* **Application Intent** — Infers the purpose (WebServer, CliTool, Worker, Serverless, BatchJob, etc.) from framework detection and command patterns. +* **Capability Classes** — Detects capabilities (NetworkListen, DatabaseSql, ProcessSpawn, SecretAccess, etc.) via import/dependency analysis and framework signatures. +* **Attack Surface** — Maps capabilities to potential threat vectors (SqlInjection, Xss, Ssrf, Rce, PathTraversal) with CWE IDs and OWASP Top 10 categories. +* **Data Boundaries** — Traces I/O edges (HttpRequest, DatabaseQuery, FileInput, EnvironmentVar) with direction and sensitivity classification. +* **Confidence Scoring** — Each inference carries a score (0.0–1.0), tier (Definitive/High/Medium/Low/Unknown), and reasoning chain. + +Language-specific adapters (`PythonSemanticAdapter`, `JavaSemanticAdapter`, `NodeSemanticAdapter`, `DotNetSemanticAdapter`, `GoSemanticAdapter`) recognize framework patterns: +* **Python**: Django, Flask, FastAPI, Celery, Click/Typer, Lambda handlers +* **Java**: Spring Boot, Quarkus, Micronaut, Kafka Streams +* **Node**: Express, NestJS, Fastify, CLI bin entries +* **.NET**: ASP.NET Core, Worker services, Azure Functions +* **Go**: net/http, Cobra, gRPC + +Semantic data flows into: +* **RichGraph nodes** via `semantic_intent`, `semantic_capabilities`, `semantic_threats` attributes +* **CycloneDX properties** via `stellaops:semantic.*` namespace +* **LanguageComponentRecord** metadata for reachability scoring + +See `docs/modules/scanner/operations/entrypoint-semantic.md` for full schema reference. + +**E) Binary Vulnerability Lookup (Sprint 20251226_014_BINIDX)** + +The **BinaryLookupStageExecutor** enriches scan results with binary-level vulnerability evidence: + +* **Identity Extraction**: For each ELF/PE/Mach-O binary, extract Build-ID, file SHA256, and architecture. Generate a `binary_key` for catalog lookups. +* **Build-ID Catalog Lookup**: Query the BinaryIndex known-build catalog using Build-ID as primary key. Returns CVE matches with high confidence (>=0.95) when the exact binary version is indexed. +* **Fingerprint Matching**: For binaries not in the catalog, compute position-independent fingerprints (basic-block, CFG, string-refs) and match against the vulnerability corpus. Returns similarity scores and confidence. * **Fix Status Detection**: For each CVE match, query distro-specific backport information to determine if the vulnerability was fixed via distro patch. Methods: `changelog`, `patch_analysis`, `advisory`. * **Valkey Cache**: All lookups are cached with configurable TTL (default 1 hour for identities, 30 minutes for fingerprints). Target cache hit rate: >80% for repeat scans. * **Runtime wiring (2026-02-12)**: `BinaryLookupStageExecutor` now publishes unified mapped findings to `analysis.binary.findings`, Build-ID to PURL lookup results to `analysis.binary.buildid.mappings`, and patch verification output to `analysis.binary.patchverification.result`. - -**BinaryFindingMapper** converts matches to standard findings format with `BinaryFindingEvidence`: -```csharp -public sealed record BinaryFindingEvidence -{ - public required string BinaryKey { get; init; } - public string? BuildId { get; init; } - public required string MatchMethod { get; init; } // buildid_catalog, fingerprint_match, range_match - public required decimal Confidence { get; init; } - public string? FixedVersion { get; init; } - public string? FixStatus { get; init; } // fixed, vulnerable, not_affected, wontfix -} -``` - -**Proof Segments**: The **Attestor** generates `binary_fingerprint_evidence` proof segments with DSSE signatures for each binary with vulnerability matches. Schema: `https://stellaops.dev/predicates/binary-fingerprint-evidence@v1`. - -**UI Badges**: Scan results display status badges: -* **Backported & Safe** (green): Distro backported the fix -* **Affected & Reachable** (red): Vulnerable and in code path -* **Unknown** (gray): Could not determine status - -**CLI Commands** (Sprint 20251226_014): -* `stella binary inspect `: Extract identity (Build-ID, hashes, architecture) -* `stella binary lookup `: Query vulnerabilities by Build-ID -* `stella binary fingerprint `: Generate position-independent fingerprint - -**F) Attestation & SBOM bind (optional)** - -* For each **file hash** or **binary hash**, query local cache of **Rekor v2** indices; if an SBOM attestation is found for **exact hash**, bind it to the component (origin=`attested`). -* For the **image** digest, likewise bind SBOM attestations (build-time referrers). - -### 5.4 Component normalization (exact only) - -* Create `Component` nodes only with deterministic identities: purl, or **`bin:{sha256}`** for unlabeled binaries. -* Record **origin** (OS DB, installed metadata, linker, attestation). - -### 5.5 SBOM assembly & emit - -* **Per-layer SBOM fragments**: components introduced by the layer (+ relationships). -* **Image SBOMs**: merge fragments; refer back to them via **CycloneDX BOM‑Link** (or SPDX ExternalRef). -* Emit both **Inventory** & **Usage** views. -* When the native analyzer reports an ELF `buildId`, attach it to component metadata and surface it as `stellaops:buildId` in CycloneDX properties (and diff metadata). This keeps SBOM/diff output in lockstep with runtime events and the debug-store manifest. -* Serialize **CycloneDX 1.7 JSON** and **CycloneDX 1.7 Protobuf**; optionally **SPDX 3.0.1 JSON-LD** (`application/spdx+json; version=3.0.1`) with legacy tag-value output (`text/spdx`) when enabled (1.6 accepted for ingest compatibility). -* Build **BOM‑Index** sidecar: purl table + roaring bitmap; flag `usedByEntrypoint` components for fast backend joins. - -The emitted `buildId` metadata is preserved in component hashes, diff payloads, and `/policy/runtime` responses so operators can pivot from SBOM entries → runtime events → `debug/.build-id//.debug` within the Offline Kit or release bundle. - -### 5.5.1 Service security analysis (Sprint 20260119_016) - -When an SBOM path is provided, the worker runs the `service-security` stage to parse CycloneDX services and emit a deterministic report covering: - -- Endpoint scheme hygiene (HTTP/WS/plaintext protocol detection). -- Authentication and trust-boundary enforcement. -- Sensitive data flow exposure and unencrypted transfers. -- Deprecated service versions and rate-limiting metadata gaps. - -Inputs are passed via scan metadata (`sbom.path` or `sbomPath`, plus `sbom.format`). The report is attached as a surface observation payload (`service-security.report`) and keyed in the analysis store for downstream policy and report assembly. See `src/Scanner/docs/service-security.md` for the policy schema and output formats. - -### 5.5.2 CBOM crypto analysis (Sprint 20260119_017) - -When an SBOM includes CycloneDX `cryptoProperties`, the worker runs the `crypto-analysis` stage to produce a crypto inventory and compliance findings for weak algorithms, short keys, deprecated protocol versions, certificate hygiene, and post-quantum readiness. The report is attached as a surface observation payload (`crypto-analysis.report`) and keyed in the analysis store for downstream evidence workflows. See `src/Scanner/docs/crypto-analysis.md` for the policy schema and inventory export formats. - -### 5.5.3 AI/ML supply chain security (Sprint 20260119_018) - -When an SBOM includes CycloneDX `modelCard` or SPDX AI profile data, the worker runs the `ai-ml-security` stage to evaluate model governance readiness. The report covers model card completeness, training data provenance, bias/fairness checks, safety risk assessment coverage, and provenance verification. The report is attached as a surface observation payload (`ai-ml-security.report`) and keyed in the analysis store for policy evaluation and audit trails. See `src/Scanner/docs/ai-ml-security.md` for policy schema, CLI toggles, and binary analysis conventions. - -### 5.5.4 Build provenance verification (Sprint 20260119_019) - -When an SBOM includes CycloneDX formulation or SPDX build profile data, the worker runs the `build-provenance` stage to verify provenance completeness, builder trust, source integrity, hermetic build requirements, and optional reproducibility checks. The report is attached as a surface observation payload (`build-provenance.report`) and keyed in the analysis store for policy enforcement and audit evidence. See `src/Scanner/docs/build-provenance.md` for policy schema, CLI toggles, and report formats. - -### 5.5.5 SBOM dependency reachability (Sprint 20260119_022) - -When configured, the worker runs the `reachability-analysis` stage to infer dependency reachability from SBOM graphs and optionally refine it with a `richgraph-v1` call graph. Advisory matches are filtered or severity-adjusted using `VulnerabilityReachabilityFilter`, with false-positive reduction metrics recorded for auditability. The stage attaches: - -- `reachability.report` (JSON) for component and vulnerability reachability. -- `reachability.report.sarif` (SARIF 2.1.0) for toolchain export. -- `reachability.graph.dot` (GraphViz) for dependency visualization. - -Configuration lives in `src/Scanner/docs/sbom-reachability-filtering.md`, including policy schema, metadata keys, and report outputs. - -### 5.5.6 VEX decision filter with reachability (Sprint 20260208_062) - -Scanner now exposes a deterministic VEX+reachability matrix filter for triage pre-processing: - -- Gate matrix component: `StellaOps.Scanner.Gate/VexReachabilityDecisionFilter` evaluates `(vendorStatus, reachabilityTier)` and returns one of `suppress`, `elevate`, `pass_through`, or `flag_for_review`. -- Matrix examples: `(not_affected, unreachable) -> suppress`, `(affected, confirmed|likely) -> elevate`, `(not_affected, confirmed|likely) -> flag_for_review`. -- API surface: `POST /api/v1/scans/vex-reachability/filter` accepts finding batches and returns annotated decisions plus action summary counts. -- Determinism: batch order is preserved, rule IDs are explicit, and no network lookups are required for matrix evaluation. - + +**BinaryFindingMapper** converts matches to standard findings format with `BinaryFindingEvidence`: +```csharp +public sealed record BinaryFindingEvidence +{ + public required string BinaryKey { get; init; } + public string? BuildId { get; init; } + public required string MatchMethod { get; init; } // buildid_catalog, fingerprint_match, range_match + public required decimal Confidence { get; init; } + public string? FixedVersion { get; init; } + public string? FixStatus { get; init; } // fixed, vulnerable, not_affected, wontfix +} +``` + +**Proof Segments**: The **Attestor** generates `binary_fingerprint_evidence` proof segments with DSSE signatures for each binary with vulnerability matches. Schema: `https://stellaops.dev/predicates/binary-fingerprint-evidence@v1`. + +**UI Badges**: Scan results display status badges: +* **Backported & Safe** (green): Distro backported the fix +* **Affected & Reachable** (red): Vulnerable and in code path +* **Unknown** (gray): Could not determine status + +**CLI Commands** (Sprint 20251226_014): +* `stella binary inspect `: Extract identity (Build-ID, hashes, architecture) +* `stella binary lookup `: Query vulnerabilities by Build-ID +* `stella binary fingerprint `: Generate position-independent fingerprint + +**F) Attestation & SBOM bind (optional)** + +* For each **file hash** or **binary hash**, query local cache of **Rekor v2** indices; if an SBOM attestation is found for **exact hash**, bind it to the component (origin=`attested`). +* For the **image** digest, likewise bind SBOM attestations (build-time referrers). + +### 5.4 Component normalization (exact only) + +* Create `Component` nodes only with deterministic identities: purl, or **`bin:{sha256}`** for unlabeled binaries. +* Record **origin** (OS DB, installed metadata, linker, attestation). + +### 5.5 SBOM assembly & emit + +* **Per-layer SBOM fragments**: components introduced by the layer (+ relationships). +* **Image SBOMs**: merge fragments; refer back to them via **CycloneDX BOM‑Link** (or SPDX ExternalRef). +* Emit both **Inventory** & **Usage** views. +* When the native analyzer reports an ELF `buildId`, attach it to component metadata and surface it as `stellaops:buildId` in CycloneDX properties (and diff metadata). This keeps SBOM/diff output in lockstep with runtime events and the debug-store manifest. +* Serialize **CycloneDX 1.7 JSON** and **CycloneDX 1.7 Protobuf**; optionally **SPDX 3.0.1 JSON-LD** (`application/spdx+json; version=3.0.1`) with legacy tag-value output (`text/spdx`) when enabled (1.6 accepted for ingest compatibility). +* Build **BOM‑Index** sidecar: purl table + roaring bitmap; flag `usedByEntrypoint` components for fast backend joins. + +The emitted `buildId` metadata is preserved in component hashes, diff payloads, and `/policy/runtime` responses so operators can pivot from SBOM entries → runtime events → `debug/.build-id//.debug` within the Offline Kit or release bundle. + +### 5.5.1 Service security analysis (Sprint 20260119_016) + +When an SBOM path is provided, the worker runs the `service-security` stage to parse CycloneDX services and emit a deterministic report covering: + +- Endpoint scheme hygiene (HTTP/WS/plaintext protocol detection). +- Authentication and trust-boundary enforcement. +- Sensitive data flow exposure and unencrypted transfers. +- Deprecated service versions and rate-limiting metadata gaps. + +Inputs are passed via scan metadata (`sbom.path` or `sbomPath`, plus `sbom.format`). The report is attached as a surface observation payload (`service-security.report`) and keyed in the analysis store for downstream policy and report assembly. See `src/Scanner/docs/service-security.md` for the policy schema and output formats. + +### 5.5.2 CBOM crypto analysis (Sprint 20260119_017) + +When an SBOM includes CycloneDX `cryptoProperties`, the worker runs the `crypto-analysis` stage to produce a crypto inventory and compliance findings for weak algorithms, short keys, deprecated protocol versions, certificate hygiene, and post-quantum readiness. The report is attached as a surface observation payload (`crypto-analysis.report`) and keyed in the analysis store for downstream evidence workflows. See `src/Scanner/docs/crypto-analysis.md` for the policy schema and inventory export formats. + +### 5.5.3 AI/ML supply chain security (Sprint 20260119_018) + +When an SBOM includes CycloneDX `modelCard` or SPDX AI profile data, the worker runs the `ai-ml-security` stage to evaluate model governance readiness. The report covers model card completeness, training data provenance, bias/fairness checks, safety risk assessment coverage, and provenance verification. The report is attached as a surface observation payload (`ai-ml-security.report`) and keyed in the analysis store for policy evaluation and audit trails. See `src/Scanner/docs/ai-ml-security.md` for policy schema, CLI toggles, and binary analysis conventions. + +### 5.5.4 Build provenance verification (Sprint 20260119_019) + +When an SBOM includes CycloneDX formulation or SPDX build profile data, the worker runs the `build-provenance` stage to verify provenance completeness, builder trust, source integrity, hermetic build requirements, and optional reproducibility checks. The report is attached as a surface observation payload (`build-provenance.report`) and keyed in the analysis store for policy enforcement and audit evidence. See `src/Scanner/docs/build-provenance.md` for policy schema, CLI toggles, and report formats. + +### 5.5.5 SBOM dependency reachability (Sprint 20260119_022) + +When configured, the worker runs the `reachability-analysis` stage to infer dependency reachability from SBOM graphs and optionally refine it with a `richgraph-v1` call graph. Advisory matches are filtered or severity-adjusted using `VulnerabilityReachabilityFilter`, with false-positive reduction metrics recorded for auditability. The stage attaches: + +- `reachability.report` (JSON) for component and vulnerability reachability. +- `reachability.report.sarif` (SARIF 2.1.0) for toolchain export. +- `reachability.graph.dot` (GraphViz) for dependency visualization. + +Configuration lives in `src/Scanner/docs/sbom-reachability-filtering.md`, including policy schema, metadata keys, and report outputs. + +### 5.5.6 VEX decision filter with reachability (Sprint 20260208_062) + +Scanner now exposes a deterministic VEX+reachability matrix filter for triage pre-processing: + +- Gate matrix component: `StellaOps.Scanner.Gate/VexReachabilityDecisionFilter` evaluates `(vendorStatus, reachabilityTier)` and returns one of `suppress`, `elevate`, `pass_through`, or `flag_for_review`. +- Matrix examples: `(not_affected, unreachable) -> suppress`, `(affected, confirmed|likely) -> elevate`, `(not_affected, confirmed|likely) -> flag_for_review`. +- API surface: `POST /api/v1/scans/vex-reachability/filter` accepts finding batches and returns annotated decisions plus action summary counts. +- Determinism: batch order is preserved, rule IDs are explicit, and no network lookups are required for matrix evaluation. + ### 5.5.7 Vulnerability-first triage clustering APIs (Sprint 20260208_063) - -Scanner triage now includes deterministic exploit-path clustering primitives for vulnerability-first triage workflows: - -- Core clustering service: `StellaOps.Scanner.Triage/Services/ExploitPathGroupingService` groups findings using common call-chain prefix similarity with configurable thresholds. -- Inbox enhancements: `GET /api/v1/triage/inbox` supports `similarityThreshold`, `sortBy`, and `descending` for deterministic cluster filtering/sorting. -- Cluster statistics: `GET /api/v1/triage/inbox/clusters/stats` returns per-cluster severity counts, reachability distribution, and priority scores. + +Scanner triage now includes deterministic exploit-path clustering primitives for vulnerability-first triage workflows: + +- Core clustering service: `StellaOps.Scanner.Triage/Services/ExploitPathGroupingService` groups findings using common call-chain prefix similarity with configurable thresholds. +- Inbox enhancements: `GET /api/v1/triage/inbox` supports `similarityThreshold`, `sortBy`, and `descending` for deterministic cluster filtering/sorting. +- Cluster statistics: `GET /api/v1/triage/inbox/clusters/stats` returns per-cluster severity counts, reachability distribution, and priority scores. - Batch triage actions: `POST /api/v1/triage/inbox/clusters/{pathId}/actions` applies one action to all findings in the cluster and emits deterministic action records. - Offline/determinism posture: no network calls, stable ordering by IDs/path IDs, deterministic path-ID hashing, and replayable batch payload digests. @@ -465,289 +465,289 @@ Scanner API flows that operate on tenant-partitioned tables now require tenant a - Generic webhook ingress by `sourceId` remains compatibility-tolerant when tenant context is absent, but enforces tenant ownership when context is present. ### 5.6 DSSE attestation (via Signer/Attestor) - -* WebService constructs **predicate** with `image_digest`, `stellaops_version`, `license_id`, `policy_digest?` (when emitting **final reports**), timestamps. -* Calls **Signer** (requires **OpTok + PoE**); Signer verifies **entitlement + scanner image integrity** and returns **DSSE bundle**. -* **Attestor** logs to **Rekor v2**; returns `{uuid,index,proof}` → stored in `artifacts.rekor`. -* **Verdict OCI idempotency**: `push-verdict` computes `sha256` idempotency from DSSE envelope bytes, writes `org.stellaops.idempotency.key`, uses stable `verdict-` manifest tags, retries transient push failures (429/5xx/timeouts), and treats conflict/already-submitted responses as success. -* **Hybrid reachability attestations**: graph-level DSSE (mandatory) plus optional edge-bundle DSSEs for runtime/init/contested edges. See [`docs/modules/reach-graph/guides/hybrid-attestation.md`](../reach-graph/guides/hybrid-attestation.md) for verification runbooks and Rekor guidance. -* Operator enablement runbooks (toggles, env-var map, rollout guidance) live in [`operations/dsse-rekor-operator-guide.md`](operations/dsse-rekor-operator-guide.md) per SCANNER-ENG-0015. - ---- - -## 6) Three‑way diff (image → layer → component) - -### 6.1 Keys & classification - -* Component key: **purl** when present; else `bin:{sha256}`. -* Diff classes: `added`, `removed`, `version_changed` (`upgraded|downgraded`), `metadata_changed` (e.g., origin from attestation vs observed). -* Layer attribution: for each change, resolve the **introducing/removing layer**. - -### 6.2 Algorithm (outline) - -``` -A = components(imageOld, key) -B = components(imageNew, key) - -added = B \ A -removed = A \ B -changed = { k in A∩B : version(A[k]) != version(B[k]) || origin changed } - -for each item in added/removed/changed: - layer = attribute_to_layer(item, imageOld|imageNew) - usageFlag = usedByEntrypoint(item, imageNew) -emit diff.json (grouped by layer with badges) -``` - -Diffs are stored as artifacts and feed **UI** and **CLI**. - ---- - -## 7) Build‑time SBOMs (fast CI path) - -**Scanner.Sbomer.BuildXPlugin** can act as a BuildKit **generator**: - -* During `docker buildx build --attest=type=sbom,generator=stellaops/sbom-indexer`, run analyzers on the build context/output; attach SBOMs as OCI **referrers** to the built image. -* Optionally request **Signer/Attestor** to produce **Stella Ops‑verified** attestation immediately; else, Scanner.WebService can verify and re‑attest post‑push. -* Scanner.WebService trusts build‑time SBOMs per policy, enabling **no‑rescan** for unchanged bases. - ---- - -## 8) Configuration (YAML) - -```yaml -scanner: - queue: - kind: valkey # uses redis:// protocol - url: "redis://queue:6379/0" - postgres: - connectionString: "Host=postgres;Port=5432;Database=scanner;Username=stellaops;Password=stellaops" - s3: - endpoint: "http://rustfs:8080" - bucket: "stellaops" - objectLock: "governance" # or 'compliance' - analyzers: - os: { apk: true, dpkg: true, rpm: true } - lang: { java: true, node: true, bun: true, python: true, go: true, dotnet: true, rust: true, ruby: true, php: true } - native: { elf: true, pe: false, macho: false } # PE/Mach-O in M2 - entryTrace: { enabled: true, shellMaxDepth: 64, followRunParts: true } - emit: - cdx: { json: true, protobuf: true } - spdx: { json: true } - compress: "zstd" - rekor: - url: "https://rekor-v2.internal" - signer: - url: "https://signer.internal" - limits: - maxParallel: 8 - perRegistryConcurrency: 2 - policyHints: - verifyImageSignature: false - trustBuildTimeSboms: true -``` - ---- - -## 9) Scale & performance - -* **Parallelism**: per‑analyzer concurrency; bounded directory walkers; file CAS dedupe by sha256. -* **Distributed locks** per **layer digest** to prevent duplicate work across Workers. -* **Registry throttles**: per‑host concurrency budgets; exponential backoff on 429/5xx. -* **Targets**: - - * **Build‑time**: P95 ≤ 3–5 s on warmed bases (CI generator). - * **Post‑build delta**: P95 ≤ 10 s for 200 MB images with cache hit. - * **Emit**: CycloneDX Protobuf ≤ 150 ms for 5k components; JSON ≤ 500 ms. - * **Diff**: ≤ 200 ms for 5k vs 5k components. - ---- - -## 10) Security posture - -* **AuthN**: Authority‑issued short OpToks (DPoP/mTLS). -* **AuthZ**: scopes (`scanner.scan`, `scanner.export`, `scanner.catalog.read`). -* **mTLS** to **Signer**/**Attestor**; only **Signer** can sign. -* **No network fetches** during analysis (except registry pulls and optional Rekor index reads). -* **Sandboxing**: non‑root containers; read‑only FS; seccomp profiles; disable execution of scanned content. -* **Release integrity**: all first‑party images are **cosign‑signed**; Workers/WebService self‑verify at startup. - ---- - -## 11) Observability & audit - -* **Metrics**: - - * `scanner.jobs_inflight`, `scanner.scan_latency_seconds` - * `scanner.layer_cache_hits_total`, `scanner.file_cas_hits_total` - * `scanner.artifact_bytes_total{format}` - * `scanner.attestation_latency_seconds`, `scanner.rekor_failures_total` - * `scanner_analyzer_golang_heuristic_total{indicator,version_hint}` — increments whenever the Go analyzer falls back to heuristics (build-id or runtime markers). Grafana panel: `sum by (indicator) (rate(scanner_analyzer_golang_heuristic_total[5m]))`; alert when the rate is ≥ 1 for 15 minutes to highlight unexpected stripped binaries. -* **Tracing**: spans for acquire→union→analyzers→compose→emit→sign→log. -* **Audit logs**: DSSE requests log `license_id`, `image_digest`, `artifactSha256`, `policy_digest?`, Rekor UUID on success. - ---- - -## 12) Testing matrix - -* **Analyzer contracts:** see `language-analyzers-contract.md` for cross-analyzer identity safety, evidence locators, and container layout rules. Per-analyzer docs: `analyzers-java.md`, `dotnet-analyzer.md`, `analyzers-python.md`, `analyzers-node.md`, `analyzers-bun.md`, `analyzers-go.md`. Implementation: `docs/implplan/SPRINT_0408_0001_0001_scanner_language_detection_gaps_program.md`. - -* **Determinism:** given same image + analyzers → byte‑identical **CDX Protobuf**; JSON normalized. -* **OS packages:** ground‑truth images per distro; compare to package DB. -* **Lang ecosystems:** sample images per ecosystem (Java/Node/Python/Go/.NET/Rust) with installed metadata; negative tests w/ lockfile‑only. -* **Native & EntryTrace:** ELF graph correctness; shell AST cases (includes, run‑parts, exec, case/if). -* **Diff:** layer attribution against synthetic two‑image sequences. -* **Performance:** cold vs warm cache; large `node_modules` and `site‑packages`. -* **Security:** ensure no code execution from image; fuzz parser inputs; path traversal resistance on layer extract. - ---- - -## 13) Failure modes & degradations - -* **Missing OS DB** (files exist, DB removed): record **files**; do **not** fabricate package components; emit `bin:{sha256}` where unavoidable; flag in evidence. -* **Unreadable metadata** (corrupt dist‑info): record file evidence; skip component creation; annotate. -* **Dynamic shell constructs**: mark unresolved edges with reasons (env var unknown) and continue; **Usage** view may be partial. -* **Registry rate limits**: honor backoff; queue job retries with jitter. -* **Signer refusal** (license/plan/version): scan completes; artifact produced; **no attestation**; WebService marks result as **unverified**. - ---- - -## 14) Optional plug‑ins (off by default) - -* **Patch‑presence detector** (signature‑based backport checks). Reads curated function‑level signatures from advisories; inspects binaries for patched code snippets to lower false‑positives for backported fixes. Runs as a sidecar analyzer that **annotates** components; never overrides core identities. -* **Runtime probes** (with Zastava): when allowed, compare **/proc//maps** (DSOs actually loaded) with static **Usage** view for precision. - ---- - -## 15) DevOps & operations - -* **HA**: WebService horizontal scale; Workers autoscale by queue depth & CPU; distributed locks on layers. -* **Retention**: ILM rules per artifact class (`short`, `default`, `compliance`); **Object Lock** for compliance artifacts (reports, signed SBOMs). -* **Upgrades**: bump **cache schema** when analyzer outputs change; WebService triggers refresh of dependent artifacts. -* **Backups**: PostgreSQL (pg_dump daily); RustFS snapshots (filesystem-level rsync/ZFS) or S3 versioning when legacy driver enabled; Rekor v2 DB snapshots. - ---- - -## 16) CLI & UI touch points - -* **CLI**: `stellaops scan `, `stellaops diff --old --new`, `stellaops export`, `stellaops verify attestation `. -* **UI**: Scan detail shows **Inventory/Usage** toggles, **Diff by Layer**, **Attestation badge** (verified/unverified), Rekor link, and **EntryTrace** chain with file:line breadcrumbs. - ---- - -## 17) Roadmap (Scanner) - -* **M2**: Windows containers (MSI/SxS/GAC analyzers), PE/Mach‑O native analyzer, deeper Rust metadata. -* **M2**: Buildx generator GA (certified external registries), cross‑registry trust policies. -* **M3**: Patch‑presence plug‑in GA (opt‑in), cross‑image corpus clustering (evidence‑only; not identity). -* **M3**: Advanced EntryTrace (POSIX shell features breadth, busybox detection). - ---- - -### Appendix A — EntryTrace resolution (pseudo) - -```csharp -ResolveEntrypoint(ImageConfig cfg, RootFs fs): - cmd = Normalize(cfg.ENTRYPOINT, cfg.CMD) - stack = [ Script(cmd, path=FindOnPath(cmd[0], fs)) ] - visited = set() - - while stack not empty and depth < MAX: - cur = stack.pop() - if cur in visited: continue - visited.add(cur) - - if IsShellScript(cur.path): - ast = ParseShell(cur.path) - foreach directive in ast: - if directive is Source include: - p = ResolveInclude(include.path, cur.env, fs) - stack.push(Script(p)) - if directive is Exec call: - p = ResolveExec(call.argv[0], cur.env, fs) - stack.push(Program(p, argv=call.argv)) - if directive is Interpreter (python -m / node / java -jar): - term = ResolveInterpreterTarget(call, fs) - stack.push(Program(term)) - else: - return Terminal(cur.path) - - return Unknown(reason) -``` - -### Appendix A.1 — EntryTrace Explainability - -### Appendix A.0 — Replay / Record mode - -- WebService ships a **RecordModeService** that assembles replay manifests (schema v1) with policy/feed/tool pins and reachability references, then writes deterministic input/output bundles to the configured object store (RustFS default, S3/Minio fallback) under `replay//.tar.zst`. -- Bundles contain canonical manifest JSON plus inputs (policy/feed/tool/analyzer digests) and outputs (SBOM, findings, optional VEX/logs); CAS URIs follow `cas://replay/...` and are attached to scan snapshots as `ReplayArtifacts`. -- Reachability graphs/traces are folded into the manifest via `ReachabilityReplayWriter`; manifests and bundles hash with stable ordering for replay verification (`docs/replay/DETERMINISTIC_REPLAY.md`). -- Worker sealed-mode intake reads `replay.bundle.uri` + `replay.bundle.sha256` (plus determinism feed/policy pins) from job metadata, persists bundle refs in analysis and surface manifest, and validates hashes before use. -- Deterministic execution switches (`docs/modules/scanner/deterministic-execution.md`) must be enabled when generating replay bundles to keep hashes stable. - -EntryTrace emits structured diagnostics and metrics so operators can quickly understand why resolution succeeded or degraded: - -| Reason | Description | Typical Mitigation | -|--------|-------------|--------------------| -| `CommandNotFound` | A command referenced in the script cannot be located in the layered root filesystem or `PATH`. | Ensure binaries exist in the image or extend `PATH` hints. | -| `MissingFile` | `source`/`.`/`run-parts` targets are missing. | Bundle the script or guard the include. | -| `DynamicEnvironmentReference` | Path depends on `$VARS` that are unknown at scan time. | Provide defaults via scan metadata or accept partial usage. | -| `RecursionLimitReached` | Nested includes exceeded the analyzer depth limit (default 64). | Flatten indirection or increase the limit in options. | -| `RunPartsEmpty` | `run-parts` directory contained no executable entries. | Remove empty directories or ignore if intentional. | -| `JarNotFound` / `ModuleNotFound` | Java/Python targets missing, preventing interpreter tracing. | Ship the jar/module with the image or adjust the launcher. | - -Diagnostics drive two metrics published by `EntryTraceMetrics`: - -- `entrytrace_resolutions_total{outcome}` — resolution attempts segmented by outcome (`resolved`, `partiallyresolved`, `unresolved`). -- `entrytrace_unresolved_total{reason}` — diagnostic counts keyed by reason. - -Structured logs include `entrytrace.path`, `entrytrace.command`, `entrytrace.reason`, and `entrytrace.depth`, all correlated with scan/job IDs. Timestamps are normalized to UTC (microsecond precision) to keep DSSE attestations and UI traces explainable. - -### Appendix B — BOM‑Index sidecar - -``` -struct Header { magic, version, imageDigest, createdAt } -vector purls -map components -optional map usedByEntrypoint -``` - -### Appendix C — Stack-Trace Exploit Path View (Sprint 061) - -The Triage library provides a stack-trace–style visualization layer on top of `ExploitPath` -clusters, designed for UI rendering as collapsible call-chain frames. - -#### Models (`StellaOps.Scanner.Triage.Models`) - -| Type | Purpose | -|------|---------| -| `StackTraceExploitPathView` | Root view: ordered frames, CVE IDs, severity label, collapsed state | -| `StackTraceFrame` | Single frame: symbol, role, source location, snippet, gate label | -| `SourceSnippet` | Syntax-highlighted source extract at a frame location | -| `FrameRole` | Entrypoint / Intermediate / Sink / GatedIntermediate | -| `StackTraceViewRequest` | Build request with source mappings and gate labels | - -#### Frame Role Assignment - -| Position | Has Gate | Role | -|----------|----------|------| -| First | — | Entrypoint | -| Last | — | Sink | -| Middle | No | Intermediate | -| Middle | Yes | GatedIntermediate | - -#### Collapse Heuristic - -Paths with > 3 frames are collapsed by default in the UI (showing only entrypoint + sink). -The user can expand to see the full call chain. - -#### Service (`IStackTraceExploitPathViewService`) - -- `BuildView(StackTraceViewRequest)` — transforms a single `ExploitPath` into a `StackTraceExploitPathView` -- `BuildViews(IReadOnlyList)` — batch transformation, ordered by priority score descending then path ID for determinism - -#### Source Snippet Integration - + +* WebService constructs **predicate** with `image_digest`, `stellaops_version`, `license_id`, `policy_digest?` (when emitting **final reports**), timestamps. +* Calls **Signer** (requires **OpTok + PoE**); Signer verifies **entitlement + scanner image integrity** and returns **DSSE bundle**. +* **Attestor** logs to **Rekor v2**; returns `{uuid,index,proof}` → stored in `artifacts.rekor`. +* **Verdict OCI idempotency**: `push-verdict` computes `sha256` idempotency from DSSE envelope bytes, writes `org.stellaops.idempotency.key`, uses stable `verdict-` manifest tags, retries transient push failures (429/5xx/timeouts), and treats conflict/already-submitted responses as success. +* **Hybrid reachability attestations**: graph-level DSSE (mandatory) plus optional edge-bundle DSSEs for runtime/init/contested edges. See [`docs/modules/reach-graph/guides/hybrid-attestation.md`](../reach-graph/guides/hybrid-attestation.md) for verification runbooks and Rekor guidance. +* Operator enablement runbooks (toggles, env-var map, rollout guidance) live in [`operations/dsse-rekor-operator-guide.md`](operations/dsse-rekor-operator-guide.md) per SCANNER-ENG-0015. + +--- + +## 6) Three‑way diff (image → layer → component) + +### 6.1 Keys & classification + +* Component key: **purl** when present; else `bin:{sha256}`. +* Diff classes: `added`, `removed`, `version_changed` (`upgraded|downgraded`), `metadata_changed` (e.g., origin from attestation vs observed). +* Layer attribution: for each change, resolve the **introducing/removing layer**. + +### 6.2 Algorithm (outline) + +``` +A = components(imageOld, key) +B = components(imageNew, key) + +added = B \ A +removed = A \ B +changed = { k in A∩B : version(A[k]) != version(B[k]) || origin changed } + +for each item in added/removed/changed: + layer = attribute_to_layer(item, imageOld|imageNew) + usageFlag = usedByEntrypoint(item, imageNew) +emit diff.json (grouped by layer with badges) +``` + +Diffs are stored as artifacts and feed **UI** and **CLI**. + +--- + +## 7) Build‑time SBOMs (fast CI path) + +**Scanner.Sbomer.BuildXPlugin** can act as a BuildKit **generator**: + +* During `docker buildx build --attest=type=sbom,generator=stellaops/sbom-indexer`, run analyzers on the build context/output; attach SBOMs as OCI **referrers** to the built image. +* Optionally request **Signer/Attestor** to produce **Stella Ops‑verified** attestation immediately; else, Scanner.WebService can verify and re‑attest post‑push. +* Scanner.WebService trusts build‑time SBOMs per policy, enabling **no‑rescan** for unchanged bases. + +--- + +## 8) Configuration (YAML) + +```yaml +scanner: + queue: + kind: valkey # uses redis:// protocol + url: "redis://queue:6379/0" + postgres: + connectionString: "Host=postgres;Port=5432;Database=scanner;Username=stellaops;Password=stellaops" + s3: + endpoint: "http://rustfs:8080" + bucket: "stellaops" + objectLock: "governance" # or 'compliance' + analyzers: + os: { apk: true, dpkg: true, rpm: true } + lang: { java: true, node: true, bun: true, python: true, go: true, dotnet: true, rust: true, ruby: true, php: true } + native: { elf: true, pe: false, macho: false } # PE/Mach-O in M2 + entryTrace: { enabled: true, shellMaxDepth: 64, followRunParts: true } + emit: + cdx: { json: true, protobuf: true } + spdx: { json: true } + compress: "zstd" + rekor: + url: "https://rekor-v2.internal" + signer: + url: "https://signer.internal" + limits: + maxParallel: 8 + perRegistryConcurrency: 2 + policyHints: + verifyImageSignature: false + trustBuildTimeSboms: true +``` + +--- + +## 9) Scale & performance + +* **Parallelism**: per‑analyzer concurrency; bounded directory walkers; file CAS dedupe by sha256. +* **Distributed locks** per **layer digest** to prevent duplicate work across Workers. +* **Registry throttles**: per‑host concurrency budgets; exponential backoff on 429/5xx. +* **Targets**: + + * **Build‑time**: P95 ≤ 3–5 s on warmed bases (CI generator). + * **Post‑build delta**: P95 ≤ 10 s for 200 MB images with cache hit. + * **Emit**: CycloneDX Protobuf ≤ 150 ms for 5k components; JSON ≤ 500 ms. + * **Diff**: ≤ 200 ms for 5k vs 5k components. + +--- + +## 10) Security posture + +* **AuthN**: Authority‑issued short OpToks (DPoP/mTLS). +* **AuthZ**: scopes (`scanner.scan`, `scanner.export`, `scanner.catalog.read`). +* **mTLS** to **Signer**/**Attestor**; only **Signer** can sign. +* **No network fetches** during analysis (except registry pulls and optional Rekor index reads). +* **Sandboxing**: non‑root containers; read‑only FS; seccomp profiles; disable execution of scanned content. +* **Release integrity**: all first‑party images are **cosign‑signed**; Workers/WebService self‑verify at startup. + +--- + +## 11) Observability & audit + +* **Metrics**: + + * `scanner.jobs_inflight`, `scanner.scan_latency_seconds` + * `scanner.layer_cache_hits_total`, `scanner.file_cas_hits_total` + * `scanner.artifact_bytes_total{format}` + * `scanner.attestation_latency_seconds`, `scanner.rekor_failures_total` + * `scanner_analyzer_golang_heuristic_total{indicator,version_hint}` — increments whenever the Go analyzer falls back to heuristics (build-id or runtime markers). Grafana panel: `sum by (indicator) (rate(scanner_analyzer_golang_heuristic_total[5m]))`; alert when the rate is ≥ 1 for 15 minutes to highlight unexpected stripped binaries. +* **Tracing**: spans for acquire→union→analyzers→compose→emit→sign→log. +* **Audit logs**: DSSE requests log `license_id`, `image_digest`, `artifactSha256`, `policy_digest?`, Rekor UUID on success. + +--- + +## 12) Testing matrix + +* **Analyzer contracts:** see `language-analyzers-contract.md` for cross-analyzer identity safety, evidence locators, and container layout rules. Per-analyzer docs: `analyzers-java.md`, `dotnet-analyzer.md`, `analyzers-python.md`, `analyzers-node.md`, `analyzers-bun.md`, `analyzers-go.md`. Implementation: `docs/implplan/SPRINT_0408_0001_0001_scanner_language_detection_gaps_program.md`. + +* **Determinism:** given same image + analyzers → byte‑identical **CDX Protobuf**; JSON normalized. +* **OS packages:** ground‑truth images per distro; compare to package DB. +* **Lang ecosystems:** sample images per ecosystem (Java/Node/Python/Go/.NET/Rust) with installed metadata; negative tests w/ lockfile‑only. +* **Native & EntryTrace:** ELF graph correctness; shell AST cases (includes, run‑parts, exec, case/if). +* **Diff:** layer attribution against synthetic two‑image sequences. +* **Performance:** cold vs warm cache; large `node_modules` and `site‑packages`. +* **Security:** ensure no code execution from image; fuzz parser inputs; path traversal resistance on layer extract. + +--- + +## 13) Failure modes & degradations + +* **Missing OS DB** (files exist, DB removed): record **files**; do **not** fabricate package components; emit `bin:{sha256}` where unavoidable; flag in evidence. +* **Unreadable metadata** (corrupt dist‑info): record file evidence; skip component creation; annotate. +* **Dynamic shell constructs**: mark unresolved edges with reasons (env var unknown) and continue; **Usage** view may be partial. +* **Registry rate limits**: honor backoff; queue job retries with jitter. +* **Signer refusal** (license/plan/version): scan completes; artifact produced; **no attestation**; WebService marks result as **unverified**. + +--- + +## 14) Optional plug‑ins (off by default) + +* **Patch‑presence detector** (signature‑based backport checks). Reads curated function‑level signatures from advisories; inspects binaries for patched code snippets to lower false‑positives for backported fixes. Runs as a sidecar analyzer that **annotates** components; never overrides core identities. +* **Runtime probes** (with Zastava): when allowed, compare **/proc//maps** (DSOs actually loaded) with static **Usage** view for precision. + +--- + +## 15) DevOps & operations + +* **HA**: WebService horizontal scale; Workers autoscale by queue depth & CPU; distributed locks on layers. +* **Retention**: ILM rules per artifact class (`short`, `default`, `compliance`); **Object Lock** for compliance artifacts (reports, signed SBOMs). +* **Upgrades**: bump **cache schema** when analyzer outputs change; WebService triggers refresh of dependent artifacts. +* **Backups**: PostgreSQL (pg_dump daily); RustFS snapshots (filesystem-level rsync/ZFS) or S3 versioning when legacy driver enabled; Rekor v2 DB snapshots. + +--- + +## 16) CLI & UI touch points + +* **CLI**: `stellaops scan `, `stellaops diff --old --new`, `stellaops export`, `stellaops verify attestation `. +* **UI**: Scan detail shows **Inventory/Usage** toggles, **Diff by Layer**, **Attestation badge** (verified/unverified), Rekor link, and **EntryTrace** chain with file:line breadcrumbs. + +--- + +## 17) Roadmap (Scanner) + +* **M2**: Windows containers (MSI/SxS/GAC analyzers), PE/Mach‑O native analyzer, deeper Rust metadata. +* **M2**: Buildx generator GA (certified external registries), cross‑registry trust policies. +* **M3**: Patch‑presence plug‑in GA (opt‑in), cross‑image corpus clustering (evidence‑only; not identity). +* **M3**: Advanced EntryTrace (POSIX shell features breadth, busybox detection). + +--- + +### Appendix A — EntryTrace resolution (pseudo) + +```csharp +ResolveEntrypoint(ImageConfig cfg, RootFs fs): + cmd = Normalize(cfg.ENTRYPOINT, cfg.CMD) + stack = [ Script(cmd, path=FindOnPath(cmd[0], fs)) ] + visited = set() + + while stack not empty and depth < MAX: + cur = stack.pop() + if cur in visited: continue + visited.add(cur) + + if IsShellScript(cur.path): + ast = ParseShell(cur.path) + foreach directive in ast: + if directive is Source include: + p = ResolveInclude(include.path, cur.env, fs) + stack.push(Script(p)) + if directive is Exec call: + p = ResolveExec(call.argv[0], cur.env, fs) + stack.push(Program(p, argv=call.argv)) + if directive is Interpreter (python -m / node / java -jar): + term = ResolveInterpreterTarget(call, fs) + stack.push(Program(term)) + else: + return Terminal(cur.path) + + return Unknown(reason) +``` + +### Appendix A.1 — EntryTrace Explainability + +### Appendix A.0 — Replay / Record mode + +- WebService ships a **RecordModeService** that assembles replay manifests (schema v1) with policy/feed/tool pins and reachability references, then writes deterministic input/output bundles to the configured object store (RustFS default, S3/Minio fallback) under `replay//.tar.zst`. +- Bundles contain canonical manifest JSON plus inputs (policy/feed/tool/analyzer digests) and outputs (SBOM, findings, optional VEX/logs); CAS URIs follow `cas://replay/...` and are attached to scan snapshots as `ReplayArtifacts`. +- Reachability graphs/traces are folded into the manifest via `ReachabilityReplayWriter`; manifests and bundles hash with stable ordering for replay verification (`docs/replay/DETERMINISTIC_REPLAY.md`). +- Worker sealed-mode intake reads `replay.bundle.uri` + `replay.bundle.sha256` (plus determinism feed/policy pins) from job metadata, persists bundle refs in analysis and surface manifest, and validates hashes before use. +- Deterministic execution switches (`docs/modules/scanner/deterministic-execution.md`) must be enabled when generating replay bundles to keep hashes stable. + +EntryTrace emits structured diagnostics and metrics so operators can quickly understand why resolution succeeded or degraded: + +| Reason | Description | Typical Mitigation | +|--------|-------------|--------------------| +| `CommandNotFound` | A command referenced in the script cannot be located in the layered root filesystem or `PATH`. | Ensure binaries exist in the image or extend `PATH` hints. | +| `MissingFile` | `source`/`.`/`run-parts` targets are missing. | Bundle the script or guard the include. | +| `DynamicEnvironmentReference` | Path depends on `$VARS` that are unknown at scan time. | Provide defaults via scan metadata or accept partial usage. | +| `RecursionLimitReached` | Nested includes exceeded the analyzer depth limit (default 64). | Flatten indirection or increase the limit in options. | +| `RunPartsEmpty` | `run-parts` directory contained no executable entries. | Remove empty directories or ignore if intentional. | +| `JarNotFound` / `ModuleNotFound` | Java/Python targets missing, preventing interpreter tracing. | Ship the jar/module with the image or adjust the launcher. | + +Diagnostics drive two metrics published by `EntryTraceMetrics`: + +- `entrytrace_resolutions_total{outcome}` — resolution attempts segmented by outcome (`resolved`, `partiallyresolved`, `unresolved`). +- `entrytrace_unresolved_total{reason}` — diagnostic counts keyed by reason. + +Structured logs include `entrytrace.path`, `entrytrace.command`, `entrytrace.reason`, and `entrytrace.depth`, all correlated with scan/job IDs. Timestamps are normalized to UTC (microsecond precision) to keep DSSE attestations and UI traces explainable. + +### Appendix B — BOM‑Index sidecar + +``` +struct Header { magic, version, imageDigest, createdAt } +vector purls +map components +optional map usedByEntrypoint +``` + +### Appendix C — Stack-Trace Exploit Path View (Sprint 061) + +The Triage library provides a stack-trace–style visualization layer on top of `ExploitPath` +clusters, designed for UI rendering as collapsible call-chain frames. + +#### Models (`StellaOps.Scanner.Triage.Models`) + +| Type | Purpose | +|------|---------| +| `StackTraceExploitPathView` | Root view: ordered frames, CVE IDs, severity label, collapsed state | +| `StackTraceFrame` | Single frame: symbol, role, source location, snippet, gate label | +| `SourceSnippet` | Syntax-highlighted source extract at a frame location | +| `FrameRole` | Entrypoint / Intermediate / Sink / GatedIntermediate | +| `StackTraceViewRequest` | Build request with source mappings and gate labels | + +#### Frame Role Assignment + +| Position | Has Gate | Role | +|----------|----------|------| +| First | — | Entrypoint | +| Last | — | Sink | +| Middle | No | Intermediate | +| Middle | Yes | GatedIntermediate | + +#### Collapse Heuristic + +Paths with > 3 frames are collapsed by default in the UI (showing only entrypoint + sink). +The user can expand to see the full call chain. + +#### Service (`IStackTraceExploitPathViewService`) + +- `BuildView(StackTraceViewRequest)` — transforms a single `ExploitPath` into a `StackTraceExploitPathView` +- `BuildViews(IReadOnlyList)` — batch transformation, ordered by priority score descending then path ID for determinism + +#### Source Snippet Integration + When source mappings are provided (keyed by `file:line`), the service attaches `SourceSnippet` records to matching frames. This enables syntax-highlighted code display in the UI without requiring the scanner to store full source files. diff --git a/docs/modules/ui/v2-rewire/S00_nav_rendering_policy.md b/docs/modules/ui/v2-rewire/S00_nav_rendering_policy.md index 82661ada4..353078e33 100644 --- a/docs/modules/ui/v2-rewire/S00_nav_rendering_policy.md +++ b/docs/modules/ui/v2-rewire/S00_nav_rendering_policy.md @@ -1,81 +1,143 @@ # S00 Nav Rendering Policy Status: Frozen -Date: 2026-02-18 +Date: 2026-03-29 Working directory: `docs/modules/ui/v2-rewire` -Sprint: `20260218_005`, task `R0-03` +Sprint: `20260218_005`, task `R0-03` (updated 2026-03-29 for 6-group restructure) ## Policy statement -Release Control-owned capabilities may be rendered as direct shortcuts in the sidebar if and only if: -1. Ownership is labeled as **Release Control** in breadcrumbs and page headers. -2. The canonical routes for those capabilities live under `/release-control/*`. +Capabilities may be rendered as direct shortcuts in the sidebar if and only if: +1. Ownership is labeled with the correct **group name** in breadcrumbs and page headers. +2. The canonical routes for those capabilities live under the group's route prefix. 3. The sidebar shortcut links to the canonical route, not an alias. This policy prevents mixed rendering where the same screen appears to be owned by two domains. +## Canonical sidebar groups (6 groups) + +The sidebar is organized into exactly 6 menu groups identified by `menuGroupId`: + +| # | menuGroupId | Rendered label | Purpose | +|---|-------------|----------------|---------| +| 1 | `home` | *(none)* | Dashboard entry point | +| 2 | `release-control` | Release Control | Deployments, Releases, Environments, Readiness | +| 3 | `security` | Security | Vulnerabilities, Posture, Scan, VEX & Exceptions, Risk & Governance (incl. Simulation, Audit) | +| 4 | `evidence` | Evidence | Evidence Overview, Decision Capsules, Audit Log, Export Center | +| 5 | `operations` | Operations | Operations Hub, Policy Packs, Scheduled Jobs, Feeds & Airgap, Agent Fleet, Signals, Scripts, Diagnostics | +| 6 | `setup-admin` | Settings | Integrations, Identity & Access, Certificates & Trust, Theme & Branding, User Preferences | + +### Policy dissolution + +The former `policy` group was dissolved in the 7-to-6 restructure. Policy is a toolset, not a persona -- its items were distributed by audience: + +- **Security** absorbed: VEX & Exceptions, Risk & Governance (including Simulation and Policy Audit). These are consumed by security practitioners during triage and risk assessment. +- **Operations** absorbed: Policy Packs. Pack authoring and management is an operational/admin workflow. + +There is no standalone "Policy" group in the sidebar. The `/ops/policy/*` route prefix is retained for backward compatibility but items are surfaced under their new parent groups. + +### New items surfaced (2026-03-29) + +- **Readiness** (`/releases/readiness`) -- added to Release Control. Surfaces gate status and release readiness checks before promotion. +- **Findings Explorer** (`/security/findings`) -- added as a child of Security Posture. Provides a filterable view of all security findings across the fleet. + +### Items removed from direct navigation + +The following items are no longer direct sidebar entries but remain routable for deep links and contextual navigation: + +- Runtime Drift, Notifications, Watchlist -- accessible from Operations Hub landing page +- Replay & Verify, Bundles, Trust -- accessible from Evidence Overview and Decision Capsules detail + ## Allowed rendering model ### Desktop (expanded sidebar) ``` -Dashboard +[Home] + Dashboard Release Control - ├── Releases [shortcut direct nav allowed] - ├── Approvals [shortcut direct nav allowed] - ├── Bundles [nested only — no direct shortcut] - ├── Deployments [nested only — no direct shortcut] - └── Regions & Environments [nested only — no direct shortcut] -Security & Risk -Evidence & Audit -Integrations -Platform Ops -Administration + Deployments [badge: failed runs + pending approvals] + Releases [badge: blocked gates] + Environments + Readiness +Security + Vulnerabilities [badge: critical findings] + Security Posture [sparkline: security trend] + Supply-Chain Data + Findings Explorer + Reachability + Unknowns + Scan Image + VEX & Exceptions + Risk & Governance + Simulation + Policy Audit +Evidence + Evidence Overview + Decision Capsules + Audit Log + Export Center +Operations + Operations Hub [sparkline: platform trend] + Policy Packs + Scheduled Jobs + Feeds & Airgap + Agent Fleet + Signals + Scripts + Diagnostics +Settings + Integrations + Identity & Access + Certificates & Trust + Theme & Branding + User Preferences ``` -`Releases` and `Approvals` may appear as direct children under `Release Control` in the sidebar -(rather than requiring expand → click). -`Bundles`, `Deployments`, and `Regions & Environments` remain nested and require expand. +### Desktop (collapsed sidebar -- icons only) -### Desktop (collapsed sidebar — icons only) - -- Show icon for Release Control root only. -- Tooltip on hover shows "Release Control". -- Click navigates to Release Control overview or last active child. -- No separate Releases / Approvals icons in collapsed mode. +- Show icon for each group root only. +- Tooltip on hover shows the group label. +- Click navigates to the first item in the group or last active child. +- No separate child icons in collapsed mode. ### Mobile (navigation drawer) -- All root domains appear as top-level items in the drawer. -- Release Control expands in-place to show child nav items. -- `Releases` and `Approvals` may appear as drawer children with Release Control as visible parent. -- No Release Control capabilities may appear as top-level drawer items separate from the Release Control group. +- All 6 groups appear as top-level items in the drawer. +- Groups expand in-place to show child nav items. +- No capabilities may appear as top-level drawer items separate from their group. ## Breadcrumb rules -Canonical format: `Root Domain > Capability > [Sub-page]` +Canonical format: `Group > Capability > [Sub-page]` | Scenario | Breadcrumb | Notes | | --- | --- | --- | -| Releases list | `Release Control > Releases` | No shortcut bypasses ownership label | +| Deployments list | `Release Control > Deployments` | | | Release detail | `Release Control > Releases > RCB-1234` | ID or name appended | -| Approvals queue | `Release Control > Approvals` | | -| Approval detail | `Release Control > Approvals > APR-5678` | | -| Bundle catalog | `Release Control > Bundles` | | -| Bundle detail | `Release Control > Bundles > my-bundle` | | -| Bundle version detail | `Release Control > Bundles > my-bundle > v1.3.0` | | -| Deployments | `Release Control > Deployments` | | -| Environments list | `Release Control > Regions & Environments` | | -| Environment detail | `Release Control > Regions & Environments > staging-eu` | | +| Readiness | `Release Control > Readiness` | New item | +| Vulnerabilities | `Security > Vulnerabilities` | | +| Findings Explorer | `Security > Security Posture > Findings Explorer` | Nested under posture | +| VEX & Exceptions | `Security > VEX & Exceptions` | Absorbed from Policy | +| Risk & Governance | `Security > Risk & Governance` | Absorbed from Policy | +| Policy Simulation | `Security > Risk & Governance > Simulation` | Nested child | +| Evidence Overview | `Evidence > Evidence Overview` | | +| Decision Capsules | `Evidence > Decision Capsules` | | +| Audit Log | `Evidence > Audit Log` | | +| Operations Hub | `Operations > Operations Hub` | | +| Policy Packs | `Operations > Policy Packs` | Absorbed from Policy | +| Integrations | `Settings > Integrations` | | +| Certificates & Trust | `Settings > Certificates & Trust` | Renamed from "Certificates" | ### Concrete counter-examples (forbidden) | Forbidden breadcrumb | Reason | | --- | --- | -| `Approvals > APR-5678` | Missing Release Control ownership prefix | -| `Releases` (no parent) | Same — no domain context | -| `Settings > Policy Governance` | Policy Governance owner is Administration, not Settings | -| `Evidence & Audit > Trust & Signing` | Trust & Signing owner is Administration; Evidence may only show a consumer link | +| `Policy > VEX & Exceptions` | Policy group no longer exists; VEX is under Security | +| `Policy > Packs` | Policy group dissolved; Packs is under Operations | +| `Approvals > APR-5678` | Missing group ownership prefix | +| `Releases` (no parent) | No domain context | +| `Evidence > Trust` | Trust item removed from Evidence nav | ## Legacy label transition behavior @@ -83,7 +145,7 @@ Where users know a surface by an old label, show a compact transition label duri Rules: - Transition labels appear only in page headers and sidebar items, not in breadcrumbs. -- Format: canonical label is primary; old label appears parenthetically — e.g., `Policy Governance (formerly Policy Studio)`. +- Format: canonical label is primary; old label appears parenthetically -- e.g., `Certificates & Trust (formerly Certificates)`. - Transition labels are removed at sprint 016 cutover unless traffic evidence requires extension. - Canonical labels are always primary; old labels never replace canonical ones. @@ -91,26 +153,26 @@ Planned transition labels: | Canonical label | Transition label (migration window only) | Remove at | | --- | --- | --- | -| `Security & Risk` | `Security & Risk (formerly Security)` | Sprint 016 | -| `Platform Ops` | `Platform Ops (formerly Operations)` | Sprint 016 | -| `Evidence & Audit` | `Evidence & Audit (formerly Evidence)` | Sprint 016 | -| `Policy Governance` | `Policy Governance (formerly Policy Studio / Policy)` | Sprint 016 | +| `Security` | `Security (formerly Security & Risk)` | Sprint 016 | +| `Operations` | `Operations (formerly Platform Ops)` | Sprint 016 | +| `Evidence` | `Evidence (formerly Evidence & Audit)` | Sprint 016 | +| `Certificates & Trust` | `Certificates & Trust (formerly Certificates)` | Sprint 016 | ## Explicit do-not list The following rendering patterns are forbidden in any sprint implementation: -1. **Do not** place Release Control capability screens (`Releases`, `Approvals`, `Bundles`, `Deployments`, `Environments`) as root-level sidebar items independent from the `Release Control` group. -2. **Do not** display a breadcrumb that omits the canonical root domain prefix. +1. **Do not** place any capability as a root-level sidebar item independent from its canonical group. +2. **Do not** display a breadcrumb that omits the canonical group prefix. 3. **Do not** show different ownership labels on desktop vs. mobile for the same screen. -4. **Do not** use legacy root-level nav paths (e.g., `/approvals`, `/releases`) as the canonical nav target — they must redirect to `/release-control/*` canonical targets. -5. **Do not** label `Trust & Signing` as owned by Evidence & Audit or Security in any nav or header. -6. **Do not** label `Policy Governance` as owned by Release Control in any nav or header. -7. **Do not** introduce a new root domain that is not in the canonical 7: Dashboard, Release Control, Security & Risk, Evidence & Audit, Integrations, Platform Ops, Administration. +4. **Do not** use legacy root-level nav paths (e.g., `/approvals`, `/releases` at root) as the canonical nav target -- they must redirect to canonical targets. +5. **Do not** reintroduce a `policy` group -- Policy items live under Security and Operations. +6. **Do not** label Policy Packs as owned by Security -- Packs authoring is an Operations concern. +7. **Do not** introduce a new root domain that is not in the canonical 6: Home, Release Control, Security, Evidence, Operations, Settings. ## Route alias requirements for migration -During the alias window, current root-level paths (`/releases`, `/approvals`) must: -- Resolve to the canonical `/release-control/releases` and `/release-control/approvals` routes. -- Render the canonical breadcrumb (e.g., `Release Control > Releases`) — not an alias-derived breadcrumb. +During the alias window, legacy paths must: +- Resolve to the canonical routes under the new group structure. +- Render the canonical breadcrumb (e.g., `Security > VEX & Exceptions`) -- not an alias-derived breadcrumb. - Not appear as primary nav items in the sidebar; the sidebar must link to canonical paths only. diff --git a/docs/technical/testing/TEST_SUITE_OVERVIEW.md b/docs/technical/testing/TEST_SUITE_OVERVIEW.md index 380e02fc1..9cfbfe902 100755 --- a/docs/technical/testing/TEST_SUITE_OVERVIEW.md +++ b/docs/technical/testing/TEST_SUITE_OVERVIEW.md @@ -1,10 +1,10 @@ -# Automated Test-Suite Overview +# Automated Test-Suite Overview This document enumerates **every automated check** executed by the Stella Ops CI pipeline, from unit level to chaos experiments. It is intended for contributors who need to extend coverage or diagnose failures. -> **Build parameters** – values such as `{{ dotnet }}` (runtime) and +> **Build parameters** – values such as `{{ dotnet }}` (runtime) and > `{{ angular }}` (UI framework) are injected at build time. --- @@ -13,7 +13,7 @@ contributors who need to extend coverage or diagnose failures. ### Core Principles -1. **Determinism as Contract**: Scan verdicts must be reproducible. Same inputs → byte-identical outputs. +1. **Determinism as Contract**: Scan verdicts must be reproducible. Same inputs → byte-identical outputs. 2. **Offline by Default**: Every test (except explicitly tagged "online") runs without network access. 3. **Evidence-First Validation**: Assertions verify the complete evidence chain, not just pass/fail. 4. **Interop is Required**: Compatibility with ecosystem tools (Syft, Grype, Trivy, cosign) blocks releases. @@ -78,16 +78,16 @@ the required test types per project model and the module-to-model mapping. | Metric | Budget | Gate | |--------|--------|------| -| API unit coverage | ≥ 85% lines | PR merge | -| API response P95 | ≤ 120 ms | nightly alert | -| Δ-SBOM warm scan P95 (4 vCPU) | ≤ 5 s | nightly alert | -| Lighthouse performance score | ≥ 90 | nightly alert | -| Lighthouse accessibility score | ≥ 95 | nightly alert | +| API unit coverage | ≥ 85% lines | PR merge | +| API response P95 | ≤ 120 ms | nightly alert | +| Δ-SBOM warm scan P95 (4 vCPU) | ≤ 5 s | nightly alert | +| Lighthouse performance score | ≥ 90 | nightly alert | +| Lighthouse accessibility score | ≥ 95 | nightly alert | | k6 sustained RPS drop | < 5% vs baseline | nightly alert | | **Replay determinism** | 0 byte diff | **Release** | -| **Interop findings parity** | ≥ 95% | **Release** | +| **Interop findings parity** | ≥ 95% | **Release** | | **Offline E2E** | All pass with no network | **Release** | -| **Unknowns budget (prod)** | ≤ configured limit | **Release** | +| **Unknowns budget (prod)** | ≤ configured limit | **Release** | | **Router Retry-After compliance** | 100% | Nightly | --- @@ -109,7 +109,7 @@ dotnet test --filter "Category=Interop" The script spins up PostgreSQL/Valkey via Testcontainers and requires: -* Docker ≥ 25 +* Docker ≥ 25 * Node 20 (for Jest/Playwright) ### PostgreSQL Testcontainers @@ -158,7 +158,7 @@ stella replay verify --manifest run-manifest.json ### Evidence Index The **Evidence Index** links verdicts to their supporting evidence chain: -- Verdict → SBOM digests → Attestation IDs → Tool versions +- Verdict → SBOM digests → Attestation IDs → Tool versions ### Golden Corpus @@ -191,7 +191,7 @@ public class OfflineTests : NetworkIsolatedTestBase --- -## Concelier OSV↔GHSA Parity Fixtures +## Concelier OSV↔GHSA Parity Fixtures The Concelier connector suite includes a regression test (`OsvGhsaParityRegressionTests`) that checks a curated set of GHSA identifiers against OSV responses. The fixture diff --git a/docs/workflow/engine/01-requirements-and-principles.md b/docs/workflow/engine/01-requirements-and-principles.md index d7b255a02..7639ac6bc 100644 --- a/docs/workflow/engine/01-requirements-and-principles.md +++ b/docs/workflow/engine/01-requirements-and-principles.md @@ -1,4 +1,4 @@ -# 01. Requirements And Principles +# 01. Requirements And Principles ## 1. Product Goal @@ -239,7 +239,7 @@ They are related, but they are not the same data structure. ### 5.3 Run To Wait -The engine should never keep a workflow instance “hot” in memory for correctness. +The engine should never keep a workflow instance “hot” in memory for correctness. Execution should run until: diff --git a/docs/workflow/tutorials/01-hello-world/README.md b/docs/workflow/tutorials/01-hello-world/README.md index 5918e6ea9..3484cf5a7 100644 --- a/docs/workflow/tutorials/01-hello-world/README.md +++ b/docs/workflow/tutorials/01-hello-world/README.md @@ -1,15 +1,15 @@ -# Tutorial 1: Hello World +# Tutorial 1: Hello World The simplest possible workflow: initialize state from a start request, activate a single human task, and complete the workflow when the task is done. ## Concepts Introduced -- `IDeclarativeWorkflow` — the contract every workflow implements -- `WorkflowSpec.For()` — the builder entry point -- `.InitializeState()` — transforms the start request into workflow state -- `.StartWith(task)` — sets the first task to activate -- `WorkflowHumanTask.For()` — defines a human task -- `.OnComplete(flow => flow.Complete())` — terminal step +- `IDeclarativeWorkflow` — the contract every workflow implements +- `WorkflowSpec.For()` — the builder entry point +- `.InitializeState()` — transforms the start request into workflow state +- `.StartWith(task)` — sets the first task to activate +- `WorkflowHumanTask.For()` — defines a human task +- `.OnComplete(flow => flow.Complete())` — terminal step ## What Happens at Runtime @@ -17,7 +17,7 @@ The simplest possible workflow: initialize state from a start request, activate 2. State initializes to `{ "customerName": "John" }` 3. Task "Greet Customer" is created with status "Pending" 4. A user assigns the task to themselves, then completes it -5. `OnComplete` executes `.Complete()` — the workflow finishes +5. `OnComplete` executes `.Complete()` — the workflow finishes ## Variants @@ -26,5 +26,5 @@ The simplest possible workflow: initialize state from a start request, activate ## Next -[Tutorial 2: Service Tasks](../02-service-tasks/) — call external services before or after human tasks. +[Tutorial 2: Service Tasks](../02-service-tasks/) — call external services before or after human tasks. diff --git a/docs/workflow/tutorials/02-service-tasks/README.md b/docs/workflow/tutorials/02-service-tasks/README.md index ac291875e..5cf929563 100644 --- a/docs/workflow/tutorials/02-service-tasks/README.md +++ b/docs/workflow/tutorials/02-service-tasks/README.md @@ -1,15 +1,15 @@ -# Tutorial 2: Service Tasks +# Tutorial 2: Service Tasks Call external services (microservices, HTTP APIs, GraphQL, RabbitMQ) from within a workflow. Handle failures and timeouts gracefully. ## Concepts Introduced -- `.Call()` — invoke a transport with payload and optional response capture -- Address types — `LegacyRabbit`, `Microservice`, `Http`, `Graphql`, `Rabbit` -- `resultKey` — store the service response in workflow state -- `whenFailure` / `whenTimeout` — recovery branches -- `WorkflowHandledBranchAction.Complete` — shorthand for "complete on error" -- `timeoutSeconds` — per-step timeout override (default: 1 hour) +- `.Call()` — invoke a transport with payload and optional response capture +- Address types — `LegacyRabbit`, `Microservice`, `Http`, `Graphql`, `Rabbit` +- `resultKey` — store the service response in workflow state +- `whenFailure` / `whenTimeout` — recovery branches +- `WorkflowHandledBranchAction.Complete` — shorthand for "complete on error" +- `timeoutSeconds` — per-step timeout override (default: 1 hour) ## Key Points @@ -25,5 +25,5 @@ Call external services (microservices, HTTP APIs, GraphQL, RabbitMQ) from within ## Next -[Tutorial 3: Decisions](../03-decisions/) — branch workflow logic based on conditions. +[Tutorial 3: Decisions](../03-decisions/) — branch workflow logic based on conditions. diff --git a/docs/workflow/tutorials/03-decisions/README.md b/docs/workflow/tutorials/03-decisions/README.md index e91f0f9ba..0d8f32b8d 100644 --- a/docs/workflow/tutorials/03-decisions/README.md +++ b/docs/workflow/tutorials/03-decisions/README.md @@ -1,13 +1,13 @@ -# Tutorial 3: Decisions +# Tutorial 3: Decisions -Branch workflow logic based on conditions — state values, payload answers, or complex expressions. +Branch workflow logic based on conditions — state values, payload answers, or complex expressions. ## Concepts Introduced -- `.WhenExpression()` — branch on any boolean expression -- `.WhenStateFlag()` — shorthand for checking a boolean state value -- `.WhenPayloadEquals()` — shorthand for checking a task completion payload value -- Nested decisions — decisions inside decisions for complex routing +- `.WhenExpression()` — branch on any boolean expression +- `.WhenStateFlag()` — shorthand for checking a boolean state value +- `.WhenPayloadEquals()` — shorthand for checking a task completion payload value +- Nested decisions — decisions inside decisions for complex routing ## Decision Types @@ -24,5 +24,5 @@ Branch workflow logic based on conditions — state values, payload answers ## Next -[Tutorial 4: Human Tasks](../04-human-tasks/) — approve/reject patterns with OnComplete flows. +[Tutorial 4: Human Tasks](../04-human-tasks/) — approve/reject patterns with OnComplete flows. diff --git a/docs/workflow/tutorials/04-human-tasks/README.md b/docs/workflow/tutorials/04-human-tasks/README.md index 2c8826f4e..aa3f9c9e7 100644 --- a/docs/workflow/tutorials/04-human-tasks/README.md +++ b/docs/workflow/tutorials/04-human-tasks/README.md @@ -1,26 +1,26 @@ -# Tutorial 4: Human Tasks with OnComplete Flows +# Tutorial 4: Human Tasks with OnComplete Flows -The approve/reject pattern — the most common human task flow in insurance workflows. +The approve/reject pattern — the most common human task flow in insurance workflows. ## Concepts Introduced -- `WorkflowHumanTask.For()` — define a task with name, type, route, and roles -- `.WithPayload()` — data sent to the UI when the task is displayed -- `.WithTimeout(seconds)` — optional deadline for the task -- `.WithRoles()` — restrict which roles can interact with this task -- `.OnComplete(flow => ...)` — sequence executed after user completes the task -- `.ActivateTask()` — pause workflow and wait for user action -- `.AddTask()` — register a task in the workflow spec (separate from activation) -- Re-activation — send the user back to the same task on validation failure +- `WorkflowHumanTask.For()` — define a task with name, type, route, and roles +- `.WithPayload()` — data sent to the UI when the task is displayed +- `.WithTimeout(seconds)` — optional deadline for the task +- `.WithRoles()` — restrict which roles can interact with this task +- `.OnComplete(flow => ...)` — sequence executed after user completes the task +- `.ActivateTask()` — pause workflow and wait for user action +- `.AddTask()` — register a task in the workflow spec (separate from activation) +- Re-activation — send the user back to the same task on validation failure ## Approve/Reject Pattern 1. Workflow starts, runs some service tasks -2. `.ActivateTask("Approve")` — workflow pauses +2. `.ActivateTask("Approve")` — workflow pauses 3. User sees the task in their inbox, assigns it, submits an answer 4. `.OnComplete` checks `payload.answer`: - - `"approve"` — run confirmation operations, convert to policy - - `"reject"` — cancel the application + - `"approve"` — run confirmation operations, convert to policy + - `"reject"` — cancel the application 5. If operations fail, re-activate the same task for correction ## Variants @@ -30,5 +30,5 @@ The approve/reject pattern — the most common human task flow in insurance ## Next -[Tutorial 5: Sub-Workflows](../05-sub-workflows/) — inline vs fire-and-forget child workflows. +[Tutorial 5: Sub-Workflows](../05-sub-workflows/) — inline vs fire-and-forget child workflows. diff --git a/docs/workflow/tutorials/05-sub-workflows/README.md b/docs/workflow/tutorials/05-sub-workflows/README.md index cbb0dbd5a..c4bc88eae 100644 --- a/docs/workflow/tutorials/05-sub-workflows/README.md +++ b/docs/workflow/tutorials/05-sub-workflows/README.md @@ -1,14 +1,14 @@ -# Tutorial 5: Sub-Workflows & Continuations +# Tutorial 5: Sub-Workflows & Continuations -Compose workflows by invoking child workflows — either inline (SubWorkflow) or fire-and-forget (ContinueWith). +Compose workflows by invoking child workflows — either inline (SubWorkflow) or fire-and-forget (ContinueWith). ## SubWorkflow vs ContinueWith | Feature | `.SubWorkflow()` | `.ContinueWith()` | |---------|-----------------|-------------------| -| Parent waits | Yes — resumes after child completes | No — parent completes immediately | -| State flows back | Yes — child state merges into parent | No — child is independent | -| Same instance | Yes — tasks appear under parent instance | No — new workflow instance | +| Parent waits | Yes — resumes after child completes | No — parent completes immediately | +| State flows back | Yes — child state merges into parent | No — child is independent | +| Same instance | Yes — tasks appear under parent instance | No — new workflow instance | | Use when | Steps must complete before parent continues | Fire-and-forget, scheduled work | ## Variants @@ -18,5 +18,5 @@ Compose workflows by invoking child workflows — either inline (SubWorkflo ## Next -[Tutorial 6: Advanced Patterns](../06-advanced-patterns/) — Fork, Repeat, Timer, External Signal. +[Tutorial 6: Advanced Patterns](../06-advanced-patterns/) — Fork, Repeat, Timer, External Signal. diff --git a/docs/workflow/tutorials/06-advanced-patterns/README.md b/docs/workflow/tutorials/06-advanced-patterns/README.md index 35282fa15..5e7ac3ede 100644 --- a/docs/workflow/tutorials/06-advanced-patterns/README.md +++ b/docs/workflow/tutorials/06-advanced-patterns/README.md @@ -1,4 +1,4 @@ -# Tutorial 6: Advanced Patterns +# Tutorial 6: Advanced Patterns Fork (parallel branches), Repeat (retry loops), Timer (delays), and External Signal (wait for events). @@ -18,5 +18,5 @@ Fork (parallel branches), Repeat (retry loops), Timer (delays), and External Sig ## Next -[Tutorial 7: Shared Helpers](../07-shared-helpers/) — organizing reusable workflow components. +[Tutorial 7: Shared Helpers](../07-shared-helpers/) — organizing reusable workflow components. diff --git a/docs/workflow/tutorials/07-shared-helpers/README.md b/docs/workflow/tutorials/07-shared-helpers/README.md index 41553f19c..7b736ce40 100644 --- a/docs/workflow/tutorials/07-shared-helpers/README.md +++ b/docs/workflow/tutorials/07-shared-helpers/README.md @@ -1,4 +1,4 @@ -# Tutorial 7: Shared Support Helpers +# Tutorial 7: Shared Support Helpers When building many workflows for the same domain (e.g., 50+ policy change workflows), extract reusable components into a support helper class. @@ -6,19 +6,19 @@ When building many workflows for the same domain (e.g., 50+ policy change workfl | Component | Example | |-----------|---------| -| **Address constants** | `LegacyRabbitAddress`, `HttpAddress` — centralized routing | -| **Workflow references** | `WorkflowReference` — for SubWorkflow/ContinueWith targets | +| **Address constants** | `LegacyRabbitAddress`, `HttpAddress` — centralized routing | +| **Workflow references** | `WorkflowReference` — for SubWorkflow/ContinueWith targets | | **Payload builders** | Static methods returning `WorkflowExpressionDefinition` | | **State initializers** | Base state + override pattern | | **Flow extensions** | Extension methods on `WorkflowFlowBuilder` for common sequences | ## C#-Only Tutorial -This tutorial has no JSON equivalent — it covers C# code organization patterns. +This tutorial has no JSON equivalent — it covers C# code organization patterns. - [C# Example](csharp/) ## Next -[Tutorial 8: Expressions](../08-expressions/) — path navigation, functions, and operators. +[Tutorial 8: Expressions](../08-expressions/) — path navigation, functions, and operators. diff --git a/docs/workflow/tutorials/08-expressions/README.md b/docs/workflow/tutorials/08-expressions/README.md index 506fdc383..b2fdaca24 100644 --- a/docs/workflow/tutorials/08-expressions/README.md +++ b/docs/workflow/tutorials/08-expressions/README.md @@ -1,4 +1,4 @@ -# Tutorial 8: Expressions +# Tutorial 8: Expressions The expression system enables declarative logic that compiles to portable canonical JSON. All expressions are evaluable at runtime without recompilation. @@ -32,5 +32,5 @@ The expression system enables declarative logic that compiles to portable canoni ## Next -[Tutorial 9: Testing](../09-testing/) — unit test setup with recording transports. +[Tutorial 9: Testing](../09-testing/) — unit test setup with recording transports.