234 lines
7.5 KiB
Markdown
234 lines
7.5 KiB
Markdown
# 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.
|