fixdossier / Docs / Build the record
Build a dated record of security fixes from GitHub history
If your project already uses GitHub releases, security advisories and Dependabot or Renovate, most of the facts for a security-fix history are already in the API: what was fixed, on which date, and which release contained it. fixdossier collects those facts into one dated file per run, and marks each row with how it was linked to a release.
Quick start
This commits the files to the default branch on every release and once a week.
name: security-record
on:
release:
types: [published]
schedule:
- cron: '0 6 * * 1'
workflow_dispatch:
permissions:
contents: write # needed to commit the files
pull-requests: read
jobs:
record:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: itulo/fixdossier@v0
with:
support_period_months: 24
eol_date: '2030-12-31'
contact: security@example.com
mode: commitFor artifact mode (default), drop mode and use contents: read. The artifact is named security-record. Artifacts expire under the repository's retention setting (GitHub docs); commit mode keeps every version in git history.
What it reads
Five list endpoints of the GitHub REST API, 100 items per page, up to max_pages pages each (default 10):
| Source | Endpoint | Used for |
|---|---|---|
| Releases | GET /repos/{owner}/{repo}/releases | Release tag, date, pre-release flag. Drafts and unpublished releases are dropped. |
| Tags | GET /repos/{owner}/{repo}/tags | Commit SHA behind each tag, used for exact SHA matches; tags with no GitHub Release are listed in tags_without_release. |
| Pull requests | GET /repos/{owner}/{repo}/pulls?state=closed&sort=updated&direction=desc | Merged PRs opened by dependabot[bot], renovate[bot], dependabot-preview[bot] or renovate-bot. |
| Security advisories | GET /repos/{owner}/{repo}/security-advisories?state=published | Published repository advisories (GHSA ID, CVE ID, severity, patched_versions). |
| Commits | GET /repos/{owner}/{repo}/commits | Default branch history. Messages that look like security fixes become rows. |
Which commits count as security fixes
- Any commit whose message contains a
CVE-YYYY-NNNNorGHSA-xxxx-xxxx-xxxxID. - Otherwise, a commit whose first line contains one of: security, vulnerab…, vuln, exploit, xss, csrf, ssrf, rce, injection, prototype pollution, denial of service, redos.
- A first line that is a
docsconventional commit is skipped unless the message contains an ID. The scope may be anything, and the colon is optional:docs(cra): ...,docs(security): ...,docs: update SECURITY.md, anddocs(cra) #1474. First lines starting withtest:,ci:,build:, orchore(docs):are skipped the same way.fix(security): ...is still a fix. - Commits authored by the bots above are skipped here; they appear as dependency updates instead.
This is keyword matching. A fix committed as “handle edge case in parser” is not found, and some non-fixes are. The record says so in its footer.
Which bot PRs are flagged as security
Every merged bot PR is listed under “Dependency updates by bots”. The “Security?” column is yes only if the title or body contains a CVE/GHSA ID, the title contains one of the words above, or a label matches secur or vuln.
What it writes
SECURITY-RECORD.md
Summary from the axios/axios example (generated 29 Oct 2026, max_pages=2):
## Summary | Item | Count | |---|---| | Published GitHub security advisories | 51 | | Commits that look like security fixes | 1 | | Merged bot dependency updates (security-flagged) | 47 (13) | | Releases | 126 | | Fixes not yet in a release | 0 |
Three rows from the same file:
| Date | Type | IDs | Fix | Shipped in | Release date | How mapped | |---|---|---|---|---|---|---| | 2026-09-16 | advisory (medium) | GHSA-j8rh-479h-cp32, CVE-2026-101904 | [Header Injection via Inherited headers After Minimal Interceptor](https://github.com/axios/axios/security/advisories/GHSA-j8rh-479h-cp32) | v1.20.0 | 2026-08-24 | advisory patched_versions | | 2026-09-16 | advisory (medium) | GHSA-9fr6-4gfg-395g, CVE-2026-101902 | [Prototype-Pollution Gadget in Axios Default Instance Allows Inherited Object.prototype.method to Override HTTP Method](https://github.com/axios/axios/security/advisories/GHSA-9fr6-4gfg-395g) | v0.34.0, v1.20.0 | 2026-09-13, 2026-08-24 | advisory patched_versions | | 2026-06-22 | commit | — | [chore(deps): bump form-data floor to ^4.0.6 to patch CRLF injection (#11028)](https://github.com/axios/axios/commit/5d2af0485354580dce4b6b77890b84cadc0ead0e) | v1.19.0 | 2026-07-26 | date heuristic (first release on/after commit) |
For advisories, “Date” is the advisory's publish date, so it can be later than the release that fixed it (above: advisory 2026-09-16, v1.20.0 released 2026-08-24). For commits it is the commit date. When an advisory matches several releases (one per patched range, for example a fix backported to 0.x and 1.x), “Shipped in” and “Release date” list all of them, comma-separated, in the same order.
The dependency-updates table has the same “Shipped in” and “How mapped” columns:
| Merged | Bot | Update | Security? | Shipped in | How mapped | |---|---|---|---|---|---| | 2026-10-27 | dependabot[bot] | [chore(deps): bump qs from 6.16.0 to 6.17.0](https://github.com/axios/axios/pull/11395) | | _unreleased_ | unreleased |
The file also has a release list (latest 50), a “Gaps in this record” section when anything was missed, and a footer that explains each mapping label. Fix and dependency tables are capped at 200 rows each; the JSON has everything.
security-record.json
Top level of examples/axios-axios/security-record.json, arrays shortened:
{
"schema": "fixdossier/v1",
"repo": "axios/axios",
"generated_at": "2026-10-29T13:16:44.058Z",
"sources_checked": { "releases": 126, "tags": 148, "closed_pulls": 200, "advisories": 51, "commits": 200 },
"gaps": [
"pull requests: stopped after 2 pages (raise max_pages for full history)",
"commits: stopped after 2 pages (raise max_pages for full history)"
],
"summary": { "advisories": 51, "security_commits": 1, "dependency_updates": 47,
"security_dependency_updates": 13, "unreleased_fixes": 0 },
"releases": [ ... ], "tags_without_release": [ ... ],
"security_fixes": [ ... ], "dependency_updates": [ ... ]
}Every row in security_fixes and dependency_updates has release and release_date (the first matched release, or null), matched_releases (all matched releases as {tag, date}), and mapping. Advisory rows also carry patched_versions as normalised ranges. An example is on the mapping page.
SUPPORT.md
A support-period statement plus a per-release “supported until” table. See Generate SUPPORT.md with a support period.
Run it locally
The entry point reads the same inputs as --name=value flags. Anonymous API access works for public repos (60 requests/hour); set GITHUB_TOKEN for more. Real output from a run on 29 Oct 2026 (it fetched 1 page per source, so every source except advisories reports a gap):
$ git clone https://github.com/itulo/fixdossier && cd fixdossier
$ node src/index.js --repo=fastify/fastify --max_pages=1 --output_dir=/tmp/fdrun \
--support_period_months=24 --eol_date=2030-12-31 --contact=security@example.com
fixdossier: fastify/fastify — {"advisories":16,"security_commits":0,"dependency_updates":0,"security_dependency_updates":0,"unreleased_fixes":0} — 5 API calls
::warning title=fixdossier gap::releases: stopped after 1 pages (raise max_pages for full history)
::warning title=fixdossier gap::tags: stopped after 1 pages (raise max_pages for full history)
::warning title=fixdossier gap::pull requests: stopped after 1 pages (raise max_pages for full history)
::warning title=fixdossier gap::commits: stopped after 1 pages (raise max_pages for full history)Run outside Actions, max_pages defaults to 5, not 10.
Use the outputs in later steps
fix_count and gap_count are step outputs. Give the step an id and read steps.<id>.outputs.gap_count, for example to post a summary or open an issue when gaps appear. An example is on the coverage page.
Next
- How rows are mapped to releases, and why backports need a manual check.
- What the record covers and what it does not.
Docs pages
- Docs home
- Build the record
- Advisory → release mapping
- Coverage and limits
- SUPPORT.md and support periods
- PLD evidence checklist
- Source on GitHub (MIT)
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.