Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -234,17 +234,26 @@ jobs:
command: composer audit --locked || [ "${VORTEX_CI_COMPOSER_AUDIT_IGNORE_FAILURE:-0}" -eq 1 ]

#;< TOOL_NPM
# The threshold is passed here rather than in 'audit-ci.jsonc', because
# audit-ci passes every advisory when no threshold is set.
- run:
name: Audit npm packages
when: always
command: npm audit --package-lock-only || [ "${VORTEX_CI_NPM_AUDIT_IGNORE_FAILURE:-0}" -eq 1 ]
command: |
config=()
[ -f audit-ci.jsonc ] && config=(--config audit-ci.jsonc)
npx --yes audit-ci@7.1.0 --high --extra-args=--package-lock-only "${config[@]}" || [ "${VORTEX_CI_NPM_AUDIT_IGNORE_FAILURE:-0}" -eq 1 ]
#;> TOOL_NPM

#;< DRUPAL_THEME
- run:
name: Audit npm packages of the theme
when: always
command: npm audit --package-lock-only --prefix="${WEBROOT}/themes/custom/${DRUPAL_THEME}" || [ "${VORTEX_CI_NPM_AUDIT_IGNORE_FAILURE:-0}" -eq 1 ]
command: |
dir="${WEBROOT}/themes/custom/${DRUPAL_THEME}"
config=()
[ -f "${dir}/audit-ci.jsonc" ] && config=(--config "${dir}/audit-ci.jsonc")
npx --yes audit-ci@7.1.0 --high --extra-args=--package-lock-only --directory "${dir}" "${config[@]}" || [ "${VORTEX_CI_NPM_AUDIT_IGNORE_FAILURE:-0}" -eq 1 ]
#;> DRUPAL_THEME

#;< CI_GITLEAKS
Expand Down
13 changes: 11 additions & 2 deletions .github/workflows/audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,14 +115,23 @@ jobs:
#;< TOOL_NPM
- name: Audit npm packages
if: ${{ !cancelled() }}
run: npm audit --package-lock-only --json | tee .logs/audit/npm-audit.json
# The threshold is passed here rather than in 'audit-ci.jsonc', because
# audit-ci passes every advisory when no threshold is set.
run: |
args=(--high --extra-args=--package-lock-only --output-format json --report-type full)
[ -f audit-ci.jsonc ] && args+=(--config audit-ci.jsonc)
npx --yes audit-ci@7.1.0 "${args[@]}" | tee .logs/audit/npm-audit.json
continue-on-error: ${{ vars.VORTEX_CI_NPM_AUDIT_IGNORE_FAILURE == '1' }}
#;> TOOL_NPM

#;< DRUPAL_THEME
- name: Audit npm packages of the theme
if: ${{ !cancelled() }}
run: npm audit --package-lock-only --prefix="${WEBROOT}/themes/custom/${DRUPAL_THEME}" --json | tee .logs/audit/npm-audit-theme.json
run: |
dir="${WEBROOT}/themes/custom/${DRUPAL_THEME}"
args=(--high --extra-args=--package-lock-only --output-format json --report-type full --directory "${dir}")
[ -f "${dir}/audit-ci.jsonc" ] && args+=(--config "${dir}/audit-ci.jsonc")
npx --yes audit-ci@7.1.0 "${args[@]}" | tee .logs/audit/npm-audit-theme.json
continue-on-error: ${{ vars.VORTEX_CI_NPM_AUDIT_IGNORE_FAILURE == '1' }}
#;> DRUPAL_THEME

Expand Down
2 changes: 2 additions & 0 deletions .gitignore.artifact
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ web/sites/simpletest
/.twig-cs-fixer.php
/AGENTS.md
/CLAUDE.md
/audit-ci.jsonc
/docker-compose.yml
/eslint.config.mjs
/renovate.json
Expand Down Expand Up @@ -86,6 +87,7 @@ web/sites/simpletest
web/themes/**/node_modules

# Custom theme asset sources - only compiled assets in 'build' are deployed.
web/themes/custom/your_site_theme/audit-ci.jsonc
web/themes/custom/your_site_theme/eslint.config.mjs
web/themes/custom/your_site_theme/fonts
web/themes/custom/your_site_theme/images
Expand Down
4 changes: 2 additions & 2 deletions .vortex/docs/content/continuous-integration/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ The workflow runs the same 4 checks in both providers, and no check needs the
application containers or installed dependencies:

