CONCELIER-LNM-26-001 · Linkset Correlation Rules (v2)

Supersedes linkset-correlation-21-002.md for new linkset builds. V1 linksets remain valid; migration job will recompute confidence using v2 algorithm.

Purpose: Address critical failure modes in v1 correlation (intersection transitivity, false conflict emission) and introduce higher-discriminative signals (patch lineage, version compatibility, IDF-weighted package matching).


Scope


Key Changes from v1

Aspectv1 Behaviorv2 Behavior
Alias matchingIntersection across all inputsGraph connectivity (LCC ratio)
PURL matchingIntersection across all inputsPairwise coverage + IDF weighting
Reference clashEmitted on zero overlapOnly on true URL contradictions
Conflict penaltySingle -0.1 for any conflictTyped severities with per-reason penalties
Patch lineageNot usedTop-tier signal (+0.35 for exact SHA)
Version rangesDivergence noted onlyClassified (Equivalent/Overlapping/Disjoint)

Deterministic Confidence Calculation (0-1)

Signal Weights

confidence = clamp(
  0.30 * alias_connectivity +
  0.10 * alias_authority +
  0.20 * package_coverage +
  0.10 * version_compatibility +
  0.10 * cpe_match +
  0.10 * patch_lineage +
  0.05 * reference_overlap +
  0.05 * freshness_score
) - typed_penalty

Signal Definitions

alias_connectivity (weight: 0.30)

Graph-based scoring replacing intersection-across-all.

  1. Build bipartite graph: observation nodes ↔ alias nodes
  2. Connect observations that share any alias (transitive bridging)
  3. Compute LCC (largest connected component) ratio: |LCC| / N
ScenarioScore
All observations in single connected component1.0
80% of observations connected0.8
No alias overlap at all0.0

Why this matters: Sources A (CVE-X), B (CVE-X + GHSA-Y), C (GHSA-Y) now correctly correlate via transitive bridging, whereas v1 produced score = 0.

alias_authority (weight: 0.10)

Scope-based weighting using existing canonical key prefixes:

Alias TypeAuthority Score
CVE-* (global)1.0
GHSA-* (ecosystem)0.8
Vendor IDs (RHSA, MSRC, CISCO, VMSA)0.6
Distribution IDs (DSA, USN, SUSE)0.4
Unknown scheme0.2

package_coverage (weight: 0.20)

Pairwise + IDF weighting replacing intersection-across-all.

  1. Extract package keys (PURL without version) from each observation
  2. For each package key, compute IDF weight: log(N / (1 + df)) where N = corpus size, df = observations containing package
  3. Score = weighted overlap ratio across pairs
ScenarioScore
All sources share same rare package~1.0
All sources share common package (lodash)~0.6
One “thin” source with no packagesOther sources still score > 0
No package overlap0.0

IDF fallback: When cache unavailable, uniform weights (1.0) are used.

version_compatibility (weight: 0.10)

Classifies version relationships per shared package:

RelationScoreConflict
Equivalent: ranges normalize identically1.0None
Overlapping: non-empty intersection0.6Soft (affected-range-divergence)
Disjoint: no intersection0.0Hard (disjoint-version-ranges)
Unknown: parse failure0.5None

Uses SemanticVersionRangeResolver for semver; delegates to ecosystem-specific comparers for rpm EVR, dpkg, apk.

cpe_match (weight: 0.10)

Unchanged from v1:

patch_lineage (weight: 0.10)

New signal: correlation via shared fix commits.

  1. Extract patch references from observation references (type: patch, fix, commit)
  2. Normalize to commit SHAs using PatchLineageNormalizer
  3. Any pairwise SHA match: 1.0; otherwise 0.0

Why this matters: “These advisories fix the same code” is high-confidence evidence most platforms lack.

reference_overlap (weight: 0.05)

