335 lines
6.5 KiB
Markdown
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
|