composer-update: derive ~min_safe.minor.0 constraints from composer audit data#25
Merged
Merged
Conversation
…udit data
Vuln-update PRs were over-bumping when only a patch was needed:
wpackagist-plugin/woocommerce 10.5.2 10.7.0 # CVE fixed in 10.5.3
wpackagist-plugin/simple-tags 3.44.0 3.50.0 # CVE fixed at >3.44.0
phpunit/phpunit 9.6.20 11.5.55 # CVE fixed in 9.6.33
`composer update -W <pkg>` and `composer require -W <pkg>` were both
running unconstrained, so composer was free to pick the highest version
inside the project's existing `^X.Y` constraint — bumping minor (or even
major) versions when a same-minor patch would have fixed the CVE.
Fix: composer-update gains a `vulns_json` input — the JSON array of
vulnerability records produced by parsing `composer audit --format=json`
(shape: `{package, affected, ...}`). The action runs a small PHP helper
that:
- finds the affected-range that contains each package's currently
locked version
- derives `~X.Y.Z` from the range's exclusive upper bound (`<X.Y.Z`),
or `~X.Y.(Z+1)` from an inclusive one (`<=X.Y.Z`) as a heuristic
next-patch cap
- emits a JSON map of `{pkg: constraint}` consumed by build_pkg_arg()
build_pkg_arg() then attaches `:constraint` to each package name when
invoking composer, so the update/require calls see
`wpackagist-plugin/woocommerce:~10.5.3` instead of just the bare name.
composer still picks the highest matching version, but the tilde-3-comp
form caps that at patch-level within the safe-version minor.
The vulnerability-scan workflow forwards parsed_vulns.json (already
built for the PR body) as the new input. Consumer repos need no
changes — they continue to call the reusable workflow as before.
Side-effect benefit for the audit-blocked-transitive case
(wordpress-no-content): when the constraint's range is entirely behind
security advisories, composer fails to resolve, the per-package retry
leaves the lock unchanged, and the package is reported as untouched
instead of getting a courtesy bump from another package's update.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Vuln-update PRs have been over-bumping when only a patch was needed:
wpackagist-plugin/woocommerce10.5.210.7.010.5.3wpackagist-plugin/simple-tags3.44.03.50.0>3.44.0phpunit/phpunit9.6.2011.5.559.6.33composer update -W <pkg>andcomposer require -W <pkg>were both running unconstrained, so composer was free to pick the highest version inside the project's existing^X.Yconstraint — bumping minor or even major versions when a same-minor patch would have fixed the CVE.How it works
composer-update gains an optional
vulns_jsoninput — the JSON array of vulnerability records produced by parsingcomposer audit --format=json(shape:{package, affected, ...}). A small PHP helper atcomposer-update/scripts/compute-min-safe-constraints.phpderives a tight~X.Y.Zconstraint per package:<X.Y.Z→~X.Y.Z(simple exclusive bound —>=X.Y.Z, <X.(Y+1))<=X.Y.Z→~X.Y.(Z+1)(heuristic next-patch; if no such version exists composer fails to resolve and the package is reported as untouched, which is preferable to a silent over-bump)>=9.0.0,<9.6.33|>=10.0.0,<10.5.62|...→ finds the range that contains the currently locked version, uses its upper boundbuild_pkg_arg()then attaches:constraintto each name when invoking composer, so the update/require calls see e.g.wpackagist-plugin/woocommerce:~10.5.3instead of the bare name. composer still picks the highest matching version, but the tilde-3-component form caps that at patch-level within the safe-version minor.Per-project config
None required. The vulnerability-scan reusable workflow already parses
composer auditJSON for the PR body — it now also forwards that parsed JSON to composer-update via the new input. Consumer repos continue to call the workflow exactly as before.Side benefit: audit-blocked transitives
When a CVE's safe range is entirely behind security advisories (e.g. gorans where
wordpress-no-content6.8+ are audit-blocked), the tight constraint will fail to resolve, the per-package retry will leave the lock unchanged, and the package will be reported as untouched — instead of getting a courtesy bump from another package's-Wupdate.Verification
Ran the PHP helper against the 6 packages flagged in holmasto #92 using their current locked versions; all produced the expected
~X.Y.Zform:{ "wpackagist-plugin/woocommerce": "~10.5.3", "wpackagist-plugin/wordpress-seo": "~27.1.2", "phpunit/phpunit": "~9.6.33", "roots/wordpress-no-content": "~6.9.2", "wpackagist-plugin/simple-tags": "~3.44.1", "generoi/gravityforms": "~2.9.31" }Test plan
woocommercelands on10.5.x(not10.7.x),wordpress-seostays within~27.1.x, etc.phpunit/phpunitstays within~9.6.xinstead of jumping to^11.5.🤖 Generated with Claude Code