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
17 changes: 16 additions & 1 deletion .github/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,7 @@ npm-only and PHP-only repositories.
| `Secrets` | Committed `.env` files anywhere in the repository, and Gitleaks. On a pull request, Gitleaks scans only the pull request's commits. On push and schedule, it scans 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 --locked` and `npm audit` against the lock files. No install is needed |
| `Dependency audit` | `composer audit --locked` and `npm audit` against the lock files. No install is needed. `npm audit` runs on the Node.js version from `.nvmrc`, so `engines` and `devEngines` in `package.json` apply |

| Input | Default | Example values |
| --- | --- | --- |
Expand All @@ -426,6 +426,8 @@ npm-only and PHP-only repositories.
| `gitleaks-full-history` | `false` (pull requests scan their own commits) | `true` (every run scans the full history) |
| `npm-min-release-age` | `'7'` | `'14'` (days; 7 or more) |
| `npm-audit-level` | `high` | `low`, `moderate`, `critical` |
| `npm-audit-omit-dev` | `false` (audit all dependencies) | `true` (audit only the production dependencies, `npm audit --omit=dev`) |
| `node-version-file` | `''` (the `.nvmrc` in `working-directory` when it exists, else the Node.js of the runner) | `.nvmrc`, `frontend/.nvmrc`, `package.json` |
| `strict-dev-configs` | `false` (warn only) | `true` (fail on suspicious commands) |
| `allowed-dev-scripts` | `''` (no scripts allowed) | `.claude/hooks/*.sh` (one glob per line; `*` also matches `/`) |

Expand Down Expand Up @@ -456,6 +458,19 @@ jobs:
uses: scify/.github/.github/workflows/security.yml@v0.1
```

When a dev tool has an advisory without a fixed release, for example `braces`
through `stylelint`, audit only the production dependencies. The dev tools do
not reach the production assets. Use this only when no fix exists, and check
the dev dependencies again when a fix appears:

```yaml
jobs:
security:
uses: scify/.github/.github/workflows/security.yml@v0.1
with:
npm-audit-omit-dev: true
```

When your repository keeps hook scripts in `.claude/hooks/`, review them and allow them:

```yaml
Expand Down
44 changes: 42 additions & 2 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
# dev-configs scripts and suspicious commands in .vscode/.claude/.cursor/.idea
# (scan-dev-configs action; allow reviewed scripts with allowed-dev-scripts)
# npm-hardening .npmrc and lockfile checks (verify-npm-hardening action)
# audit `composer audit` and `npm audit` against the lock files
# audit `composer audit` and `npm audit` against the lock files; npm audit
# runs on the Node.js version from .nvmrc when the repository has one
#
# Each check runs only when the repository has the matching files, so the
# same call works for Laravel, npm-only and PHP-only repositories.
Expand All @@ -45,6 +46,14 @@ on:
description: Lowest npm advisory severity that fails the audit (low, moderate, high, critical).
type: string
default: high
npm-audit-omit-dev:
description: 'Audit only the production dependencies (npm audit --omit=dev). Use it when a dev tool has an advisory without a fix. Example: true, false'
type: boolean
default: false
node-version-file:
description: 'File that pins the Node.js version for npm audit, relative to the repository root. Empty: the .nvmrc in working-directory when it exists, else the Node.js of the runner. Example: .nvmrc, frontend/.nvmrc, package.json'
type: string
default: ''
strict-dev-configs:
description: Fail on suspicious commands in dev tool configs. Default only warns.
type: boolean
Expand Down Expand Up @@ -221,11 +230,42 @@ jobs:
ABANDONED: ${{ inputs.composer-abandoned }}
run: composer audit --locked --abandoned="$ABANDONED" --format=table

