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.

yaml
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: commit

For 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):

SourceEndpointUsed for
ReleasesGET /repos/{owner}/{repo}/releasesRelease tag, date, pre-release flag. Drafts and unpublished releases are dropped.
TagsGET /repos/{owner}/{repo}/tagsCommit SHA behind each tag, used for exact SHA matches; tags with no GitHub Release are listed in tags_without_release.
Pull requestsGET /repos/{owner}/{repo}/pulls?state=closed&sort=updated&direction=descMerged PRs opened by dependabot[bot], renovate[bot], dependabot-preview[bot] or renovate-bot.
Security advisoriesGET /repos/{owner}/{repo}/security-advisories?state=publishedPublished repository advisories (GHSA ID, CVE ID, severity, patched_versions).
CommitsGET /repos/{owner}/{repo}/commitsDefault branch history. Messages that look like security fixes become rows.

Which commits count as security fixes

  • Any commit whose message contains a CVE-YYYY-NNNN or GHSA-xxxx-xxxx-xxxx ID.
  • 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 docs conventional 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, and docs(cra) #1474. First lines starting with test:, ci:, build:, or chore(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):

markdown
## 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:

markdown
| 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:

markdown
| 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:

json
{
  "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):

shell
$ 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

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.