From 99ae7efa55e599bb77aea52a9195be9df471033e Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Thu, 8 Oct 2026 09:23:24 +0300 Subject: [PATCH 1/6] refactor: Add security scan for multiple scopes in CI workflow --- .github/workflows/security-audit.yml | 49 ++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/.github/workflows/security-audit.yml b/.github/workflows/security-audit.yml index 5ea17b63696..b54ebdf31f5 100644 --- a/.github/workflows/security-audit.yml +++ b/.github/workflows/security-audit.yml @@ -36,3 +36,52 @@ jobs: - name: Use audit-ci run: npm run audit + + security-scan-latest: + name: "Security Audit · latest (${{ matrix.scope }}) · ${{ matrix.branch }}" + runs-on: ubuntu-latest + strategy: + fail-fast: false # Continue all jobs even if one fails + matrix: + branch: ["main", "v4"] # List of branches to run the security audit on + scope: ["dev + prod", "prod only"] + include: + # "dev + prod": everything the repository pulls in, resolved to latest + - scope: "dev + prod" + audit_flags: "" + # "prod only": exactly what consumers install, resolved to latest + - scope: "prod only" + audit_flags: "-- --skip-dev" + + steps: + - name: Checkout '${{ matrix.branch }}' branch + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: ${{ matrix.branch }} + + # Use Node.js 24 to get npm 11 instead of npm 10. The default runner Node.js version is 22, which + # comes with npm 10. This could lead to different installation paths that could conflict with the + # allowlist in audit-ci.jsonc. + - name: Setup Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: "24" + + # Drop the lockfile so npm re-resolves every range to the latest satisfying version. + # Unlike the pinned "npm ci" audit above, this reflects what actually lands in a fresh + # installation today (we publish without a lockfile), so we can react to new advisories + # before Dependabot bumps the committed package-lock.json. + # + # We install the full tree (including devDependencies) on purpose: audit-ci is itself a + # devDependency, so "--omit=dev" would remove the audit tool. The audit scope is instead + # controlled by the "--skip-dev" flag below, which filters the dependency graph npm audit + # reads from the lockfile — it does not require devDependencies to be absent from disk. + - name: Resolve latest dependencies (ignore lockfile) + run: | + rm -rf node_modules package-lock.json + npm install --no-audit --no-fund + + # For the "prod only" scope, "--skip-dev" makes audit-ci pass "--omit=dev" to npm audit so + # only the production dependencies consumers receive are audited. + - name: Use audit-ci + run: npm run audit ${{ matrix.audit_flags }} From 384c43884313b64a2e6312b3148efdff8587d701 Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Thu, 8 Oct 2026 09:42:04 +0300 Subject: [PATCH 2/6] fix: CodeQL's security recommendations --- .github/workflows/security-audit.yml | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/.github/workflows/security-audit.yml b/.github/workflows/security-audit.yml index b54ebdf31f5..dca3db65c07 100644 --- a/.github/workflows/security-audit.yml +++ b/.github/workflows/security-audit.yml @@ -22,6 +22,10 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ matrix.branch }} + # This workflow runs in the privileged default-branch context (schedule / dispatch) but + # checks out other branches. Do not persist the GITHUB_TOKEN in the git config so the + # checked-out code cannot inherit push/API credentials (CodeQL cache-poisoning). + persist-credentials: false # Use Node.js 24 to get npm 11 instead of npm 10. The default runner Node.js version is 22, which # comes with npm 10. This could lead to different installation paths that could conflict with the @@ -31,8 +35,11 @@ jobs: with: node-version: "24" + # --ignore-scripts: never run dependency lifecycle scripts here. The audit only needs the + # resolved tree, and this removes execution of untrusted code in the privileged + # default-branch context (CodeQL cache-poisoning). - name: Install dependencies - run: npm ci + run: npm ci --ignore-scripts - name: Use audit-ci run: npm run audit @@ -58,6 +65,10 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: ${{ matrix.branch }} + # This workflow runs in the privileged default-branch context (schedule / dispatch) but + # checks out other branches. Do not persist the GITHUB_TOKEN in the git config so the + # checked-out code cannot inherit push/API credentials (CodeQL cache-poisoning). + persist-credentials: false # Use Node.js 24 to get npm 11 instead of npm 10. The default runner Node.js version is 22, which # comes with npm 10. This could lead to different installation paths that could conflict with the @@ -76,10 +87,14 @@ jobs: # devDependency, so "--omit=dev" would remove the audit tool. The audit scope is instead # controlled by the "--skip-dev" flag below, which filters the dependency graph npm audit # reads from the lockfile — it does not require devDependencies to be absent from disk. + # + # --ignore-scripts: never run dependency lifecycle scripts here. The audit only needs the + # resolved tree, and this removes execution of untrusted code in the privileged + # default-branch context (CodeQL cache-poisoning). - name: Resolve latest dependencies (ignore lockfile) run: | rm -rf node_modules package-lock.json - npm install --no-audit --no-fund + npm install --no-audit --no-fund --ignore-scripts # For the "prod only" scope, "--skip-dev" makes audit-ci pass "--omit=dev" to npm audit so # only the production dependencies consumers receive are audited. From 3d0bb2dfdb1d68c80aac05aa988076271f416f2b Mon Sep 17 00:00:00 2001 From: d3xter666 Date: Thu, 8 Oct 2026 10:32:34 +0300 Subject: [PATCH 3/6] refactor: Extract audit into an external action --- .github/actions/dependency-audit/action.yml | 32 ++++++ .github/actions/dependency-audit/audit.sh | 54 +++++++++++ .github/workflows/security-audit-main.yml | 27 ++++++ .github/workflows/security-audit-v4.yml | 32 ++++++ .github/workflows/security-audit.yml | 102 -------------------- 5 files changed, 145 insertions(+), 102 deletions(-) create mode 100644 .github/actions/dependency-audit/action.yml create mode 100644 .github/actions/dependency-audit/audit.sh create mode 100644 .github/workflows/security-audit-main.yml create mode 100644 .github/workflows/security-audit-v4.yml delete mode 100644 .github/workflows/security-audit.yml diff --git a/.github/actions/dependency-audit/action.yml b/.github/actions/dependency-audit/action.yml new file mode 100644 index 00000000000..388b9b801e8 --- /dev/null +++ b/.github/actions/dependency-audit/action.yml @@ -0,0 +1,32 @@ +name: "Dependency security audit" +description: > + Audits the already-checked-out project's dependencies in three scopes — locked (npm ci), + latest (dev + prod), and latest (production only) — so a repository can see what its committed + lockfile pins versus what a fresh install resolves to today. Operates on the current workspace; + the caller is responsible for the checkout. + +inputs: + node-version: + description: "Node.js version to set up (npm 11+ recommended for stable audit paths)." + default: "24" + audit-command: + description: "Command that runs the audit and exits non-zero on findings." + default: "npm run audit" + prod-only-args: + description: "Extra arguments appended to audit-command for the production-only scope." + default: "-- --skip-dev" + +runs: + using: composite + steps: + - name: Setup Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: ${{ inputs.node-version }} + + - name: Run dependency audit (all scopes) + shell: bash + env: + AUDIT_COMMAND: ${{ inputs.audit-command }} + PROD_ONLY_ARGS: ${{ inputs.prod-only-args }} + run: bash "${{ github.action_path }}/audit.sh" diff --git a/.github/actions/dependency-audit/audit.sh b/.github/actions/dependency-audit/audit.sh new file mode 100644 index 00000000000..e3496ae7345 --- /dev/null +++ b/.github/actions/dependency-audit/audit.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# +# Audits the current workspace's dependencies in three scopes and reports each one. +# All scopes always run; the script exits non-zero if any scope reported advisories. +# +# locked · ci (dev + prod) What the committed package-lock.json pins — the reproducible +# tree we build and test against. +# latest (dev + prod) What the whole repo resolves to today when the lockfile is +# ignored — early warning across everything we pull in. +# latest (prod only) What a fresh install gives consumers today (we publish without +# a lockfile) — the scope that reaches end users. +# +# npm resolves "latest" by re-installing after the lockfile is removed. "--ignore-scripts" is +# used everywhere: the audit only needs the resolved dependency tree, and never executing +# dependency lifecycle code keeps this safe to run on untrusted/unpinned versions. + +# Note: no "-e" — a failing scope must not stop the remaining scopes from running. +set -uo pipefail + +AUDIT_COMMAND="${AUDIT_COMMAND:-npm run audit}" +PROD_ONLY_ARGS="${PROD_ONLY_ARGS:- -- --skip-dev}" + +failed=0 + +# run_scope