Positive-only (no conflict on zero overlap):

  1. Normalize URLs (lowercase, strip tracking params, https://)
  2. Compute max pairwise overlap ratio
  3. Map to score: 0.5 + (overlap * 0.5)
ScenarioScore
100% URL overlap1.0
50% URL overlap0.75
Zero URL overlap0.5 (neutral)

No reference-clash emission for simple disjoint sets.

freshness_score (weight: 0.05)

Unchanged from v1:


Conflict Emission (Typed Severities)

Severity Levels

SeverityPenalty RangeMeaning
Hard0.30 - 0.40Significant disagreement; likely prevents high-confidence linking
Soft0.05 - 0.10Minor disagreement; link with reduced confidence
Info0.00Informational; no penalty

Conflict Types and Penalties

Conflict ReasonSeverityPenaltyTrigger Condition
distinct-cvesHard-0.40Two different CVE-* identifiers in cluster
disjoint-version-rangesHard-0.30Same package key, ranges have no intersection
alias-inconsistencySoft-0.10Disconnected alias graph (but no CVE conflict)
affected-range-divergenceSoft-0.05Ranges overlap but differ
severity-mismatchSoft-0.05CVSS base score delta > 1.0
reference-clashInfo0.00Reserved for true contradictions only
metadata-gapInfo0.00Required provenance missing

Penalty Calculation

typed_penalty = min(0.6, sum(penalty_per_conflict))

Saturates at 0.6 to prevent complete collapse; minimum confidence = 0.1 when any evidence exists.

Conflict Record Shape

{
  "field": "aliases",
  "reason": "distinct-cves",
  "severity": "Hard",
  "values": ["nvd:CVE-2025-1234", "ghsa:CVE-2025-5678"],
  "sourceIds": ["nvd", "ghsa"]
}

Linkset Output Shape

Additions from v1:

{
  "key": {
    "vulnerabilityId": "CVE-2025-1234",
    "productKey": "pkg:npm/lodash",
    "confidence": 0.85
  },
  "conflicts": [
    {
      "field": "affected.versions[pkg:npm/lodash]",
      "reason": "affected-range-divergence",
      "severity": "Soft",
      "values": ["nvd:>=4.0.0,<4.17.21", "ghsa:>=4.0.0,<4.18.0"],
      "sourceIds": ["nvd", "ghsa"]
    }
  ],
  "signalScores": {
    "aliasConnectivity": 1.0,
    "aliasAuthority": 1.0,
    "packageCoverage": 0.85,
    "versionCompatibility": 0.6,
    "cpeMatch": 0.5,
    "patchLineage": 1.0,
    "referenceOverlap": 0.75,
    "freshness": 1.0
  },
  "provenance": {
    "observationHashes": ["sha256:abc...", "sha256:def..."],
    "toolVersion": "concelier/2.0.0",
    "correlationVersion": "v2"
  }
}

Algorithm Pseudocode

function Compute(observations):
    if observations.empty:
        return (confidence=1.0, conflicts=[])

    conflicts = []

    # 1. Alias connectivity (graph-based)
    aliasGraph = buildBipartiteGraph(observations)
    aliasConnectivity = LCC(aliasGraph) / observations.count
    if hasDistinctCVEs(aliasGraph):
        conflicts.add(HardConflict("distinct-cves"))
    elif aliasConnectivity < 1.0:
        conflicts.add(SoftConflict("alias-inconsistency"))

    # 2. Alias authority
    aliasAuthority = maxAuthorityScore(observations)

    # 3. Package coverage (pairwise + IDF)
    packageCoverage = computeIDFWeightedCoverage(observations)

    # 4. Version compatibility
    for sharedPackage in findSharedPackages(observations):
        relation = classifyVersionRelation(observations, sharedPackage)
        if relation == Disjoint:
            conflicts.add(HardConflict("disjoint-version-ranges"))
        elif relation == Overlapping:
            conflicts.add(SoftConflict("affected-range-divergence"))
    versionScore = averageRelationScore(observations)

    # 5. CPE match
    cpeScore = computeCpeOverlap(observations)

    # 6. Patch lineage
    patchScore = 1.0 if anyPairSharesCommitSHA(observations) else 0.0

    # 7. Reference overlap (positive-only)
    referenceScore = 0.5 + (maxPairwiseURLOverlap(observations) * 0.5)

    # 8. Freshness
    freshnessScore = computeFreshness(observations)

    # Calculate weighted sum
    baseConfidence = (
        0.30 * aliasConnectivity +
        0.10 * aliasAuthority +
        0.20 * packageCoverage +
        0.10 * versionScore +
        0.10 * cpeScore +
        0.10 * patchScore +
        0.05 * referenceScore +
        0.05 * freshnessScore
    )

    # Apply typed penalties
    penalty = min(0.6, sum(conflict.penalty for conflict in conflicts))
    finalConfidence = max(0.1, baseConfidence - penalty)

    return (confidence=finalConfidence, conflicts=dedupe(conflicts))

Implementation

Code Locations

ComponentPath
V2 Algorithmsrc/Concelier/__Libraries/StellaOps.Concelier.Core/Linksets/LinksetCorrelationV2.cs
Conflict Modelsrc/Concelier/__Libraries/StellaOps.Concelier.Core/Linksets/AdvisoryLinkset.cs
Patch Normalizersrc/Concelier/__Libraries/StellaOps.Concelier.Merge/Identity/Normalizers/PatchLineageNormalizer.cs
Version Resolversrc/Concelier/__Libraries/StellaOps.Concelier.Merge/Comparers/SemanticVersionRangeResolver.cs

Configuration

concelier:
  correlation:
    version: v2  # v1 | v2
    weights:
      aliasConnectivity: 0.30
      aliasAuthority: 0.10
      packageCoverage: 0.20
      versionCompatibility: 0.10
      cpeMatch: 0.10
      patchLineage: 0.10
      referenceOverlap: 0.05
      freshness: 0.05
    idf:
      enabled: true
      cacheKey: "concelier:package:idf"
      refreshIntervalMinutes: 60
    textSimilarity:
      enabled: false  # Phase 3

Telemetry

InstrumentTypeTagsPurpose
concelier.linkset.confidenceHistogramversionConfidence score distribution
concelier.linkset.conflicts_totalCounterreason, severityConflict counts by type
concelier.linkset.signal_scoreHistogramsignalPer-signal score distribution
concelier.linkset.patch_lineage_hitsCounter-Patch SHA matches found
concelier.linkset.idf_cache_hitCounterhitIDF cache effectiveness

Migration

Recompute Job

stella db linksets recompute --correlation-version v2 --batch-size 1000

Recomputes confidence for existing linksets using v2 algorithm. Does not modify observation data.

Rollback

Set concelier:correlation:version: v1 to revert to intersection-based scoring.


Fixtures

All fixtures use ASCII ordering and ISO-8601 UTC timestamps.

Accuracy Gate

The real-world corpus is the release-facing accuracy gate for cross-source linkset identity. A candidate is accepted only when confidence is at or above the corpus threshold and no hard conflict is emitted. The gate fails on any accepted false positive, any missed true positive, any review case outside its expected confidence range, or any regression case whose score changes with input order.

This gate measures deterministic behavior against labeled offline cases. Every seed references a local fixture file and must point to a CVE present in that fixture. Product keys and alternate product keys are corpus metadata used to exercise linkset identity and conflict behavior when an upstream fixture lacks PURL detail. It does not convert linksets into source truth: raw observations, source keys, evidence references, aliases, and conflicts remain preserved for downstream policy and operator review.


Change Control