Your CI pipeline must fail whenever a Composer dependency has a known security advisory; how do composer audit and config.policy blocking cover that in Composer 2.10?
answer
- install from a lock does not block advisories
- update, require, remove block by default
- composer audit exits 0 or 1
- abandoned also fails by default
- advisories published after locking
basics
~20 sRun composer audit, which exits 1 when locked or installed packages match an advisory, as a CI step and on a schedule. Update-time blocking stops new vulnerable versions entering the lock, but composer install from an existing lock does not check advisories.
solid answer
~40 sComposer 2.10 has two separate mechanisms. **Blocking**, under `config.policy`, drops versions with active advisories while resolving, so `update`, `require` and `remove` cannot lock them; `policy.advisories.block` defaults to `true`. `composer install` from an existing lock does not resolve anything, so it does not block advisories (only malware by default). **Auditing** is `composer audit`: it checks installed packages, or the lock with `--locked`, against the Packagist advisory API and any configured repositories, and in 2.10 exits `0` or `1`. Advisories and abandoned packages both fail it by default. So CI runs `composer audit --locked` on every change and on a nightly schedule, because an advisory published after the lock was written only shows up when you audit again.
code
yaml · 6 lines# CI job fragment
steps:
- run: composer validate --strict
- run: composer install --no-interaction --no-progress
- run: composer audit --locked --format=json > audit.json
# a nightly scheduled run repeats the audit step on the main branchgo deeper
Know that composer audit lists packages with known security advisories and returns a failing exit code a CI step can use.
Separate blocking during update from auditing after install, and know that install from a lock does not block advisories.
Design the gate: audit --locked on every change and nightly, blocking left on, scoped ignores with reasons, and the 2.10 exit-code change reflected in scripts.
Set the organisation's policy on what fails a build: severities, abandoned packages, dev dependencies, and how long an accepted advisory may stay ignored.
## Two mechanisms that are easy to confuse Composer protects a project from known-vulnerable packages in two different places, and a CI design has to use both. | | Blocking | Auditing | |---|---|---| | Configured by | `config.policy.<policy>.block` | `config.policy.<policy>.audit`, the `audit` command's flags | | Runs during | `update`, `require`, `remove` (malware also on `install`) | `composer audit`; a short summary after `update`/`require`; `install --audit` | | Acts on | candidate versions while resolving | what is installed in `vendor/`, or the lock file with `--locked` | | Effect | vulnerable versions cannot be chosen | a report and an exit code | **Blocking** removes flagged versions from the pool before the solver runs. With `policy.advisories.block` at its default `true`, `composer update` cannot move you onto a version that has an active advisory, and if the only versions matching your constraints are affected, the update fails with an error saying a policy blocked them. **Auditing** looks at what you already have. That matters because the lock file is written once and advisories keep being published. ## The gap: install does not block advisories `composer install` reads `composer.lock` and installs exactly those versions. It does not resolve, so advisory blocking does not apply. In Composer 2.10 only the **malware** policy blocks at install time by default (`policy.malware.block-scope` defaults to `all`). A lock committed last month therefore installs cleanly today even if one of its packages received an advisory yesterday. The CI gate has to be an explicit audit. ## composer audit in 2.10 `composer audit` checks packages against the Packagist.org security advisory API by default, and against other repositories that publish advisories. Behaviour worth knowing: - **Exit code**: `0` when nothing matched, `1` when any package matched a policy or required packages were missing. Composer 2.10 simplified this; earlier 2.x releases used `1` for advisories, `2` for abandoned packages and `3` for both, so scripts that tested for `2` need updating. - **Abandoned packages fail too**: `policy.abandoned.audit` defaults to `fail`. Override with `--abandoned=report`, the `COMPOSER_AUDIT_ABANDONED` environment variable, or the config key. - `--locked` audits `composer.lock` without needing `vendor/`, so it can run as an early, cheap CI job. - `--no-dev` skips `require-dev` packages. This is useful for a production-only gate, while a full audit still covers tooling that runs with CI credentials. - `--format` accepts `table` (the command's default), `plain`, `json` and `summary`; `json` suits a report artifact. - `--ignore-severity=low` drops advisories of that severity for one run. `composer install --audit` runs the same audit after installing and exits with `5` when it fails. ## A pipeline that actually catches advisories 1. On every pull request: `composer validate --strict`, then `composer install --no-interaction`, then `composer audit --locked`. A failing audit blocks the merge. 2. On a nightly schedule against the main branch: `composer audit --locked`. This catches advisories published after the lock was written, when nobody has touched `composer.json`. 3. When a fix is needed: update the affected package with blocking left on, so the new lock cannot pick another affected version. 4. For an advisory the team has assessed as not applicable: record it in `policy.advisories.ignore-id` with a reason, rather than switching the gate off. ## Reading a finding Each advisory in the report carries the affected package and version range, a severity (`low`, `medium`, `high` or `critical`), an identifier and, where one exists, a CVE. Identifiers can be CVE, GHSA or Packagist's own `PKSA` IDs, and any of them can be used later in `policy.advisories.ignore-id`. Triage then has three outcomes: - a fixed version exists inside your constraints: update the package; - the fix needs a new major version: plan the upgrade, and decide whether to accept the advisory meanwhile; - the advisory does not apply to how you use the package: record an ignore with a reason and a review date. ## Mistakes to avoid - Relying on `composer install` alone, on the assumption that it checks advisories. - Running the audit only when dependencies change, which misses advisories published later. - Setting `"policy": false` or exporting `COMPOSER_NO_BLOCKING=1` in CI to get a build green. Both switch the controls off for every package, not just the one under review. - Setting `COMPOSER_NO_AUDIT=1` and assuming that disables `composer audit`. It only suppresses the automatic audit after `require`, `update`, `remove` and `create-project`. The audit needs network access to the advisory sources. `policy.ignore-unreachable` defaults to `["update", "install"]`, which does **not** include `audit`, so an audit that cannot reach a source fails instead of passing silently.
- The audit passed on Monday and fails on Tuesday with no code change. Is CI broken?No. A new advisory was published for a package in your lock file. That is exactly why the audit runs on a schedule. Update the affected package with blocking on, or, if you have assessed that the advisory does not apply, record its ID under `policy.advisories.ignore-id` with a reason and review it later.
- Why might composer audit fail even though no package has a security advisory?In Composer 2.10, `policy.abandoned.audit` defaults to `fail`, so an abandoned package in the lock fails the audit with exit code 1. Malware matches and custom policies also fail it. Use `--abandoned=report` or set `policy.abandoned.audit` to `report` if abandoned packages should be reported but not fail the build.
- Should the CI audit use --no-dev?Only for a gate that is specifically about what ships to production. Development packages still run on developer machines and in CI with access to tokens and source code, so a full audit, without `--no-dev`, is usually kept as well.
saying these in an interview costs you the question
- composer install blocks vulnerable versions from the lock file.
- Auditing on dependency changes alone is enough.
- composer audit returns 2 for abandoned packages in Composer 2.10.
- COMPOSER_NO_AUDIT=1 turns off the composer audit command.
- Setting policy to false in CI is a safe way to unblock a build.