Skip to content

Audit foundation: finding identity v2 (pattern, occurrence, lineage) with provenance #266

Description

@PhysShell

Classification

  • Change type: enhancement / foundational contract
  • Priority: P0
  • Expected value: very high — prerequisite for lifecycle tracking, evidence aggregation and honest calibration
  • Implementation risk: medium — schema compatibility and identity drift

Canonical ownership

Canonical implementation owner: PhysShell/OwnAudit.

Own.NET/audit/aggregate/normalize.py is the deprecated reference implementation, used only as the parity source for the normalization-port slice. No new identity, schema, aggregation, lineage or provenance implementation lands there.

This supersedes the original "Canonical owner: Own.NET/audit/" line, which predates the 2026-07-18 canonicality reversal recorded in OwnAudit/AGENTS.md and docs/collector-plan.md: Own.NET stays a pure SAST engine that emits findings; aggregation, reporting, runtime and fix work live in OwnAudit.

Downstream consumers must consume exported fields or the versioned contract; they must not reverse-engineer either repository's private implementation.

Execution plan

Slice Where What
0 OwnAudit Freeze finding-pattern/v1 — one implementation, canonical vectors, legacy alias. ✅ OwnAudit#55
1A OwnAudit Port the normalized-finding pipeline with a parity fixture. No identity/provenance fields, no taxonomy/suppression/verdict changes.
1B OwnAudit schema_version, pattern_id, occurrence_id (only when provable), provenance with explicit null/unknown, identity limitations.
2 OwnAudit Lineage mapping model — evidence, confidence classes, rename/diff support, unresolved and one-to-many.
3 007 / OwnAudit Downstream adoption: action evidence, fix-proof.json.

Slice 1B uses the local identity/pattern.py directly — schema and domain identity now live in one canonical repository, so there is no vendored contract copy and no cross-repo equality test to keep honest.

Problem

The current FP-judge finding_id is intentionally line-independent and pattern-level:

sha1(path + rule + message)[:16]

That is correct for deduplicating repeated instances before an expensive judge call, but it is not enough for lifecycle tracking. One pattern ID can represent several physical findings in the same file. If one occurrence is fixed and another remains, a single identity cannot represent both outcomes honestly.

The audit pipeline needs three distinct identities:

  1. pattern_id — the existing line-independent logical pattern; backward-compatible with today's finding_id.
  2. occurrence_id — one concrete physical finding emitted by one producer run.
  3. lineage_id — the best-supported cross-commit identity for following an occurrence through line drift, rename or movement.

Required contract

pattern_id

  • Preserve the current FP-judge recipe and semantics.
  • Keep accepting legacy overlays keyed by finding_id during a documented compatibility window.
  • Make the intentional many-occurrences-to-one-pattern relationship explicit.

Frozen in slice 0 as finding-pattern/v1 (identity/pattern.py, contracts/finding-pattern-v1.json).

occurrence_id

Derive from stable run-local inputs, including at minimum:

producer_run_id
producer/tool identity
rule
path
physical anchor

Requirements:

  • two identical findings at different physical sites receive distinct IDs;
  • rerendering the same normalized run is deterministic;
  • presentation-only ordering does not change the ID;
  • occurrence identity never pretends to survive an unrelated producer run;
  • never synthesized from unknowns. If producer_run_id is unavailable, emit "occurrence_id": null plus an explicit identity_limitations entry (e.g. occurrence-id-unavailable:producer-run-id). Hashing the literal string "unknown" would give two unrelated sessions one "stable" id — a tidily formatted lie, and those are the most durable kind.
  • physical anchor for the first slice is path + startLine + startColumn (column when available). Never the result's ordinal position in the array.

lineage_id

  • Link occurrences across commits only when supported by explicit mapping evidence.
  • Prefer symbol/context/AST fingerprints plus Git diff/rename mapping over raw line numbers.
  • Represent ambiguity as unresolved or one-to-many evidence; never guess a single lineage merely to keep dashboards tidy.
  • Store the mapping evidence and confidence class separately from the ID.

Provenance fields

Each occurrence must carry enough provenance to reproduce or reject it:

producer_run_id
producer_name
producer_version
input_digest
config_digest
source_commit
schema_version

Fields may be nullable, but must be present: null means unknown; a missing field is a schema violation. Unknown/missing provenance stays visible as a coverage limitation, never silently synthesized.

Compatibility and migration

  • Version the normalized finding schema.
  • Existing findings.json and fp-verdicts.json fixtures remain readable.
  • Existing finding_id consumers either map it to pattern_id or fail with an actionable version error.
  • No existing diagnostic code, severity, message or analyzer verdict changes in this issue.

SARIF export

New fingerprints are added alongside the existing key, never in place of it:

"partialFingerprints": {
  "ownAudit/v1": "...legacy...",
  "own/pattern-v1": "...",
  "own/occurrence-v1": "..."
}

ownAudit/v1 is a legacy GitHub-correlation key, not finding-pattern/v1: it normalizes the message, keeps the full digest and appends a /ordinal suffix for repeats. The ordinal alone disqualifies it as identity — presentation order must never change an id. Replacing it would reset every code-scanning alert's history while claiming to improve correlation.

Required controls

  1. Two identical messages/rules in one file on different lines:
    • same pattern_id;
    • different occurrence_id.
  2. Pure line drift between commits:
    • new occurrence;
    • same supported lineage.
  3. One repeated occurrence fixed, another retained:
    • lineage outcomes remain independent.
  4. File rename with unchanged symbol/context:
    • lineage may continue with rename evidence.
  5. Ambiguous copy/move:
    • lineage remains unresolved or explicitly branched, never guessed.
  6. Producer/config/input digest changes:
    • provenance changes are observable.
  7. Legacy FP overlay:
    • still expands one pattern verdict to all matching current occurrences only when the input digest contract is valid.

Acceptance

  • Versioned identity contract is documented and implemented — slice 0, finding-pattern/v1
  • pattern_id, occurrence_id and lineage_id have distinct tested semantics.
  • Repeated physical findings no longer share one lifecycle state.
  • Legacy finding_id compatibility is explicit and tested — slice 0, byte-identical alias pinned per vector
  • Every emitted occurrence carries producer/input/config provenance or an explicit unknown marker.
  • Ambiguous lineage fails honestly rather than producing a convenient false match.
  • Existing analyzer outputs remain semantically unchanged.

Non-goals

  • No Evidence Ledger implementation beyond the identity/provenance seam.
  • No ranking, probability or calibration model.
  • No auto-suppression or merge blocking.
  • No analyzer precision/soundness changes.
  • No forced lift-out from Own.NET/audit/ to OwnAudit. Cancelled by the 2026-07-18 canonicality reversal: the lift-out already happened and OwnAudit is the canonical audit repository. Retained here struck through rather than deleted, so the change of direction stays visible to anyone reading the issue's history.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions