PHPackages                             innobrain/soak-time - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Security](/categories/security)
4. /
5. innobrain/soak-time

ActiveComposer-plugin[Security](/categories/security)

innobrain/soak-time
===================

Protects against supply chain attacks by filtering recently published packages.

v1.6.0(1w ago)0290↑189.7%MITPHPPHP ^8.1

Since May 21Pushed 1w agoCompare

[ Source](https://github.com/innobraingmbh/composer-soak-time)[ Packagist](https://packagist.org/packages/innobrain/soak-time)[ RSS](/packages/innobrain-soak-time/feed)WikiDiscussions main Synced 1w ago

READMEChangelog (7)Dependencies (3)Versions (9)Used By (0)

[![Latest Version on Packagist](https://camo.githubusercontent.com/41df91b3767018a029bd72e19b47b1975d18293c50bf2fe65f63aff775417d4e/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f762f696e6e6f627261696e2f736f616b2d74696d652e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/innobrain/soak-time)[![Total Downloads](https://camo.githubusercontent.com/82770e2326e73d7e6f360f9acfbe026f0e33bdcce9989a39d0fadfb37d30f8be/68747470733a2f2f696d672e736869656c64732e696f2f7061636b61676973742f64742f696e6e6f627261696e2f736f616b2d74696d652e7376673f7374796c653d666c61742d737175617265)](https://packagist.org/packages/innobrain/soak-time)

Innobrain Soak Time 🛡️
======================

[](#innobrain-soak-time-️)

A Composer plugin that enforces a **soak time** — a minimum age — on every package version before install. New releases stay out of the solver pool until they age past the threshold, blocking zero-day malicious releases: typosquats, account takeovers, malicious co-maintainer pushes.

A release-date filter alone is defeatable: an attacker can force-push an *old* tag at a new malicious commit with a backdated `GIT_COMMITTER_DATE`, and Packagist serves the backdated timestamp. So the plugin also pins the **git SHA**, source URL, dist URL, and **dist sha256** of every version it sees. Those can't be backdated. If `vendor/pkg@1.2.3` later resolves to different integrity metadata or a different zip hash than the recorded one, the install hard-fails.

One product — soak time — done in a way that holds against both fresh malicious releases and altered historical releases (the laravel-lang / intercom-php 2026 pattern).

See [SECURITY\_MODEL.md](SECURITY_MODEL.md) for the short threat model, guarantees, and limits.

🧭 How soak time stays honest
----------------------------

[](#-how-soak-time-stays-honest)

Three checks run on every install/update. Together they make soak time undefeatable by `GIT_COMMITTER_DATE` rewrites.

CheckHookCatchesWhy it works**Timestamp filter** (`PackageFilter`)`PRE_POOL_CREATE`Fresh malicious releasesDrops recent versions from the solver pool until they reach minimum age.**Reference drift** (`ReferenceDriftCheck`)`PRE_POOL_CREATE`Altered historical releases (force-pushed tag)The git SHA is content-addressed over the commit's tree, parents, author, committer, message, *and dates*. Backdating `GIT_COMMITTER_DATE` still yields a different SHA, and the SHA can't be forged.**Hash pinning** (`HashVerifier`)`POST_FILE_DOWNLOAD`Cache poisoning at `~/.composer/cache/files/`Composer's native sha1 check is empty for GitHub-served packages; when Composer exposes the downloaded archive, we compute sha256 ourselves and compare against the pinned value.**Source install pinning** (`PackageIntegrityRecorder`)`POST_PACKAGE_INSTALL` / `POST_PACKAGE_UPDATE``--prefer-source` and source-only installsIf no dist archive is downloaded, the installed source reference is pinned and later checked. If Composer installs from dist without exposing the archive to the plugin, the run fails closed.The timestamp filter alone is **not** enough — the SHA/source-reference pin is the load-bearing primitive against altered historical releases. New pins are written to `composer-integrity.lock` as they are observed and saved again at `POST_INSTALL_CMD` / `POST_UPDATE_CMD`.

📦 Installation
--------------

[](#-installation)

Install as a dev dependency:

```
composer require --dev innobrain/soak-time
```

Or install globally to protect all local projects:

```
composer global require innobrain/soak-time
```

> **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
> ```

⚙️ Configuration
----------------

[](#️-configuration)

Default minimum age: **168 hours (7 days)**. Override in the `extra` section of `composer.json`:

```
{
    "extra": {
        "soak-time-hours": 360
    }
}
```

### Overriding the Soak Time per Run

[](#overriding-the-soak-time-per-run)

Override the configured soak time for a single run via the `SOAK_TIME_HOURS` environment variable (hours; takes precedence over `composer.json`):

**Linux / macOS:**

```
SOAK_TIME_HOURS=336 composer update
```

**Windows (PowerShell):**

```
$env:SOAK_TIME_HOURS=336; composer update
```

If the value isn't a non-negative integer (e.g. a typo), the plugin ignores it and warns rather than silently disabling protection.

### Whitelisting Packages

[](#whitelisting-packages)

Some packages need to update constantly — security advisories, internal company packages — and should bypass the soak filter. Whitelist them permanently via `soak-time-whitelist`:

```
{
    "extra": {
        "soak-time-hours": 168,
        "soak-time-whitelist": [
            "roave/security-advisories",
            "your-company/internal-package",
            "your-company/*"
        ]
    }
}
```

Entries support `*` wildcards in the **name** half only — the vendor half must always be a literal you've explicitly typed. `your-company/*` covers every package under the `your-company` vendor namespace; `your-company/lib-*` matches a name prefix within that vendor. `SOAK_TIME_SKIP` accepts the same wildcards (e.g. `SOAK_TIME_SKIP=your-company/* composer update`).

Patterns with a wildcard in the vendor half (`*/something`, `*/*`, bare `*`) are rejected and the plugin warns. A whitelist entry should always name a vendor the user trusts; allowing `*` on the left would let a single config line silently bypass the soak filter for every dependency. Use `SOAK_TIME_SKIP=1` for an intentional one-run global bypass instead.

Package versions with no release date are filtered out unless whitelisted. Add path repositories or internal repositories to the whitelist only when you intentionally trust their release metadata outside Packagist.

🔐 Integrity lock file
---------------------

[](#-integrity-lock-file)

The plugin maintains `composer-integrity.lock` next to `composer.json`. For every package version installed, it records:

- `sha256` of the downloaded zip, when Composer exposes the archive to the plugin
- `sourceReference` (git commit SHA)
- `sourceUrl`
- `distUrl`
- `firstSeenAt` timestamp

**Commit this file alongside `composer.lock`.** On every subsequent install, the SHA, source reference, source URL, and dist URL for each `name@version` are verified against the recorded values. Any drift hard-fails the run — the signal of an altered historical release.

Opt out (not recommended) with `extra.soak-time-integrity: false`, or relocate the file via `extra.soak-time-integrity-lock`.

The first time a version is seen is trust-on-first-use. Review and commit the generated integrity lock with the same care as `composer.lock`; subsequent installs enforce the recorded evidence.

Some Composer paths, including plugin self-updates, can install from dist without delivering the archive to the active plugin instance. In that case the plugin fails closed. Reinstall the package from source, for example:

```
composer global reinstall innobrain/soak-time --prefer-source
```

```
{
    "extra": {
        "soak-time-integrity": true,
        "soak-time-integrity-lock": "composer-integrity.lock"
    }
}
```

🚨 Emergency Freshness Skip (Security Patches)
---------------------------------------------

[](#-emergency-freshness-skip-security-patches)

To install a critical security patch released hours ago, skip the freshness filter for that package with `SOAK_TIME_SKIP`:

**Linux / macOS:**

```
SOAK_TIME_SKIP=vendor/package-name composer update vendor/package-name
```

**Windows (PowerShell):**

```
$env:SOAK_TIME_SKIP="vendor/package-name"; composer update vendor/package-name
```

`SOAK_TIME_SKIP=1` still skips the freshness filter for the whole run, but integrity checks remain enabled. Use the package-name form for emergency patches so the rest of the pool still soaks normally.

🔍 Debugging
-----------

[](#-debugging)

Run with `-v` to see which versions are dropped. Output is grouped per package:

```
composer update -v
```

```
[Soak Time] Inspecting packages (minimum age: 168h)...
  monolog/monolog — dropped 3 version(s) newer than 168h: 3.8.2, 3.8.3, 3.8.4 (newest released 2026-05-20 14:30)
[Soak Time] Filtered out 3 recent version(s) across 1 package(s).

```

🧩 When Resolution Fails
-----------------------

[](#-when-resolution-fails)

If the soak time hides **every** available version of a required package, Composer can't resolve dependencies. The plugin detects this up front and names the responsible package along with its latest version's age, instead of leaving a cryptic solver error:

```
[Soak Time] Every version of "vendor/package" was filtered out (newest is only 6h old; soak time is 168h).
            If Composer now fails to resolve dependencies, this is the likely cause. Options:
              - Lower the soak time:  SOAK_TIME_HOURS=6 composer update
              - Whitelist it:         add "vendor/package" to extra.soak-time-whitelist
              - One-run skip:         SOAK_TIME_SKIP=vendor/package composer update

```

Whitelist the package if it legitimately needs frequent updates; use a one-run skip for an emergency patch.

🙏 Credits
---------

[](#-credits)

Fork of [`cotonet/soak-time`](https://github.com/cotonet-resiliencia-digital/composer-soak-time) by **Cotonet - Resiliência Digital**. Thanks to the original authors for the foundation this fork builds on.

📄 License
---------

[](#-license)

MIT License — see [LICENSE](LICENSE).

Copyright Cotonet - Resiliência Digital (original) and Innobrain GmbH (fork).

###  Health Score

45

—

FairBetter than 91% of packages

Maintenance98

Actively maintained with recent releases

Popularity17

Limited adoption so far

Community8

Small or concentrated contributor base

Maturity47

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 77.3% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~1 days

Total

8

Last Release

13d ago

PHP version history (3 changes)v1.2.0PHP ^7.4 || ^8.0 || ^8.1 || ^8.2 || ^8.3 || ^8.4 || ^8.5

v1.3.0PHP ^8.2

v1.5.0PHP ^8.1

### Community

Maintainers

![](https://www.gravatar.com/avatar/beb1d357716423ec46cf7e3ff0827574b0e17a095bc2cd7b218bf5955056be9b?d=identicon)[innobrain](/maintainers/innobrain)

---

Top Contributors

[![kauffinger](https://avatars.githubusercontent.com/u/62616071?v=4)](https://github.com/kauffinger "kauffinger (17 commits)")[![cotonet-resiliencia-digital](https://avatars.githubusercontent.com/u/254997756?v=4)](https://github.com/cotonet-resiliencia-digital "cotonet-resiliencia-digital (5 commits)")

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/innobrain-soak-time/health.svg)

```
[![Health](https://phpackages.com/badges/innobrain-soak-time/health.svg)](https://phpackages.com/packages/innobrain-soak-time)
```

###  Alternatives

[symfony/runtime

Enables decoupling PHP applications from global state

74694.9M933](/packages/symfony-runtime)[drupal/core-composer-scaffold

A flexible Composer project scaffold builder.

5344.1M525](/packages/drupal-core-composer-scaffold)[drupal/core-vendor-hardening

Hardens the vendor directory for when it's in the docroot.

174.7M40](/packages/drupal-core-vendor-hardening)[drupal/core-project-message

Adds a message after Composer installation.

2124.0M192](/packages/drupal-core-project-message)[drupal-composer/drupal-paranoia

Composer Plugin for improving the security of composer-based Drupal projects by moving all PHP files out of docroot.

652.2M3](/packages/drupal-composer-drupal-paranoia)[altis/core

Core module for Altis

19222.5k2](/packages/altis-core)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
