10 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)showHidden(bool, default false) — include gated findings in resultsgatingReason(optional, enum) — filter by specific gating reasonlane(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,
"actionableCount": 847,
"mutedCounts": { "reach": 1904, "vex": 513, "compensated": 18 },
"gatedBuckets": {
"unreachableCount": 1904,
"policyDismissedCount": 234,
"backportedCount": 456,
"vexNotAffectedCount": 513,
"supersededCount": 12,
"userMutedCount": 18,
"totalHiddenCount": 3137
},
"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",
"gatingReason": null,
"isHiddenByDefault": false
}
]
}
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}
8. Gating Explainability
8.1 GatingReason enum
Reason why a finding is hidden by default in quiet-by-design triage:
| Value | Description |
|---|---|
none |
Not gated - visible in default view |
unreachable |
Not reachable from any application entrypoint |
policy_dismissed |
Waived or tolerated by policy rules |
backported |
Patched via distro backport |
vex_not_affected |
VEX statement declares not affected with sufficient trust |
superseded |
Superseded by newer advisory |
user_muted |
Explicitly muted by user |
8.2 GatedBucketsSummary
{
"unreachableCount": 1904,
"policyDismissedCount": 234,
"backportedCount": 456,
"vexNotAffectedCount": 513,
"supersededCount": 12,
"userMutedCount": 18,
"totalHiddenCount": 3137
}
8.3 VEX Trust Scoring
VEX statements include trust scoring for policy-driven acceptance:
{
"status": "not_affected",
"justification": "vulnerable_code_not_in_execute_path",
"issuedBy": "vendor.example",
"issuedAt": "2025-12-15T10:00:00Z",
"trustScore": 0.85,
"policyTrustThreshold": 0.80,
"meetsPolicyThreshold": true,
"trustBreakdown": {
"authority": 0.90,
"accuracy": 0.85,
"timeliness": 0.80,
"verification": 0.85
}
}
Trust score components:
- Authority (0-1): Issuer reputation and category
- Accuracy (0-1): Historical correctness
- Timeliness (0-1): Response speed
- Verification (0-1): Signature validity
8.4 Finding gating fields
Extended fields on finding response:
| Field | Type | Description |
|---|---|---|
gatingReason |
string | Why this finding is hidden (null if visible) |
isHiddenByDefault |
boolean | True if gated by default view |
subgraphId |
string | Link to reachability subgraph |
deltasId |
string | Link to delta comparison |
gatingExplanation |
string | Human-readable explanation |
9. Unified Evidence
9.1 Get unified evidence
GET /findings/{findingId}/evidence
Returns all evidence tabs in one call.
Response 200:
{
"findingId": "uuid",
"cveId": "CVE-2024-1234",
"componentPurl": "pkg:npm/lodash@4.17.20",
"sbom": { ... },
"reachability": { ... },
"vexClaims": [ ... ],
"attestations": [ ... ],
"deltas": { ... },
"policy": { ... },
"manifests": {
"artifactDigest": "sha256:...",
"feedDigest": "sha256:...",
"policyDigest": "sha256:...",
"manifestDigest": "sha256:..."
},
"verification": {
"status": "verified",
"hashesMatch": true,
"signaturesValid": true,
"isFresh": true
},
"replayCommand": "stella scan replay --artifact sha256:abc...",
"generatedAt": "2025-12-16T01:02:03Z"
}
9.2 Export evidence bundle
GET /findings/{findingId}/evidence/export?format=zip
Downloads complete evidence package as archive (ZIP or TAR.GZ).
Response headers:
Content-Type: application/zip or application/gzipContent-Disposition: attachment; filename="evidence-{findingId}.zip"X-Archive-Digest: sha256:{digest}
9.3 Get replay command
GET /findings/{findingId}/replay-command
Returns copy-ready CLI command for deterministic reproduction.
Response 200:
{
"fullCommand": "stella scan replay --artifact sha256:abc... --manifest sha256:def...",
"shortCommand": "stella replay snapshot --verdict V-12345",
"bundleDownloadUrl": "/v1/triage/findings/uuid/evidence/export",
"inputHashes": {
"artifactDigest": "sha256:...",
"manifestHash": "sha256:...",
"feedSnapshotHash": "sha256:...",
"policyHash": "sha256:..."
}
}
Document Version: 1.1 Updated: 2025-12-24 (Sprint 9200 - Gating Explainability) Target Platform: .NET 10, PostgreSQL >= 16