name: npm Security Audit on: workflow_dispatch: workflow_call: # Called by release-gate.yml permissions: contents: read jobs: npm-audit: runs-on: ubuntu-latest timeout-minutes: 10 steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 with: egress-policy: audit - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - name: Setup Node.js uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 with: node-version: '24' - name: Run npm audit on all package.json locations env: # Advisories with NO upstream fix, accepted ONLY because they live in # dev/test-only tooling that never ships to users. Keep tight + justified: # GHSA-h67p-54hq-rp68 — js-yaml 3.x DoS, pulled transitively by # @lhci/utils (accessibility_tests) & @istanbuljs/load-nyc-config # (infrastructure_tests); both pin js-yaml ^3, no patched 3.x exists. # Dev-only, parses trusted local config. (dismissed alerts #97/#7893) AUDIT_ALLOWLIST: "GHSA-h67p-54hq-rp68" run: | FAILED=false FILTER="${GITHUB_WORKSPACE}/.github/scripts/npm_audit_allowlist.py" # Audit root and every test directory that contains a package.json DIRS=( . tests tests/ui_tests tests/ui_tests/playwright tests/accessibility_tests tests/api_tests_with_login tests/infrastructure_tests tests/puppeteer ) for dir in "${DIRS[@]}"; do echo "=== Running npm audit on ${dir} ===" if [ ! -f "${dir}/package.json" ]; then echo "No package.json found in ${dir}" continue fi # Require committed lockfile for reproducible security audits if [ ! -f "${dir}/package-lock.json" ]; then echo "::error::Missing ${dir}/package-lock.json. Please commit your lockfile for security audits." FAILED=true continue fi # Audit, then filter out allowlisted (no-fix, dev-only) advisories. # `|| true` is on the assignment (not inside $()) so npm's non-zero # exit is swallowed without the SC2015 `A && B || C` idiom; an # empty/error capture still fails safe in the filter. audit_json="$(cd "${dir}" && npm audit --audit-level=moderate --json 2>/dev/null)" || true if ! printf '%s' "${audit_json}" | python3 "${FILTER}"; then FAILED=true fi done if [[ "$FAILED" == "true" ]]; then echo "❌ npm audit found non-allowlisted moderate or higher severity vulnerabilities" echo "Fix: bump the dep / 'npm audit fix'; or if no fix exists and it is dev-only, add the advisory to AUDIT_ALLOWLIST with justification." exit 1 else echo "✅ No non-allowlisted moderate or higher severity vulnerabilities found" fi