Give the spreadsheet a strict schema, clean it with a validation pass, and import it in a small batch first. Each row needs a component identity, advisory ID, decision, reason, owner and date. Free text alone cannot be matched to findings, so the work is mostly turning prose into fields.
Why spreadsheets are common
Small teams and open source scanner users often triage in a shared sheet because the scanner has no state. Someone runs a scan, pastes the output, and the team marks rows in a column. It works until the team grows or the scanner changes. Then the sheet becomes the only record of years of decisions, with inconsistent columns and abbreviations.
A target schema
| Column | Format | Notes |
|---|---|---|
| repo | text | Matches the new project name |
| ecosystem | text | npm, pypi, maven and so on, lower case |
| package | text | Exact name, normalized |
| version | text | As found in the lockfile |
| advisory_id | text | One ID per row |
| advisory_aliases | text | Other IDs, separated by a semicolon |
| decision | enum | open, dismissed, accepted, fixed |
| reason | enum | From your translation table |
| comment | text | Original reasoning |
| decided_by | text | Name or handle |
| decided_on | date | ISO format |
| review_by | date | Blank means needs a review date |
| ticket | text | URL or ID |
Using enumerations for decision and reason is the single most useful change. It makes bulk import possible and reporting honest.
Cleaning steps
- Freeze the sheet. Copy it and work on the copy. Nobody edits the original during the clean.
- Split multi advisory rows. A cell reading "CVE A, CVE B" becomes two rows.
- Normalize names. Trim whitespace, fix case, and align package names with the scanner's identifiers. Ecosystems treat case and punctuation differently.
- Normalize dates. Convert every date to one format and flag the ones you cannot parse.
- Fill enumerations. Map free text decisions to the allowed values, and mark ambiguous rows for review rather than guessing.
- De-duplicate. The same finding often appears once per scan. Keep the latest decision per repo, package, version and advisory.
Validate before you import
Write a few checks. No row without an advisory ID. No decision other than the allowed values. No date in the future. No "dismissed" row without a reason. Count the failures and fix them. Then sample ten rows and look at each one against the actual repository.
Import small, then large
Run the first import on one repository. Compare the number of matched findings with the number of rows. Rows that matched nothing follow the same sorting as any other orphan: removed component, changed ID, scanner disagreement or coverage gap.
Keep the sheet afterward
Do not delete the original. Store it read only with the migration record. The new tool holds the working state, but the sheet is the evidence of what was decided before the migration, in the team's own words.
Quick check
Do I need a script, or can I paste by hand?
Under a few dozen rows, by hand is fine. Above that, use a script so that every row goes through the same checks.
What if some rows have no version?
Mark them for re-triage. Without a version, a decision cannot be matched to a specific finding.
Last reviewed: 19 October 2026.