Skip to main content
Version: v1

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:

# 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

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.block to false: security advisories don't block composer update, composer require, or composer remove, so dependency updates stay reproducible even when a new advisory is published upstream against an existing dependency. composer install installs exactly what the lock file pins and resolves nothing, so this setting never affects it. (Composer's own default is true.)
  • advisories.audit to fail: the composer audit command still fails when it finds an advisory, so vulnerabilities stay visible locally and in CI.
  • abandoned.audit to report: the audit reports abandoned packages as warnings without failing. (Composer's own default is fail.)

The advisories and abandoned sections accept the following keys:

  • block: refuses the affected packages during composer update, composer require, and composer remove
  • audit: ignore, report, or fail, for the composer audit command
  • ignore: 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.

Composer 2.10+

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.

It doesn't change what the audit does. composer audit still reports the advisory and still fails the workflow until the advisory is added 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 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 one, the installer removes the theme's audit steps from the CI configuration.

Usage​

Run the audit for each tree with the following commands:

# 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"

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:

  1. Add the entry to the package.json of the affected tree. The root package.json already has an overrides object, so add the entry to it.

  2. Run npm install for that tree to update its lock file. npm resolves every dependent to the pinned version, so the affected version leaves the tree:

    # Root tree
    ahoy cli "npm install"
    # Theme tree
    ahoy cli "npm install --prefix=\${WEBROOT}/themes/custom/\${DRUPAL_THEME}"
  3. 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 expiry date 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 active set to false doesn'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.