Releases: sharkyger/homebrew-safe-upgrade
v0.2.7 — ecosystem-aware NVD matching, package-anchored output, CVE-named age bypass
Security-precision and UX release closing all four open user reports — thanks @aleksandrs-ledovskis for the detailed issues.
Fixed
- Same-named packages from other ecosystems no longer block a Homebrew formula (#74). NVD keyword search matches on text, so a formula could collide with an identically-named package from a language ecosystem — the canonical
cmakewas permanently blocked by an advisory for the long-dead npmcmakepackage. The scanner now reads the CPE 2.3target_swfield and skips advisories whose every applicability statement is pinned to a different language ecosystem. Fail-closed bounds kept: one generic/matching applicability statement — or no CPE data at all — still flags. CPE relevance is also evaluated when no version is supplied; distro- and OS-scoped advisories are filtered on that path too. oracleremoved from the distro-vendor CPE filter. Oracle is also the upstream vendor of widely brewed software (MySQL, OpenJDK, VirtualBox), so its application CPEs are now evaluated like any other upstream vendor. Oracle Linux distro CPEs are OS-type and remain filtered.- Per-package output lines no longer print above the package they belong to (#73), in
brew safe-upgradeandbrew safe-installalike.
Added
- The age-check CVE bypass now names the CVEs it acted on (#72) — severity, CVSS score, and source for up to 5 findings print under the bypass line.
Documentation
- README documents where the standalone scanner lives per install route (#75):
$(brew --prefix safe-upgrade)/libexec/for tap installs,$(brew --prefix)/bin/for script installs.
Install / upgrade
- Tap (recommended):
brew install sharkyger/tap/safe-upgrade— formula bump to v0.2.7 follows shortly; thenbrew update && brew upgrade safe-upgrade - Script: re-run
install.sh(pinned to this tag, SHA256-verified) orbrew safe-update
Full details in the CHANGELOG.
v0.2.6 — brew dispatcher --help fix
Patch release. Follow-up to #66 that completes the --help fix for the real-world Homebrew dispatcher path.
Fixed
brew safe-upgrade --help(andsafe-install/safe-update) now render the tool's own usage instead of Homebrew's genericExample usage:banner (#66 follow-up). Homebrew's dispatcher intercepts--helpbefore exec'ing an external command and renders help only from lines beginning with#:(see External Commands); the scripts carried plain#comments, so brew fell back to its built-in banner andprint_help()never ran on that path. Eachbrew-safe-*script now carries a#:help block mirroring itsprint_help(), and a regression test drivesbrew <cmd> --helpthrough the real dispatcher to assert the generic banner is gone. The direct-invocation--helpadded in 0.2.5 was unaffected and continues to work.
Dogfooded before publish: --help on all three commands renders the tool's own usage via the real brew dispatcher on linuxbrew (Linux) and macOS-arm64; --version reports 0.2.6; no /opt/homebrew vs /usr/local hardcoding.
Full changelog: https://github.com/sharkyger/homebrew-safe-upgrade/blob/v0.2.6/CHANGELOG.md
v0.2.5 — CLI --help, hardened self-updater, static-analysis floor
brew-safe-upgrade v0.2.5
User-facing CLI help, a hardened self-updater, and a static-analysis floor.
Added
--help/-hfor all three commands (brew safe-upgrade,brew safe-install,brew safe-update) — a synopsis, flag listing, and examples, printed even on a partially-installed tree. Closes #66 (thanks @aleksandrs-ledovskis for the request).
Changed
- The self-updater (
brew-safe-update) now matchesinstall.sh's supply-chain hardening: it updates to the latest published release tag (never a moving branch) and verifies every file against that release'sSHA256SUMSmanifest before an atomic, fail-closed swap — including the fresh updater it re-execs, verified before it takes over. Both curl-fetch routes now share one pin-and-verify path. - Quieter
brew updatestep — Homebrew's own harmless description-cache backtrace is filtered out; real warnings/errors still show.
Tooling
- Added mypy, shfmt, markdownlint, an assertive CodeRabbit profile, and a pre-commit config wrapping the lint floor — all enforced in CI.
Docs
- The README install section now leads with the Homebrew tap one-liner (
brew install sharkyger/tap/safe-upgrade); the script install route is the documented secondary path.
Install / upgrade
brew install sharkyger/tap/safe-upgrade # new install
brew update && brew upgrade safe-upgrade # tap upgrade
v0.2.4 — harden the script installer
v0.2.4 — harden the script installer
The curl | bash script installer now pins to an immutable release tag and
verifies every file against a published SHA256 manifest before anything is
installed — bringing the script route in line with the integrity guarantees the
Homebrew tap route already provides.
Changed
- Pinned, not moving. Downloads come from an immutable release tag, never the
mainbranch — acurl | bashrun always gets exactly one published release. - Verify before install. A published
SHA256SUMSmanifest drives the release
file set; every file is staged and checksum-verified before anything lands in
your Homebrewbin. A truncated, tampered, or missing file aborts the whole
install — no partial state. - Atomic, same-filesystem swap and a fail-closed completeness floor so a
truncated manifest can't yield a "successful" partial install.
Fixed
- Single-source version.
pyproject.tomlis realigned with theVERSION
file, with CI keeping them — and the installer's pinned tag — in lockstep.
Tests / CI
- A new hermetic end-to-end install smoke runs the real installer over a local
server in CI on Linux (bash 5) and macOS (bash 3.2) — the script-install route
is exercised in CI for the first time. Full suite: 151 passing.
Trust model: the manifest and files travel the same TLS channel, so the
checksums are defense-in-depth (transfer integrity + tag immutability) on top of
HTTPS, not a replacement for it. The Homebrew tap route was already immutable
and is unaffected —brew install sharkyger/tap/safe-upgrade.
See CHANGELOG.md for full history.
v0.2.3 — fail-closed freshness hold (Casks, taps, lib*) + single-package upgrade
Security + UX release. The --min-age freshness hold now covers Casks, tap formulae, and lib* formulae, and fails closed when it can't verify a release age — plus single-package upgrades and live progress during the dependency scan.
Fixed
- Freshness hold now covers Casks, tap formulae, and
lib*formulae — and fails closed (#62). The age check only resolved release dates fromhomebrew-coreatFormula/<first-letter>/<name>.rb, so three classes of package came back "age unknown" and silently skipped the--min-agehold: Casks (which live inhomebrew-cask), tap formulae (their own tap repo), andlib*formulae (libgit2,libheif,libusb, … — homebrew-core shards these underFormula/lib/, notFormula/l/). Lookups are now routed to the correct repo and path per package type, and a release age that genuinely cannot be verified is now held rather than allowed through. The same routing + fail-closed policy applies to the transitive-dependency age check in bothsafe-upgradeandsafe-install. The CVE-aware bypass (skip the hold when the installed version has known CVEs) is unchanged, so security patches are never withheld.
Added
- Single-package upgrades (#61):
brew safe-upgrade <name> [<name> …]restricts the run to the named outdated package(s) instead of everythingbrew outdatedreports. Matches the full name or its basename; a named package that isn't outdated is reported, not silently ignored. --allow-unknown-ageonsafe-upgradeandsafe-install— the explicit opt-out to permit packages whose release age can't be verified (default: such packages are held).- Dependency-scan progress (#60): the transitive-dependency scan now prints
[i/N] checking <dep> <version>…before each dependency's network checks, so a large scan shows live progress instead of a silent wait.
Upgrade
- Homebrew tap:
brew update && brew upgrade safe-upgrade - Script install: re-run
install.sh
Thanks to @aleksandrs-ledovskis for the detailed report in #62.
v0.2.2 — install hardening (atomic self-updater, --version)
Reliability release: fixes a recurring install bug where brew safe-upgrade could fail closed with bottle SHA resolver not found after an update.
Fixed
- Self-updater no longer strands helper files. A
brew safe-updatefrom an older updater could fetch only some files yet print "All tools updated", leavingbottle_resolver.py/cask_nvd_map.pymissing — so the nextbrew safe-upgradefailed closed. The updater now re-execs the freshly-fetched updater before the download loop and is atomic + fail-closed: it swaps files into place only if every declared file arrives intact, and never reports success on a partial set. - Scripts resolve their own directory through symlinks (portable, no macOS-hostile
readlink -f), so helpers resolve for script, Homebrew-formula, and symlinked layouts on both architectures. - A missing helper now prints actionable guidance (
run 'brew safe-update').
Added
--versionon all three commands, with self-diagnosis: prints the version, the detected install route (formula vs script), whether every helper file is present, and a warning if both install routes are onPATH.
Verified
- Real install-and-run dogfood on Intel and Apple Silicon Macs (script route) + macOS arm64 and x86_64-Linux CI.
Update: tap users brew update && brew upgrade safe-upgrade; script users brew safe-update.
Full changelog: v0.2.1...v0.2.2
v0.2.1 — PEP 440-correct pre-release version comparison
A correctness patch for the CVE range matcher's version comparison.
Fixed
- PEP 440-correct pre-release version comparison. The CVE range matcher now uses a small, dependency-free pre-release-aware comparator (
dev < alpha < beta < rc < final, with trailing-zero equivalence) in place of the previous tuple parser, so pre-release versions such as1.0-betasort correctly relative to their final release when evaluated against advisory ranges. Constraint parsing is tightened and every comparison site is None-safe — unparseable versions or constraints fail closed (treated as affected). The tool stays dependency-free (nopackagingruntime dependency). Covered bytests/test_version_validation.py. - The
test_installed_old_version_is_treated_as_incomingtest no longer reads the live Homebrew openssl@3 release date, so it stops failing for ~3 days after every openssl@3 bump.
Install / upgrade
- Tap users:
brew update && brew upgrade safe-upgrade - Script install:
brew safe-update
Full changelog: v0.2.0...v0.2.1
v0.2.0 — arch-aware bottle-SHA + self-healing updater
Security-first wrapper around brew upgrade / brew install — checks every package against NIST NVD, OSV.dev, and GitHub Advisory before the upgrade proceeds. Fail-closed by design.
Highlights since v0.1.1
- Arch-aware bottle-SHA resolver — fixes a uniform Intel (x86_64) false positive where the SHA check flagged every bottle as
[BLOCKED] SHA mismatch(it compared the Intel bottle's SHA against the arm64 bottle's). Now resolves the bottle tag by the host's real arch + OS, never crossing architecture. Confirmed fixed on real Intel hardware. - SHA verification is default-on for
brew safe-install/safe-upgrade(local bottle SHA vs the canonical SHA atformulae.brew.sh). Tampering blocks and is never overridable by--yes. Opt out with--no-verify-sha. --min-age3-day freshness hold by default — holds back formulae published in the last N days (supply-chain worm window), with a CVE-aware bypass so security patches still reach you.--min-age 0to disable.- Curated cask → NVD keyword map (
cask_nvd_map.py) for better cask CVE coverage. - Self-healing
brew safe-update— updates itself first, then re-runs, so a singlebrew safe-updatealways pulls the full current file set (no more "run it twice" /bottle SHA resolver not found).
Full details in CHANGELOG.md.
Install
curl -fsSL https://raw.githubusercontent.com/sharkyger/homebrew-safe-upgrade/main/install.sh | bashv0.1.1 — hardening pass
Internal correctness + defense-in-depth fixes following the v0.1.0 review. No flag/env-var surface change.
Highlights
brew list --versionsparsing now usesawk '\$NF'so multi-keg installations compare against the newest installed versionINCOMING_DEPSis a proper bash array; pathological tap dep names with whitespace survive dedup + iteration--min-agefor tap-namespaced deps emits a[skip-dep-age]log line instead of relying on a homebrew-core lookup that doesn't apply- Dep names validated against
^[a-zA-Z0-9@._/-]+\$before flowing into any URL or subprocess argument; same regex now also guards the main-package age check in both wrappers - Pre-install/upgrade warning split into distinct sections — known CVEs vs.
--min-ageholds — so two different risk signals are no longer conflated - In-code design note explains why transitive deps don't get the CVE-aware
--min-agebypass that applies to the user-named package
See CHANGELOG.md for the full breakdown. PR: #25.
Notes
This release tag is GPG-signed — first signed tag on the repo.
v0.1.0 — first tagged release
First tagged release.
Highlights
- Transitive dependency CVE checking for
brew safe-installandbrew safe-upgrade— every new dependency version coming in with the operation is checked against OSV.dev, GitHub Advisory, and NIST NVD before brew touches your system. Default-on. Already-installed deps that aren't changing are deliberately skipped (that'sbrew-vulns' job). --no-depsflag andBREW_SAFE_NO_DEPS={1,true,yes}env var — per-invocation opt-out for power users. No persistent config file.--min-age N— hold packages published less than N days ago, with CVE-aware bypass for the explicitly-named package.--verify-sha— verify bottle SHA againstformulae.brew.shbefore upgrading.- Cask support for both wrappers.
- macOS bash 3.2 compatible.
Pre-tag history
Several rounds of fixes and hardening landed before this first tag — see CHANGELOG.md for the grouped backfill.
Known follow-ups
Six items from the pre-merge multi-agent review have been filed as issues #19–#24. None are blockers; each is a worthwhile improvement.