Skip to content
Merged
47 changes: 37 additions & 10 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -146,12 +146,6 @@ jobs:
command: docker run --rm -v "${PWD}":/app zavoloklom/dclint:3.1.0 . || [ "${VORTEX_CI_DCLINT_IGNORE_FAILURE:-0}" -eq 1 ]
#;> TOOL_DCLINT

#;< CI_GITLEAKS
- run:
name: Scan for committed secrets with Gitleaks
command: docker run --rm -v "${PWD}":/repo -w /repo ghcr.io/gitleaks/gitleaks:v8.30.1 dir . --no-banner || [ "${VORTEX_CI_GITLEAKS_IGNORE_FAILURE:-0}" -eq 1 ]
#;> CI_GITLEAKS

- run:
name: Build stack
command: docker compose up --no-deps --detach cli
Expand All @@ -166,10 +160,6 @@ jobs:
docker compose exec $(env | cut -f1 -d= | sed 's/^/-e /') -T cli bash -c "yarn install --frozen-lockfile"
#;> TOOL_ESLINT_STYLELINT

- run:
name: Audit Composer packages
command: docker compose exec -T cli composer audit || [ "${VORTEX_CI_COMPOSER_AUDIT_IGNORE_FAILURE:-0}" -eq 1 ]

- run:
name: Validate Composer configuration is normalized
command: docker compose exec -T cli composer normalize --dry-run || [ "${VORTEX_CI_COMPOSER_NORMALIZE_IGNORE_FAILURE:-0}" -eq 1 ]
Expand Down Expand Up @@ -218,6 +208,34 @@ jobs:
docker compose exec -T cli bash -c "yarn --cwd=\${WEBROOT}/themes/custom/\${DRUPAL_THEME} run lint" || [ "${VORTEX_CI_NODEJS_LINT_IGNORE_FAILURE:-0}" -eq 1 ]
#;> DRUPAL_THEME

# Audit job runs in its own workflow, independently of the commit workflow.
audit:
<<: *runner_config
steps:
- checkout
- *load_variables_from_dotenv

#;< CI_GITLEAKS
- *step_setup_remote_docker

- run:
name: Scan for committed secrets with Gitleaks
command: docker run --rm -v "${PWD}":/repo -w /repo ghcr.io/gitleaks/gitleaks:v8.30.1 dir . --no-banner || [ "${VORTEX_CI_GITLEAKS_IGNORE_FAILURE:-0}" -eq 1 ]
#;> CI_GITLEAKS

#;< VORTEX_DEV
#; Vortex does not track 'composer.lock', so resolve dependencies to give
#; the audit below something to read. Nothing is installed, so the runner
#; does not need to provide the platform extensions Drupal requires.
- run:
name: Create Composer lock file
command: composer update --no-install --no-audit --ignore-platform-reqs
#;> VORTEX_DEV

- run:
name: Audit Composer packages
command: composer audit --locked || [ "${VORTEX_CI_COMPOSER_AUDIT_IGNORE_FAILURE:-0}" -eq 1 ]

#;< !PROVISION_TYPE_PROFILE
# Database handling is a first step of the build.
# - $VORTEX_CI_DB_CACHE_TIMESTAMP is used to determine if a fresh DB dump
Expand Down Expand Up @@ -719,6 +737,15 @@ workflows:
#=============================================================================
#;> VORTEX_DEV

# Security audit workflow. Runs for every commit push to the remote repository,
# independently of the commit workflow.
audit:
jobs:
- audit:
filters:
tags:
only: /.*/

#;< !PROVISION_TYPE_PROFILE
# Nightly database workflow runs overnight to capture fresh database and cache it.
nightly-db:
Expand Down
106 changes: 106 additions & 0 deletions .github/workflows/audit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
# GitHub Actions configuration file.
#
# Security checks run in their own workflow so that a failing audit is
# distinguishable from a failing linter and can be re-run on its own.
#;
#; Comments starting with '#;<' and '#;>' are internal Vortex comments
#; and will be removed during installation or update of Vortex.
name: Security audit

