Skip to content
Open
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
89 changes: 85 additions & 4 deletions .github/workflows/audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ jobs:
audit:
runs-on: ubuntu-latest

permissions:
# The code scanning upload reads the workflow run in a private repository.
actions: read
contents: read
security-events: write

container:
# https://hub.docker.com/r/drevops/ci-runner
image: drevops/ci-runner:26.8.0@sha256:80be478cfff66d81fa804ccc707dbeae478655d8b850f1ae06824c194bd6528e
Expand Down Expand Up @@ -95,28 +101,103 @@ jobs:
run: composer update --no-install --no-audit --ignore-platform-reqs
#;> VORTEX_DEV

- name: Create the directory for the audit reports
if: ${{ !cancelled() }}
run: mkdir -p .logs/audit

#; Each audit runs once and its machine-readable report is teed to a file,
#; so the log still carries the output that the conversion below reads.
- name: Audit Composer packages
if: ${{ !cancelled() }}
run: composer audit --locked
run: composer audit --locked --format=json | tee .logs/audit/composer-audit.json
continue-on-error: ${{ vars.VORTEX_CI_COMPOSER_AUDIT_IGNORE_FAILURE == '1' }}

#;< TOOL_NPM
- name: Audit npm packages
if: ${{ !cancelled() }}
run: npm audit --package-lock-only
run: npm audit --package-lock-only --json | 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}"
run: npm audit --package-lock-only --prefix="${WEBROOT}/themes/custom/${DRUPAL_THEME}" --json | tee .logs/audit/npm-audit-theme.json
continue-on-error: ${{ vars.VORTEX_CI_NPM_AUDIT_IGNORE_FAILURE == '1' }}
#;> DRUPAL_THEME

- name: Install Vortex tooling
if: ${{ !cancelled() }}
run: ./scripts/vortex-tooling.sh

- name: Convert Composer audit findings to SARIF
if: ${{ !cancelled() }}
# Reporting must not decide the workflow result; the audit steps above
# are what fail on findings.
continue-on-error: true
run: ./vendor/bin/vortex-convert-audit-sarif .logs/audit/composer-audit.json composer

- name: Upload Composer audit findings to code scanning
if: ${{ !cancelled() && hashFiles('.logs/audit/composer-audit.sarif') != '' }}
uses: github/codeql-action/upload-sarif@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4
# The upload fails without a code scanning entitlement, and on fork
# pull requests where the token is read-only. The audit steps above are
# what report the findings and fail the workflow.
continue-on-error: true
with:
sarif_file: .logs/audit/composer-audit.sarif
category: composer-audit

#;< TOOL_NPM
- name: Convert npm audit findings to SARIF
if: ${{ !cancelled() }}
continue-on-error: true
run: ./vendor/bin/vortex-convert-audit-sarif .logs/audit/npm-audit.json npm

- name: Upload npm audit findings to code scanning
if: ${{ !cancelled() && hashFiles('.logs/audit/npm-audit.sarif') != '' }}
uses: github/codeql-action/upload-sarif@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4
continue-on-error: true
with:
sarif_file: .logs/audit/npm-audit.sarif
category: npm-audit
#;> TOOL_NPM

#;< DRUPAL_THEME
- name: Convert theme npm audit findings to SARIF
if: ${{ !cancelled() }}
continue-on-error: true
run: ./vendor/bin/vortex-convert-audit-sarif .logs/audit/npm-audit-theme.json npm "${WEBROOT}/themes/custom/${DRUPAL_THEME}/package-lock.json"

- name: Upload theme npm audit findings to code scanning
if: ${{ !cancelled() && hashFiles('.logs/audit/npm-audit-theme.sarif') != '' }}
uses: github/codeql-action/upload-sarif@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4
continue-on-error: true
with:
sarif_file: .logs/audit/npm-audit-theme.sarif
category: npm-audit-theme
#;> DRUPAL_THEME

#;< CI_GITLEAKS
- name: Scan for committed secrets with Gitleaks
if: ${{ !cancelled() }}
run: docker run --rm -v "${PWD}":/repo -w /repo ghcr.io/gitleaks/gitleaks:v8.30.1 dir . --no-banner
run: docker run --rm --volumes-from "${HOSTNAME}" -w "${PWD}" ghcr.io/gitleaks/gitleaks:v8.30.1 dir . --no-banner --report-format sarif --report-path .logs/audit/gitleaks.sarif
continue-on-error: ${{ vars.VORTEX_CI_GITLEAKS_IGNORE_FAILURE == '1' }}

- name: Upload Gitleaks findings to code scanning
if: ${{ !cancelled() && hashFiles('.logs/audit/gitleaks.sarif') != '' }}
uses: github/codeql-action/upload-sarif@b96794f015dfd88f77b49b1c93e0fa7110f94c63 # v4
continue-on-error: true
with:
sarif_file: .logs/audit/gitleaks.sarif
category: gitleaks
#;> CI_GITLEAKS

