Documentation
¶
Overview ¶
Package identity holds Plumber's finding-identity recipe: the one selection of fields that says which findings are the same finding instance across runs.
Two consumers need that answer and must never answer it differently. The CLI hashes the selection into the short `fingerprint` every export format carries (JSON, CSV, SARIF, GitLab SAST, OCSF). A platform grouping findings into long-lived issues needs the same selection, but as data it can store and query rather than an opaque hash. So the selection lives here, once, and both read it: Of returns the identity field set, Fingerprint hashes exactly what Of returned. They cannot drift.
What the recipe selects, and why:
- code, file, job: the canonical coordinates of a finding.
- one subject key: what the rule actually flagged (an action ref, a component path, a variable). Taken from the rule's structured payload in the priority order of SubjectKeys, first match only. Preferring this over the prose message is what makes identity survive a message rewording.
- step, when the workflow resolved one: the last discriminator between two steps of one job that reference the same action.
What it deliberately leaves out: line and url (they move whenever unrelated code above the finding is edited), advisories (grows as CVEs are published), latestVersion (moves on any upstream release), metadata (refetched every run), and reasons/status (they track current settings, not identity). Any of those in the identity would make an unchanged finding look new.
See docs/FINGERPRINT.md for the full contract, and RecipeVersion for what a change to the selection costs.
Index ¶
Constants ¶
const RecipeVersion = 2
RecipeVersion is the version of the identity recipe. It tracks identity OUTCOMES, not just the code in this package, so bump it whenever findings come out keyed differently than they did before:
- the algorithm here changes: a new subject key, a reordering of SubjectKeys, a field entering or leaving the identity; or
- a control starts or stops emitting a subject key, which moves its findings between prose identity and structured identity just as much.
The second case is the easy one to miss: nothing in this package changes, so its tests stay green. The per-control pins in policies/rules_test.go are what fire there.
Treat a bump as a breaking change, made deliberately. A re-keyed finding is read downstream as the old issue disappearing and a new one appearing in its place, and a consumer holding only the hash (SARIF, OCSF, CSV) has no other signal that it happened. Stored next to a grouped finding, this constant is that signal.
History:
1 The recipe as first shipped: canonical coordinates, the SubjectKeys
priority list, the resolved step.
2 Eleven finding blocks changed what they identify on:
- ISSUE-401 gained hardcodedJob. It kept job too: that field holds
a real job name here, so it was correctly left in the identity.
- ISSUE-402 GitLab / ISSUE-403 / ISSUE-404 gained includePath,
ISSUE-405 / ISSUE-406 gained templatePath, ISSUE-408 / ISSUE-409
gained componentPath, and ISSUE-417 gained requiredAction. Each
of these also lost job: the field used to smuggle that same
subject through a mislabelled job value, which the new key
replaces.
- ISSUE-501 / ISSUE-505 kept their existing branchName and lost
job, for the same reason: job held the branch name, not a job.
Ten of the eleven blocks lost job; only ISSUE-401 kept it. The
algorithm is unchanged; their fingerprints are not.
Variables ¶
This section is empty.
Functions ¶
func Fingerprint ¶
Fingerprint returns the short, line-independent identifier of a finding: the hash of the very field set Of selects. Empty for a codeless finding.
func SubjectKeys ¶
func SubjectKeys() []string
SubjectKeys returns the subject-key priority list. The caller gets a copy, so sorting or trimming the result cannot re-key the findings this process computes afterwards.
Types ¶
type Fields ¶
type Fields struct {
Code string `json:"code"`
File string `json:"file"`
Job string `json:"job"`
// Subject is the one key/value pair that says what the finding is about.
// Its Key is the winning member of SubjectKeys, or "message" when the rule
// emitted none of them.
Subject Field `json:"subject"`
// SubjectFromMessage reports that Subject holds the prose fallback rather
// than a structured key. Such a finding's identity is tied to the wording of
// its rule, so rewording that rule re-keys it.
SubjectFromMessage bool `json:"subjectFromMessage"`
// Step is the resolved step name, empty when the finding has none.
Step string `json:"step,omitempty"`
// Version is the RecipeVersion that produced this field set, so a consumer
// storing it can tell later which selection it was built from.
Version int `json:"version"`
}
Fields is the identity field set of one finding: everything the recipe selected, as data. Pairs renders it in the order it contributes to identity.
func Of ¶
Of returns the identity field set of a finding. ok is false for a codeless finding: there is nothing stable to report it against, so it has no identity and gets no fingerprint.
A coded finding always has an identity, even a narrow one. With no subject key and no message the subject is an empty "message" pair and the finding is identified by code, file and job alone, so every such finding of that control in one job shares a fingerprint. That is deterministic, not an error.
type Finding ¶
type Finding struct {
Code string
File string
Job string
Message string
// Data is the rule's structured payload: the subject keys above, the
// resolved `step`, and anything else the rule emitted.
Data map[string]any
}
Finding is the view of a finding the recipe reads. The CLI fills it from its own finding type; anything reading Plumber's serialized output builds it with FromMap. There is no Line or URL field on purpose: they are not inputs, and a type that carried them would suggest otherwise.
func FromMap ¶
FromMap builds a Finding from a serialized finding: the flat JSON object Plumber writes, where the canonical fields and the structured payload sit side by side at the top level. Everything that is not a canonical field stays in Data, so a subject key added by a later rule is readable without a change here, and so is the volatile payload, which the recipe simply never selects.
FromMap expects a whole serialized finding, not an exported issue entry from a *Result block in Plumber's JSON report: an issue entry has no file and no message, so running FromMap over one silently produces a different identity for any finding that has a file or that falls back to the prose subject.