on:
push:
# Pushes to the following branches will trigger the workflow.
branches:
- production
- main
- master
- develop
- release/**
- hotfix/**
- project/**
#;< VORTEX_DEV
- '*.x'
#;> VORTEX_DEV
# Pushes of tags will also trigger the workflow.
tags:
- '*'
# Pull requests to the following branches will trigger the workflow.
pull_request:
types:
- opened
- synchronize
- reopened
branches:
- production
- main
- master
- develop
- release/**
- hotfix/**
- feature/**
- bugfix/**
- project/**
#;< VORTEX_DEV
- '*.x'
#;> VORTEX_DEV

workflow_dispatch:

defaults:
run:
shell: bash

# Workaround for the Actions runner creating /root/.docker/config.json
# with permissions that prevent the container from reading it.
# https://github.com/actions/runner/issues/863
env:
DOCKER_CONFIG: /tmp/.docker

permissions:
contents: read

jobs:

audit:
runs-on: ubuntu-latest

container:
# https://hub.docker.com/r/drevops/ci-runner
image: drevops/ci-runner:26.7.0@sha256:6f917acfc2903e77e1bd9a2a32f3633ff49538553474fe43df3033352e0059b0
env:
TZ: ${{ vars.TZ || 'UTC' }}
TERM: xterm-256color
VORTEX_DEBUG: ${{ vars.VORTEX_DEBUG }}

steps:
- name: Preserve $HOME set in the container
run: echo HOME=/root >> "$GITHUB_ENV" # https://github.com/actions/runner/issues/863

- name: Check out code
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false

- name: Fix Git ownership permissions
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"

- name: Load environment variables from .env
run: t=$(mktemp) && export -p >"${t}" && set -a && . ./.env && set +a && . "${t}" && env >> "$GITHUB_ENV"

#;< CI_GITLEAKS
- name: Scan for committed secrets with Gitleaks
run: docker run --rm -v "${PWD}":/repo -w /repo ghcr.io/gitleaks/gitleaks:v8.30.1 dir . --no-banner
continue-on-error: ${{ vars.VORTEX_CI_GITLEAKS_IGNORE_FAILURE == '1' }}
#;> CI_GITLEAKS

#;< VORTEX_DEV
#; Vortex does not track 'composer.lock', so resolve dependencies to give
#; the audit below something to read. Nothing is installed, so the runner
#; does not need to provide the platform extensions Drupal requires.
- name: Create Composer lock file
run: composer update --no-install --no-audit --ignore-platform-reqs
#;> VORTEX_DEV

- name: Audit Composer packages
run: composer audit --locked
continue-on-error: ${{ vars.VORTEX_CI_COMPOSER_AUDIT_IGNORE_FAILURE == '1' }}
10 changes: 0 additions & 10 deletions .github/workflows/build-test-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -141,12 +141,6 @@ jobs:
continue-on-error: ${{ vars.VORTEX_CI_DCLINT_IGNORE_FAILURE == '1' }}
#;> TOOL_DCLINT

#;< CI_GITLEAKS
- name: Scan for committed secrets with Gitleaks
run: docker run --rm -v "${PWD}":/repo -w /repo ghcr.io/gitleaks/gitleaks:v8.30.1 dir . --no-banner
continue-on-error: ${{ vars.VORTEX_CI_GITLEAKS_IGNORE_FAILURE == '1' }}
#;> CI_GITLEAKS

- name: Build stack
run: docker compose up --no-deps --detach cli

Expand All @@ -159,10 +153,6 @@ jobs:
docker compose exec $(env | cut -f1 -d= | sed 's/^/-e /') -T cli bash -c "yarn install --frozen-lockfile"
#;> TOOL_ESLINT_STYLELINT

- name: Audit Composer packages
run: docker compose exec -T cli composer audit
continue-on-error: ${{ vars.VORTEX_CI_COMPOSER_AUDIT_IGNORE_FAILURE == '1' }}

- name: Validate Composer configuration is normalized
run: docker compose exec -T cli composer normalize --dry-run
continue-on-error: ${{ vars.VORTEX_CI_COMPOSER_NORMALIZE_IGNORE_FAILURE == '1' }}
Expand Down
31 changes: 20 additions & 11 deletions .vortex/docs/content/_code-lifecycle.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -25,25 +25,25 @@
│ │ ▼ │ │ exists? │ │
│ │ Composer validate │ │ │ No │ │
│ │ ▼ │ │ ▼ │ │
│ │ Composer audit │ │ Download ──► Sanitize ──► Store database │ │
│ │ Composer normalize │ │ Download ──► Sanitize ──► Store database │ │
│ │ ▼ │ │ production database cache │ │
│ │ Composer normalize │ │ database Remove │ │
│ │ Hadolint │ │ database Remove │ │
│ │ ▼ │ │ sensitive data │ │
│ │ Hadolint │ │ │ │
│ │ DCLint │ │ │ │
│ │ ▼ │ └───────────────────────────┬─────────────────────────────┘ │
│ │ DCLint │ │ │
│ │ PHPCS │ │ │
│ │ ▼ │ ▼ │
│ │ PHPCS │ ┌─ Build Job ────────────────────────────────────────────┐ │
│ │ PHPStan │ ┌─ Build Job ────────────────────────────────────────────┐ │
│ │ ▼ │ │ │ │
│ │ PHPStan │ │ Code assembly │ │
│ │ Rector │ │ Code assembly │ │
│ │ ▼ │ │ Build Docker ──► Composer deps ──► NPM deps ──► Assets│ │
│ │ Rector │ │ │ │ │
│ │ Twig CS Fixer │ │ │ │ │
│ │ ▼ │ │ ▼ │ │
│ │ Twig CS Fixer │ │ Website setup │ │
│ │ Gherkin Lint │ │ Website setup │ │
│ │ ▼ │ │ Import cached DB ──► drush deploy ──► Custom scripts │ │
│ │ Gherkin Lint │ │ │ │ │
│ │ │ │ ▼ │ │
│ │ ESLint / Stylelint │ │ Testing │ │
│ │ ESLint / Stylelint │ │ │ │ │
│ │ │ │ ▼ │ │
│ │ │ │ Testing │ │
│ │ │ │ PHPUnit tests ──► Behat tests │ │
│ │ │ │ │ │
│ │ │ └────────────────────────────┬───────────────────────────┘ │
Expand Down Expand Up @@ -74,4 +74,13 @@
═════════════════════════════════════════════════════════════════════════════════════════
┊ PR Environment ┊ Dev Staging Production
┊ (auto-removed) ┊ develop branch main branch production branch or tag


Security Audit Workflow
═════════════════════════════════════════════════════════════════════════════════════════
Push, pull request or manual run ──► Gitleaks secret scan ──► Composer advisory audit
(committed secrets) (composer audit --locked)

Runs as its own workflow, independently of the pipeline above, and does not gate
deployment.
```
24 changes: 23 additions & 1 deletion .vortex/docs/content/continuous-integration/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import CodeLifecycle from '../_code-lifecycle.mdx';
- Lints Dockerfiles and Docker Compose files
- Installs development dependencies
- Runs all code linters: PHPCS, PHPStan, Rector, Twig CS Fixer, Gherkin Lint, ESLint, Stylelint
- Audits and normalizes Composer packages
- Checks that Composer configuration is normalized

### 2. Database

Expand All @@ -57,6 +57,28 @@ import CodeLifecycle from '../_code-lifecycle.mdx';
- Adds required secrets and environment variables
- Triggers a deployment using a router script

## Security audit

Security checks run in their own workflow, separate from the pipeline above, so that a failing audit is never confused with a failing linter and can be re-run on its own:

| Provider | Location |
|----------|----------|
| GitHub Actions | The `Security audit` workflow in `.github/workflows/audit.yml` |
| CircleCI | The `audit` workflow in `.circleci/config.yml` |

The workflow runs the same two checks in both providers, and needs neither the application containers nor installed dependencies:

- [Gitleaks](/docs/tools/gitleaks) scans the codebase for committed secrets
- `composer audit --locked` checks the packages pinned in `composer.lock` against published security advisories

It is triggered by the same pushes, pull requests and tags as the main pipeline, and can also be started on demand - in GitHub Actions from **Actions → Security audit → Run workflow**, and in CircleCI by re-running the `audit` workflow from the pipeline view.

:::note

Because the audit is a 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.

:::

## Caching strategy

Database is fetched overnight and cached so that the next continuous integration run on the same
Expand Down
2 changes: 1 addition & 1 deletion .vortex/docs/content/continuous-integration/circleci.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ in the Installation guide and select **CircleCI**.

### Update deployment branches

All CI jobs (`database`, `lint`, `build`) run on every branch. The `deploy` job
All jobs in the `commit` and `audit` workflows (`database`, `lint`, `build`, `audit`) run on every branch. The `deploy` job
only runs for specific branch patterns, controlled by a regex filter in the
`workflows` section of `.circleci/config.yml`:

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,15 @@ pull requests, not on direct pushes.
To add or remove branches, update the `push` and `pull_request` sections
in the workflow file.

### Security audit workflow

Security checks live in a second workflow, `.github/workflows/audit.yml`, which
triggers on the same branches and tags as the main workflow and can also be
started on demand from **Actions → Security audit → Run workflow**.

See [Security audit](/docs/continuous-integration#security-audit) for what it
runs and how to make it block merges and deployments.

### Update nightly database schedule

The nightly database job caches a fresh database dump for faster builds the next
Expand Down
6 changes: 3 additions & 3 deletions .vortex/docs/content/development/composer.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -387,7 +387,7 @@ Each section accepts `block` (refuse affected versions during `composer update`/

### Why advisories do not block installation

Coupling installation to advisory publication makes builds non-deterministic: a newly published advisory against an already-installed dependency can fail every build - including work unrelated to security - until the advisory is assessed and ignored. **Vortex** decouples the two by setting `advisories.block` to `false` while keeping `advisories.audit` at `fail`, so installs and updates remain reproducible while vulnerabilities are still surfaced by `composer audit` and the CI lint job.
Coupling installation to advisory publication makes builds non-deterministic: a newly published advisory against an already-installed dependency can fail every build - including work unrelated to security - until the advisory is assessed and ignored. **Vortex** decouples the two by setting `advisories.block` to `false` while keeping `advisories.audit` at `fail`, so installs and updates remain reproducible while vulnerabilities are still surfaced by `composer audit` and the CI security audit workflow.

If your project requires installation to hard-stop on advisories - for example, a production site with strict supply-chain controls - set `advisories.block` to `true`.

Expand Down Expand Up @@ -451,6 +451,6 @@ Check your dependencies for security issues manually:

### CI/CD integration

**Vortex** runs `composer audit` as part of the CI lint job. Because `advisories.audit` is `fail`, the audit reports vulnerabilities and the lint job fails when any are found - even though installs and updates are not blocked.
**Vortex** runs `composer audit --locked` in the [security audit workflow](/docs/continuous-integration#security-audit), so the audited set is exactly what `composer.lock` pins. Because `advisories.audit` is `fail`, the audit reports vulnerabilities and the workflow fails when any are found - even though installs and updates are not blocked.

By default that failure gates the build. Set the repository variable `VORTEX_CI_COMPOSER_AUDIT_IGNORE_FAILURE` to `1` to make the audit step run and report without failing the build - useful as a one-off bypass while a known advisory is being addressed.
That failure is reported by the audit workflow, which runs independently of the build. Set the repository variable `VORTEX_CI_COMPOSER_AUDIT_IGNORE_FAILURE` to `1` to make the audit step run and report without failing that workflow - useful as a one-off bypass while a known advisory is being addressed.
2 changes: 1 addition & 1 deletion .vortex/docs/content/development/faqs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,7 @@ Provided that your stack is already running:

## What should I do when `composer audit` reports a security vulnerability?

**Vortex** does not block installation on security advisories by default, so `composer install` and `composer update` keep working. Advisories are surfaced by `composer audit`, which runs locally and in the CI lint job. When one appears:
**Vortex** does not block installation on security advisories by default, so `composer install` and `composer update` keep working. Advisories are surfaced by `composer audit`, which runs locally and in the CI security audit workflow. When one appears:

1. **Update the affected package**: try a newer version that resolves the advisory: `composer update vendor/package-name`.
2. **Review the advisory**: run `composer audit` for details and assess whether it affects your project.
Expand Down
2 changes: 1 addition & 1 deletion .vortex/docs/content/tools/gitleaks.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ https://github.com/gitleaks/gitleaks

**Vortex** does not install Gitleaks locally. Please follow the [instructions](https://github.com/gitleaks/gitleaks#installing) to install it on your system.

In CI, Gitleaks runs from its official Docker image as part of the lint job.
In CI, Gitleaks runs from its official Docker image as part of the [security audit workflow](/docs/continuous-integration#security-audit).

:::

Expand Down
1 change: 1 addition & 0 deletions .vortex/installer/src/Prompts/Handlers/CiProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ public function process(): void {

if ($remove_gha) {
File::remove($t . '/.github/workflows/build-test-deploy.yml');
File::remove($t . '/.github/workflows/audit.yml');
File::remove($t . '/' . $this->webroot . '/sites/default/includes/providers/settings.gha.php');
File::removeTokenAsync('CI_PROVIDER_GHA');
File::removeTokenAsync('SETTINGS_PROVIDER_GHA');
Expand Down
Loading