# Excititor VEX Observation & Linkset APIs > Implementation reference for Sprint 121 (`EXCITITOR-LNM-21-201`, `EXCITITOR-LNM-21-202`). Documents the REST endpoints implemented in `src/Excititor/StellaOps.Excititor.WebService/Endpoints/ObservationEndpoints.cs` and `LinksetEndpoints.cs`. ## Authentication & Headers All endpoints require: - **Authorization**: Bearer token with `vex.read` scope - **X-Stella-Tenant**: Tenant identifier (required) ## /vex/observations ### List observations with filters ``` GET /vex/observations?vulnerabilityId=CVE-2024-0001&productKey=pkg:maven/org.demo/app@1.2.3&limit=50 GET /vex/observations?providerId=ubuntu-csaf&limit=50 ``` **Query Parameters:** - `vulnerabilityId` + `productKey` (required together) - Filter by vulnerability and product - `providerId` - Filter by provider - `limit` (optional, default: 50, max: 100) - Number of results - `cursor` (optional) - Pagination cursor from previous response **Response 200:** ```json { "items": [ { "observationId": "vex:obs:sha256:abc123...", "tenant": "default", "providerId": "ubuntu-csaf", "vulnerabilityId": "CVE-2024-0001", "productKey": "pkg:maven/org.demo/app@1.2.3", "status": "affected", "createdAt": "2025-11-18T12:34:56Z", "lastObserved": "2025-11-18T12:34:56Z", "purls": ["pkg:maven/org.demo/app@1.2.3"] } ], "nextCursor": "MjAyNS0xMS0xOFQxMjozNDo1NlonfHZleDpvYnM6c2hhMjU2OmFiYzEyMy4uLg==" } ``` **Error Responses:** - `400 ERR_PARAMS` - At least one filter is required - `400 ERR_TENANT` - X-Stella-Tenant header is required - `403` - Missing required scope ### Get observation by ID ``` GET /vex/observations/{observationId} ``` **Response 200:** ```json { "observationId": "vex:obs:sha256:abc123...", "tenant": "default", "providerId": "ubuntu-csaf", "streamId": "ubuntu-csaf-vex", "upstream": { "upstreamId": "USN-9999-1", "documentVersion": "2024.10.22", "fetchedAt": "2025-11-18T12:34:00Z", "receivedAt": "2025-11-18T12:34:05Z", "contentHash": "sha256:...", "signature": { "type": "cosign", "keyId": "ubuntu-vex-prod", "issuer": "https://token.actions.githubusercontent.com", "verifiedAt": "2025-11-18T12:34:10Z" } }, "content": { "format": "csaf", "specVersion": "2.0" }, "statements": [ { "vulnerabilityId": "CVE-2024-0001", "productKey": "pkg:maven/org.demo/app@1.2.3", "status": "affected", "lastObserved": "2025-11-18T12:34:56Z", "locator": "#/statements/0", "justification": "component_not_present", "introducedVersion": null, "fixedVersion": "1.2.4" } ], "linkset": { "aliases": ["USN-9999-1"], "purls": ["pkg:maven/org.demo/app@1.2.3"], "cpes": [], "references": [{"type": "advisory", "url": "https://ubuntu.com/security/notices/USN-9999-1"}] }, "createdAt": "2025-11-18T12:34:56Z" } ``` **Error Responses:** - `404 ERR_NOT_FOUND` - Observation not found ### Count observations ``` GET /vex/observations/count ``` **Response 200:** ```json { "count": 12345 } ``` ## /vex/linksets ### List linksets with filters At least one filter is required: `vulnerabilityId`, `productKey`, `providerId`, or `hasConflicts=true`. ``` GET /vex/linksets?vulnerabilityId=CVE-2024-0001&limit=50 GET /vex/linksets?productKey=pkg:maven/org.demo/app@1.2.3&limit=50 GET /vex/linksets?providerId=ubuntu-csaf&limit=50 GET /vex/linksets?hasConflicts=true&limit=50 ``` **Query Parameters:** - `vulnerabilityId` - Filter by vulnerability ID - `productKey` - Filter by product key - `providerId` - Filter by provider - `hasConflicts` - Filter to linksets with disagreements (true/false) - `limit` (optional, default: 50, max: 100) - Number of results - `cursor` (optional) - Pagination cursor **Response 200:** ```json { "items": [ { "linksetId": "sha256:tenant:CVE-2024-0001:pkg:maven/org.demo/app@1.2.3", "tenant": "default", "vulnerabilityId": "CVE-2024-0001", "productKey": "pkg:maven/org.demo/app@1.2.3", "providerIds": ["ubuntu-csaf", "suse-csaf"], "statuses": ["affected", "fixed"], "aliases": [], "purls": [], "cpes": [], "references": [], "disagreements": [ { "providerId": "suse-csaf", "status": "fixed", "justification": null, "confidence": 0.85 } ], "observations": [ {"observationId": "vex:obs:...", "providerId": "ubuntu-csaf", "status": "affected", "confidence": 0.9}, {"observationId": "vex:obs:...", "providerId": "suse-csaf", "status": "fixed", "confidence": 0.85} ], "createdAt": "2025-11-18T12:34:56Z" } ], "nextCursor": null } ``` **Error Responses:** - `400 ERR_AGG_PARAMS` - At least one filter is required ### Get linkset by ID ``` GET /vex/linksets/{linksetId} ``` **Response 200:** ```json { "linksetId": "sha256:...", "tenant": "default", "vulnerabilityId": "CVE-2024-0001", "productKey": "pkg:maven/org.demo/app@1.2.3", "providerIds": ["ubuntu-csaf", "suse-csaf"], "statuses": ["affected", "fixed"], "confidence": "low", "hasConflicts": true, "disagreements": [ { "providerId": "suse-csaf", "status": "fixed", "justification": null, "confidence": 0.85 } ], "observations": [ {"observationId": "vex:obs:...", "providerId": "ubuntu-csaf", "status": "affected", "confidence": 0.9}, {"observationId": "vex:obs:...", "providerId": "suse-csaf", "status": "fixed", "confidence": 0.85} ], "createdAt": "2025-11-18T12:00:00Z", "updatedAt": "2025-11-18T12:34:56Z" } ``` **Error Responses:** - `400 ERR_AGG_PARAMS` - linksetId is required - `404 ERR_AGG_NOT_FOUND` - Linkset not found ### Lookup linkset by vulnerability and product ``` GET /vex/linksets/lookup?vulnerabilityId=CVE-2024-0001&productKey=pkg:maven/org.demo/app@1.2.3 ``` **Response 200:** Same as Get linkset by ID **Error Responses:** - `400 ERR_AGG_PARAMS` - vulnerabilityId and productKey are required - `404 ERR_AGG_NOT_FOUND` - No linkset found for the specified vulnerability and product ### Count linksets ``` GET /vex/linksets/count ``` **Response 200:** ```json { "total": 5000, "withConflicts": 127 } ``` ### List linksets with conflicts (shorthand) ``` GET /vex/linksets/conflicts?limit=50 ``` **Response 200:** Same format as List linksets ## Error Codes | Code | Description | |------|-------------| | `ERR_PARAMS` | Missing or invalid query parameters (observations) | | `ERR_TENANT` | X-Stella-Tenant header is required | | `ERR_NOT_FOUND` | Observation not found | | `ERR_AGG_PARAMS` | Missing or invalid query parameters (linksets) | | `ERR_AGG_NOT_FOUND` | Linkset not found | ## Pagination - Uses cursor-based pagination with base64-encoded `timestamp|id` cursors - Default limit: 50, Maximum limit: 100 - Cursors are opaque; treat as strings and pass back unchanged ## Determinism - Results are sorted by timestamp (descending), then by ID - Array fields are sorted lexicographically - Status enums are lowercase strings ## SDK Example (TypeScript) ```typescript const listObservations = async ( baseUrl: string, token: string, tenant: string, vulnerabilityId: string, productKey: string ) => { const params = new URLSearchParams({ vulnerabilityId, productKey, limit: "100" }); const response = await fetch(`${baseUrl}/vex/observations?${params}`, { headers: { Authorization: `Bearer ${token}`, "X-Stella-Tenant": tenant } }); if (!response.ok) { const error = await response.json(); throw new Error(`${error.error.code}: ${error.error.message}`); } return response.json(); }; const getLinksetWithConflicts = async ( baseUrl: string, token: string, tenant: string ) => { const response = await fetch(`${baseUrl}/vex/linksets/conflicts?limit=50`, { headers: { Authorization: `Bearer ${token}`, "X-Stella-Tenant": tenant } }); return response.json(); }; ``` ## Related Documentation - `vex_observations.md` - VEX Observation domain model and storage schema - `evidence-contract.md` - Evidence bundle format and attestation - `AGENTS.md` - Component development guidelines