Skip to content

🔒 Security Vulnerability Fix #34

🔒 Security Vulnerability Fix

🔒 Security Vulnerability Fix #34

Workflow file for this run

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