openapi: 3.1.0 info: title: StellaOps Concelier – Link-Not-Merge Policy APIs version: "1.0.0" description: | Fact-only advisory/linkset retrieval for Policy Engine consumers. ## Philosophy Link-Not-Merge (LNM) provides raw advisory data with full provenance: - **Link**: Observations from multiple sources are linked via shared identifiers. - **Not Merge**: Conflicting data is preserved rather than collapsed. - **Surface, Don't Resolve**: Conflicts are clearly marked for consumers. ## Authentication All endpoints require the `X-Stella-Tenant` header for multi-tenant isolation. ## Pagination List endpoints support cursor-based pagination with `page` and `pageSize` parameters. Maximum page size is 200 items. ## Documentation See `/docs/modules/concelier/api/` for detailed examples and conflict resolution strategies. servers: - url: / description: Relative base path (API Gateway rewrites in production). tags: - name: Linksets description: Link-Not-Merge linkset retrieval paths: /v1/lnm/linksets: get: summary: List linksets tags: [Linksets] parameters: - $ref: '#/components/parameters/Tenant' - name: includeConflicts in: query required: false schema: { type: boolean, default: true } - name: includeObservations in: query required: false schema: { type: boolean, default: false } - $ref: '#/components/parameters/purl' - $ref: '#/components/parameters/cpe' - $ref: '#/components/parameters/ghsa' - $ref: '#/components/parameters/cve' - $ref: '#/components/parameters/advisoryId' - $ref: '#/components/parameters/source' - $ref: '#/components/parameters/severityMin' - $ref: '#/components/parameters/severityMax' - $ref: '#/components/parameters/publishedSince' - $ref: '#/components/parameters/modifiedSince' - $ref: '#/components/parameters/page' - $ref: '#/components/parameters/pageSize' - $ref: '#/components/parameters/sort' responses: "200": description: Deterministically ordered list of linksets content: application/json: schema: $ref: '#/components/schemas/PagedLinksets' examples: single-linkset: summary: Single linkset result value: 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 Command Injection vulnerability" publishedAt: "2021-02-15T13:15:00Z" modifiedAt: "2024-08-04T19:16:00Z" severity: "high" provenance: ingestedAt: "2025-11-20T10:30:00Z" connectorId: "nvd-osv-connector" evidenceHash: "sha256:a1b2c3d4e5f6" conflicts: [] cached: false page: 1 pageSize: 50 total: 1 with-conflicts: summary: Linkset with severity conflict value: items: - advisoryId: "CVE-2024-1234" source: "aggregated" purl: ["pkg:npm/example@1.0.0"] cpe: [] severity: "high" provenance: ingestedAt: "2025-11-20T10:30:00Z" connectorId: "multi-source" conflicts: - field: "severity" reason: "severity-mismatch" observedValue: "critical" observedAt: "2025-11-18T08:00:00Z" evidenceHash: "sha256:conflict-hash" cached: false page: 1 pageSize: 50 total: 1 "400": description: Invalid request parameters content: application/json: schema: $ref: '#/components/schemas/ErrorEnvelope' example: type: "https://stellaops.io/errors/validation-failed" title: "Validation Failed" status: 400 detail: "The 'pageSize' parameter exceeds the maximum allowed value." error: code: "ERR_PAGE_SIZE_EXCEEDED" message: "Page size must be between 1 and 200." target: "pageSize" /v1/lnm/linksets/{advisoryId}: get: summary: Get linkset by advisory ID tags: [Linksets] parameters: - $ref: '#/components/parameters/Tenant' - name: advisoryId in: path required: true schema: type: string - name: source in: query required: false schema: { type: string } - name: includeConflicts in: query required: false schema: { type: boolean, default: true } - name: includeObservations in: query required: false schema: { type: boolean, default: false } responses: "200": description: Linkset with provenance and conflicts content: application/json: schema: $ref: '#/components/schemas/Linkset' "404": description: Not found /v1/lnm/linksets/search: post: summary: Search linksets (body filters) tags: [Linksets] parameters: - $ref: '#/components/parameters/Tenant' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/LinksetSearchRequest' responses: "200": description: Deterministically ordered search results content: application/json: schema: $ref: '#/components/schemas/PagedLinksets' components: parameters: Tenant: name: Tenant in: header required: true schema: type: string description: Tenant identifier (required). purl: name: purl in: query schema: type: array items: { type: string } style: form explode: true cpe: name: cpe in: query schema: { type: string } ghsa: name: ghsa in: query schema: { type: string } cve: name: cve in: query schema: { type: string } advisoryId: name: advisoryId in: query schema: { type: string } source: name: source in: query schema: type: string severityMin: name: severityMin in: query schema: type: number format: float severityMax: name: severityMax in: query schema: type: number format: float publishedSince: name: publishedSince in: query schema: type: string format: date-time modifiedSince: name: modifiedSince in: query schema: type: string format: date-time page: name: page in: query schema: type: integer minimum: 1 default: 1 pageSize: name: pageSize in: query schema: type: integer minimum: 1 maximum: 200 default: 50 sort: name: sort in: query schema: type: string enum: - modifiedAt desc - modifiedAt asc - publishedAt desc - publishedAt asc - severity desc - severity asc - source - advisoryId description: Default modifiedAt desc; ties advisoryId asc, source asc. schemas: LinksetSearchRequest: type: object properties: purl: { type: array, items: { type: string } } cpe: { type: array, items: { type: string } } ghsa: { type: string } cve: { type: string } advisoryId: { type: string } source: { type: string } severityMin: { type: number } severityMax: { type: number } publishedSince: { type: string, format: date-time } modifiedSince: { type: string, format: date-time } includeTimeline: { type: boolean, default: false } includeObservations: { type: boolean, default: false } includeConflicts: { type: boolean, default: true } page: { type: integer, minimum: 1, default: 1 } pageSize: { type: integer, minimum: 1, maximum: 200, default: 50 } sort: { type: string, enum: [modifiedAt desc, modifiedAt asc, publishedAt desc, publishedAt asc, severity desc, severity asc, source, advisoryId] } PagedLinksets: type: object properties: items: type: array items: { $ref: '#/components/schemas/Linkset' } page: { type: integer } pageSize: { type: integer } total: { type: integer } Linkset: type: object required: [advisoryId, source, purl, cpe, provenance] properties: advisoryId: { type: string } source: { type: string } purl: { type: array, items: { type: string } } cpe: { type: array, items: { type: string } } summary: { type: string } publishedAt: { type: string, format: date-time } modifiedAt: { type: string, format: date-time } severity: { type: string, description: Source-native severity label } status: { type: string } provenance: { $ref: '#/components/schemas/LinksetProvenance' } conflicts: type: array items: { $ref: '#/components/schemas/LinksetConflict' } timeline: type: array items: { $ref: '#/components/schemas/LinksetTimeline' } normalized: type: object properties: aliases: { type: array, items: { type: string } } purl: { type: array, items: { type: string } } cpe: { type: array, items: { type: string } } versions: { type: array, items: { type: string } } ranges: { type: array, items: { type: object } } severities: { type: array, items: { type: object } } cached: type: boolean description: True if served from cache; provenance.evidenceHash present for integrity. remarks: type: array items: { type: string } observations: type: array items: { type: string } LinksetProvenance: type: object properties: ingestedAt: { type: string, format: date-time } connectorId: { type: string } evidenceHash: { type: string } dsseEnvelopeHash: { type: string } LinksetConflict: type: object properties: field: { type: string } reason: { type: string } observedValue: { type: string } observedAt: { type: string, format: date-time } evidenceHash: { type: string } LinksetTimeline: type: object properties: event: { type: string } at: { type: string, format: date-time } evidenceHash: { type: string } ErrorEnvelope: type: object description: RFC 7807 Problem Details with StellaOps extensions properties: type: type: string format: uri description: URI identifying the problem type title: type: string description: Short, human-readable summary status: type: integer description: HTTP status code detail: type: string description: Specific explanation of the problem instance: type: string format: uri description: URI of the specific occurrence traceId: type: string description: Distributed trace identifier error: $ref: '#/components/schemas/ErrorDetail' ErrorDetail: type: object description: Machine-readable error information properties: code: type: string description: Machine-readable error code (e.g., ERR_VALIDATION_FAILED) message: type: string description: Human-readable error message target: type: string description: Field or resource that caused the error metadata: type: object additionalProperties: true description: Additional contextual data innerErrors: type: array items: $ref: '#/components/schemas/ValidationError' description: Nested validation errors ValidationError: type: object properties: field: type: string description: Field path (e.g., "data.severity") code: type: string description: Error code for this field message: type: string description: Human-readable message