491 lines
10 KiB
Markdown
491 lines
10 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)
|
|
* `showHidden` (bool, default false) — include gated findings in results
|
|
* `gatingReason` (optional, enum) — filter by specific gating reason
|
|
* `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,
|
|
"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:
|
|
|
|
```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}`
|
|
|
|
## 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
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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:
|
|
|
|
```json
|
|
{
|
|
"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/gzip
|
|
* `Content-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:
|
|
|
|
```json
|
|
{
|
|
"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
|