The hard part of changing security scanners is not the scanner. It is the three years of human decisions attached to the old one.
Every "this is a false positive", every "accepted, compensating control in place", every "not applicable, that code path is disabled", every linked ticket and approval. That is the accumulated judgement of your team, it is the reason your finding count is manageable, and it is usually the thing nobody costs into the migration.
This post is how to move it, what cannot be moved, and how to avoid the failure where a platform migration hands every engineer a fresh backlog of things they already dismissed. For whoever is running the switch.
What actually has to move
Not findings. Findings regenerate on the first scan, and the new tool's list will differ anyway.
What has to move is the decisions:
- Suppressions and dismissals, with the reason and the person.
- Risk acceptances, with their expiry and approver.
- Severity overrides, where your team decided the vendor's rating was wrong for your context.
- Links to tickets, pull requests, and incident records.
- Exclusion scope: the paths, repositories, and packages deliberately not scanned, and why.
The last one is routinely forgotten and it produces the loudest failure: a new tool scanning the vendored third-party directory, the test fixtures, and the archived repositories that the old tool was configured to skip. The finding count triples on day one and everyone concludes the new tool is noisy.
The matching problem
You cannot copy a dismissal across, because a dismissal points at a finding, and findings are identified differently by every tool.
There is no universal identifier. So you have to match on what the two tools agree about, and the honest answer is a composite key that works for most findings and fails for some:
(repository, file path, vulnerability id, package name, package version)
Notes on each part. The vulnerability identifier is your most portable field, which is an argument for preferring CVE and GHSA identifiers over vendor-specific rule names. File paths break whenever the repository was restructured between the dismissal and the migration. Package version is the field that decides whether a dismissal should even carry over, which is the next section.
Expect roughly 60 to 80 percent to match automatically on a codebase that has not been heavily restructured. The remainder needs a human, and knowing that number before you start is what lets you schedule the work rather than discover it.
Not everything should carry over
This is the part worth slowing down for, because a migration is a rare opportunity and most teams waste it.
A dismissal was made against a version. "Not exploitable in our configuration" was true for version 2.1. You are now on 2.6 and the code path changed. Carrying the dismissal forward silently reasserts a claim nobody has checked in three years.
Risk acceptances expire. If yours have expiry dates, honour them: anything past its date becomes an open finding for review rather than a migrated suppression. If they do not have expiry dates, that is the finding, and now is the moment to add them.
"False positive" often meant "the old tool was wrong". A different engine may be right about the same code. Migrating that dismissal imports the old tool's blind spot into the new one.
The defensible policy is a middle position: migrate suppressions where the package version is unchanged and the dismissal is under about twelve months old. Everything else moves to a review queue, sorted by severity. It will be a substantial queue and it is real work you were already carrying without seeing it.
The sequence that works
Export before you sign anything. Confirm you can get suppressions, reasons, authors, and timestamps out of the incumbent, in a format you can parse, before the commercial conversation concludes. This is the moment of maximum leverage and the only one where a vendor has a reason to help you leave. If the answer is that export is not supported, that is a finding about the vendor, and it belongs in the evaluation.
Run both tools in parallel for a full cycle. A month, or whatever your release cadence is. You need the overlap to build the matching table and to see where the tools genuinely disagree.
Match and classify into three buckets. Auto-migrated, needs review, and cannot match. Report the size of each before you cut over, because those numbers are the project plan.
Work the review queue before cutover, not after. After cutover it competes with feature work and loses. Before cutover it is part of the migration nobody argues with.
Keep the old data. Export the incumbent's full state to storage you control and keep it beyond the contract end. When someone asks in a year why a finding was dismissed, the reason is in there, and the account it lived in is gone.
Preventing the next one
The reason this is painful is that triage decisions lived in a vendor's database. The fix is to stop doing that.
Suppressions as code, in the repository, next to what they suppress:
# .security/suppressions.yml
- id: CVE-2023-12345
package: example-lib
version: "2.1.4"
reason: "Vulnerable function is in the admin CLI, not built into the service image."
approved_by: platform-team
expires: 2027-03-01
This is reviewable, greppable, versioned, portable between tools, and it expires. It also puts the decision next to the code it is about, so the person changing that code sees it. The cost is that your tool has to read it, which most now do.
The concession
Suppressions as code has a real failure mode: files that grow to thousands of entries nobody reads, where a pull request adding twenty suppressions gets approved because reviewing them properly is nobody's job. A vendor database at least has a UI, filters, and reporting.
So the format is not the whole answer. The expiry field is doing most of the work, because it forces a decision to be re-made rather than inherited forever. If you take one thing from this, take that: whatever holds your suppressions, make them expire.
The implication
Ask of your current setup: if this contract ended next month, what would you lose. If the answer includes three years of triage judgement, that is not a migration risk, it is a dependency you already have.
Exporting it once, into a format you own, is a day of work and it is worth doing whether or not you ever change tools.