# Excititor Advisory-AI evidence APIs (projection + chunks) > Covers the read-only evidence surfaces shipped in Sprints 119–120: `/v1/vex/observations/{vulnerabilityId}/{productKey}` and `/v1/vex/evidence/chunks`. ## Scope and determinism - **Aggregation-only**: no consensus, severity merging, or reachability. Responses carry raw statements plus provenance/signature metadata. - **Stable ordering**: both endpoints sort by `lastSeen` DESC; pagination uses a deterministic `limit`. - **Limits**: observation projection default `limit=200`, max `500`; chunk stream default `limit=500`, max `2000`. - **Tenancy**: reads respect `X-Stella-Tenant` when provided; otherwise fall back to `DefaultTenant` configuration. - **Auth**: bearer token with `vex.read` scope required. ## `/v1/vex/observations/{vulnerabilityId}/{productKey}` - **Response**: JSON object with `vulnerabilityId`, `productKey`, `generatedAt`, `totalCount`, `truncated`, `statements[]`. - **Statement fields**: `observationId`, `providerId`, `status`, `justification`, `detail`, `firstSeen`, `lastSeen`, `scope{key,name,version,purl,cpe,componentIdentifiers[]}`, `anchors[]`, `document{digest,format,revision,sourceUri}`, `signature{type,keyId,issuer,verifiedAt}`. - **Filters**: - `providerId` (multi-valued, comma-separated) - `status` (values in `VexClaimStatus`) - `since` (ISO-8601, UTC) - `limit` (ints within bounds) - **Mapping back to storage**: - `observationId` = `{providerId}:{document.digest}` - `document.digest` locates the raw record in `vex_raw`. - `anchors` contain JSON pointers/paragraph locators from source metadata. Headers: - `Excititor-Results-Truncated: true|false` - `Excititor-Results-Total: ` ## `/v1/vex/evidence/chunks` - **Query params**: `vulnerabilityId` (required), `productKey` (required), optional `providerId`, `status`, `since`, `limit`. - **Limits**: default `limit=500`, max `2000`. - **Response**: **NDJSON** stream; each line is a `VexEvidenceChunkResponse`. - **Chunk fields**: `observationId`, `linksetId`, `vulnerabilityId`, `productKey`, `providerId`, `status`, `justification`, `detail`, `scopeScore` (from confidence or signals), `firstSeen`, `lastSeen`, `scope{...}`, `document{digest,format,sourceUri,revision}`, `signature{type,subject,issuer,keyId,verifiedAt,transparencyRef}`, `metadata` (flattened additionalMetadata). - **Headers**: `Excititor-Results-Total`, `Excititor-Results-Truncated` (mirrors projection API naming). - **Streaming guidance (SDK/clients)**: - Use HTTP client that supports response streaming; read line-by-line and JSON-deserialize per line. - Treat stream as an NDJSON list up to `limit`; no outer array. - Back-off or paginate by adjusting `since` or narrowing providers/statuses. OpenAPI (excerpt): ```yaml paths: /v1/vex/evidence/chunks: get: summary: Stream evidence chunks for a vulnerability/product parameters: - in: query name: vulnerabilityId schema: { type: string } required: true - in: query name: productKey schema: { type: string } required: true - in: query name: providerId schema: { type: string } description: Comma-separated provider ids - in: query name: status schema: { type: string } description: Comma-separated VEX statuses - in: query name: since schema: { type: string, format: date-time } - in: query name: limit schema: { type: integer, minimum: 1, maximum: 2000, default: 500 } responses: "200": description: NDJSON stream of VexEvidenceChunkResponse headers: Excititor-Results-Total: { schema: { type: integer } } Excititor-Results-Truncated: { schema: { type: boolean } } content: application/x-ndjson: schema: type: string description: One JSON object per line (VexEvidenceChunkResponse) ``` Example (curl): ```bash curl -s -H "Authorization: Bearer " \ -H "X-Stella-Tenant: acme" \ "https://exc.example.test/v1/vex/evidence/chunks?vulnerabilityId=CVE-2025-0001&productKey=pkg:docker/demo&limit=2" | head -n 2 ``` Sample NDJSON line: ```json {"observationId":"provider-a:4d2f...","linksetId":"CVE-2025-0001:pkg:docker/demo","vulnerabilityId":"CVE-2025-0001","productKey":"pkg:docker/demo","providerId":"provider-a","status":"Affected","justification":"ComponentNotPresent","detail":"demo detail","scopeScore":0.9,"firstSeen":"2025-11-10T12:00:00Z","lastSeen":"2025-11-12T12:00:00Z","scope":{"key":"pkg:docker/demo","name":"demo","version":"1.0.0","purl":"pkg:docker/demo","cpe":null,"componentIdentifiers":["component-a"]},"document":{"digest":"sha256:e7...","format":"sbomcyclonedx","sourceUri":"https://example.test/vex.json","revision":"r1"},"signature":{"type":"cosign","subject":"demo","issuer":"issuer","keyId":"kid","verifiedAt":"2025-11-12T12:00:00Z","transparencyRef":null},"metadata":{}} ``` ## `/v1/vex/attestations/{attestationId}` - **Purpose**: Lookup attestation provenance (supplier ↔ observation/linkset ↔ product/vulnerability) without touching consensus. - **Response**: `VexAttestationPayload` with fields: - `attestationId`, `supplierId`, `observationId`, `linksetId`, `vulnerabilityId`, `productKey`, `justificationSummary`, `issuedAt`, `metadata{}`. - **Semantics**: - `attestationId` matches the export/attestation ID used when signing (Resolve/Worker flows). - `observationId`/`linksetId` map back to evidence identifiers; clients can stitch provenance for citations. - **Auth**: `vex.read` scope; tenant header optional (payloads are tenant-agnostic). ## Error model - Standard API envelope with `ValidationProblem` for missing required params. - `scope` failures return `403` with problem details. - Tenancy parse failures return `400`. ## Backwards compatibility - No legacy routes are deprecated by these endpoints; they are additive and remain aggregation-only. ## References - Implementation: `src/Excititor/StellaOps.Excititor.WebService/Program.cs` (`/v1/vex/observations/**`, `/v1/vex/evidence/chunks`). - Telemetry: `src/Excititor/StellaOps.Excititor.WebService/Telemetry/EvidenceTelemetry.cs` (`excititor.vex.observation.*`, `excititor.vex.chunks.*`). - Data model: `src/Excititor/StellaOps.Excititor.WebService/Contracts/VexObservationContracts.cs`, `Contracts/VexEvidenceChunkContracts.cs`.