Dependency audit
Every package your project installs is code you didn't write, and security advisories against those packages appear all the time. This page is for the developers and leads who act on those advisories in a project built from Vortex. It shows how to run the audit, decide what fails it, record an assessed exception and find each result in continuous integration (CI).
The dependency audit checks your packages against those advisories with each ecosystem's own package manager:
composer auditfor the PHP packagesnpm auditfor the JavaScript packages
You can run both locally. The CI security audit workflow runs them on the same pushes, pull requests and tags as the build pipeline.
PHP dependencies
composer audit checks the installed packages against published security
advisories and reports abandoned packages.
Composer's config.policy setting, available from Composer 2.10.0,
controls 2 behaviors:
- Security auditing reports known security vulnerabilities and abandoned packages.
- Version blocking refuses to install or update affected packages.
Vortex uses the setting to keep vulnerabilities visible while leaving day-to-day dependency work unblocked.
Usage
To run the audit at any time, use 1 of the following commands:
- Ahoy
- Docker Compose
# Audit all installed packages
ahoy composer audit
# Audit only production dependencies (exclude dev)
ahoy composer audit --no-dev
# Audit packages from lock file (faster)
ahoy composer audit --locked
# Control abandoned package behavior
ahoy composer audit --abandoned=ignore # Skip abandoned packages
ahoy composer audit --abandoned=report # Show but don't fail
# Audit all installed packages
docker compose exec cli composer audit
# Audit only production dependencies (exclude dev)
docker compose exec cli composer audit --no-dev
# Audit packages from lock file (faster)
docker compose exec cli composer audit --locked
# Control abandoned package behavior
# Skip abandoned packages
docker compose exec cli composer audit --abandoned=ignore
# Show but don't fail
docker compose exec cli composer audit --abandoned=report
Configuration
Configure the policy in your composer.json under the config section:
{
"config": {
"policy": {
"advisories": {
"block": false,
"audit": "fail"
},
"abandoned": {
"audit": "report"
}
}
}
}
Vortex sets the following values:
advisories.blocktofalse. Security advisories then leavecomposer update,composer requireandcomposer removeunblocked, so dependency updates stay reproducible when a new advisory appears against an existing dependency.composer installinstalls exactly what the lock file pins and resolves nothing, so this setting never affects it. Composer's own default istrue.advisories.audittofail. Thecomposer auditcommand still fails when it finds an advisory, so vulnerabilities stay visible locally and in CI.abandoned.audittoreport. The audit reports abandoned packages as warnings without failing. Composer's own default isfail.
The advisories and abandoned sections accept the following keys:
block: refuses the affected packages duringcomposer update,composer requireandcomposer removeaudit:ignore,reportorfail, for thecomposer auditcommandignore: package names to exempt
For assessed exceptions, advisories also accepts ignore-id and
ignore-severity. Composer's policy
documentation covers the
rest, including the malware and cooldown policies and the per-entry
on-audit and on-block scoping of ignore-id.
config.policy requires Composer 2.10 or newer, which ships in the
Vortex container images. On older Composer versions, use the legacy
config.audit keys (block-insecure, abandoned) instead. Composer 2.10
and newer still read those keys, but only when the matching config.policy
section is absent.
Why advisories don't block dependency updates
If advisories could block resolution, whether an update works would depend
on the day you run it. An advisory published this morning against a
package you already have can fail every composer update or
composer require. That includes work unrelated to security, until someone
assesses the advisory and ignores it. Setting advisories.block to false
takes advisory publication out of dependency resolution, so updates stay
reproducible. advisories.audit stays at fail, so composer audit and
the CI security audit workflow still surface every vulnerability.
If your project needs dependency resolution to stop hard on advisories, as
a production site with strict supply-chain controls might, set
advisories.block to true.
Ignore PHP advisories
When you've assessed an advisory and found it doesn't affect your project,
add it to ignore-id. Use the value from the advisory's Advisory ID or
CVE row in the composer audit output as the key, and the reason as the
value:
{
"config": {
"policy": {
"advisories": {
"ignore-id": {
"CVE-2024-1234": "Component is not used in our implementation.",
"GHSA-xxxx-yyyy-zzzz": "Patched via custom security fix."
}
}
}
}
}
Record the reason in your Git commit message too, so your team can later see why the vulnerability was acceptable and review the decision.
Confirm that ahoy composer audit no longer fails on the advisory. The
output lists it after the following line, with your reason in the
Ignore reason row:
Found 1 ignored security vulnerability advisory affecting 1 package:
Continuous integration
The security audit
workflow runs
composer audit --locked, so it audits exactly what composer.lock pins
and installs nothing. With advisories.audit at fail, any advisory fails
the workflow, even though installs and updates go through.
Ignore failures
Set VORTEX_CI_COMPOSER_AUDIT_IGNORE_FAILURE to 1 to let the workflow
pass anyway, as a repository variable in GitHub Actions or an environment
variable in CircleCI. The audit still runs and reports every advisory in
the job log. Treat the variable as a one-off bypass while you deal with a
known advisory, because it lets every new advisory through too.
➡️ See Ignore tool failures.
Where findings appear
In GitHub Actions, the workflow also uploads the audit findings to the
repository's Security > Code scanning tab under the composer-audit
category. Each advisory becomes an alert at the package's entry in
composer.lock. The alert carries the following details:
- The advisory's severity
- The affected version range
- A link to the advisory
Abandoned packages show up there too, at note severity. So do dependency
policy matches, including Composer's built-in malware policy, which is
active by default.
Advisories and policy matches fail the audit. Abandoned packages are
reported without failing it. Advisories already listed under ignore-id
aren't uploaded, so the code scanning inventory holds the same advisories
the audit fails on. An alert is identified by its advisory and package, so
it stays the same alert when its line in composer.lock moves.
➡️ See Code scanning.
Dismiss a finding
Dismissing an alert in the Security > Code scanning tab records a reason against that finding and removes it from the open list. Dismiss an alert to track an assessment.
The audit itself stays the same. composer audit still reports the
advisory and still fails the workflow until the advisory is in ignore-id,
as described in Ignore PHP advisories. Dismiss
the alert to record the assessment. To stop the build failing, also add the
ignore-id entry.
JavaScript dependencies
npm audit checks the packages resolved from package-lock.json against
the advisories published in the npm registry.
A project can have up to 2 JavaScript dependency trees, each with its own
package.json and lock file. The audit checks each tree separately:
- The root tree holds the linters and the test runner for the custom modules. It ships when you keep ESLint, Stylelint or Jest at the installer's Development tools prompt. Without them, the installer removes the root tree's audit steps from the CI configuration.
- The custom theme's tree holds its frontend build. It ships with a custom theme. Without a custom theme, the installer removes the theme's audit steps from the CI configuration.
Usage
Run the audit for each tree with the following commands:
- Ahoy
- Docker Compose
# Audit the root packages
ahoy cli "npm audit"
# Audit the theme packages
ahoy cli "npm audit --prefix=\${WEBROOT}/themes/custom/\${DRUPAL_THEME}"
# Audit from the lock file, without an installed tree
ahoy cli "npm audit --package-lock-only"
# Audit the root and the theme packages with the allowlist applied
ahoy cli "npx --yes audit-ci --high --extra-args=--package-lock-only \
--config package-audit.jsonc"
ahoy cli "npx --yes audit-ci --high --extra-args=--package-lock-only \
--directory \${WEBROOT}/themes/custom/\${DRUPAL_THEME} \
--config \
\${WEBROOT}/themes/custom/\${DRUPAL_THEME}/package-audit.jsonc"
# Audit the root packages
docker compose exec cli npm audit
# Audit the theme packages
docker compose exec cli bash -c \
"npm audit --prefix=\${WEBROOT}/themes/custom/\${DRUPAL_THEME}"
# Audit from the lock file, without an installed tree
docker compose exec cli npm audit --package-lock-only
# Audit the root and the theme packages with the allowlist applied
docker compose exec cli npx --yes audit-ci --high \
--extra-args=--package-lock-only --config package-audit.jsonc
docker compose exec cli bash -c \
"npx --yes audit-ci --high --extra-args=--package-lock-only \
--directory \${WEBROOT}/themes/custom/\${DRUPAL_THEME} \
--config \
\${WEBROOT}/themes/custom/\${DRUPAL_THEME}/package-audit.jsonc"
Severity threshold
npm fails the audit on any advisory by default. Development dependency
trees as large as these 2 carry low-severity advisories in transitive
packages most of the time. Those advisories would block every build, so
Vortex raises the floor to high in .npmrc:
audit-level=high
npm reads the project configuration from the directory of the
package.json it audits. The project root and the custom theme therefore
each have an .npmrc file with the setting. The accepted values are info,
low, moderate, high, critical and none. Lower the value to widen
what fails npm audit.
CI runs the audit through audit-ci,
which takes its threshold from the --high flag in each CI audit step.
When you change the threshold, update both .npmrc files and the flag in
each CI audit step. The flag lives in the CI configuration, outside
package-audit.jsonc, because audit-ci passes every advisory when no
threshold is set.
The threshold is deliberately left out of the Vortex variables: it belongs to npm's own configuration, where a project changes it directly.
Ignore JavaScript advisories
When the advisory is in a transitive dependency and a patched version
exists, pin that version with an overrides entry like the following:
{
"overrides": {
"vulnerable-package": "^2.1.4"
}
}
To apply the pin, complete these steps:
-
Add the entry to the
package.jsonof the affected tree. The rootpackage.jsonalready has anoverridesobject, so add the entry to it. -
Run
npm installfor that tree to update its lock file. npm resolves every dependent to the pinned version, so the affected version leaves the tree:# Root treeahoy cli "npm install"# Theme treeahoy cli "npm install --prefix=\${WEBROOT}/themes/custom/\${DRUPAL_THEME}" -
Commit both files, naming the advisory that the entry addresses in the commit message, the same as for the Composer exceptions.
Confirm that the advisory is gone: run the audit from Usage again and check that its ID no longer appears in the output.
When no patched version exists, or the advisory doesn't affect your
project, add it to the allowlist in package-audit.jsonc, which sits
next to the same package.json. npm audit has no per-advisory ignore
list, so CI runs the audit through
audit-ci, which applies the allowlist.
Put the reason in notes and the date to review the decision in expiry:
{
"allowlist": [
{
"GHSA-xxxx-yyyy-zzzz": {
"active": true,
"notes": "Reached only through build tooling that expands glob patterns written in this project.",
"expiry": "2026-12-31"
}
}
]
}
The allowlist follows these rules:
- An advisory that isn't allowlisted still fails the audit when its severity meets the severity threshold.
- An entry stops applying when its
expirydate is reached, or straight away if the date can't be parsed. The advisory then fails the audit again until it's resolved or the entry is renewed. - An entry with
activeset tofalsedoesn't apply. - In the text output, an entry that matches no advisory prints
Consider not allowlisting advisory:followed by its ID, as a suggestion to remove the entry.
Key each entry by the advisory's GitHub ID, the last part of the advisory
link that npm audit prints. audit-ci also accepts the following key
forms, described in the audit-ci README:
- A package name, which allowlists every advisory against that package
- An advisory ID paired with a dependency path, which limits the entry to that path
The root and the custom theme each have their own package-audit.jsonc,
so an advisory found in both trees needs an entry in both files. The file
is optional: without it, the audit of that tree runs with no allowlist.
Confirm that the entry applies: that tree's audit-ci command from
Usage lists the advisory under
Found vulnerable allowlisted advisories: and doesn't fail on it.
Continuous integration
The security audit
workflow audits
both dependency trees with audit-ci, pinned to an exact version in the CI
configuration. Each audit step passes --high and --package-lock-only,
and adds the tree's package-audit.jsonc when the file exists. The audit
reads what the lock files pin, so it needs no installed tree.
In GitHub Actions, the audit steps print audit-ci's JSON output, which is
npm's own report. The code scanning upload converts that same report. The
JSON output leaves out the messages about allowlisted and unused entries,
which the text output prints in CircleCI and in the Usage
commands.
Ignore failures
Set VORTEX_CI_NPM_AUDIT_IGNORE_FAILURE to 1 to let the root and theme
npm audit steps pass anyway. Use it as a one-off bypass while you deal
with a known advisory. The audit still prints the finding, so a tolerated
advisory stays visible in the job log.
➡️ See Ignore tool failures.
Where npm findings appear
In GitHub Actions, both trees are uploaded to Security > Code scanning under separate categories, so you can trace each finding back to its tree:
npm-auditfor the rootnpm-audit-themefor the custom theme
Each advisory in a package's via list becomes its own alert, identified by
its GHSA ID. A via entry that only names another vulnerable package
raises no alert. The alert carries the advisory's CVSS score, or a score
based on its severity when the advisory has none. It points at the
package's entry in that tree's package-lock.json.
An advisory allowlisted in a tree's package-audit.jsonc isn't uploaded
for that tree while its entry applies. After the entry expires or you
remove it, the next run uploads the advisory again. The upload ignores an
entry limited to a dependency path, so that advisory still raises an
alert.
Dismissing an alert records the assessment, and the check still fails. To
stop the failure, add an overrides entry or an allowlist entry, as
described in Ignore JavaScript advisories.
➡️ See Code scanning.