7.5 KiB
7.5 KiB
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)
{
"purl": ["pkg:npm/lodash@4.17.20"],
"includeConflicts": true,
"includeObservations": true,
"pageSize": 10
}
Response (200)
{
"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:
{
"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
{
"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 ascpublishedAt descpublishedAt ascseverity descseverity ascsourceadvisoryId
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
normalizedfield contains processed data suitable for version matching and range evaluation. - Observation IDs in the
observationsarray 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.