- `composer audit --locked` checks the packages pinned in `composer.lock` against published security advisories
- `npm audit --package-lock-only` checks the packages pinned in `package-lock.json` against published security advisories
- `npm audit --package-lock-only`, run through [audit-ci](https://github.com/IBM/audit-ci), checks the packages pinned in `package-lock.json` against published security advisories, skipping the ones allowlisted in `audit-ci.jsonc`
- the same `npm audit` runs against the custom theme's own `package-lock.json`
- [Gitleaks](../development/security/secret-scanning.mdx) scans the codebase for committed secrets

Expand Down Expand Up @@ -114,7 +114,7 @@ In GitHub Actions, every check also writes its findings as [SARIF](https://docs.

Code scanning keeps a history of every finding and tracks whether it's open or fixed. Dismissing a false positive records the reason in the alert instead of a committed ignore entry.

Gitleaks writes SARIF itself. Neither `composer audit` nor `npm audit` has a SARIF formatter, so each check runs once with its JSON format and pipes the output through `tee`. The report reaches the job log and a file in `.logs/audit/` in the same run. The `vortex-convert-audit-sarif` script from the [Vortex tooling package](../contributing/maintenance/tooling.mdx) then converts that file, reading the format from [`VORTEX_CONVERT_AUDIT_SARIF_FORMAT`](../development/variables.mdx#vortex_convert_audit_sarif_format).
Gitleaks writes SARIF itself. Neither `composer audit` nor `npm audit` has a SARIF formatter, so each check runs once with its JSON format and pipes the output through `tee`. The npm checks run `audit-ci` with `--output-format json --report-type full`, which prints npm's own report. The report reaches the job log and a file in `.logs/audit/` in the same run. The `vortex-convert-audit-sarif` script from the [Vortex tooling package](../contributing/maintenance/tooling.mdx) then converts that file, reading the format from [`VORTEX_CONVERT_AUDIT_SARIF_FORMAT`](../development/variables.mdx#vortex_convert_audit_sarif_format).

Nothing in the reporting path decides whether the build passes. The 4 check steps are what fail the workflow on findings, and they run before the conversion and upload steps and independently of them.

Expand Down
7 changes: 4 additions & 3 deletions .vortex/docs/content/development/faqs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -311,10 +311,11 @@ The CI security audit workflow runs `npm audit` against the root and the theme d

1. **Update the affected package**: try a newer version that resolves the advisory: `npm update <package-name>`.
2. **Review the advisory**: run `npm audit` for details, including which direct dependency pulls the affected package in, and assess whether it affects your project.
3. **Pin a patched transitive version**: when the affected package isn't a direct dependency, add an `overrides` entry for it in `package.json` and run `npm install`. `npm audit` has no per-advisory ignore list, so this is how a specific advisory leaves the tree.
4. **Bypass CI for a known issue**: set `VORTEX_CI_NPM_AUDIT_IGNORE_FAILURE` to `1` to let CI pass while the advisory is being addressed.
3. **Pin a patched transitive version**: when the affected package isn't a direct dependency, add an `overrides` entry for it in `package.json` and run `npm install`.
4. **Record an assessed exception**: if no fixed version exists or the vulnerability doesn't affect you, add it to the `allowlist` in `audit-ci.jsonc` with notes and an expiry date.
5. **Bypass CI for a known issue**: set `VORTEX_CI_NPM_AUDIT_IGNORE_FAILURE` to `1` to let CI pass while the advisory is being addressed.

➡️ See [Security > Dependency audit](security/dependency-audit.mdx#javascript-dependencies) for the severity threshold and the `overrides` mechanism.
➡️ See [Security > Dependency audit](security/dependency-audit.mdx#javascript-dependencies) for the severity threshold, the `overrides` mechanism, and the allowlist format.

## How to set a custom maintenance theme?

Expand Down
44 changes: 38 additions & 6 deletions .vortex/docs/content/development/security/dependency-audit.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,9 @@ A project has 2 JavaScript dependency trees, each with its own `package.json` an
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 audit-ci.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}/audit-ci.jsonc"
```
</TabItem>
<TabItem value="docker-compose" label="Docker Compose">
Expand All @@ -173,6 +176,9 @@ A project has 2 JavaScript dependency trees, each with its own `package.json` an
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 audit-ci.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}/audit-ci.jsonc"
```
</TabItem>
</Tabs>
Expand All @@ -185,13 +191,13 @@ npm fails the audit on any advisory by default. A development dependency tree of
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 the build.
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 the build. CI runs the audit through [audit-ci](https://github.com/IBM/audit-ci), which takes its threshold from the `--high` flag in each CI audit step instead, so change both when you move the bar. The flag stays in the CI configuration rather than in `audit-ci.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 without a template variable existing for it.

### Ignoring

`npm audit` has no per-advisory ignore list, so an advisory is excluded by removing the affected version from the tree. When the advisory is in a transitive dependency and a patched version exists, pin it with an `overrides` entry in `package.json`:
When the advisory is in a transitive dependency and a patched version exists, remove the affected version from the tree by pinning the patched one with an `overrides` entry in `package.json`:

```json
{
Expand All @@ -209,12 +215,38 @@ Record the advisory that each `overrides` entry addresses in your Git commit mes

:::

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 `audit-ci.jsonc` file next to the same `package.json`. `npm audit` has no per-advisory ignore list, so CI runs the audit through [audit-ci](https://github.com/IBM/audit-ci), which applies the allowlist. Put the reason in `notes` and the date to review the decision in `expiry`:

```jsonc
{
"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"
}
}
]
}
```

- An advisory that isn't allowlisted still fails the audit when its severity meets the [severity threshold](#severity-threshold).
- An entry stops applying once 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.
- In the text output, an entry that matches no advisory is reported with 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. The root and the custom theme each have their own `audit-ci.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.

### Continuous integration

**Vortex** runs `npm audit --package-lock-only` in the
**Vortex** runs the audit in the
[security audit workflow](../../continuous-integration/README.mdx#security-audit)
for both dependency trees, so the audited set is exactly what the lock files
pin and no installed tree is needed.
for both dependency trees with `audit-ci`, pinned to an exact version in the
workflow. Each audit step passes `--high` and `--package-lock-only`, and adds
the tree's `audit-ci.jsonc` when it exists. The audited set is exactly what the
lock files pin, so no installed tree is needed.

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](#usage-1) commands.

#### Ignoring failures

Expand All @@ -229,6 +261,6 @@ printed, so a tolerated advisory stays visible in the job log.

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. A finding can be traced back to the tree it came from. Each entry in a package's `via` list becomes its own alert, identified by its GHSA advisory, carrying the advisory's own CVSS score and located at the package's entry in that tree's `package-lock.json`.

Dismissing an alert records the assessment but doesn't stop the check failing; that still needs the affected version removed from the tree, as described in [Ignoring](#ignoring-1).
The report is npm's own output from the same audit, before the allowlist applies, so allowlisted advisories are uploaded too. 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](#ignoring-1).

➡️ See [Code scanning](../../continuous-integration/README.mdx#code-scanning).
2 changes: 1 addition & 1 deletion .vortex/installer/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
"alexskrypnyk/snapshot": "^1.5.0",
"bamarni/composer-bin-plugin": "^1.9.1",
"dealerdirect/phpcodesniffer-composer-installer": "^1.2.1",
"drevops/phpcs-standard": "^0.6.2",
"drevops/phpcs-standard": "^1.1.0",
"drupal/coder": "^9.0.1@alpha",
"ergebnis/composer-normalize": "^2.54.0",
"laravel/serializable-closure": "^2.1.0",
Expand Down
26 changes: 13 additions & 13 deletions .vortex/installer/composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion .vortex/installer/src/Prompts/Handlers/Tools.php
Original file line number Diff line number Diff line change
Expand Up @@ -620,7 +620,7 @@ public static function getToolDefinitions(string $filter = 'all'): array {
// the 'TOOL_ESLINT' removal of the single tool.
'frontend_all' => [
'tools' => [self::ESLINT, self::STYLELINT, self::JEST],
'files' => ['package.json', 'package-lock.json', '.npmrc'],
'files' => ['package.json', 'package-lock.json', '.npmrc', 'audit-ci.jsonc'],
'token' => 'TOOL_NPM',
],
];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,21 @@ jobs:

- name: Audit npm packages
if: ${{ !cancelled() }}
run: npm audit --package-lock-only --json | tee .logs/audit/npm-audit.json
# The threshold is passed here rather than in 'audit-ci.jsonc', because
# audit-ci passes every advisory when no threshold is set.
run: |
args=(--high --extra-args=--package-lock-only --output-format json --report-type full)
[ -f audit-ci.jsonc ] && args+=(--config audit-ci.jsonc)
npx --yes audit-ci@__VERSION__ "${args[@]}" | tee .logs/audit/npm-audit.json
continue-on-error: ${{ vars.VORTEX_CI_NPM_AUDIT_IGNORE_FAILURE == '1' }}

- name: Audit npm packages of the theme
if: ${{ !cancelled() }}
run: npm audit --package-lock-only --prefix="${WEBROOT}/themes/custom/${DRUPAL_THEME}" --json | tee .logs/audit/npm-audit-theme.json
run: |
dir="${WEBROOT}/themes/custom/${DRUPAL_THEME}"
args=(--high --extra-args=--package-lock-only --output-format json --report-type full --directory "${dir}")
[ -f "${dir}/audit-ci.jsonc" ] && args+=(--config "${dir}/audit-ci.jsonc")
npx --yes audit-ci@__VERSION__ "${args[@]}" | tee .logs/audit/npm-audit-theme.json
continue-on-error: ${{ vars.VORTEX_CI_NPM_AUDIT_IGNORE_FAILURE == '1' }}

- name: Install Vortex tooling
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// audit-ci allowlist for projects based on Vortex.
//
// Lists the npm advisories that the security audit skips. An entry stops
// applying once its expiry date is reached.
//
// https://github.com/IBM/audit-ci
{
"$schema": "https://github.com/IBM/audit-ci/raw/main/docs/schema.json",
"allowlist": [
{
"GHSA-vfj7-8cjw-p6xm": {
"active": true,
"notes": "No patched braces release exists yet. It is reached only through stylelint, which expands the glob patterns in this project's lint scripts, so no outside input reaches it and nothing from it ships with the site.",
"expiry": "__VERSION__"
}
}
]
}
Loading