# npm checks engines and devEngines in package.json, so npm audit needs the
# repository's Node.js version, not the one on the runner.
- name: Find the Node.js version file
id: node
if: hashFiles(format('{0}/package-lock.json', inputs.working-directory)) != ''
env:
NODE_VERSION_FILE: ${{ inputs.node-version-file }}
WORKING_DIRECTORY: ${{ inputs.working-directory }}
run: |
if [ -n "$NODE_VERSION_FILE" ]; then
echo "file=$NODE_VERSION_FILE" >> "$GITHUB_OUTPUT"
elif [ -f .nvmrc ]; then
echo "file=$WORKING_DIRECTORY/.nvmrc" >> "$GITHUB_OUTPUT"
else
echo "No .nvmrc, npm audit uses the Node.js of the runner."
fi

- name: Set up Node.js
if: steps.node.outputs.file != ''
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: ${{ steps.node.outputs.file }}
# npm audit reads the lock file only; there is nothing to cache.
package-manager-cache: false

- name: npm audit
if: hashFiles(format('{0}/package-lock.json', inputs.working-directory)) != ''
env:
LEVEL: ${{ inputs.npm-audit-level }}
run: npm audit --audit-level="$LEVEL"
OMIT_DEV: ${{ inputs.npm-audit-omit-dev }}
run: |
args=(--audit-level="$LEVEL")
if [ "$OMIT_DEV" = true ]; then
args+=(--omit=dev)
fi
npm audit "${args[@]}"

- name: Nothing to audit
if: hashFiles(format('{0}/composer.lock', inputs.working-directory), format('{0}/package-lock.json', inputs.working-directory)) == ''
Expand Down
7 changes: 6 additions & 1 deletion .github/workflows/self-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
# - 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),
# and on tests/fixtures/security (composer.lock, package-lock.json, .env template)
# and on tests/fixtures/security (composer.lock, package-lock.json, .env template).
# The fixture fails unless npm audit runs on the Node.js from its .nvmrc
# (devEngines) and skips its dev dependency braces (an advisory without a fix)
# - 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 @@ -186,6 +188,9 @@ jobs:
uses: ./.github/workflows/security.yml
with:
working-directory: tests/fixtures/security
# The fixture's dev dependency braces has a high advisory without a fix.
# Without this input the audit fails.
npm-audit-omit-dev: true

laravel-defaults:
name: Laravel CI, defaults
Expand Down
2 changes: 1 addition & 1 deletion tests/fixtures/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ order.
| `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`, `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` |
| `security/` | `composer.lock` without dependencies; `package-lock.json` with one dev dependency, `braces` 3.0.3, which has a high advisory without a fix; `.nvmrc` and a `devEngines` rule that allows only that Node.js version; a hardened `.npmrc`; and a Jinja `.env` template. The audit fails unless it runs on the `.nvmrc` version and with `npm-audit-omit-dev: true` | `security-fixture` |

## Refresh the Laravel fixture

Expand Down
1 change: 1 addition & 0 deletions tests/fixtures/security/.nvmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
24.22.1
54 changes: 53 additions & 1 deletion tests/fixtures/security/package-lock.json

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

12 changes: 11 additions & 1 deletion tests/fixtures/security/package.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
{
"name": "scify-security-fixture",
"private": true
"private": true,
"devEngines": {
"runtime": {
"name": "node",
"version": "24.22.1",
"onFail": "error"
}
},
"devDependencies": {
"braces": "3.0.3"
}
}
2 changes: 2 additions & 0 deletions workflow-templates/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ jobs:
# php-version: '8.4' # e.g. '8.3' (runs composer audit)
# composer-abandoned: report # e.g. fail, ignore
# npm-audit-level: high # e.g. moderate, critical
# npm-audit-omit-dev: false # true: audit only production dependencies
# node-version-file: '' # e.g. .nvmrc (default: the .nvmrc in working-directory)
# npm-min-release-age: '7' # e.g. '14' (days, 7 or more)
# gitleaks-full-history: false # true: pull requests scan the full history too
# strict-dev-configs: false # true: fail on suspicious commands in dev tool configs
Expand Down
Loading