fixdossier / Docs / Advisory → release mapping

How fixdossier maps a GitHub security advisory to a release

A security advisory tells users which versions are fixed. A release tells them what they can install. fixdossier links the two for every advisory, commit and bot PR it finds, and writes the method it used into the “How mapped” column. This page describes the three methods in fixdossier 0.1.1, the matched_releases field, and the cases that still need a person to check.

Quick start

yaml
permissions:
  contents: read
  pull-requests: read
jobs:
  record:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: itulo/fixdossier@v0
        with:
          max_pages: 20   # more releases fetched = more patched_versions matches

The labels you will see

These are the exact strings written to SECURITY-RECORD.md and to the mapping field in security-record.json:

LabelRow typeMeaning
advisory patched_versionsAdvisoryAt least one patched range in the advisory was satisfied by a fetched release tag. Method 1.
tag commit matchCommit, bot PRThe commit SHA (for PRs, the merge commit SHA) is the commit a release tag points at. Method 2.
date heuristic (first release on/after publish)AdvisoryNo patched range matched; first release published on or after the advisory's publish date. Method 3.
date heuristic (first release on/after commit)CommitNo tag points at the commit; first release on or after the commit date. Method 3.
date heuristic (first release on/after merge)Bot PRNo tag points at the merge commit; first release on or after the merge date. Method 3.
unreleasedCommit, bot PRNo tag match and no release on or after the date. Shown as _unreleased_.
noneAdvisoryNo patched range matched and no release on or after the publish date. “Shipped in” shows _unreleased_.

1. Advisory patched_versions

Used for advisories. fixdossier reads patched_versions from every package listed in the advisory and parses each string into one or more version ranges:

  • Comparators >=, >, <=, <, = (also ==), separated by commas or spaces, are combined into one range: >=5.4.12, <6.0.0 is one range.
  • A new lower bound starts a new range, so >=4.5.2 <5.0.0 >=5.0.12 is two ranges. || also separates ranges.
  • ^1.2.3 becomes >=1.2.3 <2.0.0 and ~1.2.3 becomes >=1.2.3 <1.3.0.
  • A bare version such as 1.2.3 is an exact match.
  • Several bare versions in one value are parsed separately. Whitespace, commas, and || are separators, so 11.5.15 12.3.3 is 11.5.15 and 12.3.3, not the single version 11.5.1512.3.3. A comparator such as >= 1.2.3 is still one range.

For each range, the release chosen is the fetched GitHub Release with the lowest version that satisfies it. Tag versions are read with or without a leading v, and from name@version tags such as create-vite@9.3.3. When both kinds satisfy a range, plain tags (v1.2.3) win over name@version tags; the package name in a scoped tag is not compared with the advisory's package. Stable versions win over pre-releases, and a pre-release only satisfies a range whose own bound is a pre-release of the same version.

Each range adds at most one release, and duplicates are dropped. All matched releases are listed, in the order of the ranges. The JSON patched_versions field holds the parsed ranges in a normalised form, for example >=2.9.17 <3.0.0.

In the vite example, GHSA-859w-5945-r5v3 has five patched ranges and all five matched:

