Releases: hexblot/composer-remediate
Release list
0.6.1
A housekeeping release so that a tag sits on a green CI run: the 0.6.0 commit shipped with a stale
generated case-studies page, which failed the PHP 8.4 job's staleness check while every test passed.
Deptrac now classifies third-party code too, and the README gained an architecture badge.
Fixed
docs/case-studies.mdregenerated for the two combined commands that changed in 0.6.0 (BookStack
socialite and Open Social); the CI check that compares the committed page with a fresh render had
been failing since the release commit.
Changed
-
Deptrac classifies third-party code as layers of its own (
Semver,ComposerPlugin,
ComposerApi,SymfonyConsole,SymfonyProcess,SymfonyYaml) and each project layer states
which it may use; the run fails on any dependency left unclassified (--fail-on-uncovered), so a
new library or a new corner of Composer's API has to be allowed deliberately. The report went from
296 uncovered dependencies to zero, with 468 allowed. The README carries an architecture badge fed
by the PHP 8.4 CI job ("passing", or "failing (n)"), next to the coverage badge. -
The roadmap page records the assurance work alongside the phases (adversarial review and rechecks,
the testing sprint, the Aikido answers, Deptrac) and marks Phase 5 as next.
0.6.0
A feature release with changed defaults. Global planning (Phase 4) searches for the smallest command
that fixes every finding and explains the search; the answers to an Aikido code scan make incomplete
advisory sources, unverified database downloads and unread coverage gaps fail closed, so exit codes can
differ from 0.5.0 in CI; three CLI options, a combined_search block in the JSON summary and Deptrac
layer rules arrive with it. The Plan::SEVERITIES constant is gone, replaced by the
Engine\Advisory\Severity enum.
Added
- Global planning (Phase 4). The planner now searches for one command that fixes every finding
with as little change as possible. The per-package winners are merged as before; when that merge
does not resolve, or fixes only some findings, the search swaps in the next-ranked candidate of a
finding that is in the way and tries again, within a budget of ten further solves, keeping the
combination that fixes the most findings, then the smallest diff. A combination that fixes
everything is then shrunk by dropping, in turn, each contribution whose package a sibling's fix
already moves, and keeping the smaller command when it still fixes everything: in the BookStack
socialite fixturerobrichards/xmlseclibsleaves the command because theonelogin/php-saml
update carries it, and in the Open Social fixturetwig/twigleaves because thedrupal/core
update does. Every attempt is listed in the text and HTML summaries and
insummary.combined_searchof the JSON report, with the solver's reason, so the recommended
command is explained rather than asserted. Tests: a merge that does not resolve until a
lower-ranked candidate is swapped in, a merge that undoes one finding's fix, a shrink to a parent
update that covers its sibling, and an exhausted search that keeps the best partial result.
Changed
- Structure. The longest methods were split without behaviour change, after a reader pointed at
them:RemediateCommand::executenow delegates to methods for report targets, runtime checks, the
advisory source, the planner, gating and output; the global search's bookkeeping moved from the
planner intoEngine\Plan\CombinedSearch, withrepair()andshrink()as separate steps;
TextRendererrenders one report section per method;CandidateGeneratorbuilds each candidate
family in its own method;RangeNormalizer::fromOsvandDatabaseWriter::writeare split by stage.
Severity labels are an enum (Engine\Advisory\Severity) instead of a lookup table onPlan. - Architecture rules.
deptrac.yamlstates the layers (Plugin → Command → Output → Engine →
Advisory) and CI checks them on the PHP 8.4 job;composer deptracruns them locally (Deptrac is
installed undertools/deptrac, since it needs PHP 8.2 or newer). The first run found one
violation, the advisory model reading the severity table from the plan class, which the enum fixes. - Fixture reports under
tests/Fixture/third-party/*/reportsrecord the sha256 of the committed
composer.fixture.jsonrather than of the scratch copy, whose injected repository path differed on
every run; regenerating a report now yields the same bytes.
Security
Answers to an Aikido code scan (nine findings), each with a regression test.
- Incomplete advisory sources fail closed. A source that only knows the current lock's
advisories (composer auditoutput through--advisories-file, the audit fallback) cannot check a
candidate lock, so the planner no longer verifies candidates against it: findings are reported with
a blocker and no remediation, and the run exits 2 instead of recommending fixes verified against
nothing. Before, the run warned and still reported the fixes as verified. - Coverage gaps gate the exit code. A lock with no findings but with advisory records about
locked (or replaced/provided) packages that the source could not read exits 4, not 0;
--accept-coverage-gapsrestores 0 once the gaps have been read. The JSON summary carries
coverage_gapsandcoverage_gaps_accepted. Gaps about packages a locked package replaces or
provides are now reported at all; before, only the locked names were checked. - Advisory database downloads must be verified. A URL without a published
<url>.sha256and
without an expected digest is refused;--allow-unverified-databaserestores the old warn-and-
accept behaviour.--database-sha256=<hex>pins the digest the operator trusts, which is checked
on the download and on every later use of the cached copy, and is the trust anchor for a database
someone else publishes (the sidecar comes from the same host). Onlyhttps://locations are
downloaded, wherever they are configured (option, environment orcomposer.json). Credentials
embedded in a URL are redacted from every message and from the cache metadata. Symbolic links at
the cache paths are refused and the cache directory is created private; the status file is written
atomically. - A same-version lock change that moves the commit (a re-tagged release, not only a moved dev
branch) is now a change, so the release-age guard refuses it when the date is unknown or too young
and reports it as one change of unclassifiable size. - One advisory id on two replaced components (a CVE spanning several Symfony components under
symfony/symfony) produced one finding; the finding key now includes the replaced target
(advisory@package/target), so both are reported. Baseline entries for such findings use the new
key. - Console output sanitised. Advisory titles, links, upstream record ids, solver output and file
names are stripped of control characters and escape sequences before they reach a terminal, and
console formatting tags in them are escaped in decorated output (Remediate\Output\ConsoleText),
so a crafted advisory cannot restyle the report or rewrite the line above it.remediate:db-build
andremediate:db-statussanitise the upstream text they print. - The GitLab pipeline verifies the Composer installer against its published signature instead of
piping the download into PHP. - The advisory-database release job runs in the
advisory-dbGitHub environment, whose
deployment-branch policy admits onlymain. Aworkflow_dispatchfrom another branch executes
that branch's copy of the workflow file, so no check inside the file (a pinned checkout ref, a
validated input) can stop an actor with write access from running modified code with the
release-capable token; the environment policy is enforced by GitHub before the job starts. A
github.refguard gives a clearer error for an accidental dispatch from a branch.
0.5.0
A feature release. The advisory database now carries exploit data (FIRST EPSS scores and CISA's
Known Exploited Vulnerabilities catalogue) and reports order findings by that urgency; abandoned
packages on a dependency path are named; and the Phase 3 fixture corpus is complete with five Drupal
and five Symfony cases, seventeen real historical projects in all. The GitHub workflows were hardened
after a scan.
Added
-
Exploit data in the advisory database.
remediate:db-buildenriches every CVE with its FIRST
EPSS probability and percentile and its CISA KEV listing date (--enrich, on by default;
--epss-file/--kev-fileread local copies). Only the CVEs the database names are stored. A
feed that cannot be fetched is recorded in the metadata and the build continues without it;
remediate:db-statusshows what the database carries. Reports order findings by urgency (known
exploited first, then EPSS, then severity; development-only findings last), printEPSS 0.93 (97th percentile); listed in CISA KEV since …under each advisory, and count KEV-listed packages in the
summary. JSON gainsepss,epss_percentileandkev_addedper advisory and
packages_known_exploitedin the summary; SARIF tags such rulesknown-exploited; CycloneDX and
GitLab reports carry the values. The dataset hash includes KEV membership (a new listing changes
what to fix first) but not EPSS scores. Databases built without the data still read; their reports
sayno exploit data. Tests: both feeds with scripted downloads and local files, build-to-report
round trip, a failing feed, hash behaviour, an old database, ordering rules, every output format. -
Abandoned packages on the dependency path. When Packagist marks the vulnerable package or a
parent on its path abandoned (the marker travels incomposer.lock, so this needs no network), the
report says so with the replacement Packagist names, under the finding and in the summary; JSON
gainsabandonedper finding andpackages_with_abandoned_dependencyin the summary; SARIF,
CycloneDX and GitLab reports carry the names. The lock snapshot now preserves the marker. Tests:
planner detection on a scripted lock, every output format. -
Phase 3 fixture corpus complete: five Drupal and five Symfony cases. New real historical
fixtures: Mass.gov on Drupal 10.3 (the meta-package's tilde pins let every November 2024 fix through
as a plain update; an abandoned Goutte on the path), Open Social's project template (the
distribution's~10.2.5pin permits the Drupal 10.2.9 patch; Twig 3.14 needs a package the lock
never had), Acquia CMS (Drupal 10.3 withoutcore-recommended, a monorepo whose modules come from
pathrepositories with branch aliases), wallabag (Symfony 5.4 on PHP 7.4: eleven of sixteen
advisories fixable, Guzzle 5 stuck behind the root constraint and an abandoned adapter) and Mautic 5
(a monorepo whose ownpathpackage pins PhpSpreadsheet below the fix). Each carries provenance,
the reasoning behind the expected command and stored reports; the case-studies page grows with them. -
bin/build-fixture.phplearned what these projects needed: it fetches the requirement closure of
every version it keeps (metadata Composer never loaded for the locked graph), takespath/
artifactpackages from the lock file with their branch aliases and a neutral dist while dropping
other versions of those names (a path repository takes precedence), splits and retries advisory API
batches that come back unreadable, and stores fixture manifests ascomposer.fixture.json/
composer.fixture.lock.expected.jsongainedroot_versionfor projects whose dependencies
conflict with the root package by version.
Security
- GitHub workflows hardened after an Aikido scan: every action is pinned to a commit SHA with its
version noted (and a Dependabot configuration keeps the pins current); no workflow expression is
interpolated into a shell script any more, values reach scripts through the environment (the
forceinput of the advisory-database workflow was the reported template-injection vector); the
advisory-database workflow's write permissions moved from the workflow to its single job and the CI
workflow defaults to read; every checkout runs withpersist-credentials: false(the badge push
and the release steps use explicit tokens).bin/run-fixture.phpvalidates the fixture name before
building a path from it.
Changed
- Findings are ordered by urgency (see above); before, they were ordered by package name. The stored
fixture reports and the case-studies page are regenerated accordingly. - Test fixtures moved from
tests/Fixture/<name>totests/Fixture/third-party/<name>, and their
manifests are stored ascomposer.fixture.json/composer.fixture.lock. The fixtures' historical
lock files are vulnerable on purpose and raised hundreds of Dependabot alerts; GitHub's dependency
graph parses everycomposer.jsonandcomposer.lockin a repository whatever the directory (the
third-partyname alone did not exempt them), and does not parse the renamed files.
bin/build-fixture.phpwrites the new layout by default.
0.4.2
A correctness release answering the third adversarial recheck. The recheck of 0.4.1 confirmed the six
earlier findings as fixed and reported three new ones, all rated P1; each is fixed here with a test
that reproduces the reviewer's case, and the reviewer's closure pass on these fixes found nothing
further.
Recheck (third round)
- The
composer audit --lockedfallback wired in 0.4.1 launched its child without--no-plugins --no-scripts, so Composer activated the analysed project's allowed plugins in that child: the
compatibility-recovery route broke the plugin boundary the standalone binary exists to keep. The
child now carries both switches. Covered by an integration test that installs a real plugin whose
activate()writes a marker, shows that a plaincomposer auditdoes trigger it, and asserts the
fallback adapter never does. - OSV
limitevents: the normaliser kept the smallest of several limits and ignored an explicit
limit: "*". OSV's BeforeLimits predicate accepts a version below any limit, so the cap is the
largest limit and a*limit lifts it; the old behaviour truncated genuinely affected versions
(introduced 1.0.0 with limits 2.0.0 and 3.0.0 lost 2.x). Covered by unit tests for both shapes. - Coverage gaps: a package whose value in a Packagist-shaped document is not a list of advisories
({"advisories":{"acme/lib":"upstream-error"}}) was skipped without a gap, so a private
--includefile with that shape built a clean database; it is now a gap for that package, which
fails a private-file build and is retained for public feeds. An OSV record naming several packages
recorded a gap only when every package's range was unreadable; a readable sibling hid the failure.
The record is kept for the readable packages and a gap is recorded for each unreadable one.
Covered by mapper, private-file and OSV-source tests.
0.4.1
A testing release. The suite grows from 111 to 171 tests and line coverage from 74% to 94%, with
no source file below 75%: the command layer, the advisory feed readers and both advisory adapters,
none of which had a test before, are now covered. Two defects surfaced on the way and are fixed
below, and the composer audit fallback that the design had promised since 0.1.0 is wired in.
Added
- Tests: the command layer, driven through Composer's console application the way
composer remediateruns, against the synthetic fixture.remediate: option validation (format, report
spec, solver, release age), the lock-file check, the exit-code contract including a search budget
too small to reach the fix, every report file format next to the JSON on standard output,
--format=none, an unwritable report path,--fail-on,--ignore, the whole--baseline/
--update-baselineworkflow,--offline, advisory-source selection (missing snapshot, missing
database via option,REMEDIATE_DATABASEandextra.remediate.database) and platform flags
repeated in the recommended command.remediate:db-buildfrom a local FriendsOfPHP checkout plus
--include, the default build path, an unknown source;remediate:db-statuson the result
(metadata, sources, coverage gaps), on a missing file, via the environment variable, and on a
database built before coverage gaps were recorded. Plugin capability and command registration. - Tests: the feed readers with a scripted HTTP layer and archives built in the test.
ZipArchiveReader
(filtering, directory entries, a body that is not an archive, download failures, cleanup of the
download);OsvDumpSource(alias ranking, ADVISORY reference as link, "MODERATE" mapped to
medium, published/modified/withdrawn dates, several Packagist packages per document, other
ecosystems ignored, invalid JSON and unreadable versions as coverage gaps, gaps reset per fetch,
custom URL);PackagistApiSource(mapping, gaps, a body that is not an object, a document without
advisories, transport failures);FriendsOfPhpSourcefrom the GitHub archive (aliases, earliest
branch time, non-Composer advisories skipped without a gap, scalar documents, empty ranges and
invalid YAML as gaps naming the package from the path). The default advisory adapter
ComposerRepositoryAdvisoryProviderwith a fake Composer repository: conversion of full and partial
advisories, per-package caching and incremental queries, merging across repositories with
duplicate ids dropped, the failure when no repository provides advisories, transport failures,
construction from aRepositoryManager. Thecomposer auditfallback provider with a stand-in
binary (leading noise before the JSON, one run per process, no JSON, undecodable JSON).
NormalizedAdvisory,StrategyandVersionStephelpers.
Changed
- The
composer audit --lockedfallback adapter, described in the design decisions since 0.1.0 but
never wired in, is now used when Composer's in-process advisory API fails with a PHP error (a
removed method or changed signature in Composer's@internalclasses). The run switches once,
the report's advisory source shows the fallback, and a warning explains that only current-lock
advisories are known. Lookup failures are not retried through it: no repository providing
advisories, or a network failure, still exits with "advisory data unavailable" rather than a clean
result. Composer older than 2.4 is still rejected up front. Covered by unit tests of the switch
(PHP error switches and warns once, lookup failures and ordinary exceptions propagate, the primary
is not retried) and a command test that the default source with packagist.org disabled exits 4.
Fixed
- FriendsOfPHP advisories lost their report date: the upstream files write
time: 2024-05-01 10:00:00
unquoted, which the YAML parser hands over as an integer timestamp, and the source only accepted
text. Integer timestamps are now read, soreportedAtis populated for FriendsOfPHP records. - Test bootstrap: Composer reads
$_SERVERbeforegetenv(), so the cache isolation was lost when
the environment already exportedCOMPOSER_CACHE_DIR(DDEV does); tests now set both.
0.4.0
This release responds to an adversarial adoption review of 0.3.0 (twenty findings, nine rated as
able to undermine a security decision) and to the reviewer's recheck of the first response (six
remaining findings). Each item below names its change; the tests that establish it are listed in the
"Tests" bullets, and where a boundary is documented rather than removed the text says so.
Recheck (second round)
- The subprocess solver pins
COMPOSERto the scratch manifest. Before, the child inherited a
COMPOSER=/path/alternate.jsonfrom the parent and updated the analysed project's real lock while
the planner read the untouched scratch lock. Covered by an integration test that runs the real
Composer binary withCOMPOSERset to another manifest and asserts that file is unchanged. composer-remediatenever includes any project'svendor/autoload.php(Composer's autoloader
executesautoload.files, which is project code). It boots Composer from the phar it finds and
registers the plugin's own classes through a PSR-4 mapping. Covered by an entry-point test that
installs the binary in a project whose autoloader plants a probe and asserts the probe never runs.- Database ingestion keeps coverage gaps: an upstream record the build cannot interpret is stored in
agaptable with source, id, package and reason;composer remediatewarns for every gap that
names a package in the lock ("treated as unaffected by that record");remediate:db-statuslists
them; databases built before gap tracking are flagged. A private--includefile with an
unreadable record fails the build, matching--advisories-file. Database reads use the strict range
parser; the lenient one is gone. - OSV events are evaluated per the specification: sorted by version,
introducedopens an interval,
the nextfixed/last_affectedcloses it, andlimitcaps the whole range instead of closing an
interval of its own. "introduced 1.0, fixed 1.1, limit 2.0" no longer marks 1.5 affected. - The locator records whether a cached download was verified (
<cache>.status.json) and repeats the
disclosure on every cache hit, offline included; caches written by earlier versions are flagged as
unverified. - The fixture harness executes the printed command through a shell, verbatim, with a
composeron
PATH that adds--no-install; quoted constraints are no longer split on whitespace. - Documentation regrouped by intent (Use it, Understand it, Integrate it, Evidence, Project) with
three new pages: Reading the report, Comparison with other tools, and Case studies generated from
the fixture corpus (bin/case-studies.php, checked for staleness in CI).
Added
composer-remediatebinary: runs the same commands with the analysed project's plugins and
scripts disabled from the first instruction, reusing the installed Composer for its classes and
the fallback solver.composer remediate(the plugin command) keeps Composer's usual behaviour of
activating the project's other allowed plugins at startup; SECURITY.md and the privacy page now
state both boundaries precisely.--solver=auto|in-process|subprocess: the documented subprocess fallback is now wired. A
candidate whose in-process solve errors (not a conflict, not a network failure) is retried through
composer update --no-install; the report's solver line counts the retries.--solve-budget(default 60): hard ceiling on solver runs per finding across candidates, conflict
expansion, parent descent and simplification. Every solve is counted; the report shows the count
per finding (solver_runs) and in total, and a bounded search that finds nothing reads "none found
within the search budget" instead of "none".- Typed outcomes for findings without a fix:
none,none found within the search budget,
unknown: solver error(exit 3) orunknown: network failure while solving(exit 5). A tool
failure can no longer produce the actionable-policy exit 2. --ignore-platform-req/--ignore-platform-reqsare repeated in every recommended command, so
the printed command is the request that was verified.- Blocking-risk note when a recommended command moves a package to a version that still carries
another advisory (Composer 2.10+ advisory blocking may refuse it). - Client-side sha256 verification of a downloaded advisory database against the publisher's
.sha256sidecar; a missing sidecar and a stale cache reused after a failed refresh are reported
as warnings with the cache age. IgnorePolicy:config.audit.ignoreentries withapply: blockandconfig.policy.advisories
entries withon-audit: falseno longer suppress findings; package rules are matched as packages
(with their constraint), not as advisory ids.- Tests: planner rules with a scripted solver (unknown severity, tool-error and network exits,
combined-command cooldown,--no-devbaseline, all-advisory fixed range, platform flags, blocking
risk, budget), parser strictness, OSVversions/limit/event order, ignore scoping, HTML link
safety, fallback solver composition (with scripted routes), the real subprocess solver, the
standalone entry point, database download/checksum/cache behaviour with a scripted HTTP layer,
coverage gaps from build to report; freshly rendered JSON validated against the schema for every
fixture; the synthetic fixture's recommended command executed verbatim by a shell with the real
Composer binary and the resulting lock re-matched. Not covered by tests: the CLI driven against a
live advisory repository, and the Composer 2.4 advisory adapter beyond the CI matrix job.
Changed
- No advisory-capable repository, a malformed advisory document (an error payload, a malformed
entry, an unparsable range) now stop the run with exit 4 instead of reading as a clean lock.
composer audit --format=jsonoutput passed as--advisories-fileis recognised as covering the
current lock only. - OSV normalisation honours explicit
versionsin addition torangesand thelimitevent. - Unknown and unrecognised severities count towards
--fail-on. - The combined command is held to the same rules as individual candidates: no new advisories and
the--min-release-agecooldown; its simplified spelling is re-matched instead of inheriting the
original's results. Releases without a known date are refused by the cooldown. - Under
--no-devan untouched development finding is no longer counted as "newly introduced" by a
production fix. - The fixed range escapes every advisory known for the package, not only those affecting the locked
version; ignored advisories do not shrink it. - "Identical lock" for simplification now compares source and dist references and the
production/development split, not only versions. composerJsonPath()/lockPath()followCOMPOSER=alternate.json.--minimal-changesis documented as a Composer 2.7.0 feature (2.9 extended it), and the subprocess
solver's threshold follows. Composer older than 2.4 is refused at runtime. The Composer 2.4
SecurityAdvisoryclass has noseverityproperty; the adapter no longer reads it unconditionally.- HTML reports turn only
http(s)advisory links into anchors. --min-release-agerejects non-numeric input instead of coercing it to zero.- The advisory-database workflow publishes
sha256sumoutput (digest and filename) so
sha256sum -cworks as documented. - README no longer claims the smallest fix; the search is bounded and ranked.
- JSON report (schema still version 1, additive):
solver_runsandsearch_exhaustedper finding,
outcomeon findings without a fix,blocking_riskon verified ones,solver_runsin the
metadata.
[0.3.0] - 2026-09-09
Added
- SARIF 2.1.0 output (
--output=results.sarif,--format=sarif) for GitHub Code Scanning: one rule
per advisory with a numericsecurity-severity, one result per vulnerable package located at its
composer.lockline, the verified command in the message. --fail-on <severity>: only findings at or above the threshold affect the exit code; findings of
unknown severity always count. Shown in the summary and in the JSON report.- Published JSON Schema for the report (
docs/schema/report.schema.json), validated against every
stored fixture report. - Seven more historical fixtures (BookStack 2023 and 2024, Pixelfed, USAGov Drupal, Invoice Ninja,
Kimai 1.x, Shopware 6.4.20.2), each with stored console, JSON, HTML and SARIF reports. - Composer version matrix in CI: 2.4, 2.7, 2.8, 2.9 and latest on matching PHP versions.
- Generated CLI reference page, CI integration guide with gate policies.
Changed
- Composer releases before 2.10 have no
Installer::getLockTransaction(); the in-process solver
now performs a lock-only update inside the scratch copy there and reads the lock back. - Solver results are copied into detached package objects and cycles are collected after each
solve; peak memory for 136 solves dropped from 1.5 GB to 87 MB.
Advisory database db-2026-09-10.22
Advisory database built from Packagist, OSV and FriendsOfPHP. Dataset hash f6b93a192d58102490994b378bd72d1a58dcf619168834942b7f2a4d1bd45306. Use with composer remediate --database-location=https://github.com/hexblot/composer-remediate/releases/download/db-2026-09-10.22/advisories.sqlite. Aggregated advisory data; see docs/advisory-database.md for source licences.
Advisory database db-2026-09-10.19
Advisory database built from Packagist, OSV and FriendsOfPHP. Dataset hash 77dd96cd0586bcb27efbee76696d74d729aca7f5de6de7aeeea5a1c97bb6c5e7. Use with composer remediate --database-location=https://github.com/hexblot/composer-remediate/releases/download/db-2026-09-10.19/advisories.sqlite. Aggregated advisory data; see docs/advisory-database.md for source licences.
Advisory database db-2026-09-10.16
Advisory database built from Packagist, OSV and FriendsOfPHP. Dataset hash 10ebab3a041ad0273dfe265bed2c56561d811e22cf628711d2fea71b71c0c95c. Use with composer remediate --database-location=https://github.com/hexblot/composer-remediate/releases/download/db-2026-09-10.16/advisories.sqlite. Aggregated advisory data; see docs/advisory-database.md for source licences.
Advisory database db-2026-09-10.06
Advisory database built from Packagist, OSV and FriendsOfPHP. Dataset hash 08f6ceeff29c9345138ba11fb435f949bce18ea52a33be94d9ba61e4c8d8fac4. Use with composer remediate --database-location=https://github.com/hexblot/composer-remediate/releases/download/db-2026-09-10.06/advisories.sqlite. Aggregated advisory data; see docs/advisory-database.md for source licences.