up
This commit is contained in:
@@ -1,91 +1,168 @@
|
||||
# Excititor VEX linkset APIs (observations + linksets)
|
||||
# Excititor VEX Observation & Linkset APIs
|
||||
|
||||
> Draft examples for Sprint 119 (EXCITITOR-LNM-21-203). Aligns with WebService endpoints implemented in `src/Excititor/StellaOps.Excititor.WebService/Program.cs`.
|
||||
> 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`.
|
||||
|
||||
## /v1/vex/observations
|
||||
## Authentication & Headers
|
||||
|
||||
All endpoints require:
|
||||
- **Authorization**: Bearer token with `vex.read` scope
|
||||
- **X-Stella-Tenant**: Tenant identifier (required)
|
||||
|
||||
## /vex/observations
|
||||
|
||||
### List observations with filters
|
||||
|
||||
### List
|
||||
```
|
||||
GET /v1/vex/observations?vulnerabilityId=CVE-2024-0001&productKey=pkg:maven/org.demo/app@1.2.3&providerId=ubuntu-csaf&status=affected&limit=2
|
||||
Headers:
|
||||
Authorization: Bearer <token>
|
||||
X-Tenant: default
|
||||
Response 200 (application/json):
|
||||
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",
|
||||
"observationId": "vex:obs:sha256:...",
|
||||
"providerId": "ubuntu-csaf",
|
||||
"document": {
|
||||
"digest": "sha256:...",
|
||||
"uri": "https://example.com/csaf/1.json",
|
||||
"signature": null
|
||||
},
|
||||
"scope": {
|
||||
"vulnerabilityId": "CVE-2024-0001",
|
||||
"productKey": "pkg:maven/org.demo/app@1.2.3"
|
||||
},
|
||||
"statements": [
|
||||
{
|
||||
"vulnerabilityId": "CVE-2024-0001",
|
||||
"productKey": "pkg:maven/org.demo/app@1.2.3",
|
||||
"status": "affected",
|
||||
"justification": {
|
||||
"type": "component_not_present",
|
||||
"reason": "Not shipped in base profile"
|
||||
},
|
||||
"signals": { "severity": { "score": 7.5 } },
|
||||
"provenance": {
|
||||
"providerId": "ubuntu-csaf",
|
||||
"sourceId": "USN-9999-1",
|
||||
"fieldMasks": ["statements"]
|
||||
}
|
||||
}
|
||||
],
|
||||
"linkset": {
|
||||
"aliases": ["USN-9999-1"],
|
||||
"purls": ["pkg:maven/org.demo/app"],
|
||||
"cpes": [],
|
||||
"references": [{"type": "advisory", "url": "https://..."}],
|
||||
"disagreements": []
|
||||
},
|
||||
"createdAt": "2025-11-18T12:34:56Z"
|
||||
"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": "eyJ2dWxuZXJhYmlsaXR5SWQiOiJDVkUtMjAyNC0wMDAxIiwiY3JlYXRlZEF0IjoiMjAyNS0xMS0xOFQxMjozNDo1NloifQ=="
|
||||
"nextCursor": "MjAyNS0xMS0xOFQxMjozNDo1NlonfHZleDpvYnM6c2hhMjU2OmFiYzEyMy4uLg=="
|
||||
}
|
||||
```
|
||||
|
||||
### Get by key
|
||||
**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 /v1/vex/observations/CVE-2024-0001/pkg:maven/org.demo/app@1.2.3
|
||||
Headers: Authorization + X-Tenant
|
||||
Response 200: same projection shape as list items (single object).
|
||||
GET /vex/observations/{observationId}
|
||||
```
|
||||
|
||||
## /v1/vex/linksets
|
||||
**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"
|
||||
}
|
||||
```
|
||||
GET /v1/vex/linksets?vulnerabilityId=CVE-2024-0001&productKey=pkg:maven/org.demo/app@1.2.3&status=affected&limit=2
|
||||
Headers: Authorization + X-Tenant
|
||||
Response 200:
|
||||
|
||||
**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": "CVE-2024-0001:pkg:maven/org.demo/app@1.2.3",
|
||||
"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",
|
||||
"providers": ["ubuntu-csaf", "suse-csaf"],
|
||||
"providerIds": ["ubuntu-csaf", "suse-csaf"],
|
||||
"statuses": ["affected", "fixed"],
|
||||
"aliases": ["USN-9999-1"],
|
||||
"purls": ["pkg:maven/org.demo/app"],
|
||||
"aliases": [],
|
||||
"purls": [],
|
||||
"cpes": [],
|
||||
"references": [{"type": "advisory", "url": "https://..."}],
|
||||
"disagreements": [{"providerId": "suse-csaf", "status": "fixed", "justification": null, "confidence": null}],
|
||||
"references": [],
|
||||
"disagreements": [
|
||||
{
|
||||
"providerId": "suse-csaf",
|
||||
"status": "fixed",
|
||||
"justification": null,
|
||||
"confidence": 0.85
|
||||
}
|
||||
],
|
||||
"observations": [
|
||||
{"observationId": "vex:obs:...", "providerId": "ubuntu-csaf", "status": "affected", "severity": 7.5},
|
||||
{"observationId": "vex:obs:...", "providerId": "suse-csaf", "status": "fixed", "severity": null}
|
||||
{"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"
|
||||
}
|
||||
@@ -94,36 +171,152 @@ Response 200:
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Pagination: `limit` (default 200, max 500) + `cursor` (opaque base64 of `vulnerabilityId` + `createdAt`).
|
||||
- Filters: `vulnerabilityId`, `productKey`, `providerId`, `status`; multiple query values allowed.
|
||||
- Headers: `Excititor-Results-Count`, `Excititor-Results-Cursor` (observations) and `Excititor-Results-Total` / `Excititor-Results-Truncated` (chunks) already implemented.
|
||||
- Determinism: responses sorted by `vulnerabilityId`, then `productKey`; arrays sorted lexicographically.
|
||||
**Error Responses:**
|
||||
- `400 ERR_AGG_PARAMS` - At least one filter is required
|
||||
|
||||
## SDK generation
|
||||
- Source of truth for EXCITITOR-LNM-21-203 SDK samples (TypeScript/Go/Python) and OpenAPI snippets.
|
||||
- Suggested generation inputs:
|
||||
- Schema: this doc + `docs/modules/excititor/vex_observations.md` for field semantics.
|
||||
- Auth: bearer token + `X-Stella-Tenant` header (required).
|
||||
- Pagination: `cursor` (opaque) + `limit` (default 200, max 500).
|
||||
- Minimal client example (TypeScript, fetch):
|
||||
```ts
|
||||
const resp = await fetch(
|
||||
`${baseUrl}/v1/vex/observations?` + new URLSearchParams({
|
||||
vulnerabilityId: "CVE-2024-0001",
|
||||
productKey: "pkg:maven/org.demo/app@1.2.3",
|
||||
### 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": "default"
|
||||
"X-Stella-Tenant": tenant
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.json();
|
||||
throw new Error(`${error.error.code}: ${error.error.message}`);
|
||||
}
|
||||
);
|
||||
const body = await resp.json();
|
||||
|
||||
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();
|
||||
};
|
||||
```
|
||||
- Determinism requirements for SDKs:
|
||||
- Preserve server ordering; do not resort items client-side.
|
||||
- Treat `cursor` as opaque; echo it back for next page.
|
||||
- Keep enums case-sensitive as returned by API.
|
||||
|
||||
## 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
|
||||
|
||||
Reference in New Issue
Block a user