Refactor code structure for improved readability and maintainability
This commit is contained in:
281
docs/modules/concelier/api/conflicts.md
Normal file
281
docs/modules/concelier/api/conflicts.md
Normal file
@@ -0,0 +1,281 @@
|
||||
# Linkset Conflicts API Reference (v1)
|
||||
|
||||
Status: stable; aligns with LNM v1 (frozen 2025-11-17).
|
||||
|
||||
## Intent
|
||||
- Document conflict detection and representation in the Link-Not-Merge system.
|
||||
- Conflicts are surfaced but never automatically resolved; consumers implement their own resolution strategy.
|
||||
- This reference describes conflict types, detection logic, and how conflicts appear in API responses.
|
||||
|
||||
## Philosophy: Link-Not-Merge
|
||||
The Concelier LNM (Link-Not-Merge) approach differs from traditional advisory aggregation:
|
||||
- **Link**: Observations from multiple sources are linked together via shared identifiers (CVE, GHSA, PURL, CPE).
|
||||
- **Not Merge**: Conflicting data is preserved with full provenance rather than collapsed into a single "truth".
|
||||
- **Surface, Don't Resolve**: Conflicts are clearly marked for downstream consumers to handle according to their own policies.
|
||||
|
||||
## Conflict Types
|
||||
|
||||
### severity-mismatch
|
||||
Sources disagree on severity rating.
|
||||
```json
|
||||
{
|
||||
"field": "severity",
|
||||
"reason": "severity-mismatch",
|
||||
"observedValue": "critical",
|
||||
"observedAt": "2025-11-18T08:00:00Z",
|
||||
"evidenceHash": "sha256:f6e5d4c3b2a1098765432109876543210fedcba0987654321fedcba098765432"
|
||||
}
|
||||
```
|
||||
|
||||
**Detection**: Triggered when severity labels (critical, high, medium, low) or CVSS scores differ by more than 1.0 points between observations.
|
||||
|
||||
**Common causes**:
|
||||
- Different CVSS versions (v2 vs v3 vs v3.1)
|
||||
- Vendor-specific severity assessments
|
||||
- Time lag between source updates
|
||||
|
||||
### version-range-conflict
|
||||
Sources disagree on affected version ranges.
|
||||
```json
|
||||
{
|
||||
"field": "affected.ranges",
|
||||
"reason": "version-range-conflict",
|
||||
"observedValue": "{\"fixed\": \"2.0.0\"}",
|
||||
"observedAt": "2025-11-19T12:00:00Z",
|
||||
"evidenceHash": "sha256:a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456"
|
||||
}
|
||||
```
|
||||
|
||||
**Detection**: Triggered when version range events differ between observations for the same package.
|
||||
|
||||
**Common causes**:
|
||||
- Backports creating different fix points per distribution
|
||||
- Vendor patches not reflected in upstream
|
||||
- Different ecosystem-specific versioning
|
||||
|
||||
### status-conflict
|
||||
Sources disagree on vulnerability status.
|
||||
```json
|
||||
{
|
||||
"field": "status",
|
||||
"reason": "status-conflict",
|
||||
"observedValue": "not_affected",
|
||||
"observedAt": "2025-11-20T09:00:00Z",
|
||||
"evidenceHash": "sha256:b2c3d4e5f6a789012345678901234567890abcdef1234567890abcdef1234567b"
|
||||
}
|
||||
```
|
||||
|
||||
**Detection**: Triggered when status values (affected, not_affected, under_investigation, fixed) differ.
|
||||
|
||||
**Common causes**:
|
||||
- VEX statements from vendors
|
||||
- Incomplete upstream analysis
|
||||
- Context-specific applicability (e.g., platform-dependent)
|
||||
|
||||
### cpe-mismatch
|
||||
Sources disagree on CPE identifiers.
|
||||
```json
|
||||
{
|
||||
"field": "cpe",
|
||||
"reason": "cpe-mismatch",
|
||||
"observedValue": "cpe:2.3:a:example:lib:*:*:*:*:*:*:*:*",
|
||||
"observedAt": "2025-11-18T16:00:00Z",
|
||||
"evidenceHash": "sha256:c3d4e5f6a789012345678901234567890abcdef1234567890abcdef123456789c"
|
||||
}
|
||||
```
|
||||
|
||||
**Detection**: Triggered when CPE identifiers for the same advisory don't match.
|
||||
|
||||
**Common causes**:
|
||||
- Different CPE dictionary versions
|
||||
- Vendor vs product naming variations
|
||||
- Platform-specific CPE assignments
|
||||
|
||||
### reference-conflict
|
||||
Sources provide conflicting reference information.
|
||||
```json
|
||||
{
|
||||
"field": "references",
|
||||
"reason": "reference-conflict",
|
||||
"observedValue": "https://example.com/advisory/different",
|
||||
"observedAt": "2025-11-21T10:00:00Z",
|
||||
"evidenceHash": "sha256:d4e5f6a789012345678901234567890abcdef1234567890abcdef123456789def"
|
||||
}
|
||||
```
|
||||
|
||||
## Conflict in Linkset Response
|
||||
|
||||
Full linkset with multiple conflicts:
|
||||
```json
|
||||
{
|
||||
"advisoryId": "CVE-2024-9999",
|
||||
"source": "aggregated",
|
||||
"purl": ["pkg:maven/org.example/library@1.5.0"],
|
||||
"cpe": [
|
||||
"cpe:2.3:a:example:library:1.5.0:*:*:*:*:*:*:*",
|
||||
"cpe:2.3:a:example_inc:lib:1.5.0:*:*:*:*:java:*:*"
|
||||
],
|
||||
"summary": "Deserialization vulnerability in example library",
|
||||
"publishedAt": "2024-09-01T00:00:00Z",
|
||||
"modifiedAt": "2024-09-15T00:00:00Z",
|
||||
"severity": "high",
|
||||
"status": "affected",
|
||||
"provenance": {
|
||||
"ingestedAt": "2025-11-20T10:30:00Z",
|
||||
"connectorId": "multi-source-aggregator",
|
||||
"evidenceHash": "sha256:aggregated-evidence-hash"
|
||||
},
|
||||
"conflicts": [
|
||||
{
|
||||
"field": "severity",
|
||||
"reason": "severity-mismatch",
|
||||
"observedValue": "critical",
|
||||
"observedAt": "2025-11-15T10:00:00Z",
|
||||
"evidenceHash": "sha256:nvd-observation-hash"
|
||||
},
|
||||
{
|
||||
"field": "severity",
|
||||
"reason": "severity-mismatch",
|
||||
"observedValue": "medium",
|
||||
"observedAt": "2025-11-18T14:00:00Z",
|
||||
"evidenceHash": "sha256:vendor-observation-hash"
|
||||
},
|
||||
{
|
||||
"field": "cpe",
|
||||
"reason": "cpe-mismatch",
|
||||
"observedValue": "cpe:2.3:a:example_inc:lib:1.5.0:*:*:*:*:java:*:*",
|
||||
"observedAt": "2025-11-17T08:00:00Z",
|
||||
"evidenceHash": "sha256:github-observation-hash"
|
||||
},
|
||||
{
|
||||
"field": "affected.ranges",
|
||||
"reason": "version-range-conflict",
|
||||
"observedValue": "{\"type\": \"SEMVER\", \"events\": [{\"introduced\": \"1.0.0\"}, {\"fixed\": \"1.5.1\"}]}",
|
||||
"observedAt": "2025-11-19T12:00:00Z",
|
||||
"evidenceHash": "sha256:distro-observation-hash"
|
||||
}
|
||||
],
|
||||
"timeline": [
|
||||
{"event": "first-observed", "at": "2025-11-15T10:00:00Z", "evidenceHash": "sha256:nvd-observation-hash"},
|
||||
{"event": "conflict-detected", "at": "2025-11-17T08:00:00Z", "evidenceHash": "sha256:github-observation-hash"},
|
||||
{"event": "conflict-detected", "at": "2025-11-18T14:00:00Z", "evidenceHash": "sha256:vendor-observation-hash"},
|
||||
{"event": "conflict-detected", "at": "2025-11-19T12:00:00Z", "evidenceHash": "sha256:distro-observation-hash"}
|
||||
],
|
||||
"normalized": {
|
||||
"aliases": ["CVE-2024-9999", "GHSA-xxxx-yyyy-zzzz"],
|
||||
"severities": [
|
||||
{"source": "nvd", "type": "CVSS_V3", "score": 9.8, "label": "critical"},
|
||||
{"source": "github", "type": "CVSS_V3", "score": 7.5, "label": "high"},
|
||||
{"source": "vendor", "type": "CVSS_V3", "score": 5.3, "label": "medium"}
|
||||
],
|
||||
"ranges": [
|
||||
{"source": "nvd", "type": "SEMVER", "introduced": "0", "fixed": "1.6.0"},
|
||||
{"source": "distro", "type": "SEMVER", "introduced": "1.0.0", "fixed": "1.5.1"}
|
||||
]
|
||||
},
|
||||
"cached": false,
|
||||
"observations": [
|
||||
"obs:nvd:CVE-2024-9999:2025-11-15",
|
||||
"obs:github:GHSA-xxxx-yyyy-zzzz:2025-11-17",
|
||||
"obs:vendor:CVE-2024-9999:2025-11-18",
|
||||
"obs:distro:CVE-2024-9999:2025-11-19"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Querying for Conflicts
|
||||
|
||||
### List Only Linksets with Conflicts
|
||||
```bash
|
||||
GET /v1/lnm/linksets?includeConflicts=true&hasConflicts=true
|
||||
X-Stella-Tenant: acme
|
||||
```
|
||||
|
||||
### Filter by Conflict Type
|
||||
```bash
|
||||
POST /v1/lnm/linksets/search
|
||||
X-Stella-Tenant: acme
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"conflictTypes": ["severity-mismatch", "version-range-conflict"],
|
||||
"includeConflicts": true,
|
||||
"pageSize": 50
|
||||
}
|
||||
```
|
||||
|
||||
### Advisory Summary with Conflicts
|
||||
```bash
|
||||
GET /advisories/summary?conflicts_only=true
|
||||
X-Stella-Tenant: acme
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"meta": {
|
||||
"tenant": "acme",
|
||||
"count": 3,
|
||||
"sort": "advisory"
|
||||
},
|
||||
"items": [
|
||||
{
|
||||
"advisoryKey": "CVE-2024-9999",
|
||||
"aliases": ["CVE-2024-9999", "GHSA-xxxx-yyyy-zzzz"],
|
||||
"source": "aggregated",
|
||||
"confidence": 0.65,
|
||||
"conflicts": [
|
||||
{"field": "severity", "reason": "severity-mismatch", "sourceIds": ["nvd", "vendor", "github"]}
|
||||
],
|
||||
"counts": {
|
||||
"observations": 4,
|
||||
"conflictFields": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Conflict Resolution Strategies
|
||||
|
||||
Concelier does not resolve conflicts, but here are common strategies consumers implement:
|
||||
|
||||
### Source Priority
|
||||
Prioritize sources by trust level:
|
||||
```
|
||||
nvd > vendor > github > community
|
||||
```
|
||||
|
||||
### Most Recent
|
||||
Use the most recently observed value:
|
||||
```
|
||||
Sort by observedAt desc, take first
|
||||
```
|
||||
|
||||
### Most Conservative
|
||||
For severity, use the highest rating:
|
||||
```
|
||||
critical > high > medium > low
|
||||
```
|
||||
|
||||
### Voting/Consensus
|
||||
Use the value with most agreement:
|
||||
```
|
||||
Count occurrences, take majority
|
||||
```
|
||||
|
||||
## Conflict Confidence Impact
|
||||
The `confidence` field in linksets reflects conflict presence:
|
||||
- **No conflicts**: confidence ≥ 0.9
|
||||
- **Minor conflicts** (1-2 fields): confidence 0.7-0.9
|
||||
- **Major conflicts** (3+ fields or severity): confidence < 0.7
|
||||
|
||||
## Notes
|
||||
- Conflicts are preserved indefinitely; they are only removed if all observations align.
|
||||
- Evidence hashes allow consumers to trace conflicts back to specific observations.
|
||||
- The `timeline` array shows when conflicts were first detected.
|
||||
- Multiple conflicts on the same field from different sources create multiple entries.
|
||||
|
||||
## Changelog
|
||||
- 2025-12-06: Initial conflict reference documentation (CONCELIER-WEB-OAS-62-001).
|
||||
- 2025-11-17: LNM v1 conflict model frozen.
|
||||
233
docs/modules/concelier/api/lnm-linksets.md
Normal file
233
docs/modules/concelier/api/lnm-linksets.md
Normal file
@@ -0,0 +1,233 @@
|
||||
# Link-Not-Merge Linksets API (v1)
|
||||
|
||||
Status: stable; frozen 2025-11-17 per LNM v1 spec.
|
||||
|
||||
## Intent
|
||||
- Provide fact-only advisory linkset retrieval for Policy Engine, graph overlays, and console clients.
|
||||
- Preserve provenance and tenant isolation; results are deterministically ordered and stable for identical queries.
|
||||
- Surface conflicts between observations without resolving them (Link-Not-Merge philosophy).
|
||||
|
||||
## Endpoints
|
||||
|
||||
### List Linksets
|
||||
- Method: `GET`
|
||||
- Path: `/v1/lnm/linksets`
|
||||
|
||||
### Get Linkset by ID
|
||||
- Method: `GET`
|
||||
- Path: `/v1/lnm/linksets/{advisoryId}`
|
||||
|
||||
### Search Linksets
|
||||
- Method: `POST`
|
||||
- Path: `/v1/lnm/linksets/search`
|
||||
|
||||
## Headers
|
||||
| Header | Required | Description |
|
||||
|--------|----------|-------------|
|
||||
| `X-Stella-Tenant` | Yes | Tenant identifier for multi-tenant isolation. |
|
||||
| `X-Stella-Request-Id` | No | Optional correlation ID for distributed tracing. |
|
||||
|
||||
## Query Parameters (GET)
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `purl` | string[] | Filter by Package URLs (repeatable). |
|
||||
| `cpe` | string | Filter by CPE identifier. |
|
||||
| `cve` | string | Filter by CVE identifier. |
|
||||
| `ghsa` | string | Filter by GHSA identifier. |
|
||||
| `advisoryId` | string | Filter by advisory ID. |
|
||||
| `source` | string | Filter by upstream source. |
|
||||
| `severityMin` | float | Minimum severity score. |
|
||||
| `severityMax` | float | Maximum severity score. |
|
||||
| `publishedSince` | datetime | Published after this timestamp. |
|
||||
| `modifiedSince` | datetime | Modified after this timestamp. |
|
||||
| `includeConflicts` | boolean | Include conflict details (default: true). |
|
||||
| `includeObservations` | boolean | Include observation IDs (default: false). |
|
||||
| `page` | integer | Page number (default: 1). |
|
||||
| `pageSize` | integer | Items per page (default: 50, max: 200). |
|
||||
| `sort` | string | Sort order (see sorting section). |
|
||||
|
||||
## Request Example (Search)
|
||||
```json
|
||||
{
|
||||
"purl": ["pkg:npm/lodash@4.17.20"],
|
||||
"includeConflicts": true,
|
||||
"includeObservations": true,
|
||||
"pageSize": 10
|
||||
}
|
||||
```
|
||||
|
||||
## Response (200)
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"advisoryId": "CVE-2021-23337",
|
||||
"source": "nvd",
|
||||
"purl": ["pkg:npm/lodash@4.17.20"],
|
||||
"cpe": ["cpe:2.3:a:lodash:lodash:4.17.20:*:*:*:*:node.js:*:*"],
|
||||
"summary": "Lodash versions prior to 4.17.21 are vulnerable to Command Injection via the template function.",
|
||||
"publishedAt": "2021-02-15T13:15:00Z",
|
||||
"modifiedAt": "2024-08-04T19:16:00Z",
|
||||
"severity": "high",
|
||||
"status": "affected",
|
||||
"provenance": {
|
||||
"ingestedAt": "2025-11-20T10:30:00Z",
|
||||
"connectorId": "nvd-osv-connector",
|
||||
"evidenceHash": "sha256:a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456",
|
||||
"dsseEnvelopeHash": null
|
||||
},
|
||||
"conflicts": [
|
||||
{
|
||||
"field": "severity",
|
||||
"reason": "severity-mismatch",
|
||||
"observedValue": "critical",
|
||||
"observedAt": "2025-11-18T08:00:00Z",
|
||||
"evidenceHash": "sha256:f6e5d4c3b2a1098765432109876543210fedcba0987654321fedcba098765432"
|
||||
}
|
||||
],
|
||||
"timeline": [
|
||||
{
|
||||
"event": "observed",
|
||||
"at": "2025-11-15T10:00:00Z",
|
||||
"evidenceHash": "sha256:a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456"
|
||||
},
|
||||
{
|
||||
"event": "conflict-detected",
|
||||
"at": "2025-11-18T08:00:00Z",
|
||||
"evidenceHash": "sha256:f6e5d4c3b2a1098765432109876543210fedcba0987654321fedcba098765432"
|
||||
}
|
||||
],
|
||||
"normalized": {
|
||||
"aliases": ["CVE-2021-23337", "GHSA-35jh-r3h4-6jhm"],
|
||||
"purl": ["pkg:npm/lodash@4.17.20"],
|
||||
"cpe": ["cpe:2.3:a:lodash:lodash:4.17.20:*:*:*:*:node.js:*:*"],
|
||||
"versions": ["4.17.20"],
|
||||
"ranges": [
|
||||
{
|
||||
"type": "SEMVER",
|
||||
"events": [
|
||||
{"introduced": "0.0.0"},
|
||||
{"fixed": "4.17.21"}
|
||||
]
|
||||
}
|
||||
],
|
||||
"severities": [
|
||||
{"type": "CVSS_V3", "score": 7.2}
|
||||
]
|
||||
},
|
||||
"cached": false,
|
||||
"remarks": [],
|
||||
"observations": ["obs:nvd:CVE-2021-23337:2025-11-15", "obs:github:GHSA-35jh-r3h4-6jhm:2025-11-18"]
|
||||
}
|
||||
],
|
||||
"page": 1,
|
||||
"pageSize": 10,
|
||||
"total": 1
|
||||
}
|
||||
```
|
||||
|
||||
## Response (Air-gapped deployment)
|
||||
When deployed in air-gapped mode, responses include freshness metadata:
|
||||
```json
|
||||
{
|
||||
"items": [
|
||||
{
|
||||
"advisoryId": "CVE-2021-23337",
|
||||
"source": "nvd",
|
||||
"purl": ["pkg:npm/lodash@4.17.20"],
|
||||
"cpe": [],
|
||||
"provenance": {
|
||||
"ingestedAt": "2025-11-20T10:30:00Z",
|
||||
"connectorId": "offline-bundle-importer",
|
||||
"evidenceHash": "sha256:a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456"
|
||||
},
|
||||
"conflicts": [],
|
||||
"cached": true,
|
||||
"freshness": {
|
||||
"staleness": {
|
||||
"lastRefreshedAt": "2025-11-20T10:30:00Z",
|
||||
"ageSeconds": 86400,
|
||||
"isStale": false,
|
||||
"thresholdSeconds": 172800,
|
||||
"status": "fresh"
|
||||
},
|
||||
"bundleProvenance": {
|
||||
"bundleId": "offline-2025-11-20",
|
||||
"bundleVersion": "1.0.0",
|
||||
"sourceId": "nvd-mirror",
|
||||
"importedAt": "2025-11-20T10:30:00Z",
|
||||
"contentHash": "sha256:bundle-hash-here",
|
||||
"signatureStatus": "verified",
|
||||
"signatureKeyId": "key:stellaops:offline-signing:2025",
|
||||
"isAirGapped": true
|
||||
},
|
||||
"computedAt": "2025-11-21T10:30:00Z"
|
||||
}
|
||||
}
|
||||
],
|
||||
"page": 1,
|
||||
"pageSize": 10,
|
||||
"total": 1
|
||||
}
|
||||
```
|
||||
|
||||
## Errors
|
||||
| Status | Code | Description |
|
||||
|--------|------|-------------|
|
||||
| 400 | `ERR_VALIDATION_FAILED` | Invalid query parameters or request body. |
|
||||
| 400 | `ERR_PAGE_SIZE_EXCEEDED` | Page size exceeds maximum of 200. |
|
||||
| 401 | `ERR_UNAUTHORIZED` | Missing or invalid authentication. |
|
||||
| 403 | `ERR_FORBIDDEN` | Tenant access denied. |
|
||||
| 404 | `ERR_RESOURCE_NOT_FOUND` | Linkset not found (for GET by ID). |
|
||||
| 429 | `ERR_RATE_LIMITED` | Too many requests; check Retry-After header. |
|
||||
|
||||
### Error Response Example
|
||||
```json
|
||||
{
|
||||
"type": "https://stellaops.io/errors/validation-failed",
|
||||
"title": "Validation Failed",
|
||||
"status": 400,
|
||||
"detail": "The 'pageSize' parameter exceeds the maximum allowed value of 200.",
|
||||
"instance": "/v1/lnm/linksets",
|
||||
"traceId": "trace-id-abc123",
|
||||
"error": {
|
||||
"code": "ERR_PAGE_SIZE_EXCEEDED",
|
||||
"message": "Page size must be between 1 and 200.",
|
||||
"target": "pageSize",
|
||||
"metadata": {
|
||||
"provided": 500,
|
||||
"maximum": 200
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Sorting
|
||||
Available sort options:
|
||||
- `modifiedAt desc` (default)
|
||||
- `modifiedAt asc`
|
||||
- `publishedAt desc`
|
||||
- `publishedAt asc`
|
||||
- `severity desc`
|
||||
- `severity asc`
|
||||
- `source`
|
||||
- `advisoryId`
|
||||
|
||||
Tie-breaking: when primary sort values are equal, results are ordered by `advisoryId asc`, then `source asc`.
|
||||
|
||||
## Determinism & Caching
|
||||
- All results are deterministically ordered based on sort parameters.
|
||||
- Timestamps are UTC ISO-8601 format.
|
||||
- Hashes are lowercase hex with algorithm prefix (e.g., `sha256:`).
|
||||
- Cache key includes: `tenant|filters|sort|page|pageSize`.
|
||||
- Cache headers: `X-Stella-Cache-Hit`, `X-Stella-Cache-Key`.
|
||||
|
||||
## Notes
|
||||
- Linksets represent the current aggregate state of all observations for an advisory.
|
||||
- Conflicts are surfaced but not resolved; consumers must implement their own conflict resolution strategy.
|
||||
- The `normalized` field contains processed data suitable for version matching and range evaluation.
|
||||
- Observation IDs in the `observations` array can be used to fetch raw observation details via the observations API.
|
||||
|
||||
## Changelog
|
||||
- 2025-12-06: Added curated examples with conflict and air-gap scenarios (CONCELIER-WEB-OAS-62-001).
|
||||
- 2025-11-17: LNM v1 specification frozen.
|
||||
264
docs/modules/concelier/api/observations.md
Normal file
264
docs/modules/concelier/api/observations.md
Normal file
@@ -0,0 +1,264 @@
|
||||
# Observations API (v1)
|
||||
|
||||
Status: stable; aligns with LNM v1 (frozen 2025-11-17).
|
||||
|
||||
## Intent
|
||||
- Provide raw observation retrieval for graph overlays, audit trails, and detailed provenance inspection.
|
||||
- Observations are the immutable evidence units that feed into linkset aggregation.
|
||||
- Each observation represents a single upstream source's statement about an advisory at a point in time.
|
||||
|
||||
## Endpoints
|
||||
|
||||
### Query Observations
|
||||
- Method: `GET`
|
||||
- Path: `/advisories/observations`
|
||||
|
||||
### Get Observations for Advisory
|
||||
- Method: `GET`
|
||||
- Path: `/concelier/observations`
|
||||
|
||||
## Headers
|
||||
| Header | Required | Description |
|
||||
|--------|----------|-------------|
|
||||
| `X-Stella-Tenant` | Yes | Tenant identifier for multi-tenant isolation. |
|
||||
| `X-Stella-Request-Id` | No | Optional correlation ID for distributed tracing. |
|
||||
|
||||
## Query Parameters
|
||||
| Parameter | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `advisoryKey` | string | Filter by advisory key (CVE, GHSA, etc.). |
|
||||
| `purl` | string[] | Filter by Package URLs (repeatable). |
|
||||
| `source` | string | Filter by upstream source. |
|
||||
| `format` | string | Filter by content format (OSV, GHSA, etc.). |
|
||||
| `limit` | integer | Maximum observations to return (default: 50, max: 200). |
|
||||
| `cursor` | string | Opaque cursor for pagination. |
|
||||
|
||||
## Response (200)
|
||||
```json
|
||||
{
|
||||
"observations": [
|
||||
{
|
||||
"id": "obs:nvd:CVE-2024-1234:2025-11-20T10:30:00Z",
|
||||
"tenant": "acme",
|
||||
"advisoryKey": "CVE-2024-1234",
|
||||
"aliases": ["CVE-2024-1234", "GHSA-abcd-efgh-ijkl"],
|
||||
"source": "nvd",
|
||||
"format": "OSV",
|
||||
"purls": ["pkg:npm/vulnerable-package@1.0.0"],
|
||||
"cpes": ["cpe:2.3:a:example:vulnerable-package:1.0.0:*:*:*:*:node.js:*:*"],
|
||||
"severity": {
|
||||
"type": "CVSS_V3",
|
||||
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
|
||||
"baseScore": 9.8,
|
||||
"label": "critical"
|
||||
},
|
||||
"summary": "Remote code execution vulnerability in vulnerable-package",
|
||||
"publishedAt": "2024-06-15T12:00:00Z",
|
||||
"modifiedAt": "2024-06-20T08:00:00Z",
|
||||
"observedAt": "2025-11-20T10:30:00Z",
|
||||
"provenance": {
|
||||
"connectorId": "nvd-osv-connector",
|
||||
"retrievedAt": "2025-11-20T10:30:00Z",
|
||||
"contentHash": "sha256:a1b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456",
|
||||
"signaturePresent": false,
|
||||
"signatureVerified": false
|
||||
},
|
||||
"raw": {
|
||||
"id": "CVE-2024-1234",
|
||||
"modified": "2024-06-20T08:00:00Z",
|
||||
"published": "2024-06-15T12:00:00Z",
|
||||
"aliases": ["CVE-2024-1234"],
|
||||
"summary": "Remote code execution vulnerability in vulnerable-package",
|
||||
"details": "A critical vulnerability exists in vulnerable-package versions prior to 2.0.0...",
|
||||
"severity": [
|
||||
{
|
||||
"type": "CVSS_V3",
|
||||
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"
|
||||
}
|
||||
],
|
||||
"affected": [
|
||||
{
|
||||
"package": {
|
||||
"ecosystem": "npm",
|
||||
"name": "vulnerable-package"
|
||||
},
|
||||
"ranges": [
|
||||
{
|
||||
"type": "SEMVER",
|
||||
"events": [
|
||||
{"introduced": "0"},
|
||||
{"fixed": "2.0.0"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"type": "ADVISORY",
|
||||
"url": "https://nvd.nist.gov/vuln/detail/CVE-2024-1234"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "obs:github:GHSA-abcd-efgh-ijkl:2025-11-18T14:00:00Z",
|
||||
"tenant": "acme",
|
||||
"advisoryKey": "CVE-2024-1234",
|
||||
"aliases": ["CVE-2024-1234", "GHSA-abcd-efgh-ijkl"],
|
||||
"source": "github",
|
||||
"format": "GHSA",
|
||||
"purls": ["pkg:npm/vulnerable-package@1.0.0"],
|
||||
"cpes": [],
|
||||
"severity": {
|
||||
"type": "CVSS_V3",
|
||||
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
|
||||
"baseScore": 9.8,
|
||||
"label": "critical"
|
||||
},
|
||||
"summary": "Critical RCE in vulnerable-package",
|
||||
"publishedAt": "2024-06-15T14:00:00Z",
|
||||
"modifiedAt": "2024-06-18T10:00:00Z",
|
||||
"observedAt": "2025-11-18T14:00:00Z",
|
||||
"provenance": {
|
||||
"connectorId": "github-advisory-connector",
|
||||
"retrievedAt": "2025-11-18T14:00:00Z",
|
||||
"contentHash": "sha256:b2c3d4e5f6789012345678901234567890abcdef1234567890abcdef123456ab",
|
||||
"signaturePresent": true,
|
||||
"signatureVerified": true
|
||||
},
|
||||
"raw": {
|
||||
"id": "GHSA-abcd-efgh-ijkl",
|
||||
"aliases": ["CVE-2024-1234"],
|
||||
"summary": "Critical RCE in vulnerable-package",
|
||||
"severity": [{"type": "CVSS_V3", "score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"}],
|
||||
"database_specific": {
|
||||
"github_reviewed": true,
|
||||
"github_reviewed_at": "2024-06-15T14:00:00Z"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"linkset": {
|
||||
"aliases": ["CVE-2024-1234", "GHSA-abcd-efgh-ijkl"],
|
||||
"purls": ["pkg:npm/vulnerable-package@1.0.0"],
|
||||
"cpes": ["cpe:2.3:a:example:vulnerable-package:1.0.0:*:*:*:*:node.js:*:*"],
|
||||
"references": [
|
||||
{"url": "https://nvd.nist.gov/vuln/detail/CVE-2024-1234", "type": "ADVISORY"},
|
||||
{"url": "https://github.com/advisories/GHSA-abcd-efgh-ijkl", "type": "ADVISORY"}
|
||||
],
|
||||
"scopes": ["npm"],
|
||||
"relationships": [],
|
||||
"confidence": 0.95,
|
||||
"conflicts": []
|
||||
},
|
||||
"nextCursor": null,
|
||||
"hasMore": false,
|
||||
"freshness": null
|
||||
}
|
||||
```
|
||||
|
||||
## Response with Conflicts
|
||||
When observations from different sources disagree:
|
||||
```json
|
||||
{
|
||||
"observations": [
|
||||
{
|
||||
"id": "obs:nvd:CVE-2024-5678:2025-11-20T10:30:00Z",
|
||||
"advisoryKey": "CVE-2024-5678",
|
||||
"source": "nvd",
|
||||
"severity": {
|
||||
"type": "CVSS_V3",
|
||||
"baseScore": 9.8,
|
||||
"label": "critical"
|
||||
},
|
||||
"observedAt": "2025-11-20T10:30:00Z"
|
||||
},
|
||||
{
|
||||
"id": "obs:vendor:CVE-2024-5678:2025-11-22T08:00:00Z",
|
||||
"advisoryKey": "CVE-2024-5678",
|
||||
"source": "vendor-security",
|
||||
"severity": {
|
||||
"type": "CVSS_V3",
|
||||
"baseScore": 7.5,
|
||||
"label": "high"
|
||||
},
|
||||
"observedAt": "2025-11-22T08:00:00Z"
|
||||
}
|
||||
],
|
||||
"linkset": {
|
||||
"aliases": ["CVE-2024-5678"],
|
||||
"purls": ["pkg:npm/another-package@3.0.0"],
|
||||
"cpes": [],
|
||||
"confidence": 0.72,
|
||||
"conflicts": [
|
||||
{
|
||||
"field": "severity",
|
||||
"code": "severity-mismatch",
|
||||
"observedValues": [
|
||||
{"source": "nvd", "value": "critical", "observedAt": "2025-11-20T10:30:00Z"},
|
||||
{"source": "vendor-security", "value": "high", "observedAt": "2025-11-22T08:00:00Z"}
|
||||
],
|
||||
"reason": "Sources disagree on severity classification: nvd reports critical (9.8), vendor-security reports high (7.5)"
|
||||
}
|
||||
]
|
||||
},
|
||||
"nextCursor": null,
|
||||
"hasMore": false
|
||||
}
|
||||
```
|
||||
|
||||
## Errors
|
||||
| Status | Code | Description |
|
||||
|--------|------|-------------|
|
||||
| 400 | `ERR_VALIDATION_FAILED` | Invalid query parameters. |
|
||||
| 400 | `ERR_INVALID_CURSOR` | Malformed or expired cursor. |
|
||||
| 401 | `ERR_UNAUTHORIZED` | Missing or invalid authentication. |
|
||||
| 403 | `ERR_FORBIDDEN` | Tenant access denied. |
|
||||
| 404 | `ERR_RESOURCE_NOT_FOUND` | No observations found for advisory. |
|
||||
|
||||
### Error Response Example
|
||||
```json
|
||||
{
|
||||
"type": "https://stellaops.io/errors/validation-failed",
|
||||
"title": "Validation Failed",
|
||||
"status": 400,
|
||||
"detail": "The 'advisoryKey' parameter is required when 'source' is specified.",
|
||||
"instance": "/advisories/observations",
|
||||
"traceId": "trace-id-xyz789",
|
||||
"error": {
|
||||
"code": "ERR_VALIDATION_FAILED",
|
||||
"message": "Missing required parameter.",
|
||||
"target": "advisoryKey",
|
||||
"innerErrors": [
|
||||
{
|
||||
"field": "advisoryKey",
|
||||
"code": "REQUIRED_WHEN",
|
||||
"message": "advisoryKey is required when source is specified"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Observation Lifecycle
|
||||
1. **Ingested**: Raw advisory data retrieved from upstream source.
|
||||
2. **Validated**: Schema validated against content format (OSV, GHSA, etc.).
|
||||
3. **Stored**: Immutable observation record created with provenance.
|
||||
4. **Linked**: Observation contributes to linkset aggregation.
|
||||
|
||||
## Determinism & Ordering
|
||||
- Observations are ordered by `observedAt desc`, then `source asc`, then `id asc`.
|
||||
- The same query with identical parameters returns identical results.
|
||||
- Cursor-based pagination ensures stable iteration even as new data arrives.
|
||||
|
||||
## Notes
|
||||
- Observations are immutable; updates from upstream create new observation records.
|
||||
- The `raw` field contains the unmodified upstream content.
|
||||
- Provenance includes connector identity and content hashes for audit.
|
||||
- Multiple observations may exist for the same advisory from different sources.
|
||||
- Conflicts are detected and surfaced in the aggregate linkset, not resolved.
|
||||
|
||||
## Changelog
|
||||
- 2025-12-06: Added curated examples with conflict scenarios (CONCELIER-WEB-OAS-62-001).
|
||||
- 2025-11-17: LNM v1 specification frozen.
|
||||
@@ -27,11 +27,28 @@
|
||||
| `ledger_attachments_encryption_failures_total` | Counter | `tenant`, `stage` (`encrypt`, `sign`, `upload`) | Ensures secure attachment pipeline stays healthy. |
|
||||
| `ledger_db_connections_active` | Gauge | `role` (`writer`, `projector`) | Helps tune pool size. |
|
||||
| `ledger_app_version_info` | Gauge | `version`, `git_sha` | Static metric for fleet observability. |
|
||||
| `ledger_scoring_latency_seconds` | Histogram | `tenant`, `policy_version`, `result` | Latency of risk scoring operations per finding. P95 target <500 ms. |
|
||||
| `ledger_scoring_operations_total` | Counter | `tenant`, `policy_version`, `result` | Total number of scoring operations by result (success, partial_success, error, etc.). |
|
||||
| `ledger_scoring_provider_gaps_total` | Counter | `tenant`, `provider`, `reason` | Count of findings where scoring provider was unavailable or returned no data. |
|
||||
| `ledger_severity_distribution_critical` | Gauge | `tenant`, `policy_version` | Current count of critical severity findings by tenant and policy. |
|
||||
| `ledger_severity_distribution_high` | Gauge | `tenant`, `policy_version` | Current count of high severity findings by tenant and policy. |
|
||||
| `ledger_severity_distribution_medium` | Gauge | `tenant`, `policy_version` | Current count of medium severity findings by tenant and policy. |
|
||||
| `ledger_severity_distribution_low` | Gauge | `tenant`, `policy_version` | Current count of low severity findings by tenant and policy. |
|
||||
| `ledger_severity_distribution_unknown` | Gauge | `tenant`, `policy_version` | Current count of unknown/unscored findings by tenant and policy. |
|
||||
| `ledger_score_freshness_seconds` | Gauge | `tenant` | Time since last scoring operation completed by tenant. Alert when >3600 s. |
|
||||
| `ledger_scored_findings_exports_total` | Counter | `tenant`, `record_count` | Count of scored findings export operations. |
|
||||
| `ledger_scored_findings_export_duration_seconds` | Histogram | `tenant`, `record_count` | Duration of scored findings export operations. |
|
||||
| `ledger_airgap_staleness_seconds` | Histogram | `domain` | Current staleness of air-gap imported data by domain. |
|
||||
| `ledger_airgap_staleness_gauge_seconds` | Gauge | `domain` | Current staleness of air-gap data by domain (observable gauge). |
|
||||
| `ledger_staleness_validation_failures_total` | Counter | `domain` | Count of staleness validation failures blocking exports. |
|
||||
|
||||
### Derived dashboards
|
||||
- **Writer health:** `ledger_write_latency_seconds` (P50/P95/P99), backlog gauge, event throughput.
|
||||
- **Projection health:** `ledger_projection_lag_seconds`, `ledger_projection_apply_seconds`, projection throughput, conflict counts (from logs).
|
||||
- **Anchoring:** Anchor duration histogram, failure counter, root hash timeline.
|
||||
- **Risk scoring:** `ledger_scoring_latency_seconds` (P50/P95/P99), severity distribution gauges, provider gap counter, score freshness.
|
||||
- **Export operations:** `ledger_scored_findings_exports_total`, export duration histogram, record counts.
|
||||
- **Air-gap health:** `ledger_airgap_staleness_gauge_seconds`, staleness validation failures, domain freshness trends.
|
||||
|
||||
## 3. Logs & traces
|
||||
- **Log structure:** Serilog JSON with fields `tenant`, `chainId`, `sequence`, `eventId`, `eventType`, `actorId`, `policyVersion`, `hash`, `merkleRoot`.
|
||||
@@ -50,6 +67,9 @@
|
||||
| **ProjectionLag** | `ledger_projection_lag_seconds` > 30 s | Trigger rebuild, verify change streams. |
|
||||
| **AnchorFailure** | `ledger_merkle_anchor_failures_total` increase > 0 | Collect logs, rerun anchor, verify signing service. |
|
||||
| **AttachmentSecurityError** | `ledger_attachments_encryption_failures_total` increase > 0 | Audit attachments pipeline; check key material and storage endpoints. |
|
||||
| **ScoringFreshnessStale** | `ledger_score_freshness_seconds` > 3600 s for any tenant | Check scoring pipeline, verify provider connectivity, re-trigger scoring job. |
|
||||
| **ScoringProviderGaps** | `ledger_scoring_provider_gaps_total` increase > 10 in 5 min | Investigate provider failures; check rate limits or connectivity. |
|
||||
| **AirgapDataStale** | `ledger_airgap_staleness_gauge_seconds` > threshold for 15 min | Re-import air-gap bundle; verify export pipeline in source enclave. |
|
||||
|
||||
Alerts integrate with Notifier channel `ledger.alerts`. For air-gapped deployments emit to local syslog + CLI incident scripts.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user