6.5 KiB
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).
concelierandexcititorpreserve prune source: API surfaces source chains viasourceRefs.
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:
{
"error": {
"code": "string",
"message": "string",
"details": { "any": "json" },
"traceId": "string"
}
}
Common codes:
not_foundvalidation_errorconflictunauthorizedforbiddenrate_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:
{
"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:
{
"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:
sourceRefsprovides preserved provenance chains (including pruned markers when applicable).
3. Evidence
3.1 List evidence for case
GET /cases/{caseId}/evidence
Response 200:
{
"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/jsonfor JSON evidenceapplication/octet-streamfor binary- MUST include
Content-SHA256header (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:
{
"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:
{
"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
Snapshotwith triggerDECISION.
4.2 Revoke decision
POST /decisions/{decisionId}/revoke
Body (optional):
{ "reason": "Mistake; reachability now observed." }
Response 200:
{ "revokedAt": "2025-12-16T02:00:00Z", "signatureRef": "dsse:rekor:uuid" }
5. Snapshots & Smart-Diff
5.1 List snapshots
GET /cases/{caseId}/snapshots
Response 200:
{
"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:
{
"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:
{
"exportId": "uuid",
"status": "QUEUED"
}
6.2 Poll export
GET /exports/{exportId}
Response 200:
{
"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