markdown
| 2025-04-30 | advisory (medium) | GHSA-859w-5945-r5v3, CVE-2025-46565 | [`server.fs.deny` bypassed with `/.` for files under project `root`](https://github.com/vitejs/vite/security/advisories/GHSA-859w-5945-r5v3) | v6.3.4, v6.2.7, v6.1.6, v5.4.19, v4.5.14 | 2025-04-30, 2025-04-30, 2025-04-30, 2025-04-30, 2025-04-30 | advisory patched_versions |

22 of the 23 vite advisories and all advisories in the axios and fastify examples were mapped this way.

matched_releases in the JSON

axios advisory GHSA-9fr6-4gfg-395g was fixed in both the 0.x and 1.x lines:

json
{
  "date": "2026-09-16T09:00:51Z",
  "type": "advisory",
  "ids": ["GHSA-9fr6-4gfg-395g", "CVE-2026-101902"],
  "title": "Prototype-Pollution Gadget in Axios Default Instance Allows Inherited Object.prototype.method to Override HTTP Method",
  "severity": "medium",
  "url": "https://github.com/axios/axios/security/advisories/GHSA-9fr6-4gfg-395g",
  "patched_versions": [">=0.34.0", ">=1.20.0"],
  "release": "v0.34.0",
  "release_date": "2026-09-13T16:40:14Z",
  "matched_releases": [
    { "tag": "v0.34.0", "date": "2026-09-13T16:40:14Z" },
    { "tag": "v1.20.0", "date": "2026-08-24T13:40:40Z" }
  ],
  "mapping": "advisory patched_versions"
}

matched_releases is the full list. release and release_date are the first entry, kept for tools that read one value. The first entry follows the order of the advisory's ranges, not the release dates: here v0.34.0 (2026-09-13) comes first although v1.20.0 (2026-08-24) is older. To answer “when was this fixed in line X?”, read matched_releases.

2. Tag commit match

Used for commits and bot PRs, before the date heuristic. fixdossier records the commit each release tag points at and compares it with the commit SHA (for bot PRs, the PR's merge commit SHA). The row is labelled tag commit match when:

  • the SHAs are equal (case and surrounding spaces ignored). If several release tags point at the same commit, all of them are listed, oldest release first; or
  • one SHA is an abbreviation of the other, at least 7 hex characters long, and it matches exactly one release. Ambiguous or shorter prefixes are not used.

Annotated tags are peeled to the commit they point at; the SHA of the tag object itself is never compared. The tags list endpoint normally returns the commit already. If it returns a tag object instead, fixdossier makes one extra API request per such tag to read its commit. If that request fails, the tag is not used for SHA matching and the record gets a gap line: tags: could not peel annotated tag <name> (HTTP <status>).

A tag commit match happens only when a release was tagged on the fix commit or merge commit itself. Fix commits are rarely tagged directly, so none of the three examples contains one.

3. Date heuristic

Used when methods 1 and 2 find nothing. fixdossier takes the earliest stable release (not marked pre-release) with a publish date on or after the reference date. If there is none, it takes the earliest release of any kind. The reference date is:

  • the commit date, for commits;
  • the merge date, for bot PRs;
  • the advisory publish date, for advisories. Advisories are often published after the fixed release, so this can point at a later release than the one that contained the fix.

One vite advisory fell back this way. Its patched versions are exact versions (2.9.18, 3.2.10, 4.5.3, …) and none of them was among the fetched releases:

markdown
| 2024-04-03 | advisory (medium) | GHSA-8jhw-289h-jh2g, CVE-2024-31207 | [`server.fs.deny` did not deny requests for patterns with directories.](https://github.com/vitejs/vite/security/advisories/GHSA-8jhw-289h-jh2g) | v6.2.3 | 2025-03-24 | date heuristic (first release on/after publish) |

A 2024 advisory shown as shipped in a 2025 release is a sign that history was cut short. With more releases fetched, method 1 would have found the exact versions.

What still needs a manual check

Releases outside the fetched history

Method 1 can only choose from releases it fetched. If the real patched release is older than the oldest fetched one, the lowest fetched version that satisfies the range is used instead: that release contains the fix but did not introduce it. The vite example was generated with max_pages=2, and its oldest fetched release is from 2025-03-24. A January 2024 advisory therefore maps to 2025 releases:

markdown
| 2024-01-19 | advisory (medium) | GHSA-c24v-8rfc-w8vw, CVE-2024-23331 | [Vite dev server option `server.fs.deny` can be bypassed when hosted on case-insensitive filesystem](https://github.com/vitejs/vite/security/advisories/GHSA-c24v-8rfc-w8vw) | v4.5.11, v5.4.16 | 2025-03-31, 2025-03-31 | advisory patched_versions |
json
"patched_versions": [">=2.9.17 <3.0.0", ">=3.2.8 <4.0.0", ">=4.5.2 <5.0.0", ">=5.0.12"]

The ranges for 2.x and 3.x had no fetched release, so they are not listed, and the 4.x and 5.x ranges matched v4.5.11 and v5.4.16 rather than v4.5.2 and v5.0.12. Compare patched_versions with matched_releases: a range with no matching entry, or a release date long after the advisory, means history is missing. Raise max_pages until the “stopped after N pages” gap for releases disappears.

Fixes on maintenance branches

The commits endpoint returns the default branch only. A fix committed only to a v0.x or 4.x branch does not appear as a commit row. Publish an advisory for it, or reference the CVE/GHSA ID in a commit that lands on the default branch.

Date heuristic across release lines

Stable releases from all branches are sorted by date together. A fix committed to main on Monday maps to whatever stable release came next, which may be a backport release of an older line that does not contain the fix.

Tags without GitHub Releases

Only GitHub Releases count as releases. Tags with no Release object are listed in tags_without_release (62 in the vite example, 22 in axios) and are never used for mapping. If your release process pushes tags only, create Releases for them.

Making the mapping more accurate

  • In advisories, give each affected release line its own patched range or version, using versions you actually tag.
  • Create a GitHub Release for every tag you ship, including backport releases.
  • Put the CVE or GHSA ID in the commit message of the fix.
  • Raise max_pages until the “stopped after N pages” gaps disappear.
  • Review every row whose label starts with date heuristic and record your conclusion next to the file, for example in the PR that updates it.

More on what is not covered at all: What the record covers.

Docs pages

Not legal advice. fixdossier is an engineering tool for evidence collection and review support. It is not legal advice and not a compliance certificate. It does not make a product compliant with the PLD, the CRA or anything else, and its output can be incomplete or wrong. Review it, and ask a lawyer about your situation.