- name: Upload security findings as an artifact
if: ${{ !cancelled() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: security-findings
path: .logs/audit
include-hidden-files: true
if-no-files-found: ignore
1 change: 1 addition & 0 deletions .gitleaks.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ paths = [
'''(^|/)\.artifacts/''',
'''(^|/)\.data/''',
'''(^|/)\.git/''',
'''(^|/)\.logs/''',
#;< VORTEX_DEV
'''(^|/)\.vortex/''',
#;> VORTEX_DEV
Expand Down
27 changes: 25 additions & 2 deletions .vortex/docs/content/continuous-integration/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,8 @@ on its own:
| GitHub Actions | The `Security audit` workflow in `.github/workflows/audit.yml` |
| CircleCI | The `audit` workflow in `.circleci/config.yml` |

The workflow runs the same 4 checks in both providers, and needs neither the
application containers nor installed dependencies:
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
Expand All @@ -101,6 +101,29 @@ protection rules as a required status check.

:::

### Code scanning

In GitHub Actions, every check also writes its findings as [SARIF](https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning) and uploads them to the repository's **Security → Code scanning** tab, each under its own category:

| Category | Findings |
|----------|----------|
| `composer-audit` | `composer audit` advisories, abandoned packages and dependency policy matches |
| `npm-audit` | `npm audit` advisories for the root `package-lock.json` |
| `npm-audit-theme` | `npm audit` advisories for the custom theme's `package-lock.json` |
| `gitleaks` | Committed secrets |

Code scanning keeps a history of every finding, tracks whether it is open or fixed, and lets a false positive be dismissed with a recorded reason 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).

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

Code scanning has to be available on the repository - it is included on public repositories and on private repositories with GitHub Advanced Security. Where it is not available, and on pull requests from forks where the workflow token is read-only, the upload step fails without failing the workflow. No finding is lost: the checks above still report them in the job log and still fail the workflow, and every report is attached to the run as the `security-findings` artifact.

CircleCI has no code scanning destination, so the two providers are deliberately asymmetric here. Under CircleCI the same checks run and fail the `audit` workflow, but their findings live only in the job log.

➡️ See [Dependency audit](../development/security/dependency-audit.mdx#dismissing-a-finding) and [Secret scanning](../development/security/secret-scanning.mdx#dismissing-a-finding) for what dismissing a finding does and does not change.

## Working tree check

A build can produce files that were meant to be committed - exported
Expand Down
4 changes: 4 additions & 0 deletions .vortex/docs/content/development/security/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,7 @@ separate workflow, it is not a dependency of the `deploy` job - a failing audit
does not by itself stop a deployment. To block merges and deployments on it,
add its check to the repository's branch protection rules as a required status
check.

In GitHub Actions, both scans also upload their findings to the repository's **Security → Code scanning** tab, so a project has an inventory of what is currently open rather than only a sequence of passing and failing builds.

➡️ See [Code scanning](../../continuous-integration/README.mdx#code-scanning).
22 changes: 22 additions & 0 deletions .vortex/docs/content/development/security/dependency-audit.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,20 @@ one-off bypass while a known advisory is being addressed.

➡️ See [Ignore tool failures](../../continuous-integration/README.mdx#ignore-tool-failures).

### Where findings appear

In GitHub Actions, the audit findings are also uploaded 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 are reported there as well, at note severity, and so are dependency policy matches - including Composer's built-in `malware` policy, which is active by default - since `composer audit` fails on all three.

Advisories already listed under `ignore-id` are not uploaded, so the code scanning inventory holds the same set of findings the audit fails on.

➡️ See [Code scanning](../../continuous-integration/README.mdx#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, which is the right tool for tracking an assessment.

It does not 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](#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.
Expand Down Expand Up @@ -210,3 +224,11 @@ one-off bypass while a known advisory is being addressed. The finding is still
printed, so a tolerated advisory stays visible in the job log.

➡️ See [Ignore tool failures](../../continuous-integration/README.mdx#ignore-tool-failures).

#### Where npm findings appear

In GitHub Actions, the two trees are uploaded to **Security → Code scanning** under separate categories, `npm-audit` for the root and `npm-audit-theme` for the custom theme, so 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 does not stop the check failing; that still needs the affected version removed from the tree, as described in [Ignoring](#ignoring-1).

➡️ See [Code scanning](../../continuous-integration/README.mdx#code-scanning).
12 changes: 12 additions & 0 deletions .vortex/docs/content/development/security/secret-scanning.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -57,3 +57,15 @@ Set `VORTEX_CI_GITLEAKS_IGNORE_FAILURE` environment variable to `1` to ignore
failures. The tool will still run and report violations, if any.

➡️ See [Ignore tool failures](../../continuous-integration/README.mdx#ignore-tool-failures).

### Where findings appear

In GitHub Actions, Gitleaks findings are also uploaded to the repository's **Security → Code scanning** tab under the `gitleaks` category. Each finding becomes an alert naming the rule that matched, located at the line it was found on.

➡️ See [Code scanning](../../continuous-integration/README.mdx#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, which is the right tool for tracking an assessment.

It does not change what the scan does. Gitleaks still matches the value and still fails the workflow until the line carries a `gitleaks:allow` comment or the value is allowlisted in `.gitleaks.toml`, as described in [Ignoring](#ignoring).
Loading
Loading