🔒 Security Vulnerability Fix #34
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: 🔒 Security Vulnerability Fix | |
| on: | |
| schedule: | |
| - cron: "0 8 * * 1" # Every Monday at 8:00 UTC | |
| workflow_dispatch: | |
| inputs: | |
| dry_run: | |
| description: "Dry run (report only, no PRs)" | |
| required: false | |
| type: boolean | |
| default: false | |
| severity: | |
| description: "Minimum severity to fix" | |
| required: false | |
| type: choice | |
| options: | |
| - low | |
| - moderate | |
| - high | |
| - critical | |
| default: moderate | |
| concurrency: | |
| group: security-fix | |
| cancel-in-progress: false | |
| jobs: | |
| scan-and-fix: | |
| name: 🔍 Scan & Fix Vulnerabilities | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| permissions: | |
| contents: write | |
| pull-requests: write | |
| security-events: read | |
| steps: | |
| - name: 📥 Checkout code | |
| uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 1 | |
| ref: newjitsu | |
| # setup-node MUST run before setup-pnpm: pnpm/action-setup@v4 binds its | |
| # CLI shim to whichever Node is active at install time, so if pnpm goes | |
| # first, every later `pnpm` call still runs on the runner's default | |
| # Node 20 (triggers "Unsupported engine: wanted {node:>=22}") even after | |
| # setup-node bumps the PATH to Node 22. | |
| - name: 🔧 Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| # No `version:` — pnpm/action-setup reads `packageManager` from | |
| # package.json (pnpm@10.22.0). Specifying both conflicts and fails the step. | |
| - name: 🔧 Setup pnpm | |
| uses: pnpm/action-setup@v4 | |
| - name: 🔧 Setup Go | |
| uses: actions/setup-go@v6 | |
| with: | |
| go-version: "1.26" | |
| cache-dependency-path: "bulker/**/*.sum" | |
| - name: 📦 Install dependencies | |
| if: false | |
| run: pnpm install --frozen-lockfile | |
| continue-on-error: true | |
| - name: 🔍 Run npm audit | |
| id: npm-audit | |
| if: false | |
| run: | | |
| pnpm audit --json > .npm-audit.json 2>/dev/null || true | |
| echo "npm audit complete" | |
| # Summary for logs | |
| jq -r '.advisories // {} | length' .npm-audit.json 2>/dev/null || echo "0" | |
| - name: 🔍 Run govulncheck | |
| id: go-vuln | |
| if: false | |
| run: | | |
| go install golang.org/x/vuln/cmd/govulncheck@latest | |
| mkdir -p .go-vulns | |
| for moddir in bulker/*/; do | |
| if [ -f "${moddir}go.mod" ]; then | |
| modname=$(basename "${moddir}") | |
| echo "Scanning ${moddir}..." | |
| govulncheck -C "${{ github.workspace }}/${moddir}" -json ./... > ".go-vulns/${modname}.json" 2>/dev/null || true | |
| fi | |
| done | |
| jq -s 'flatten' .go-vulns/*.json > .go-vulns.json 2>/dev/null || echo '[]' > .go-vulns.json | |
| echo "govulncheck complete" | |
| - name: 🔍 Fetch Dependabot alerts | |
| id: dependabot | |
| env: | |
| GH_TOKEN: ${{ secrets.DEPENDABOT_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| gh api repos/${{ github.repository }}/dependabot/alerts --paginate \ | |
| --jq '[.[] | select(.state == "open") | { | |
| number: .number, | |
| cve: .security_advisory.cve_id, | |
| severity: .security_advisory.severity, | |
| summary: .security_advisory.summary, | |
| package: .security_vulnerability.package.name, | |
| ecosystem: .security_vulnerability.package.ecosystem, | |
| vulnerable_range: .security_vulnerability.vulnerable_version_range, | |
| fixed_version: .security_vulnerability.first_patched_version.identifier | |
| }]' > .dependabot-alerts.json | |
| echo "Dependabot alerts: $(jq length .dependabot-alerts.json)" | |
| - name: 📝 Write Codex prompt | |
| env: | |
| DRY_RUN: ${{ inputs.dry_run || 'false' }} | |
| MIN_SEVERITY: ${{ inputs.severity || 'moderate' }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| set -euo pipefail | |
| mkdir -p .github/codex | |
| # Runtime context goes through an UNQUOTED heredoc so $REPO/$DRY_RUN/ | |
| # $MIN_SEVERITY expand. The static prompt body below uses a QUOTED | |
| # heredoc ('PROMPT_EOF') so backticks, $vars, and glob chars inside | |
| # the markdown pass through literally instead of being interpreted. | |
| cat > .github/codex/security-fix.prompt.md <<EOF | |
| ## Runtime context | |
| - Repository: ${REPO} | |
| - Base branch: newjitsu | |
| - Dry run mode: ${DRY_RUN} | |
| - Minimum severity: ${MIN_SEVERITY} | |
| EOF | |
| cat >> .github/codex/security-fix.prompt.md << 'PROMPT_EOF' | |
| You are a security engineer fixing dependency vulnerabilities in a monorepo. | |
| ## Operating mode (read first) | |
| You are running non-interactively inside a GitHub Actions job. There is | |
| no human to answer questions. Do not ask for confirmation, do not stop | |
| to request clarification, do not print plans and wait — just execute. | |
| When a step is ambiguous, pick the option described in the Rules | |
| section below; if there is no matching rule, skip that vulnerability | |
| and move on. Finish by printing a short summary of what you did. | |
| ## Repo layout | |
| - Node.js/pnpm workspace rooted at repo root (libs/, services/, webapps/, cli/, types/). | |
| - Go workspace rooted at bulker/ (bulker/go.work). All Go modules are members of this workspace — do NOT treat them as independent repos. Workspace members are listed in bulker/go.work. | |
| - Root package.json has a pnpm.overrides block for pinning transitive versions. | |
| ## Input | |
| The primary source of work is .dependabot-alerts.json (already produced | |
| by an earlier step). It is a JSON array; each entry has: number, cve, | |
| severity, summary, package, ecosystem, vulnerable_range, fixed_version. | |
| Read it with: cat .dependabot-alerts.json | jq . | |
| ## Procedure | |
| IMPORTANT: All examples below use PLACEHOLDER names in ALL_CAPS | |
| (ACTUAL_PKG_NAME, FIXED_VER, ACTUAL_MODULE). Substitute real values | |
| from the alerts before running any command. Never run a command that | |
| still contains a placeholder. | |
| 1. List existing security PRs to avoid duplicates: | |
| gh pr list --search "security" --state open | |
| Skip any alert whose fix is already covered by an open PR. | |
| 2. Filter alerts by MIN_SEVERITY (severity order: critical > high > moderate > low). | |
| 3. For EACH remaining alert, determine ALL versions of the package | |
| actually resolved in this repo, then SKIP the alert only if every | |
| one of them is already fixed: | |
| - A package can be resolved at MULTIPLE versions at once: a pnpm | |
| lockfile routinely contains several copies of the same package, | |
| and each Go workspace member (bulker/*/go.mod) resolves the | |
| module independently. Collect every resolved version — e.g. | |
| `pnpm why -r ACTUAL_PKG_NAME` / grep pnpm-lock.yaml for npm, and | |
| `go -C bulker/ACTUAL_MODULE list -m ACTUAL_PKG` for each | |
| workspace member for Go. | |
| - Only if EVERY resolved version is >= the alert's fixed_version | |
| (or outside the vulnerable_range) is the repo already patched — | |
| skip it. If even one resolved copy is still vulnerable, the | |
| alert applies; fix that copy (without downgrading the others). | |
| Do NOT touch the manifest just to match fixed_version exactly; | |
| that is a no-op at best and a downgrade at worst. | |
| - Go major versions are distinct modules. An alert for | |
| `github.com/foo/bar` (the v0/v1 module path) does NOT apply to | |
| imports of `github.com/foo/bar/v2` (or higher) — that is a | |
| different, newer module. But both majors can be in use at once: | |
| check `grep -rE 'github.com/foo/bar[[:space:]]' bulker/*/go.mod` | |
| (the trailing whitespace class matches the space or tab `go mod | |
| tidy` uses, while still excluding /v2+ paths) before skipping. Skip | |
| the alert only when NO workspace member still requires the | |
| v0/v1 path; never "fix" a v0/v1 alert by pinning the older | |
| major-version path in a module that only uses v2+. | |
| - NEVER downgrade. Compare versions semantically: if applying the | |
| fix would move any package to a LOWER version than it currently | |
| resolves to (directly or transitively), skip the alert and note it | |
| in the PR body under "Skipped (already satisfied / would downgrade)". | |
| 4. Group the remaining alerts into AT MOST TWO PRs — one per stack: | |
| - All npm / Node.js fixes → a single PR on branch security/fix-npm-YYYY-MM-DD | |
| - All Go fixes → a single PR on branch security/fix-go-YYYY-MM-DD | |
| This is mandatory: pnpm-lock.yaml, go.sum, and bulker/go.work.sum | |
| otherwise cause merge conflicts if two concurrent PRs touch them. | |
| If a fix is risky (major version bump, breaking change), still include | |
| it in the per-stack PR but flag it prominently in the PR body so a | |
| reviewer can split it out manually. | |
| 5. Apply every ecosystem-specific step below for all alerts in that | |
| stack BEFORE creating the PR. Do NOT push incremental commits per | |
| alert — one branch, one commit per stack, one PR per stack. | |
| 6. After applying fixes, verify nothing regressed before opening the PR: | |
| - npm: `pnpm install --no-frozen-lockfile` must succeed. | |
| - Go: `go -C bulker work sync` and `go -C bulker/ACTUAL_MODULE build ./...` | |
| for each touched module must succeed. | |
| If verification fails for an alert, revert that alert's change and | |
| move it to the "Skipped" list. Never open a PR that does not build. | |
| 7. If after all of the above NO alerts remain to fix, do NOT create a | |
| branch or PR — just print a summary saying everything is already | |
| satisfied. | |
| 8. If DRY_RUN is true, output a report of what would be done and do NOT | |
| create branches, commits, or PRs. | |
| ### npm / Node.js packages | |
| - Determine if the package is a direct dependency: | |
| grep -r '"ACTUAL_PKG_NAME"' package.json */package.json libs/*/package.json webapps/*/package.json cli/*/package.json services/*/package.json types/*/package.json | |
| - Transitive-only: add an entry to the pnpm.overrides block in root package.json. | |
| Pattern: "ACTUAL_PKG_NAME@<FIXED_VER": "^FIXED_VER" | |
| The override floor (^FIXED_VER) must never be lower than the version | |
| already resolved in pnpm-lock.yaml — an override that caps a package | |
| below its current version is a downgrade; skip it instead. | |
| - Direct dependency: bump the version in the package.json that declares | |
| it — only ever UP. If the declared/resolved version is already >= | |
| FIXED_VER, leave it untouched (already satisfied). | |
| - MANDATORY after any change to package.json OR pnpm-workspace.yaml | |
| (root or any workspace member): run `pnpm install --no-frozen-lockfile` | |
| at the repo root. Omitting --no-frozen-lockfile breaks in CI because pnpm | |
| defaults to --frozen-lockfile when CI=true and refuses to update | |
| pnpm-lock.yaml when overrides / catalog / workspace config has changed. | |
| This step updates pnpm-lock.yaml. Commit the manifest changes | |
| (package.json and/or pnpm-workspace.yaml) together with pnpm-lock.yaml. | |
| - If pnpm install fails, revert the changes and skip that vulnerability. | |
| ### Go packages (Go workspace at bulker/) | |
| - Find every workspace member that depends on the package: | |
| grep -l 'ACTUAL_PKG' bulker/*/go.mod | |
| - Confirm the EXACT module path in go.mod matches the alert's package, | |
| including the major-version suffix (e.g. `/v2`). If the repo uses a | |
| higher major version than the alert references, the alert does not | |
| apply — skip it; never add or pin the older major-version path. | |
| - Check the current version in go.mod/go.sum. If it is already >= vFIXED, | |
| skip — `go get ACTUAL_PKG@vFIXED` would DOWNGRADE it, which is forbidden. | |
| - For EACH matching member whose version is below vFIXED, run inside its | |
| module directory: | |
| go -C bulker/ACTUAL_MODULE get ACTUAL_PKG@vFIXED | |
| - After updating all affected members, sync the workspace: | |
| go -C bulker work sync | |
| - Manually drop hashes for old vulnerable versions from the affected | |
| go.sum files and bulker/go.work.sum (leaving unrelated entries alone). | |
| - Commit the changed go.mod/go.sum of each affected member together | |
| with bulker/go.work.sum. | |
| - Do NOT modify bulker/go.work — the use(...) list must not change for | |
| a security fix. | |
| - If `go get` or `go work sync` fails, revert and skip. | |
| ### Creating PRs | |
| There are at most two PRs per run — one per stack — so the structure is: | |
| - Branch name: | |
| security/fix-npm-YYYY-MM-DD (all npm fixes) | |
| security/fix-go-YYYY-MM-DD (all Go fixes) | |
| - Single commit per branch: | |
| fix(security): batch npm dependency security fixes (or batch go ...) | |
| - Push and open PR: | |
| git push -u origin HEAD | |
| gh pr create --base newjitsu --title "fix(security): <stack> security fixes (YYYY-MM-DD)" --body "..." | |
| - PR body must enumerate every included fix, one bullet per CVE: | |
| - CVE_ID (SEVERITY): short vuln description — ACTUAL_PKG FROM_VER → FIXED_VER | |
| Plus a final "Risks" section calling out any major version bumps or | |
| breaking changes that a reviewer should split into a separate follow-up PR. | |
| - After creating the first PR, reset before starting the other stack: | |
| git checkout newjitsu && git reset --hard origin/newjitsu | |
| ## Rules | |
| - NEVER modify the newjitsu branch directly — always use a new branch. | |
| - NEVER run a command that still contains an ALL_CAPS placeholder. | |
| - NEVER force-push. | |
| - NEVER downgrade a dependency. Every change must move a version up or | |
| leave it unchanged — directly and transitively. If a "fix" would lower | |
| any resolved version, it is wrong: skip it. | |
| - A fixed_version from an alert is a FLOOR, not a target. If the repo is | |
| already at or above it, there is nothing to do — skip the alert. | |
| - Prefer opening NO PR over opening one that downgrades, is a no-op, or | |
| does not build. | |
| - If any command fails, revert the working tree to clean state and skip that vulnerability. | |
| - When in doubt between overriding vs. bumping a direct dependency, prefer the smaller change (override). | |
| - When in doubt between one PR or many, prefer many small PRs. | |
| PROMPT_EOF | |
| - name: 🧰 Reinstall Codex CLI (workaround) | |
| # openai/codex-action@v1 relies on a globally-installed @openai/codex. | |
| # On GitHub-hosted runners the platform-specific optional dep | |
| # (@openai/codex-linux-x64) sometimes gets skipped, producing: | |
| # "Missing optional dependency @openai/codex-linux-x64" | |
| # Force a clean reinstall with --include=optional before the action. | |
| run: | | |
| set -euo pipefail | |
| npm uninstall -g @openai/codex >/dev/null 2>&1 || true | |
| npm install -g --include=optional @openai/codex@latest | |
| codex --version | |
| - name: 🔒 Fix vulnerabilities with Codex | |
| id: run-codex | |
| # openai is a major vendor with a controlled release process; @v1 is a | |
| # sufficient pin. SHA-pinning every OpenAI action update would be more | |
| # overhead than the risk warrants. | |
| uses: openai/codex-action@v1 | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GITHUB_TOKEN: ${{ github.token }} | |
| # Belt-and-suspenders: if codex-action or the underlying codex CLI | |
| # ever reads these, they force headless behaviour. | |
| CODEX_NON_INTERACTIVE: "1" | |
| CI: "true" | |
| with: | |
| openai-api-key: ${{ secrets.OPENAI_API_KEY }} | |
| prompt-file: .github/codex/security-fix.prompt.md | |
| output-file: .github/codex/security-fix.output.md | |
| model: gpt-5.3-codex | |
| effort: high | |
| # danger-full-access: no approval gating — required because the agent | |
| # needs to run `pnpm install`, `go get`, `git commit`, `gh pr create` | |
| # without waiting for a human to approve each one. | |
| sandbox: danger-full-access | |
| codex-home: .github/codex/home | |
| # Separate job so actions: write is not available to the Codex step above. | |
| # ai-review.yml skips PRs opened by jitsu-code-review[bot] (codex-action's | |
| # write-access check fails for app-bot actors), so we dispatch it manually. | |
| # Running over all open security/* PRs is intentional — this workflow is | |
| # manual, so a re-review on an older PR is harmless. | |
| trigger-review: | |
| name: 🔍 Trigger AI Review | |
| needs: scan-and-fix | |
| if: ${{ inputs.dry_run != true }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| actions: write | |
| pull-requests: read | |
| steps: | |
| - name: Dispatch AI review for open security PRs | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| gh pr list --repo ${{ github.repository }} --base newjitsu --state open \ | |
| --json number,headRefName \ | |
| --jq '[.[] | select(.headRefName | startswith("security/")) | .number | tostring] | .[]' \ | |
| | while IFS= read -r num; do | |
| echo "Dispatching AI review for PR #$num" | |
| gh workflow run ai-review.yml --repo ${{ github.repository }} --ref newjitsu -f pr_number="$num" | |
| done | |