Skip to content

Releases: innobraingmbh/composer-soak-time

v1.10.0

Choose a tag to compare

@kauffinger kauffinger released this 16 Jun 09:13
Immutable release. Only release title and notes can be modified.
4c8e87e

Report release-time provenance in the soak filter output

Builds on v1.9.0 (prefer Packagist's server-stamped published-time) by making the filter show which clock it judged each version by — so you can see, at a glance, how much of the filtering rested on tamper-resistant data versus the backdatable time field.

Without -v

A one-line breakdown is added to the summary:

[Soak Time] Filtered out 151 recent version(s) across 62 package(s). Run with -v for details.
[Soak Time]   Judged: 140 by server-stamped publish time, 11 by self-reported time (backdatable), 0 with no release date.

The self-reported count is the residual surface a backdating attacker could still aim at (versions Packagist served no published-time for).

With -v

Every dropped version is annotated with its source and date:

  symfony/console — dropped 1 version(s) newer than 168h or without a release date:
    8.2.x-dev  [published-time 2026-06-09 11:23, server-stamped]
  vendor/example — dropped 1 version(s) newer than 168h or without a release date:
    9.9.9  [time 2026-06-16 07:09, self-reported (backdatable)]

Notes

  • A new ReleaseTime / ReleaseTimeSource pair, resolved once in PublishedTimeResolver::releaseTime(), is the single source of truth for both the filter decision and its reporting — they cannot disagree.
  • The breakdown counts the filtered (dropped) versions — the recent ones inside the soak window — so the numbers stay signal, not noise.
  • No change to the filter decision itself; this is pure observability on top of v1.9.0.

Full changelog: v1.9.0...v1.10.0

v1.9.0

Choose a tag to compare

@kauffinger kauffinger released this 16 Jun 08:54
Immutable release. Only release title and notes can be modified.
adc16d3

Prefer Packagist's server-stamped published-time for soak time

The freshness filter now prefers Packagist's published-time over the time field when deciding whether a version has aged past the soak window.

Why it matters

time is the tag's committer timestamp — whoever publishes a release controls it and can backdate it, making a fresh malicious version look mature and slip straight past the soak window. published-time is stamped by the registry at publish and cannot be moved earlier, so it closes that bypass.

The field is dropped by Composer's ArrayLoader and never reaches the package objects, so the plugin reads it back from the repository metadata cache that Composer populates while building the solver pool — before the filter runs.

Behavior

  • Preferred when present, with fallback to time for releases predating the field and for non-Packagist sources (Satis, VCS, artifact) that don't emit it.
  • Can only tighten the filter, never loosen it — every failure path falls back to the package's own release date.
  • The integrity/drift layer is unchanged; it keys on the content-addressed source reference, not timestamps.

Verified end-to-end against live Packagist: the resolver matched the authoritative published-time for 753/753 cross-checked symfony/console versions, and the filter correctly drops a version whose published-time is fresh despite a backdated time.

Full changelog: v1.8.0...v1.9.0

v1.8.0

Choose a tag to compare

@kauffinger kauffinger released this 13 Jun 10:48
Immutable release. Only release title and notes can be modified.
7419462

Ignore packages with abnormal dist plugins

Some Composer plugins install multiple dist archives under a single package@version. The clearest example is statamic/cms, which uses pixelfear/composer-dist-plugin to fetch both dist.tar.gz and dist-frontend.tar.gz — both presenting as statamic/cms@dist. Because the integrity model records one set of metadata per package@version, the second archive looked exactly like a drifted dist URL and hard-failed:

[Soak Time] Integrity metadata drift for statamic/cms@dist.
  Field:     dist URL
  Recorded:  …/v5.73.24/dist.tar.gz
  Candidate: …/v5.73.24/dist-frontend.tar.gz

New: soak-time-integrity-ignore

There is no safe way to auto-support this — accepting a new dist URL under an already-pinned version on a package's say-so is precisely the altered-historical-release surface the plugin exists to close. Instead, you can now manually greenlight a package after verifying its installs:

{
    "extra": {
        "soak-time-integrity-ignore": ["statamic/cms"]
    }
}

Or as a comma-separated env var for a one-run override:

SOAK_TIME_INTEGRITY_IGNORE=statamic/cms composer update

A listed package is exempt from all integrity checks (drift, hash, and recording). Patterns follow the same rules as the whitelist — the vendor must be a literal, * is allowed only in the name half. A warning naming the ignored package(s) prints on every run so the weakened coverage stays visible, and the soak/freshness filter is unaffected.

See SECURITY_MODEL.md for why this is an explicit operator opt-in rather than automatic behaviour.

Full changelog: v1.7.0...v1.8.0

v1.7.0

Choose a tag to compare

@kauffinger kauffinger released this 11 Jun 09:26
Immutable release. Only release title and notes can be modified.
cd480fc

v1.7.0 — Mutable dev branches & path-repository support

Adds first-class handling for two kinds of dependencies the immutable-pin model previously couldn't cope with: local path repositories and intentionally-mutable dev branches.

Added

  • soak-time-dev-branches — opt-in mutable dev versions. Dev versions (dev-main, 1.x-dev, …) advance their git SHA every time the branch moves, which used to make composer update hard-fail once a pinned reference drifted. Declare such packages and the plugin re-pins them on each advance instead of blocking:
{
    "extra": {
        "soak-time-dev-branches": ["your-company/my-lib", "your-company/*"]
    }
}
  • Also settable per-run via SOAK_TIME_DEV_BRANCHES=your-company/my-lib composer update (comma-separated). Patterns follow the whitelist rules — literal vendor, * allowed only in the name half. Re-pinning is keyed solely on the git source reference (the unforgeable commit SHA): if the SHA is held fixed but the downloaded archive's sha256 differs, the plugin still hard-fails, since that's cache poisoning rather than a branch advance. Undeclared dev drift is blocked with an error that names the config key.

Fixed

  • Local path repositories no longer break installs. A path-repo dependency has no dist archive hash and no source reference, so the integrity recorder threw Cannot pin integrity for <pkg>@dev-master and failed every install. Path packages are local code in the same trust domain as the root project, so they're now exempt from integrity pinning and drift checks. The exemption is gated on the dist URL actually being a local filesystem path, so a remote package can't dodge integrity checks by merely claiming dist.type: path.

Internal

  • Folded the mutable-dev gate into a single mutableDevAdvanced() predicate so re-pinning can never be triggered for a non-dev or undeclared package. No behavior change.
  • Test suite: 73 tests, 141 assertions.

Full Changelog: v1.6.0...v1.7.0

v1.6.0

Choose a tag to compare

@kauffinger kauffinger released this 27 May 15:52
Immutable release. Only release title and notes can be modified.
7edc873

What's new

  • feat: allow wildcards in package names for easier whitelisting

Full Changelog: v1.5.0...v1.6.0

v1.5.0

Choose a tag to compare

@kauffinger kauffinger released this 27 May 14:40
Immutable release. Only release title and notes can be modified.
d1504c5

What changed

  • Add reinstall hint to fix stuck at update
  • Allow php 8.1 usage

Full Changelog: v1.4.2...v1.5.0

v1.4.2

Choose a tag to compare

@kauffinger kauffinger released this 27 May 13:43
Immutable release. Only release title and notes can be modified.
f93e6f2

What changed

  • Fail closed when Composer installs from dist but the active plugin did not observe the archive download.
  • Keep recovery explicit: rerun affected updates with --prefer-source so Composer verifies the pinned source reference.
  • Save observed dist hashes immediately when the archive download is available.
  • Keep Composer root packages, such as Laravel apps named laravel/laravel, in the solver pool.

v1.4.1

Choose a tag to compare

@kauffinger kauffinger released this 27 May 12:58
Immutable release. Only release title and notes can be modified.
900eda3

What's changed

  • Fail closed when integrity lock entries are malformed or package metadata drifts.
  • Keep integrity checks enabled when using SOAK_TIME_SKIP; skips now affect freshness filtering only.
  • Pin source installs by source reference and source URL.
  • Filter packages with missing release dates, while allowing Composer platform packages like php and ext-*.
  • Add a short SECURITY_MODEL.md describing the threat model, trust boundary, and limits.

v1.4.0

Choose a tag to compare

@kauffinger kauffinger released this 26 May 15:29
Immutable release. Only release title and notes can be modified.
a696206

v1.4.0

Soak time now also defends against altered historical releases (force-pushed tags). Every installed name@version is pinned by git SHA and dist sha256 in composer-integrity.lock; later drift hard-fails the run. Commit the lock alongside composer.lock. Opt out with extra.soak-time-integrity: false.

Upgrading from ≤ v1.3.0? A direct composer update will fail because the old plugin's SoakTimeConfig class is still in PHP memory when the new code activates. Reinstall instead:

composer global remove innobrain/soak-time && composer global require innobrain/soak-time
# or, for project-local installs:
composer remove --dev innobrain/soak-time && composer require --dev innobrain/soak-time

Full Changelog: v1.3.0...v1.4.0

v1.3.0

Choose a tag to compare

@kauffinger kauffinger released this 21 May 14:58
Immutable release. Only release title and notes can be modified.
2e97b4a

Full Changelog: v1.2.1...v1.3.0