Dependency audit
Every package your project installs is code you didn't write, and security advisories against those packages get published all the time. This page is for the developers and leads who act on those advisories in a project built from Vortex. After reading it, you can run the audit, decide what fails it, record an assessed exception, and find each finding in continuous integration (CI).
The dependency audit checks your packages against those advisories, with
each ecosystem's own package manager: composer audit for the PHP packages
and npm audit for the JavaScript ones. You can run both locally, and 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
both security auditing (reporting known security vulnerabilities and
abandoned packages) and version blocking (refusing to install or update
affected packages). Vortex uses it to keep vulnerabilities visible
without getting in the way of day-to-day dependency work.
Usage
Run the audit yourself at any time:
- 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 don't blockcomposer update,composer require, orcomposer remove, so dependency updates stay reproducible even when a new advisory is published upstream 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 require, andcomposer removeaudit:ignore,report, orfail, 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,
including 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 (for
example, a production site with strict supply-chain controls), set
advisories.block to true.
Ignoring
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 see later 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,
without installing anything. With advisories.audit at fail, any
advisory fails the workflow, even though installs and updates go through.
Ignoring failures
Set VORTEX_CI_COMPOSER_AUDIT_IGNORE_FAILURE to 1 (a repository variable
in GitHub Actions, an environment variable in CircleCI) to let the workflow
pass anyway. The audit still runs and reports every advisory in the job log.
Treat it as a one-off bypass while you deal with a known advisory, because
it lets every new advisory through as well.
➡️ 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 carrying its severity, its affected
version range, and a link to the advisory, located at the package's entry
in composer.lock. Abandoned packages show up there as well, 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 advisories in the code scanning inventory are the
same ones the audit fails on. An alert is identified by its advisory and
package, not its line, so it stays the same alert when composer.lock
changes around it.
➡️ See Code scanning.
Dismissing a finding
Dismissing an alert in the Security → Code scanning tab records a reason against that finding and removes it from the open list. That's the right tool for tracking an assessment.
The audit itself keeps reporting the advisory and failing the workflow
until you add the advisory to ignore-id as described in
Ignoring. Dismiss the alert to record the assessment, and add
the ignore-id entry to stop the build failing.
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 Vortex CLI's Development tools prompt. Without them, the CLI 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 one, the CLI 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. A development dependency
tree of this size carries low-severity advisories in transitive packages at
most times, which 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, so both .npmrc files carry the setting: one in the project root
and one in the custom theme. 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
instead, so update both when you change the threshold. The flag lives in
the CI configuration rather than in package-audit.jsonc, because
audit-ci passes every advisory when no threshold is set.
The threshold is deliberately not a Vortex variable: it belongs to npm's own configuration, where a project changes it directly.
Ignoring
When the advisory is in a transitive dependency and a patched version
exists, pin that version with an overrides entry like the following one:
{
"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, with 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 yet, or you've assessed that the advisory
doesn't affect your project, add it to the allowlist in the
package-audit.jsonc file 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 it.
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: the audit-ci command from
Usage lists the advisory under
Found vulnerable allowlisted advisories: instead of failing 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 it 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, and the code scanning upload converts that same report.
The JSON output leaves out the notes on allowlisted and unused entries,
which the text output prints in CircleCI and in the Usage
commands.
Ignoring failures
Set VORTEX_CI_NPM_AUDIT_IGNORE_FAILURE to 1 to let both 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: npm-audit for the root and npm-audit-theme
for the custom theme. That way, you can trace each finding back to its
tree.
Each advisory in a package's via list becomes its own alert, identified by
its GHSA ID; an entry that only names another vulnerable package doesn't.
The alert carries the advisory's CVSS score, or a score based on its
severity when the advisory has none, and 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. An entry limited to a
dependency path doesn't apply to the upload, so its advisory still raises
an alert. Dismissing an alert records the assessment but doesn't stop the
check failing; that still needs an overrides entry or an allowlist entry,
as described in Ignoring.
➡️ See Code scanning.