Files
git.stella-ops.org/docs/api/triage.contract.v1.md

335 lines
6.5 KiB
Markdown

# Stella Ops Triage API Contract v1
Base path: `/api/triage/v1`
This contract is served by `scanner.webservice` (or a dedicated triage facade that reads scanner-owned tables).
All risk/lattice outputs originate from `scanner.webservice`.
Key requirements:
- Deterministic outputs (policyId + policyVersion + inputsHash).
- Proof-linking (chips reference evidenceIds).
- `concelier` and `excititor` preserve prune source: API surfaces source chains via `sourceRefs`.
## 0. Conventions
### 0.1 Identifiers
- `caseId` == `findingId` (UUID). A case is a finding scoped to an asset/environment.
- Hashes are hex strings.
### 0.2 Caching
- GET endpoints SHOULD return `ETag`.
- Clients SHOULD send `If-None-Match`.
### 0.3 Errors
Standard error envelope:
```json
{
"error": {
"code": "string",
"message": "string",
"details": { "any": "json" },
"traceId": "string"
}
}
```
Common codes:
* `not_found`
* `validation_error`
* `conflict`
* `unauthorized`
* `forbidden`
* `rate_limited`
## 1. Findings Table
### 1.1 List findings
`GET /findings`
Query params:
* `showMuted` (bool, default false)
* `lane` (optional, enum)
* `search` (optional string; searches asset, purl, cveId)
* `page` (int, default 1)
* `pageSize` (int, default 50; max 200)
* `sort` (optional: `updatedAt`, `score`, `lane`)
* `order` (optional: `asc|desc`)
Response 200:
```json
{
"page": 1,
"pageSize": 50,
"total": 12345,
"mutedCounts": { "reach": 1904, "vex": 513, "compensated": 18 },
"rows": [
{
"id": "uuid",
"lane": "BLOCKED",
"verdict": "BLOCK",
"score": 87,
"reachable": "YES",
"vex": "affected",
"exploit": "YES",
"asset": "prod/api-gateway:1.2.3",
"updatedAt": "2025-12-16T01:02:03Z"
}
]
}
```
## 2. Case Narrative
### 2.1 Get case header
`GET /cases/{caseId}`
Response 200:
```json
{
"id": "uuid",
"verdict": "BLOCK",
"lane": "BLOCKED",
"score": 87,
"policyId": "prod-strict",
"policyVersion": "2025.12.14",
"inputsHash": "hex",
"why": "Reachable path observed; exploit signal present; prod-strict blocks.",
"chips": [
{ "key": "reachability", "label": "Reachability", "value": "Reachable (92%)", "evidenceIds": ["uuid"] },
{ "key": "vex", "label": "VEX", "value": "affected", "evidenceIds": ["uuid"] },
{ "key": "gate", "label": "Gate", "value": "BLOCKED by prod-strict", "evidenceIds": ["uuid"] }
],
"sourceRefs": [
{
"domain": "concelier",
"kind": "cve_record",
"ref": "concelier:osv:...",
"pruned": false
},
{
"domain": "excititor",
"kind": "effective_vex",
"ref": "excititor:openvex:...",
"pruned": false
}
],
"updatedAt": "2025-12-16T01:02:03Z"
}
```
Notes:
* `sourceRefs` provides preserved provenance chains (including pruned markers when applicable).
## 3. Evidence
### 3.1 List evidence for case
`GET /cases/{caseId}/evidence`
Response 200:
```json
{
"caseId": "uuid",
"items": [
{
"id": "uuid",
"type": "VEX_DOC",
"title": "Vendor OpenVEX assertion",
"issuer": "vendor.example",
"signed": true,
"signedBy": "CN=Vendor VEX Signer",
"contentHash": "hex",
"createdAt": "2025-12-15T22:10:00Z",
"previewUrl": "/api/triage/v1/evidence/uuid/preview",
"rawUrl": "/api/triage/v1/evidence/uuid/raw"
}
]
}
```
### 3.2 Get raw evidence object
`GET /evidence/{evidenceId}/raw`
Returns:
* `application/json` for JSON evidence
* `application/octet-stream` for binary
* MUST include `Content-SHA256` header (hex) when possible.
### 3.3 Preview evidence object
`GET /evidence/{evidenceId}/preview`
Returns a compact representation safe for UI preview.
## 4. Decisions
### 4.1 Create decision
`POST /decisions`
Request body:
```json
{
"caseId": "uuid",
"kind": "MUTE_REACH",
"reasonCode": "NON_REACHABLE",
"note": "No entry path in this env; reviewed runtime traces.",
"ttl": "2026-01-16T00:00:00Z"
}
```
Response 201:
```json
{
"decision": {
"id": "uuid",
"kind": "MUTE_REACH",
"reasonCode": "NON_REACHABLE",
"note": "No entry path in this env; reviewed runtime traces.",
"ttl": "2026-01-16T00:00:00Z",
"actor": { "subject": "user:abc", "display": "Vlad" },
"createdAt": "2025-12-16T01:10:00Z",
"signatureRef": "dsse:rekor:uuid"
}
}
```
Rules:
* Server signs decisions (DSSE) and persists signature reference.
* Creating a decision MUST create a `Snapshot` with trigger `DECISION`.
### 4.2 Revoke decision
`POST /decisions/{decisionId}/revoke`
Body (optional):
```json
{ "reason": "Mistake; reachability now observed." }
```
Response 200:
```json
{ "revokedAt": "2025-12-16T02:00:00Z", "signatureRef": "dsse:rekor:uuid" }
```
## 5. Snapshots & Smart-Diff
### 5.1 List snapshots
`GET /cases/{caseId}/snapshots`
Response 200:
```json
{
"caseId": "uuid",
"items": [
{
"id": "uuid",
"trigger": "POLICY_UPDATE",
"changedAt": "2025-12-16T00:00:00Z",
"fromInputsHash": "hex",
"toInputsHash": "hex",
"summary": "Policy version changed; gate threshold crossed."
}
]
}
```
### 5.2 Smart-Diff between two snapshots
`GET /cases/{caseId}/smart-diff?from={inputsHashA}&to={inputsHashB}`
Response 200:
```json
{
"fromInputsHash": "hex",
"toInputsHash": "hex",
"inputsChanged": [
{ "key": "policyVersion", "before": "2025.12.14", "after": "2025.12.16", "evidenceIds": ["uuid"] }
],
"outputsChanged": [
{ "key": "verdict", "before": "SHIP", "after": "BLOCK", "evidenceIds": ["uuid"] }
]
}
```
## 6. Export Evidence Bundle
### 6.1 Start export
`POST /cases/{caseId}/export`
Response 202:
```json
{
"exportId": "uuid",
"status": "QUEUED"
}
```
### 6.2 Poll export
`GET /exports/{exportId}`
Response 200:
```json
{
"exportId": "uuid",
"status": "READY",
"downloadUrl": "/api/triage/v1/exports/uuid/download"
}
```
### 6.3 Download bundle
`GET /exports/{exportId}/download`
Returns:
* `application/zip`
* DSSE envelope embedded (or alongside in zip)
* bundle contains replay manifest, artifacts, risk result, snapshots
## 7. Events (Notify.WebService integration)
These are emitted by `notify.webservice` when scanner outputs change.
* `first_signal`
* fired on first actionable detection for an asset/environment
* `risk_changed`
* fired when verdict/lane changes or thresholds crossed
* `gate_blocked`
* fired when CI gate blocks
Event payload includes:
* caseId
* old/new verdict/lane/score (for changed events)
* inputsHash
* links to `/cases/{caseId}`
---
**Document Version**: 1.0
**Target Platform**: .NET 10, PostgreSQL >= 16