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 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 audit for the PHP packages
  • npm audit for 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:

# 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 then leave composer update, composer require and composer remove unblocked, so dependency updates stay reproducible when a new advisory appears 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. 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:

# 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. 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:

  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, 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 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 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-audit for the root
  • npm-audit-theme for 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.