# CVE-to-Symbol Mapping
Last updated: 2026-06-13 (derived patch-diff surface approved-subset export policy/format, sprint SPRINT_20260612_013). Owner: Scanner Guild + Concelier Guild.
Audience: Scanner and Concelier contributors implementing or consuming CVE-to-symbol resolution.
This guide describes how Stella Ops maps CVE identifiers to the specific binary symbols and functions that drive reachability slices — the foundation for deciding whether a vulnerable function is actually called.
1. Overview
To determine whether a vulnerability is reachable, Stella Ops resolves:
- CVE identifiers (e.g.,
CVE-2024-1234) - Package coordinates (e.g.,
pkg:npm/lodash@4.17.21) - Affected symbols (e.g.,
lodash.template,openssl:EVP_PKEY_decrypt)
The mapping is used by SliceExtractor to target the right symbols and by downstream VEX decisions.
2. Data Sources
2.1 Patch Diff Surfaces (Preferred)
Highest-fidelity source: compute method-level diffs between vulnerable and fixed versions.
Implementation: StellaOps.Scanner.VulnSurfaces
2.2 Advisory Linksets (Concelier)
Scanner queries Concelier’s LNM linksets for package coordinates and optional symbol hints.
Implementation: StellaOps.Scanner.Advisory -> Concelier /v1/lnm/linksets/{cveId} or /v1/lnm/linksets/search
2.3 Offline Bundles
For air-gapped environments, precomputed bundles map CVEs to packages and symbols.
Implementation:
- Read (Scanner, advisory-linkset fallback):
FileAdvisoryBundleStore(StellaOps.Scanner.Advisory) - Read (air-gapped projection):
BundleAffectedSymbolProvider(StellaOps.Concelier.Core/Signals) — anIAffectedSymbolProviderover the bundle file; registering it activates the Scanner’sAffectedSymbolCveSymbolMappingServicebridge, so the disconnected host uses the exact read path a connected host uses for the live projection - Write (export job):
AffectedSymbolOfflineBundleExporter(StellaOps.Concelier.Core/Signals) — per-tenant, deterministic, license-allowlist-gated (see §5) - Curated Top-N overlay (air-gapped backstop):
CuratedAffectedSymbolProvider+LadderAffectedSymbolProvider(StellaOps.Concelier.Core/Signals) — a hand-verified curated bundle laddered ON TOP of the upstream provider with curated-wins precedence (see §5.4; real curation stays open feed-time human work)
Legal boundary: upstream advisory symbols (the
AffectedSymbolprojection) enter schema v2 bundles. Stella-DERIVED patch-diff surfaces (vuln_surfaces) use a separate approved-subset export policy/format only for PyPI, RubyGems, NuGet, and Packagist. npm and Maven Central derived surfaces are not supported for kit export by design — they are redistribution-licensing excluded (npm security-data ToS clause; Sonatype Central commercial/scanner-use restriction) and the operator has decided not to pursue the registry permission that was their only path (see §5.3 “Unsupported ecosystems: npm + Maven Central”). Internal-only use of npm/Maven surfaces is unaffected (docs/legal/decisions/patch-diff-surface-airgap-export-counsel-thread.md, reply).
3. Service Contracts
3.1 CVE -> Package/Symbol Mapping
public interface IAdvisoryClient
{
Task<AdvisorySymbolMapping?> GetCveSymbolsAsync(string cveId, CancellationToken ct = default);
}
public sealed record AdvisorySymbolMapping
{
public required string CveId { get; init; }
public ImmutableArray<AdvisoryPackageSymbols> Packages { get; init; }
public required string Source { get; init; } // "concelier" | "bundle"
}
public sealed record AdvisoryPackageSymbols
{
public required string Purl { get; init; }
public ImmutableArray<string> Symbols { get; init; }
}
3.2 CVE + PURL -> Affected Symbols
public interface IVulnSurfaceService
{
Task<VulnSurfaceResult> GetAffectedSymbolsAsync(
string cveId,
string purl,
CancellationToken ct = default);
}
public sealed record VulnSurfaceResult
{
public required string CveId { get; init; }
public required string Purl { get; init; }
public required ImmutableArray<AffectedSymbol> Symbols { get; init; }
public required string Source { get; init; } // "surface" | "package-symbols" | "heuristic"
public required double Confidence { get; init; }
}
public sealed record AffectedSymbol
{
public required string SymbolId { get; init; }
public string? MethodKey { get; init; }
public string? DisplayName { get; init; }
public string? ChangeType { get; init; }
public double Confidence { get; init; }
}
4. Caching Strategy
| Data | TTL | Notes |
|---|---|---|
| Advisory linksets | 1 hour | In-memory cache; configurable TTL |
| Offline bundles | Process lifetime | Loaded once from file |
5. Offline Bundle Format
5.1 Schema v1 (legacy)
{
"items": [
{
"cveId": "CVE-2024-1234",
"source": "bundle",
"packages": [
{
"purl": "pkg:npm/lodash@4.17.21",
"symbols": ["template", "templateSettings"]
}
]
}
]
}
v1 bundles remain readable everywhere: FileAdvisoryBundleStore reads them natively, and BundleAffectedSymbolProvider loads packages[].symbols as function-granular facts.
5.2 Schema v2 (current — sprint SPRINT_20260612_012 AG1)
Produced by AffectedSymbolOfflineBundleExporter from the per-tenant Concelier AffectedSymbol projection (upstream advisory symbols only). v2 is a strict superset of v1 — v1 readers ignore the added fields — and adds per-symbol granularity + provenance so the projection read path rebuilds with zero loss:
{
"schemaVersion": 2,
"tenant": "default",
"items": [
{
"cveId": "CVE-2021-38561",
"source": "bundle",
"packages": [
{
"purl": "pkg:golang/golang.org/x/text",
"symbols": ["golang.org/x/text/language::ParseAcceptLanguage"]
}
],
"symbols": [
{
"symbol": "ParseAcceptLanguage",
"symbolType": "function",
"purl": "pkg:golang/golang.org/x/text",
"module": "golang.org/x/text/language",
"versionRange": ">=0, <0.3.7",
"observationId": "obs-go-text-1",
"provenance": {
"source": "osv",
"vendor": "open-source-vulnerabilities",
"observationHash": "sha256:...",
"upstreamId": "GO-2021-0113",
"upstreamUrl": "https://osv.dev/vulnerability/GO-2021-0113",
"extractionShape": "ecosystem_specific.imports[].symbols"
}
}
]
}
]
}
Notes:
packages[].symbolscarry canonical ids ({module}::{Type}.{Method}/{module}::{symbol}) — the same form the reachability stack matches on (CveSinkMapping.ToVulnerableSymbol). v1’s plain-name convention still loads.- Determinism: canonical bytes are produced by
CanonJson(recursively ordinal-sorted keys, compact, nulls omitted); items/packages/symbols are ordinal-sorted and de-duplicated; canonical bytes carry no timestamps (volatile ingest metadata —FetchedAt/ExtractedAt/IngestJobId— is excluded; import stamps the Unix epoch). Two exports over the same projection state are byte-identical. - Digest + manifest: the sha256 digest of the canonical bytes (
sha256:<hex>) is recorded in<bundle>.manifest.jsontogether withgeneratedAt(injectedTimeProvider), counts, allowlist exclusions, and the signing key id. The manifest is a metadata envelope — never digested or signed itself. - Signing (optional, offline):
<bundle>.dsse.jsonis a DSSE v1 envelope over the exact bundle bytes — ECDSA P-256 / SHA-256, verified against a local directory of<keyId>.pempublic keys (the FileKeyProvider pattern, identical scheme to the reachability catalog packs). WhenBundleAffectedSymbolProviderOptions.TrustedKeysDirectoryis set, verification is mandatory and fail-closed. - License allowlist gate (export): only sources on
AffectedSymbolOfflineBundleExportOptions.AllowedSources(defaultosv,ghsa,nvd— the advisory feeds already redistributed offline under the trivy-db counsel framework) are exported; excluded sources are counted in the manifest and logged, never dropped silently. - Legal boundary: upstream advisory symbols ONLY. Derived patch-diff surfaces must not enter this format; use the separate approved-subset surface export format below.
5.3 Derived Patch-Diff Surface Export v1 (approved subset)
Produced by StellaOps.Scanner.VulnSurfaces.Export from computed VulnSurface rows only after the 2026-06-12 counsel gates pass. This is a separate artifact from schema v2 advisory-symbol bundles: it does not write derived rows into the curated/upstream symbol store. The approved-subset loader verifies canonical signed bytes, trusted row-level DSSE signatures, row payload digests, registry/provenance policy, and the approved ecosystem boundary before exposing accepted rows as a read-only IVulnSurfaceService for the existing mapping read path.
{
"schemaVersion": "stellaops.vuln-surface-export/v1",
"rows": [
{
"cveId": "CVE-2026-0004",
"advisoryId": "GHSA-demo",
"ecosystem": "pypi",
"registry": "pypi.org",
"exactDownloadEndpoint": "https://files.pythonhosted.org/packages/demo-1.0.0.tar.gz",
"purl": "pkg:pypi/demo@1.0.0",
"packageName": "demo",
"vulnerableVersion": "1.0.0",
"fixedVersion": "1.0.1",
"packageLicenseExpression": "MIT",
"licenseClassification": "oss",
"tosSnapshotId": "tos-2026-06-12",
"tosSnapshotDate": "2026-06-12",
"userAgent": "StellaOps/1.0 security-surface-export contact=security@example.invalid",
"contact": "security@example.invalid",
"rateLimitPolicy": "official-api-backoff",
"retryAfterPolicy": "honor-retry-after",
"auditLogReferences": ["audit:surface:20260612"],
"computationDigest": "sha256:...",
"sinks": [
{
"methodKey": "demo::Parser.parse(value)",
"declaringType": "Parser",
"methodName": "parse",
"canonicalSignature": "parse(value)",
"changeType": "modified",
"vulnHash": "sha256:...",
"fixedHash": "sha256:...",
"parameterCount": 1,
"returnType": "string"
}
]
}
]
}
Policy and format rules:
- Approved ecosystems only:
pypi,gem/RubyGems,nuget, andcomposer/Packagist can export.npmandmavenare always excluded with auditable reason codes until written permission or a commercial arrangement is recorded. - No package content: rows carry canonical machine-generated signatures and hashes only. Package archives, source files, object/decompiled code, method bodies, comments, docstrings, README/example text, literal expressions, default values, annotation strings, and copied declarations are forbidden and fail the policy gate.
- Required provenance: every row carries ecosystem, registry, exact download endpoint, purl, vulnerable/fixed versions, CVE/advisory id, package license expression/classification, ToS snapshot id/date, registry client controls, audit-log references, and the deterministic surface computation digest.
- License gate: unknown, proprietary, custom, restricted, non-commercial, no-derivatives, and no-reverse-engineering package-license classifications fail closed. OSS/free/permissive classifications may proceed when the other gates pass.
- Registry controls: official API use, User-Agent/contact, rate-limit/Retry-After handling, caching/dedupe discipline, no bulk mirroring, and per-registry audit logs are required. Packagist rows also record the actual code download host.
- Determinism and signing: canonical bundle bytes omit timestamps, sort rows/sinks/references ordinally, and produce stable sha256 digests over two fresh exports. Signed exports add row-level DSSE envelopes over the canonical row payload and verification fails closed if a row is changed after signing.
- Loader and precedence: the disconnected loader fail-closes the whole bundle on any unsigned, non-canonical, tampered, untrusted, no-audit, no-provenance, or policy-rejected row. Loaded rows stay read-only and feed
VulnSurfaceCveSymbolMappingService, preserving the existing ladder: curated > upstream advisory symbols > derived patch-diff surfaces > package-symbols > heuristic. No approved-subset import writes package-derived rows intocve_symbol_mappings. - Third-party data notice source: derived-surface offline kits must state that the artifact contains machine-generated security-analysis facts derived from third-party package versions, not package archives or source content; ecosystem-specific registry terms and package-license classifications control whether a row is included.
Unsupported ecosystems: npm + Maven Central (not supported by design)
npm and Maven Central derived patch-diff surfaces are NOT exported into offline kits, and this is a permanent product decision — not a temporary block. The supported export set is exactly the four counsel-approved ecosystems above: PyPI, RubyGems, NuGet, Packagist. Everything else fail-closes.
Why the two are excluded:
- Redistribution licensing, not copyright. The derived surface artifact itself (method identifiers, normalized signatures, hashes, CVE/package/version provenance) is treated as uncopyrightable security-analysis facts — that part is settled in counsel’s favour (
docs/legal/decisions/patch-diff-surface-airgap-export-counsel-thread-reply.md§1). The gating risk is contractual: the registry Terms of Service govern whether Stella may redistribute surfaces derived from packages downloaded through that registry.- npm:
registry.npmjs.orgToS carries a specific security-data clause — “data about the security of Packages” (vulnerability reports, audit-status, supplementary security documentation) may be used only for personal/internal-business purposes and not provided to others or used as part of products/services. CVE-linked patch-diff surfaces are plausibly “data about the security of Packages,” so redistributing them in a kit would require written npm/GitHub confirmation that Stella-computed surfaces are out of scope of that clause (counsel reply §2–3 npm row, §6 “Keep blocked”). - Maven Central: Sonatype’s current Central terms + FAQ treat commercial/scanner-like, automated, high-volume use (explicitly “use by a commercial service, platform, tool, scanner, hosted build, cache, mirror, or similar”) as restricted, and there is no affirmative permission to redistribute derived security metadata generated from Central downloads. Export would require a Sonatype written permission or a commercial arrangement (counsel reply §2–3 Maven row, §6 “Keep blocked”).
- npm:
- Operator decision (2026-06-14): ABANDONED, not pursued. Obtaining the npm/GitHub written confirmation and the Sonatype commercial arrangement is the only path counsel left open for these two ecosystems. The operator has decided not to pursue that registry permission. npm and Maven Central are therefore declared explicitly out of scope for offline-kit surface export. They are not a pending to-do; they are an intentional, permanent exclusion. (Internal-only use of computed npm/Maven surfaces — the mapping bridge and scan-time resolution — is unaffected; only exporting derived surfaces into redistributable offline kits is excluded.)
- Fail-closed by construction. The exporter does not merely omit these ecosystems — it actively rejects them with auditable reason codes so a future caller cannot silently re-enable them:
- Producer gate:
VulnSurfaceExportPolicy.Evaluaterejectsnpm→ecosystem.blocked.npm_written_permission_requiredandmaven→ecosystem.blocked.maven_central_permission_required; any ecosystem outside the approved set{composer, gem, nuget, pypi}→ecosystem.not_approved(src/Scanner/__Libraries/StellaOps.Scanner.VulnSurfaces/Export/VulnSurfaceExportPolicy.cs:95-127). The OSV registry spellingsRubyGems/Packagistfold togem/composerfirst (Feed/VulnSurfaceEcosystems.cs:30-40), so the approved set is precisely the four counsel-approved registries. - Loader gate: the disconnected bundle loader fail-closes npm/Maven Central rows even when a trusted DSSE signature verifies — a signed-but-blocked row cannot smuggle a surface in (
VulnSurfaceExportLoaderTests.LoadSignedBundle_BlockedEcosystems_FailClosedEvenWhenTrustedSignatureVerifies,src/Scanner/__Libraries/StellaOps.Scanner.VulnSurfaces.Tests/VulnSurfaceExportLoaderTests.cs:139-142). - Pinned by tests: producer exclusion
VulnSurfaceExportPolicyTests.BuildCanonical_ExportsOnlyCounselApprovedEcosystems_AndExcludesNpmAndMaven; both the producer (13 facts) and loader (10 facts) suites are green.
- Producer gate:
- Unknown / proprietary / custom / restricted package licenses are also fail-closed for the supported four (
license.blocked.*), so the kit only ever ships surfaces over OSS/free/permissive packages from the four approved registries.
If the operator ever reverses this decision and obtains the npm/GitHub confirmation and/or Sonatype arrangement, re-enabling an ecosystem is a single, auditable change: record the written permission under docs/legal/decisions/, then add the ecosystem to VulnSurfaceExportPolicy.ApprovedEcosystems and remove its dedicated block branch. Until that recorded permission exists, npm and Maven Central stay unsupported.
5.4 Curated Top-N Bundle v1 (sprint SPRINT_20260612_012 AG2)
The deterministic backstop tier for ecosystems with NO upstream advisory symbols: the ~200–500 KEV-weighted CVEs whose affected symbols a human curator has hand-verified. Schema id stellaops.curated-affected-symbols/v1 (a distinct, versioned string — never confused with the integer-versioned upstream v2 schema above). Produced by curation tooling; loaded by CuratedAffectedSymbolProvider (StellaOps.Concelier.Core/Signals) and laddered over an upstream provider by LadderAffectedSymbolProvider.
{
"schema": "stellaops.curated-affected-symbols/v1",
"tenant": "default",
"items": [
{
"cveId": "CVE-2099-0001",
"curator": {
"curator": "stellaops-security-research",
"method": "patch-review",
"curatedAt": "2026-06-13",
"evidenceUrl": "https://example.invalid/advisories/CVE-2099-0001"
},
"symbols": [
{
"symbol": "parse",
"symbolType": "method",
"className": "Parser",
"module": "example.demo",
"purl": "pkg:nuget/Example.Demo",
"versionRange": ">=1.0.0, <1.2.0",
"confidence": 0.97
}
]
}
]
}
Format and policy rules:
- Mandatory curator provenance per item:
curator,method, andcuratedAt(ISOYYYY-MM-DD, a curator-authored calendar date — NOT a clock read) are required;evidenceUrlis optional. A “curated” tag can never be claimed without an attributable human source. - Mandatory per-entry confidence in (0,1]: the curated tier is calibrated above upstream; each symbol carries its own curator confidence (surfaced on the materialized
AffectedSymbolasAttributes["curatedConfidence"]). - Fail-closed validation (
CuratedAffectedSymbolBundleValidator): the WHOLE bundle is rejected (throws) — never partially dropped — on a wrong/blank schema id, a blankcveId, missing/blank curator fields, a non-ISOcuratedAt, an empty item, a blank symbol, an unknownsymbolType, a confidence outside (0,1], or a duplicate canonical id within one advisory. - Curated-wins precedence (
LadderAffectedSymbolProvider): the curated overlay sits on top of an upstream provider (the live projection or the offline advisory bundle) and resolves canonical-id collisions in favor of curated, closing the §6.1 laddercurated > upstream advisory symbols > derived surfaces. Upstream-only symbols survive untouched; output ordering is deterministic (canonical id ASC, ordinal). - Materialization: curated entries become
AffectedSymbolfacts with provenance sourcecurated, vendor = the curator,extractionShape = "curated:{method}", and the deterministic Unix-epoch timestamp (canonical bytes carry no clock fields). The same air-gapped read path / Scanner bridge consumes them. - Signing (optional, offline):
<bundle>.dsse.jsonis a DSSE v1 envelope over the exact bundle bytes (ECDSA P-256 / SHA-256, payload typeapplication/vnd.stellaops.curated-affected-symbol-bundle+json), verified fail-closed against a local trusted-keys directory — identical scheme to the upstream bundles, distinct payload type. - Curation itself is OPEN feed-time human work — NOT done in this sprint. This sprint ships the FORMAT, validation, loader, and curated-wins precedence with a clearly-synthetic example (
CVE-2099-*). No real Top-N list is fabricated; the actual curation of real KEV-weighted CVEs remains an open human task (see §6 below and the sprint’s “AG2 honesty risk” note). The deliverable is that a curator-produced file ships, validates, loads, and wins precedence deterministically — not that any CVE is covered.
6. Fallback Behavior
When no surface or advisory mapping is available, the service returns an empty symbol list with low confidence and Source = "heuristic". Callers may inject an IPackageSymbolProvider to supply public-symbol fallbacks.
7. Related Documentation
Created: 2025-12-22. See Sprint 3810 for implementation details.
