To prevent duplicate vulnerability tickets, keep each tool alert as an observation and create one owned work item for each distinct security problem and fix boundary. Match reports on the affected asset, vulnerable behavior or advisory, deployment scope, and required change—not just a shared severity, filename or CVE. Link every observation to the work item so independent evidence and disagreements survive. If two reports might describe the same issue but the relationship is unclear, mark the match provisional and investigate before either opening a second developer ticket or discarding evidence.
This separation matters because alert identity and work identity are different. A scanner can rerun the same rule at a new commit. Another tool can report the same weakness from a different entry point. One advisory can affect several services that require separate releases. Deduplication should reduce repeated assignments without pretending these cases are equivalent.
Correlate six observations without flattening their meaning
Constructed exercise: Cedar Notes receives six observations. The package names, advisory labels, releases and verdicts below are fictional. ADV-A and ADV-B stand for two authoritative records that explicitly identify each other as aliases; they are not real vulnerability IDs.
| Observation | Supplied facts | Initial correlation |
|---|---|---|
| A-41 | Customer API note read, release API7, missing account-bound lookup | Candidate issue NOTE-1 |
| B-19 | Same actor, route, read and API7; different cited line | Attach to NOTE-1, retaining both traces |
| C-33 | Staff export calls the same helper name; release and mandatory guard not yet supplied | Provisional link to NOTE-1; do not close or delete observation |
| D-08 | npm example-doc-parser4.2.0 in worker artifact W4; ADV-A |
Candidate component issue DEP-1 |
| E-11 | Same npm package/artifact; ADV-B explicitly aliases ADV-A | Attach advisory evidence to DEP-1; no second upgrade assignment |
| F-02 | PyPI example-doc-parser4.2.0 in a separate tool, citing ADV-A for the npm package |
Name collision; investigate mapping, never merge on display name |
A reviewer supports the API7 authorization claim from A-41/B-19 under the exercise's supplied evidence. Two observations now support one scoped finding. That does not make C-33 verified. D-08/E-11 establish one affected-component investigation; the application input path is still missing, so there is not yet an approved exploitable-parser finding. An advisory alias reduces duplicate records, not the amount of applicability evidence required. The worker owner can also plan a supported update now; establishing a locally exploitable parser path is not a prerequisite for routine maintenance of a deployed affected component.
Package coordinates make the name collision visible:
pkg:npm/example-doc-parser@4.2.0
pkg:pypi/example-doc-parser@4.2.0
The strings follow the package-URL shape for this fictional exercise. Their different ecosystems identify different packages. ADV-A's supplied affected-package entry names only npm; without another applicable record, reject F-02's specific mapping to ADV-A. That rejects the claimed advisory association, not every possible security concern about the PyPI tool.
Reverse a provisional merge when the control arrives
Documents supplies C-33's effective release, JOB3. The shared helper name concealed two facts: JOB3 ships independently of API7, and its staff-export wrapper performs the required per-record account authorization before calling the helper. The alleged missing check is defeated on this path. Updating NOTE-1 to “rejected” would incorrectly erase the supported customer API finding.
Split the provisional relationship instead:
NOTE-1: A-41, B-19 -> approved API7 finding -> API owner repairs -> API7 path reassessed
NOTE-2: C-33 -> split from provisional NOTE-1 link -> rejected for JOB3 with guard evidence
DEP-1: D-08, E-11 -> same affected-component investigation -> worker owner supplies input path
MAP-1: F-02 -> rejected advisory/package association -> original observation retained
The split event records the earlier provisional link, new release/guard evidence and reviewer decision. It does not alter A-41's original text, B-19's run or C-33's source ID. If a later JOB4 removes the wrapper, the old rejection becomes useful counterevidence to revisit; it is not a permanent suppression of all staff-export claims.
Cedar now has six observations, four review units, one approved repair and one open evidence task. Two units are rejected for stated reasons. These are different denominators. Scanner-quality measurement might classify each independent claim or each raw tool observation under a declared method; developer workload counts the repair and evidence actions. Neither number should be advertised as “six vulnerabilities reduced to one” without those definitions.
Use a two-layer record
Observation layer: preserve tool name, rule or advisory ID, run ID, revision, path, affected package/version where relevant, line or trace, detection time and original state. Do not rewrite the alert to make it look as though all tools agreed. A rerun can be a new observation of an old issue.
Issue layer: state the harmful action and asset, verified path or affected component, root cause, deployment scope, fix owner, status, priority and acceptance evidence. Add links back to every observation. The issue may change as reviewers learn more; its history should show why an alert was merged, split, rejected or left unresolved.
For dependency alerts, normalize the package ecosystem, package name, resolved version and advisory identifiers. The Package URL specification provides a structured package identity across ecosystems; an unqualified display name can refer to different packages. The OpenSSF OSV schema distinguishes aliases for the same vulnerability from related or upstream records, and records affected package and version ranges. Treat a CVE/GHSA alias pair as a useful clue, then check package and scope. Do not merge every record that merely mentions the same upstream bug.
For custom code findings, build a comparison tuple: repository and service; actor and entry point; asset and harmful effect; failed control; relevant revision; proposed fix boundary. Two alerts need not have identical line numbers to describe one issue. Conversely, two routes that call the same helper may expose different assets or need different release owners. The tuple supports review; it should not be used as an automatic hash that erases judgment.
Run the correlation workflow
- Collect, then freeze the originals. Ingest alerts into a restricted intake record with stable source IDs and timestamps. Note each tool's coverage. GitHub's Dependabot alert documentation describes what its alerts include, such as affected files, severity and a fixed version when available; those are inputs to review, not a complete application-risk decision.
- Normalize identifiers without changing claims. Map advisory aliases and package coordinates. For source findings, map renamed files and shifted lines back to a pinned revision. Keep tool-specific severity separate. Never convert “same title” into “same vulnerability.”
- Compare the actual fix boundary. Ask whether one code or package change, owned and released by one team, would address both observations in the same deployed path. If not, create linked work items under a common parent or campaign. If yes, keep one issue with both evidence sets.
- Resolve disagreement. If Tool A says the authorization check is absent and Tool B shows middleware that may stop the path, give an independent reviewer the counterevidence. Do not choose the more severe alert or a majority vote. Until the check is resolved, keep the evidence status unresolved. Any precautionary repair should state its own rationale rather than presenting tool agreement as confirmation.
- Assign one accountable owner per action. A parent incident or advisory record may coordinate multiple service fixes, but each service change needs its own owner and verification. Link, do not duplicate, the underlying advisory and observations.
- Reconcile after changes. At a new pinned revision, verify the original unsafe behavior is prevented or the affected package is no longer in the vulnerable range in the relevant build. Keep a new alert as new evidence if the problem remains. A tool going quiet is not positive fix proof.
The output is a compact ledger: issue ID → observation IDs → current verdict → owner → fix boundary → closure evidence. NIST's Secure Software Development Framework recommends tracking and responding to vulnerabilities as part of development; this ledger is one implementation pattern, not a required standard format.
Know when to split instead of merge
Split when reports involve different attacker actions, controls, deployments or teams that can independently fix and ship. A shared vulnerable library in five services can justify one advisory record and five service tasks if each service has a different release path. A SQL injection and missing tenant check on the same endpoint are two issues even if one patch eventually touches the same handler. Likewise, two alerts with the same CVE may concern different package ecosystems or distinct bundled versions.
Keep a provisional link when evidence is incomplete. In Cedar Notes, if the parser scanner reported only a manifest range and the worker's resolved artifact is unknown, do not declare the worker vulnerable or merge it with a previously closed parser ticket. Record the missing build artifact and who will provide it. If a scanner cannot inspect a generated dependency file, its silence on the worker does not establish that the dependency is absent.
Be careful with automatic closure. A merged PR, dismissed alert or replaced ticket may change workflow state without proving a fix in the deployed service. Preserve the original observation and check the actual affected revision and release. If a previously merged observation later points to a distinct route, split it with an audit trail rather than silently rewriting the old issue.
Keep merges and splits reversible in the issue record
Grouping is a working conclusion that can change as the evidence improves. Preserve the observation identifiers when two records are combined, and record why the common issue has one repair boundary. If a later review shows different affected consumers or controls, split the work without losing the original observations or their previous disposition.
In the Cedar exercise, API7 can retain both A41 and B19 because they describe the same reviewed account-binding failure. JOB3 needs a separate conclusion because its trusted staff-job context changes the path. If a later job revision begins accepting a user-selected account, attach that new observation to the changed revision and reassess it. Do not rewrite the older scoped rejection as though it had been mistaken about the old job.
The dependency records need similar care. D08 and E11 can refer to the same package instance through different identifiers, but the merge note should preserve enough identity evidence to explain the match. F02's rejected name collision should retain its original ecosystem and package evidence. A future real issue involving the similarly named package must not inherit a blanket dismissal created for the wrong mapping.
When a record is split, distribute the next actions explicitly. One branch may be ready for a code change while another still needs deployment evidence. Copying the parent's “fixed” state into both children would manufacture closure. When records are merged, reconcile conflicting states before presenting a single result: an open affected consumer cannot disappear because another record was already resolved.
Reconcile the worklist after changing a grouping
Check the developer-facing consequences of the edit. Remove redundant implementation tasks only after the owner confirms they were duplicates of the same required change. Keep separate verification or rollout actions where they still apply. Preserve links from old ticket references so a developer following an earlier comment reaches the current explanation.
The summary should state what changed in the count and why. “Two observations now link to one API repair” explains a reduction in issue count without implying that a vulnerability was fixed. “One issue split into two consumer-specific work items” explains an increase without implying new discovery. Count changes become useful management information only when they describe the underlying work rather than making the dashboard look better.
For the next intake batch, sample ten recent alerts and fill the correlation ledger before creating tickets. Count how many distinct actions remain, then have a reviewer inspect each merge and split. If the harder question is whether an alert merits any developer work, use how to triage a finding before opening a ticket. For a small, owned delivery set, continue with how to give developers a short security worklist.