From fc5af300a2de17ceeb58a39b7a7ed9acd78644bb Mon Sep 17 00:00:00 2001 From: Ulzii Otgonbaatar Date: Thu, 2 Apr 2026 14:14:45 -0600 Subject: [PATCH 1/2] ci: add automated vulnerability remediation workflow Made-with: Cursor --- .github/workflows/vuln-remediation-prompt.md | 245 +++++++++++++++++++ .github/workflows/vuln-remediation.yml | 56 +++++ 2 files changed, 301 insertions(+) create mode 100644 .github/workflows/vuln-remediation-prompt.md create mode 100644 .github/workflows/vuln-remediation.yml diff --git a/.github/workflows/vuln-remediation-prompt.md b/.github/workflows/vuln-remediation-prompt.md new file mode 100644 index 00000000..aadde807 --- /dev/null +++ b/.github/workflows/vuln-remediation-prompt.md @@ -0,0 +1,245 @@ +You are a security engineer remediating dependency vulnerabilities in a GitHub repository. + +The GitHub CLI is available as `gh` and authenticated via GH_TOKEN. Git is available. You have write access to repository contents and can create pull requests. + +# Context + +- Repo: ${GITHUB_REPOSITORY} +- Date: ${DATE} + +# Goal + +Process open Dependabot vulnerability alerts for this repository. For each alert, either fix the vulnerability by upgrading the dependency or dismiss the alert with a documented reason. This workflow follows the detector-manager-fixer pattern. + +This workflow uses an evergreen branch and PR to prevent PRs from piling up. Each week, the same branch/PR is updated with fresh vulnerability fixes. + +# Workflow + +## Step 1: Load alerts + +Read `dependabot-alerts.json` in the current directory. This file contains the raw Dependabot alerts JSON from the GitHub API. + +If the file is empty or contains `[]`, output "No open vulnerability alerts." and exit. + +Parse each alert to extract: +- `number`: the alert number (needed for dismissal API calls) +- `security_advisory.severity`: critical, high, medium, or low +- `security_advisory.summary`: short description +- `security_advisory.cve_id`: CVE identifier +- `security_advisory.vulnerabilities[].first_patched_version.identifier`: the fix version +- `dependency.package.name`: package name +- `dependency.package.ecosystem`: go, npm, or pip +- `dependency.scope`: runtime or development +- `dependency.manifest_path`: which manifest file is affected + +## Step 2: Triage alerts (Manager phase) + +For each alert, classify it into one of three categories. Process alerts in priority order: critical first, then high, then medium, then low. + +### Category: DISMISS + +Dismiss the alert if ANY of the following are true: + +1. **Development-only dependency**: `scope` is `development`. These are not deployed to production. + - Dismiss reason: `not_used` + - Comment: "Development dependency not deployed to production." + +2. **Unreachable vulnerable code (Go only)**: For Go dependencies, check if the vulnerable package's symbols are actually imported and called. Run: + ``` + grep -r '' --include='*.go' -l + ``` + If the package is not imported, or only imported in test files (`_test.go`), dismiss it. + - Dismiss reason: `not_used` + - Comment: "Vulnerable package symbols not reachable from production code paths." + +3. **Alert in non-production manifest**: If the manifest path points to a test, script, or example directory (e.g., `scripts/`, `e2e/`, `testing/`, `replays/`), the dependency is not part of the production deployment. + - Dismiss reason: `not_used` + - Comment: "Dependency only used in [test/script/example] code, not in production." + +### Category: FIX + +Fix the alert if ALL of the following are true: + +1. The dependency is a runtime dependency (`scope` is `runtime`) +2. A `first_patched_version` exists +3. The dependency is in a production manifest (not scripts/tests/examples) + +### Category: DEFER + +Defer the alert if: + +1. It is a runtime dependency but no patched version is available yet +2. The fix requires a major version bump that is likely to have breaking changes + +Do not create any dismissal or PR for deferred alerts — they will be reported in the PR body for human review. + +## Step 3: Dismiss alerts + +For each alert classified as DISMISS, call: + +``` +gh api --method PATCH "/repos/${GITHUB_REPOSITORY}/dependabot/alerts/" \ + -f state=dismissed \ + -f dismissed_reason="" \ + -f dismissed_comment="" +``` + +Valid values for `dismissed_reason`: `fix_started`, `inaccurate`, `no_bandwidth`, `not_used`, `tolerable_risk`. + +Track all dismissals for the final report. + +## Step 4: Setup the evergreen branch + +Check if the evergreen branch already exists: + +``` +git fetch origin security/vuln-remediation 2>/dev/null || true +``` + +If the branch exists, check it out and reset it to main: + +``` +git checkout -B security/vuln-remediation origin/main +``` + +If it doesn't exist, create it from main: + +``` +git checkout -b security/vuln-remediation +``` + +## Step 5: Fix vulnerabilities + +For each alert classified as FIX, grouped by manifest file: + +### Go dependencies (`go.mod`) + +From the directory containing the `go.mod` file: + +``` +go get @v +go mod tidy +``` + +If `go get` with the exact patched version fails, try `@latest`: + +``` +go get @latest +go mod tidy +``` + +### npm dependencies (`package.json` or `package-lock.json` or `pnpm-lock.yaml`) + +From the directory containing the manifest: + +If `package.json` lists the dependency directly, update the version constraint and run `bun install`. + +If the vulnerability is in a lockfile-only transitive dependency, run: + +``` +bun update +``` + +### Python dependencies (`pyproject.toml` or `requirements.txt`) + +From the directory containing the manifest: + +Edit the version constraint in `pyproject.toml` or `requirements.txt`, then: + +``` +uv sync +``` + +Or if uv is not available: + +``` +pip install -r requirements.txt +``` + +## Step 6: Verify the build + +For each directory containing a modified manifest file, verify the build still works: + +1. Check if a Makefile exists with a `build` target — if so, run `make build` +2. Otherwise, for Go: `go build ./...` +3. For Node/Bun: `bun run build` (if a build script exists) + +All builds must succeed. If a build fails due to a specific dependency upgrade: + +1. Revert that dependency change: `git checkout -- ` +2. Re-run `go mod tidy` or `bun install` to restore the previous state +3. Move the alert from FIX to DEFER +4. Continue with the next dependency + +## Step 7: Run tests + +For each directory containing a modified manifest file: + +1. Check if a Makefile exists with a `test` target — if so, run `make test` +2. Otherwise, for Go: `go test ./...` +3. For Node/Bun: `bun test` (if a test script exists) + +If tests fail due to a specific dependency upgrade: + +1. Analyze if the failure is related to the upgrade or a flaky test +2. If related to the upgrade, revert that dependency and move the alert to DEFER +3. If the test is flaky/unrelated, note it and proceed + +## Step 8: Format code + +Run `bun run format` to ensure all code is properly formatted before committing. + +## Step 9: Create or update the evergreen PR + +If any fixes were applied and builds/tests pass: + +1. Commit all changes with message: `security: vulnerability remediation (${DATE})` + Include in the commit body a list of what was fixed. + +2. Force push the branch: `git push -f origin security/vuln-remediation` + +3. Check if a PR already exists: + ``` + gh pr list --head security/vuln-remediation --state open --json number + ``` + +4. Build the PR body with this structure: + + ``` + ## Vulnerability Remediation — ${DATE} + + ### Fixed + | CVE | Package | Severity | Old Version | New Version | Manifest | + |-----|---------|----------|-------------|-------------|----------| + | ... | ... | ... | ... | ... | ... | + + ### Dismissed + | CVE | Package | Severity | Reason | Comment | + |-----|---------|----------|--------|---------| + | ... | ... | ... | ... | ... | + + ### Deferred (needs human review) + | CVE | Package | Severity | Why | + |-----|---------|----------|-----| + | ... | ... | ... | ... | + ``` + +5. If a PR exists, update its body with the new report. + +6. If no PR exists, create one with: + - Title: `security: vulnerability remediation` + - Body: the report above + +7. Find a reviewer: Use `gh pr list --state merged --limit 20 --json author` to identify users who have recently authored merged PRs, then assign one randomly using `gh pr edit --add-assignee ` + +If no fixes were applied but dismissals or deferrals were made, still post a summary as a PR comment or issue comment so there is a record. + +# Constraints + +- Process at most 10 alerts per run (prioritize by severity: critical > high > medium > low) +- Only dismiss alerts with documented reasons — never silently skip +- All builds AND tests must pass before the PR is created +- Use the evergreen branch `security/vuln-remediation` — always reset it to main before making changes +- Never force-push or modify `main` directly +- If no actionable alerts remain after triage, exit without creating/updating the PR +- Be conservative: when in doubt, classify as DEFER rather than DISMISS diff --git a/.github/workflows/vuln-remediation.yml b/.github/workflows/vuln-remediation.yml new file mode 100644 index 00000000..2918af85 --- /dev/null +++ b/.github/workflows/vuln-remediation.yml @@ -0,0 +1,56 @@ +name: Vulnerability Remediation + +on: + schedule: + - cron: '0 3 * * 3' + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + remediate: + runs-on: ubuntu-latest + steps: + - name: Generate app token + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.ADMIN_APP_ID }} + private-key: ${{ secrets.ADMIN_APP_PRIVATE_KEY }} + + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ steps.app-token.outputs.token }} + + - name: Install Cursor CLI + run: | + curl https://cursor.com/install -fsS | bash + echo "$HOME/.cursor/bin" >> $GITHUB_PATH + + - name: Configure git identity + run: | + git config user.name "kernel-internal[bot]" + git config user.email "260533166+kernel-internal[bot]@users.noreply.github.com" + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: 'go.mod' + + - name: Fetch Dependabot alerts + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + gh api "/repos/${{ github.repository }}/dependabot/alerts?state=open&per_page=100" > dependabot-alerts.json 2>/dev/null || echo "[]" > dependabot-alerts.json + + - name: Remediate vulnerabilities + env: + CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + export DATE="$(date -u +%Y-%m-%d)" + envsubst '${GITHUB_REPOSITORY} ${DATE}' < .github/workflows/vuln-remediation-prompt.md | agent -p --model ${{ secrets.CURSOR_PREFERRED_MODEL }} --trust --force --output-format=text From 3ee9d8a8550d8fda7f1441958aabade274d01f8f Mon Sep 17 00:00:00 2001 From: Ulzii Otgonbaatar Date: Thu, 2 Apr 2026 14:34:59 -0600 Subject: [PATCH 2/2] ci: switch vuln remediation from Dependabot to Socket.dev Made-with: Cursor --- .github/workflows/vuln-remediation-prompt.md | 112 ++++++++----------- .github/workflows/vuln-remediation.yml | 10 +- 2 files changed, 53 insertions(+), 69 deletions(-) diff --git a/.github/workflows/vuln-remediation-prompt.md b/.github/workflows/vuln-remediation-prompt.md index aadde807..842854e5 100644 --- a/.github/workflows/vuln-remediation-prompt.md +++ b/.github/workflows/vuln-remediation-prompt.md @@ -1,6 +1,6 @@ You are a security engineer remediating dependency vulnerabilities in a GitHub repository. -The GitHub CLI is available as `gh` and authenticated via GH_TOKEN. Git is available. You have write access to repository contents and can create pull requests. +The GitHub CLI is available as `gh` and authenticated via GH_TOKEN. Git is available. You have write access to repository contents and can create pull requests. The Socket CLI is available as `socket` and authenticated via SOCKET_SECURITY_API_KEY. # Context @@ -9,7 +9,7 @@ The GitHub CLI is available as `gh` and authenticated via GH_TOKEN. Git is avail # Goal -Process open Dependabot vulnerability alerts for this repository. For each alert, either fix the vulnerability by upgrading the dependency or dismiss the alert with a documented reason. This workflow follows the detector-manager-fixer pattern. +Process vulnerability alerts from a Socket.dev scan report for this repository. For each alert, either fix the vulnerability by upgrading the dependency or log the decision with a reason. This workflow follows the detector-manager-fixer pattern. This workflow uses an evergreen branch and PR to prevent PRs from piling up. Each week, the same branch/PR is updated with fresh vulnerability fixes. @@ -17,78 +17,65 @@ This workflow uses an evergreen branch and PR to prevent PRs from piling up. Eac ## Step 1: Load alerts -Read `dependabot-alerts.json` in the current directory. This file contains the raw Dependabot alerts JSON from the GitHub API. +Read `socket-report.json` in the current directory. This file contains the Socket scan report JSON. -If the file is empty or contains `[]`, output "No open vulnerability alerts." and exit. +If the report shows `"healthy": true` and the `alerts` map is empty, output "No vulnerability alerts. Scan is healthy." and exit. -Parse each alert to extract: -- `number`: the alert number (needed for dismissal API calls) -- `security_advisory.severity`: critical, high, medium, or low -- `security_advisory.summary`: short description -- `security_advisory.cve_id`: CVE identifier -- `security_advisory.vulnerabilities[].first_patched_version.identifier`: the fix version -- `dependency.package.name`: package name -- `dependency.package.ecosystem`: go, npm, or pip -- `dependency.scope`: runtime or development -- `dependency.manifest_path`: which manifest file is affected +The Socket report nests alerts by ecosystem, package, version, file, and location. Flatten these into a list of unique alerts. For each alert, extract: +- Alert type (e.g., `cve`, `installScripts`, `networkAccess`, `envVars`, etc.) +- Severity: `error`, `warn`, `monitor`, `ignore` +- Package name and version +- Ecosystem (npm, go, pypi, etc.) +- Description / summary if available +- CVE ID if the alert type is `cve` + +If `socket-report.json` is missing or unparseable, fall back to running a fresh scan: + +``` +socket scan create --repo= --branch=main --report --json --fold=version > socket-report.json +``` ## Step 2: Triage alerts (Manager phase) -For each alert, classify it into one of three categories. Process alerts in priority order: critical first, then high, then medium, then low. +Focus only on alerts with severity `error` or `warn`. Ignore `monitor` and `ignore` level alerts. + +For each alert, classify it into one of three categories. Process alerts in priority order: `error` first, then `warn`. ### Category: DISMISS -Dismiss the alert if ANY of the following are true: +Skip the alert if ANY of the following are true: -1. **Development-only dependency**: `scope` is `development`. These are not deployed to production. - - Dismiss reason: `not_used` - - Comment: "Development dependency not deployed to production." +1. **Non-CVE behavioral alert**: Alert types like `installScripts`, `networkAccess`, `envVars`, `shellAccess`, `filesystemAccess` are informational supply chain signals. Log them but do not attempt to fix. These are useful for awareness but not actionable via dependency bumps. -2. **Unreachable vulnerable code (Go only)**: For Go dependencies, check if the vulnerable package's symbols are actually imported and called. Run: +2. **Development-only dependency**: If the package appears only in dev dependency sections (devDependencies in package.json, test files in go.mod). These are not deployed to production. + +3. **Unreachable vulnerable code (Go only)**: For Go dependencies, check if the vulnerable package's symbols are actually imported and called: ``` grep -r '' --include='*.go' -l ``` - If the package is not imported, or only imported in test files (`_test.go`), dismiss it. - - Dismiss reason: `not_used` - - Comment: "Vulnerable package symbols not reachable from production code paths." + If the package is not imported, or only imported in test files (`_test.go`), skip it. -3. **Alert in non-production manifest**: If the manifest path points to a test, script, or example directory (e.g., `scripts/`, `e2e/`, `testing/`, `replays/`), the dependency is not part of the production deployment. - - Dismiss reason: `not_used` - - Comment: "Dependency only used in [test/script/example] code, not in production." +4. **Alert in non-production manifest**: If the manifest path points to a test, script, or example directory (e.g., `scripts/`, `e2e/`, `testing/`, `replays/`, `shared/cdp-test/`), the dependency is not part of the production deployment. ### Category: FIX Fix the alert if ALL of the following are true: -1. The dependency is a runtime dependency (`scope` is `runtime`) -2. A `first_patched_version` exists -3. The dependency is in a production manifest (not scripts/tests/examples) +1. The alert type is `cve` (a known vulnerability with a CVE ID) +2. The dependency is a runtime/production dependency +3. A newer version of the package exists that resolves the CVE +4. The dependency is in a production manifest ### Category: DEFER Defer the alert if: -1. It is a runtime dependency but no patched version is available yet +1. It is a CVE in a runtime dependency but no patched version is available yet 2. The fix requires a major version bump that is likely to have breaking changes -Do not create any dismissal or PR for deferred alerts — they will be reported in the PR body for human review. - -## Step 3: Dismiss alerts - -For each alert classified as DISMISS, call: - -``` -gh api --method PATCH "/repos/${GITHUB_REPOSITORY}/dependabot/alerts/" \ - -f state=dismissed \ - -f dismissed_reason="" \ - -f dismissed_comment="" -``` - -Valid values for `dismissed_reason`: `fix_started`, `inaccurate`, `no_bandwidth`, `not_used`, `tolerable_risk`. +Do not attempt fixes for deferred alerts — they will be reported in the PR body for human review. -Track all dismissals for the final report. - -## Step 4: Setup the evergreen branch +## Step 3: Setup the evergreen branch Check if the evergreen branch already exists: @@ -108,7 +95,7 @@ If it doesn't exist, create it from main: git checkout -b security/vuln-remediation ``` -## Step 5: Fix vulnerabilities +## Step 4: Fix vulnerabilities For each alert classified as FIX, grouped by manifest file: @@ -116,13 +103,6 @@ For each alert classified as FIX, grouped by manifest file: From the directory containing the `go.mod` file: -``` -go get @v -go mod tidy -``` - -If `go get` with the exact patched version fails, try `@latest`: - ``` go get @latest go mod tidy @@ -156,7 +136,7 @@ Or if uv is not available: pip install -r requirements.txt ``` -## Step 6: Verify the build +## Step 5: Verify the build For each directory containing a modified manifest file, verify the build still works: @@ -171,7 +151,7 @@ All builds must succeed. If a build fails due to a specific dependency upgrade: 3. Move the alert from FIX to DEFER 4. Continue with the next dependency -## Step 7: Run tests +## Step 6: Run tests For each directory containing a modified manifest file: @@ -185,11 +165,11 @@ If tests fail due to a specific dependency upgrade: 2. If related to the upgrade, revert that dependency and move the alert to DEFER 3. If the test is flaky/unrelated, note it and proceed -## Step 8: Format code +## Step 7: Format code Run `bun run format` to ensure all code is properly formatted before committing. -## Step 9: Create or update the evergreen PR +## Step 8: Create or update the evergreen PR If any fixes were applied and builds/tests pass: @@ -213,10 +193,10 @@ If any fixes were applied and builds/tests pass: |-----|---------|----------|-------------|-------------|----------| | ... | ... | ... | ... | ... | ... | - ### Dismissed - | CVE | Package | Severity | Reason | Comment | - |-----|---------|----------|--------|---------| - | ... | ... | ... | ... | ... | + ### Skipped (non-actionable) + | Alert Type | Package | Severity | Reason | + |------------|---------|----------|--------| + | ... | ... | ... | ... | ### Deferred (needs human review) | CVE | Package | Severity | Why | @@ -232,12 +212,12 @@ If any fixes were applied and builds/tests pass: 7. Find a reviewer: Use `gh pr list --state merged --limit 20 --json author` to identify users who have recently authored merged PRs, then assign one randomly using `gh pr edit --add-assignee ` -If no fixes were applied but dismissals or deferrals were made, still post a summary as a PR comment or issue comment so there is a record. +If no fixes were applied but there are deferred items, still post a summary as a PR comment or issue so there is a record. # Constraints -- Process at most 10 alerts per run (prioritize by severity: critical > high > medium > low) -- Only dismiss alerts with documented reasons — never silently skip +- Process at most 10 CVE alerts per run (prioritize by severity: error > warn) +- Only act on `cve` type alerts for fixes — behavioral alerts are informational only - All builds AND tests must pass before the PR is created - Use the evergreen branch `security/vuln-remediation` — always reset it to main before making changes - Never force-push or modify `main` directly diff --git a/.github/workflows/vuln-remediation.yml b/.github/workflows/vuln-remediation.yml index 2918af85..e771c8c7 100644 --- a/.github/workflows/vuln-remediation.yml +++ b/.github/workflows/vuln-remediation.yml @@ -41,16 +41,20 @@ jobs: with: go-version-file: 'go.mod' - - name: Fetch Dependabot alerts + - name: Install Socket CLI + run: npm install -g @socketsecurity/cli + + - name: Run Socket scan env: - GH_TOKEN: ${{ steps.app-token.outputs.token }} + SOCKET_SECURITY_API_KEY: ${{ secrets.SOCKET_API_TOKEN }} run: | - gh api "/repos/${{ github.repository }}/dependabot/alerts?state=open&per_page=100" > dependabot-alerts.json 2>/dev/null || echo "[]" > dependabot-alerts.json + socket scan create --repo="${{ github.event.repository.name }}" --branch=main --default-branch --report --json > socket-report.json 2>/dev/null || echo '{"healthy":true,"alerts":{}}' > socket-report.json - name: Remediate vulnerabilities env: CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }} GH_TOKEN: ${{ steps.app-token.outputs.token }} + SOCKET_SECURITY_API_KEY: ${{ secrets.SOCKET_API_TOKEN }} run: | export DATE="$(date -u +%Y-%m-%d)" envsubst '${GITHUB_REPOSITORY} ${DATE}' < .github/workflows/vuln-remediation-prompt.md | agent -p --model ${{ secrets.CURSOR_PREFERRED_MODEL }} --trust --force --output-format=text