Advisories in this repository come from three places: records generated from resolves annotations on homebrew/core formula patches, candidate records matched against external OSV feeds by brew advisory-match, and records contributed directly by pull request. All live under advisories/ as OSV-schema JSON and are validated against that schema on every push.
The concatenated advisory index is built from the reviewed records on main and served at formulae.brew.sh/api/advisories.json.
If a homebrew/core formula already ships a patch that fixes a CVE, the simplest route is to annotate the patch. Open a pull request against Homebrew/homebrew-core adding resolves "CVE-YYYY-NNNNN" to the relevant patch do block; see libquicktime.rb for an example. CVE identifiers appearing in the patch URL or applied file paths are picked up automatically without an explicit resolves. The daily Regenerate workflow will pick it up and write a BREW-<formula>-<CVE> record here automatically. Generated records carry "database_specific": {"source": "generated"}. The workflow refreshes their upstream-derived fields (summary, severity, references, ecosystem_specific.patches) on each run but preserves published and affected[].ranges from whatever is on disk, so a hand-corrected fixed boundary will not be overwritten.
Open a pull request adding a file under advisories/ when you want to record something the generated path cannot express, most commonly that a formula version is affected and Homebrew has not yet applied a fix.
Before opening the PR, report the issue upstream according to the project's security policy and wait for it to be acknowledged. If you believe the vulnerability is being actively exploited against Homebrew users, or the upstream project is unresponsive after a reasonable disclosure window, open a private security advisory on this repository instead of a public PR.
Name the file BREW-0000-0000.json; a maintainer assigns the final id on merge. The record must validate against the OSV schema and use the Homebrew ecosystem with a pkg:brew/<formula> purl. A minimal example:
{
"schema_version": "1.7.3",
"id": "BREW-0000-0000",
"modified": "2026-01-01T00:00:00Z",
"upstream": ["CVE-YYYY-NNNNN"],
"summary": "One-line description",
"affected": [{
"package": {"ecosystem": "Homebrew", "name": "example", "purl": "pkg:brew/example"},
"ranges": [{"type": "ECOSYSTEM", "events": [{"introduced": "0"}, {"fixed": "1.2.3"}]}]
}],
"references": [{"type": "ADVISORY", "url": "https://..."}]
}Use {"introduced": "0"} if every shipped version is affected. Omit the fixed event if there is no fixed version yet; add it in a follow-up PR once one exists. Versions are the formula version as brew info reports it, with an _N suffix when the revision is nonzero. Link the upstream CVE, GHSA or advisory in upstream and references.
Records with "database_specific": {"source": "matched", ...} are produced by brew advisory-match (in Homebrew/brew), which queries OSV.dev's GIT, language-registry and distro ecosystems plus CPANSA for a formula and emits one candidate per CVE reached. These arrive as automated PRs and need a human to confirm the match before merge.
database_specific tells you how the match was made:
strategyis the highest-precision path that reached the CVE:git(the formula's forge repo),registry(a PyPI/npm/crates/... package derived fromstable.urlor aresource),cpansa(a CPAN distribution), ordistro(a Debian/Ubuntu/... source package name mapped via Repology).confidencefollows from strategy and whether a comparable version range was found:highforgit/registrywith a range,mediumwhen no comparable range was found (typically GIT commit-SHA events),lowfordistro.upstream_evidencelists every path that reached the CVE, each with the{ecosystem, name, subject_version}that was queried.
affected[0].ecosystem_specific tells you what the range check concluded:
range_stateisaffected(the shipped version is inside an upstream affected range),fixed(past one), or absent (no comparable range; the reviewer sets the boundary).upstream_fixed_inis the upstream version the matched range says fixes it.resourceandresource_purlare set when the affected subject is a vendoredresourceblock rather than the formula's primary source."fix": "bump"and afixedevent inrangesare only present whenrange_stateis"fixed".
Before merging, check:
- The
upstream_evidencekeys point at this formula's actual upstream. Adistro-only hit can reach a CVE for an unrelated package that shares a Repology project (for example awget2CVE surfaced againstwget); close those. - Check the advisory's build and runtime conditions against the formula's installation steps. For example, Nokogiri's packaged-libxml2 advisories do not establish a Nokogiri fix boundary for builds using
--use-system-libraries; the external library must be assessed separately. - The CVE is not a distro-specific patch. Open https://osv.dev/vulnerability/CVE-YYYY-NNNNN and confirm the affected package and description match the upstream project, not a distribution's packaging of it.
- If
range_stateis"fixed", thefixedboundary inrangesis thepkg_versionat which Homebrew first shipped the fix.brew advisory-matchderives this by walkinghomebrew-corehistory; spot-check it againstgit log -p -- Formula/<letter>/<name>.rbin ahomebrew-corecheckout and correct it in the PR if wrong. The corrected value is preserved on subsequent regenerations. - Check whether affected and fixed builds share a
pkg_versionbecause a resource changed without a revision bump. A history walk returning that version does not prove that every build with that version was fixed. Likewise, regeneration with--new-historychecks preservation of existing terminal ranges, not their independent correctness. - If
range_stateis"affected"with aresource,brew info <formula> --json=v2 | jq '.formulae[0].resources[] | select(.name == "<resource>") | .version'confirms the pinned resource version is belowupstream_fixed_in. Known false-positive sources are stale CPANSA ranges and OSV records where one alias (often a PYSEC id) has an open-ended range while another has a bounded one. - If
range_stateis absent andconfidenceismediumorlow, the record is a lead rather than a determination: either set therangesboundary yourself from the upstream advisory andhomebrew-corehistory, or close if the CVE does not apply.
The Ingest workflow currently discards uncomparable candidates (range_state absent) instead of proposing them for review: their raw OSV shape ({"introduced": "0"} with no fixed) reads as every version being affected. To reject a candidate durably, add its id to data/rejected-candidates.txt — matching re-runs from scratch daily, so closing a PR only postpones it — and if the record was already merged, delete its file in the same change.
Dropped leads with a database_specific.review_reason, such as prerelease_boundary, are listed by reason and id in the Ingest run summary. A reviewed range_state override resolves the current-state ambiguity; upstream_fixed_in alone does not. If history remains ambiguous, contribute reviewed Homebrew ranges and matching provenance with the override before Ingest can refresh the record.
Keep one matched record per formula and upstream alias family. If both CVE- and GHSA-named records exist for the same vulnerability, retain the CVE-named record and all upstream identifiers, check any conflicting ranges against formula history, and remove the redundant file. Preserve the surviving record's published value; this is not necessarily the earliest publication date among the removed aliases. Ingest leaves duplicate families unchanged and fails until they are consolidated.
When the matched package identity or upstream fix changes, review affected[].ranges and its matching metadata together. Ingest will not attach changed provenance to an existing reviewed range unless history can revalidate it; if history is unavailable, establish the boundary from the upstream advisory and formula history before correcting the record.
For a reviewed upstream-range error, add a formula-and-advisory entry to data/overrides.yml. An upstream_fixed_in value replaces the calculated upstream fix and may be null; range_state may be affected, fixed, or not_applicable. When forcing an existing terminal record to affected, also remove its fixed event in the same change. Otherwise Ingest treats the mismatch as a possible reintroduction and fails closed when history cannot establish the transition.
For a hand-reviewed Homebrew boundary, set preserve_homebrew_ranges: true in the formula-and-advisory override. This is separate from upstream_fixed_in and prevents the opt-in brew advisory-match --reconcile-history mode from changing or deleting that record. Add these pins before a reconciliation sweep. Ingest's manual reconcile_history input narrows existing matched terminal ranges or deletes proven never-affected records; scheduled runs retain --new-history and use separate branches so they cannot replace pending reconciliation PRs. Review these diffs by failure class and representative formula history, including every deletion. Deleted records are sharded using their formula identity at the pinned pre-match commit.
Daily matching enumerates all formulae at one pinned core commit through the GitHub trees API; a truncated inventory fails planning. Sixteen workers each own four of the existing 64 formula buckets, with at most four workers running at once. All workers receive the same database/brew/core pins, immutable formula lists and Repology bytes. Repology is refreshed once during planning, with its committed index retained and freshness recorded if that refresh fails. Workers run --new-history --formula-list with no history cache dependency. The core commit graph is cached by core revision and Git version, verified before use and rebuilt when missing or invalid.
The daily 90-minute matching and 120-minute job limits are pilot budgets, to be revised using the slowest measured cold worker. For a bounded pilot, dispatch with daily_worker set to 00 through 0f; leave it at all for complete daily coverage. Disable publish_results for acceptance runs that should save artifacts without creating commits or PRs. The brew formula-list prerequisite must be on main before this workflow is deployed.
Reconciliation still selects existing matched formulae in 64 publication buckets, using the committed Repology index and its separate 150/180-minute limits. Select one bucket with reconciliation_shard, or use all. Removed core formulae remain unchanged and are reported. The daily_worker selector cannot be combined with reconciliation.
Workers have read-only repository permissions and upload raw recovery output plus separate filtered snapshots. A failed worker does not produce a successful filtered receipt. Raw patches have not passed filtering and must not be published directly. The coordinator validates formula ownership, file modes, source/input hashes and run attempts before combining successful snapshots. Only that job signs, pushes or opens PRs. Matching has a shorter timeout than its job so recovery has time to run, although runner loss or cancellation can still prevent recovery.
Complete daily runs retain the single standing PR for up to 500 changed paths and stable bucket PRs for larger diffs. Partial daily runs, including a single-worker pilot, publish successful buckets only: the standing PR is left untouched and may temporarily overlap those buckets. Do not treat that older standing PR as the complete current run. Partial runs and reconciliation never clean up other review branches. A successful worker's output replaces its buckets' prior proposals, including candidates no longer proposed. After complete daily coverage and successful publication, cleanup closes obsolete daily PRs, including buckets with no remaining changes. A complete zero-change daily run performs cleanup without creating a commit. This does not resolve held records; use the run artifacts to review those separately.
A failure leaves other workers running. Re-run failed jobs reuses the original plan, but the coordinator accepts only result artifacts from the new attempt; previously successful workers are reported as missing from that attempt rather than silently combined across attempts. Their published branches remain intact. Re-run all jobs for a new complete result and any transition cleanup. Artifacts are retained for 14 days; dispatch a new run if the original plan has expired.
Before scheduled rollout, compare a complete sharded run with a back-to-back single-job run at identical database/brew/core pins, Repology bytes, overrides and rejections, with publication disabled. Compare all filtered records and added/modified/deleted IDs, review hold differences and repeat against the first output to check idempotence. Live upstream inputs are not frozen by Git pins: every semantic difference needs supporting upstream record content and an explanation of its effect. A changed modified timestamp alone is insufficient. Unexplained differences block rollout; use recorded/replayed queries if live-input churn prevents a conclusive comparison. Measure cold group runtimes and graph-cache misses before relying on the pilot budgets.
The run summary includes each worker's matcher outcome and final counts for changes, unchanged records, deletions, unvisited records and hold reasons. Missing totals mean unknown coverage, not zero changes. Hold reasons can overlap, and upstream_unavailable counts formulae rather than records. A successful job can still leave records held or unvisited; review those classes separately.
Reconciliation is repeatable maintenance, separate from daily Ingest. Start with one heavy shard, inspect its runtime and artifacts, then run all shards. After merging reviewed corrections, rerunning with unchanged evidence should leave those records byte-identical and leave deleted records absent. Brew/core/database pins do not freeze live OSV responses, so changed upstream evidence can produce different results. Use manual reconciliation after relevant matcher or upstream fixes; choose any recurring cadence after measuring a complete sweep. A no-change pass still walks history and does not establish that held records are correct.
When a formula still ships a language-registry package but its source URL no longer identifies that package, add registry_package under the formula with the exact canonical OSV ecosystem and name. Brew derives the versioned purl. This formula-level identity applies while walking historical revisions and to future versions, so verify it against the upstream registry and remove or update it if the packaged project changes.
Do not merge a matched record over an existing source: "generated" record for the same (formula, CVE); the generated record already states Homebrew ships a patch and takes precedence. brew advisory-match --output skips these automatically.
Records here describe vulnerabilities in software Homebrew distributes, scoped to the Homebrew formula name and version. They are not a substitute for the upstream project's own advisory; the purpose is to let tools that read pkg:brew purls or Homebrew OSV queries answer "is this installed formula affected". Casks are out of scope for now.
The required Ruby version is recorded in .ruby-version. Install the development dependencies and run the same Ruby checks as CI with:
bundle install
bundle exec rakeWorkflow changes should also pass Homebrew's standard workflow checks:
brew install actionlint shellcheck zizmor
actionlint
zizmor .This project follows the Homebrew Code of Conduct.