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
1 change: 1 addition & 0 deletions .github/actions/verify-npm-hardening/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ steps:
| Name | Default | Description |
|-----------|---------|-------------------------------------------------------------------|
| `min-age` | `7` | Minimum acceptable `min-release-age` value in days. Must be >= 7. |
| `working-directory` | `.` | Folder that holds `package.json` and `.npmrc`, relative to the repository root. Example: `frontend` |

```yaml
- uses: ./.github/actions/verify-npm-hardening
Expand Down
10 changes: 10 additions & 0 deletions .github/actions/verify-npm-hardening/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,19 @@
# - run: npm ci
#
# Inputs:
# working-directory (optional, default: .)
# Folder that holds package.json and .npmrc.
# min-age (optional, default: 7)
# Minimum acceptable min-release-age value in days. Must be >= 7.

name: Verify npm supply chain hardening
description: Checks .npmrc settings and lockfile presence before npm ci

inputs:
working-directory:
description: 'Folder that holds package.json, relative to the repository root. Example: ., frontend'
required: false
default: .
min-age:
description: Minimum acceptable min-release-age value in days
required: false
Expand All @@ -26,6 +32,7 @@ runs:
steps:
- name: Check required files
shell: bash
working-directory: ${{ inputs.working-directory }}
run: |
errors=0

Expand All @@ -47,6 +54,7 @@ runs:

- name: Verify .npmrc hardening settings
shell: bash
working-directory: ${{ inputs.working-directory }}
env:
MIN_AGE: ${{ inputs.min-age }}
run: |
Expand Down Expand Up @@ -84,6 +92,7 @@ runs:

- name: Check for non-registry dependencies in package.json
shell: bash
working-directory: ${{ inputs.working-directory }}
run: |
# Git-hosted and URL-based dependencies bypass min-release-age
# entirely because they are not fetched from the npm registry.
Expand Down Expand Up @@ -115,6 +124,7 @@ runs:

- name: Check for non-registry URLs in lockfile
shell: bash
working-directory: ${{ inputs.working-directory }}
run: |
# A tampered lockfile could resolve packages to non-registry
# sources even if package.json looks clean.
Expand Down
6 changes: 4 additions & 2 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,13 +296,14 @@ npm-only and PHP-only repositories.

| Job | What it checks |
| --- | --- |
| `Secrets` | Committed `.env` files, and Gitleaks over the full git history |
| `Secrets` | Committed `.env` files anywhere in the repository, and Gitleaks over the full git history. Examples, `.env.testing`, `.env.ci` and templates (`.dist`, `.sample`, `.template`, `.tpl`, `.j2`, `.jinja`, `.jinja2`) are allowed |
| `Dev tool configs` | Script files and suspicious commands in `.vscode`, `.claude`, `.cursor` and `.idea`. See [`scan-dev-configs`](../actions/scan-dev-configs/README.md) |
| `npm supply chain hardening` | `.npmrc` settings and lockfile. See [`verify-npm-hardening`](../actions/verify-npm-hardening/README.md) |
| `Dependency audit` | `composer audit` and `npm audit` against the lock files |
| `Dependency audit` | `composer audit --locked` and `npm audit` against the lock files. No install is needed |

| Input | Default | Example values |
| --- | --- | --- |
| `working-directory` | `.` | `frontend`, `apps/web`. The npm hardening and audit checks run there |
| `php-version` | `'8.4'` | `'8.3'` |
| `npm-audit-level` | `high` | `low`, `moderate`, `critical` |
| `strict-dev-configs` | `false` (warn only) | `true` (fail on suspicious commands) |
Expand Down Expand Up @@ -348,6 +349,7 @@ jobs:
| `Codecov` fails on your own pull requests | The `CODECOV_TOKEN` secret is missing or not passed under `secrets:`. Pull requests from forks get no secrets, so there the upload failure does not fail the job. |
| `Environment file '...' not found` (Laravel) | Your repository has no `.env.testing` and no `.env.example`, or `env-file` points to a missing file. |
| Browser tests also run in `Backend tests` (Laravel) | Exclude the browser suite in `test-command`, for example `vendor/bin/pest --exclude-testsuite=Browser`. |
| `Environment files are committed to the repository` | A real env file, such as `.env` or `.env.production`, is committed. Remove it and rotate its secrets. A template must end in `.example`, `.dist`, `.sample`, `.template`, `.tpl`, `.j2`, `.jinja` or `.jinja2`. |
| `Script files found in dev tool directories` | A script file is in `.vscode`, `.claude`, `.cursor` or `.idea`. Remove it, or review it and add it to `allowed-dev-scripts`. |
| PHPStan or Rector re-analyse every file on each run | `analysis-cache-paths` does not match `tmpDir` in `phpstan.neon` or `cacheDirectory` in `rector.php`. |

Expand Down
37 changes: 27 additions & 10 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ name: Security
on:
workflow_call:
inputs:
working-directory:
description: 'Folder that holds composer.json and package.json, relative to the repository root. The npm hardening and audit checks run there. Example: ., frontend, apps/web'
type: string
default: .
php-version:
description: PHP version used to run `composer audit`.
type: string
Expand Down Expand Up @@ -56,12 +60,16 @@ jobs:
fetch-depth: 0
persist-credentials: false

# The whole repository, not only working-directory. Examples, test files
# and templates (.dist, .sample, .template, .tpl, Jinja) are allowed.
- name: Check for committed environment files
run: |
found=$(find . -type f -name '.env*' \
-not -path './vendor/*' -not -path './node_modules/*' -not -path './.git/*' \
-not -name '.env.example' -not -name '.env.*.example' \
-not -name '.env.testing' -not -name '.env.ci')
-not -path '*/vendor/*' -not -path '*/node_modules/*' -not -path './.git/*' \
-not -name '.env.testing' -not -name '.env.ci' \
-not -name '.env*.example' -not -name '.env*.dist' -not -name '.env*.sample' \
-not -name '.env*.template' -not -name '.env*.tpl' \
-not -name '.env*.j2' -not -name '.env*.jinja' -not -name '.env*.jinja2')
if [ -n "$found" ]; then
echo "::error::Environment files are committed to the repository:"
echo "$found"
Expand Down Expand Up @@ -123,6 +131,9 @@ jobs:
name: npm supply chain hardening
runs-on: ubuntu-latest
timeout-minutes: 5
defaults:
run:
working-directory: ${{ inputs.working-directory }}
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand All @@ -141,41 +152,47 @@ jobs:
persist-credentials: false

- name: Verify npm hardening
if: hashFiles('package.json') != ''
if: hashFiles(format('{0}/package.json', inputs.working-directory)) != ''
uses: ./.scify-github/.github/actions/verify-npm-hardening
with:
working-directory: ${{ inputs.working-directory }}

- name: No package.json
if: hashFiles('package.json') == ''
if: hashFiles(format('{0}/package.json', inputs.working-directory)) == ''
run: echo "No package.json, skipping."

audit:
name: Dependency audit
runs-on: ubuntu-latest
timeout-minutes: 10
defaults:
run:
working-directory: ${{ inputs.working-directory }}
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Set up PHP
if: hashFiles('composer.lock') != ''
if: hashFiles(format('{0}/composer.lock', inputs.working-directory)) != ''
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # 2.37.2
with:
php-version: ${{ inputs.php-version }}
coverage: none
tools: composer:v2

# --locked reads composer.lock, so no `composer install` is needed.
- name: Composer audit
if: hashFiles('composer.lock') != ''
run: composer audit --format=table
if: hashFiles(format('{0}/composer.lock', inputs.working-directory)) != ''
run: composer audit --locked --format=table

- name: npm audit
if: hashFiles('package-lock.json') != ''
if: hashFiles(format('{0}/package-lock.json', inputs.working-directory)) != ''
env:
LEVEL: ${{ inputs.npm-audit-level }}
run: npm audit --audit-level="$LEVEL"

- name: Nothing to audit
if: hashFiles('composer.lock', 'package-lock.json') == ''
if: hashFiles(format('{0}/composer.lock', inputs.working-directory), format('{0}/package-lock.json', inputs.working-directory)) == ''
run: echo "No composer.lock or package-lock.json, skipping."
9 changes: 8 additions & 1 deletion .github/workflows/self-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
# - every workflow template .properties.json file parses
# - every internal `scify/.github/...@vX` reference uses the same release tag
# - the composite actions pass on good fixtures and fail on bad ones
# - security.yml runs on this repository (it loads its actions by job.workflow_sha)
# - security.yml runs on this repository (it loads its actions by job.workflow_sha),
# and on tests/fixtures/security (composer.lock, package-lock.json, .env template)
# - smoke tests: laravel-ci.yml and node-ci.yml run against the fixture
# projects in tests/fixtures/, once with defaults and once with commands
#
Expand Down Expand Up @@ -160,6 +161,12 @@ jobs:
name: Security workflow on this repository
uses: ./.github/workflows/security.yml

security-fixture:
name: Security workflow on the fixture
uses: ./.github/workflows/security.yml
with:
working-directory: tests/fixtures/security

laravel-defaults:
name: Laravel CI, defaults
uses: ./.github/workflows/laravel-ci.yml
Expand Down
30 changes: 20 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,22 +71,32 @@ template does not reach existing copies. Re-run the command to pick it up.

### Versioning

This repository is work in progress. Releases are tagged `v0.x` and callers
pin to the current one, `v0.1`. Any `v0.x` release may change inputs or
defaults. When the workflows have run in real repositories for a while, `v1`
becomes the first stable tag and moves forward only for backwards-compatible
fixes.
This repository is work in progress. Releases follow the usual GitHub Actions
convention:

To publish a new release:
- Each release has its own tag, for example `v0.1.1`. It never moves.
- The short tag, for example `v0.1`, moves to the latest release of that line.
Callers pin to it and receive fixes without a change.
- Any `v0.x` line may change inputs or defaults. A change that breaks callers
starts a new line, for example `v0.2`.

1. Update every `@v0.x` reference in the documentation and comments to the new
tag. The self-check fails when they differ.
2. Tag and push:
When the workflows have run in real repositories for a while, `v1` becomes the
first stable line.

To publish a fix release on the `v0.1` line:

1. Merge the fix to `main` and wait for a green Self-check.
2. Tag the release and move the short tag:

```bash
git tag v0.2 && git push origin v0.2
git tag -a v0.1.1 -m v0.1.1 origin/main && git push origin v0.1.1
git tag -f -a v0.1 -m v0.1 origin/main && git push -f origin v0.1
gh release create v0.1.1 --title v0.1.1 --notes "..."
```

To start a new line, update every `@v0.x` reference in the documentation and
comments to the new tag first. The self-check fails when they differ.

### Composite actions and the release tag

The organisation requires every action to be pinned to a full commit SHA, and
Expand Down
9 changes: 6 additions & 3 deletions tests/fixtures/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

Sample projects for the CI of this repository. No other repository uses them.

`.github/workflows/self-check.yml` calls `laravel-ci.yml` and `node-ci.yml` by
local path, with `working-directory` set to a folder here. A pull request
`.github/workflows/self-check.yml` calls `laravel-ci.yml`, `node-ci.yml` and
`security.yml` by local path, with `working-directory` set to a folder here. It
also runs the composite actions against these folders. A pull request
therefore runs its own workflow changes against a real project before any
caller receives them. actionlint checks the syntax. These runs catch the
runtime errors that actionlint cannot see, such as wrong paths or a wrong step
Expand All @@ -12,7 +13,9 @@ order.
| Folder | Contents | Smoke-test jobs |
| --- | --- | --- |
| `laravel/` | The `laravel/laravel` skeleton, with `pint.json` and `.nvmrc` added | `laravel-defaults`, `laravel-commands` |
| `node/` | A project without dependencies, with `lint`, `type-check`, `test` and `build` scripts | `node-defaults`, `node-commands` |
| `node/` | A project without dependencies, with `lint`, `type-check`, `test` and `build` scripts | `node-defaults`, `node-commands`, `security-actions` |
| `dev-configs/` | A `.claude` hook script and editor settings | `security-actions` |
| `security/` | `composer.lock` and `package-lock.json` without dependencies, a hardened `.npmrc`, and a Jinja `.env` template | `security-fixture` |

## Refresh the Laravel fixture

Expand Down
2 changes: 2 additions & 0 deletions tests/fixtures/security/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
APP_NAME="Security fixture"
APP_KEY=
3 changes: 3 additions & 0 deletions tests/fixtures/security/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
engine-strict=true
ignore-scripts=true
min-release-age=7
7 changes: 7 additions & 0 deletions tests/fixtures/security/composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"name": "scify/security-fixture",
"description": "Fixture for the security workflow self-check",
"type": "project",
"license": "Apache-2.0",
"require": {}
}
18 changes: 18 additions & 0 deletions tests/fixtures/security/composer.lock

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

4 changes: 4 additions & 0 deletions tests/fixtures/security/deploy/.env.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
APP_NAME="Security fixture"
APP_ENV=production
APP_KEY={{ app_key }}
DB_PASSWORD={{ db_password }}
10 changes: 10 additions & 0 deletions tests/fixtures/security/package-lock.json

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

4 changes: 4 additions & 0 deletions tests/fixtures/security/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"name": "scify-security-fixture",
"private": true
}
Loading