Composer Audit & npm Audit in CI (15 Min)
The fastest way to stop a known-vulnerable dependency from reaching production is a two-line CI step: run npm audit or composer audit right after install, and fail the build above a severity you choose.
This guide is for developers and small-team founders who ship a Node.js or PHP project through CI and want a real security gate, not a nightly email they ignore. In 15 minutes you'll have a build step that checks every dependency against known advisories, fails loudly when something serious shows up, and stays quiet about the noise that doesn't matter yet.
This applies whether you're shipping a Node.js app, a Shopify app built with their CLI, or a PHP project managed with Composer (Laravel, Symfony, or a custom build). If your site is plain WordPress without Composer, this doesn't apply directly, since there's no composer.lock to audit; it's built for projects that use npm or Composer to manage dependencies.
What "audit" actually checks
npm audit compares your installed dependency tree against known advisories and flags matches. composer audit does the same for the packages listed in your composer.lock file. Neither tool scans your own code. They only check whether a version of a package you depend on has a published vulnerability.
The advisory data comes from the GitHub Advisory Database and from OSV.dev, an open vulnerability database. OSV also lists reports of malicious packages, which carry identifiers starting with MAL instead of a CVE number. That's a different problem from a coding bug: someone deliberately published bad code under a package name, and the fix is always "remove it," not "wait for a patch."
An advisory means a flaw was published, not that your site was attacked. Whether it actually matters depends on how your code uses the affected package: a vulnerability in a function you never call is lower priority than one in code that handles user input directly.
The 15-minute npm setup
- Make sure CI installs from the lockfile, not a loose
npm install.npm ciinstalls exactly what's inpackage-lock.jsonand fails ifpackage.jsonand the lockfile disagree, so you're always testing what you'll actually ship. - Add the audit step right after install.
name: Dependency audit
on:
push:
pull_request:
schedule:
- cron: '0 6 * * *'
jobs:
npm-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- run: npm audit --audit-level=high
That's the whole job. npm audit --audit-level=high exits with a non-zero code only when vulnerabilities at or above "high" are found, which is exactly what makes it usable as a CI gate instead of a wall of warnings. Lower it to moderate if you want to catch more; raise it to critical if your pipeline is drowning in medium-severity findings you haven't triaged yet. Full command reference is in the npm audit documentation.
The 15-minute Composer setup
composer audit ships in Composer 2.4 and later, so confirm your CI image is on at least that version first (composer --version).
name: Dependency audit
on:
push:
pull_request:
schedule:
- cron: '0 6 * * *'
jobs:
composer-audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
- run: composer install --no-interaction --no-progress
- run: composer audit
composer install deploys exactly the versions recorded in composer.lock, the same "test what you ship" principle as npm ci. composer audit then checks those locked packages against advisories and exits with a non-zero code when it finds any, which fails the build automatically. Full details are in the Composer CLI documentation.
Exit codes and severity thresholds, in plain terms
A CI job fails when a command exits with a non-zero code. That's the entire mechanism behind both tools.
| npm | Composer | |
|---|---|---|
| Install exactly what's locked | npm ci | composer install |
| Run the audit | npm audit | composer audit |
| Choose a severity threshold | npm audit --audit-level=high (or moderate, critical) | not available; any finding fails the build |
| Apply fixes within allowed version ranges | npm audit fix | composer update vendor/package |
| Find what's pulling in a vulnerable package | npm ls <package> | check composer.lock for the requiring package |
Because Composer doesn't give you a severity dial, a composer audit failure means "something was found," full stop. That's fine for most small projects, but every finding still needs a quick triage, not an automatic re-run.
What to do when the build fails
Don't just re-run the pipeline and hope it passes. Work through findings in this order:
- Try the automatic fix first.
npm audit fixapplies fixes that stay within the version ranges yourpackage.jsonalready allows, so it won't silently jump you to a breaking major version. There's no Composer equivalent that updates everything at once; update the specific package instead. - Update the specific package.
composer update vendor/packageupdates just that one dependency and leaves everything else locked. - Trace transitive dependencies. If the vulnerable package isn't one you require directly, run
npm ls <package>to see which of your direct dependencies pulled it in. You may need to wait on that parent package's own update, or replace it. - If there's genuinely no fix yet, accept the risk in writing. Note which advisory, why it doesn't apply to how your app uses the package, who signed off, and a date to re-check. A silent
--forceflag or a suppressed warning is how real findings get buried and forgotten.
Why a clean result today doesn't mean clean tomorrow
New advisories get published every day, often for versions of packages that were clean the day before. Your dependency tree didn't change overnight, but the known-vulnerability list did. That's why the audit has to run on every build and on a schedule, like the cron: '0 6 * * *' line in both workflows above, not just the one time someone remembers to check.
If you want to sanity-check a lockfile without wiring up CI yet, a quick way to start is pasting it into a free website security check: only the package names and versions are sent out for comparison against the OSV database, and the lockfile itself never leaves your browser.
Where this fits with a real security review
Dependency audits are automated pattern-matching against known, published flaws. They won't catch a logic bug in your own code, a misconfigured server, or a vulnerability nobody's reported yet. That's the gap between automated scanning and a manual penetration test: a human reviewer tests how your specific application actually behaves, not just which package versions it lists.
If your site is already showing signs of compromise, start with what to do when your website's been hacked instead of a dependency audit, since the two problems need different first steps.
Key takeaways
- Add
npm audit --audit-level=high(orcomposer audit) right afternpm ci/composer installin every CI run, and also on a daily schedule, since new advisories appear for versions that were clean yesterday. - A non-zero exit code is the whole mechanism: npm lets you set a severity floor, Composer fails on any finding.
- Work findings in order: automatic fix, update the specific package, trace the parent with
npm ls <package>, or document an accepted risk with a reason and a review date. - An advisory is a published flaw, not proof your site was attacked; check whether your code actually exercises the vulnerable path before panicking.
- Automated audits catch known, published issues in dependencies only; they're not a substitute for a human reviewing your actual application.
A 15-minute CI gate closes off one real risk: shipping a dependency with a known, published hole. It won't tell you about a bug in your own code or a misconfigured setting, which is exactly what a $49 manual audit is built to catch. Worth doing both.
A real human security engineer audits your whole site by hand and sends a full report: every issue, its severity, and the exact fix. From $49, with a 14-day money-back guarantee.
See pricing