SPRINT_3600_0001_0001 - Reachability Drift Detection Master Plan
This commit is contained in:
334
docs/api/triage.contract.v1.md
Normal file
334
docs/api/triage.contract.v1.md
Normal file
@@ -0,0 +1,334 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user