From 7912d6efc9ad0fa1c7c9a86dcede0ed3324be668 Mon Sep 17 00:00:00 2001 From: mananjain99 Date: Tue, 14 Apr 2026 14:05:34 +0530 Subject: [PATCH 1/4] feat: auto-fix vulnerable app dependencies on PR branch --- .github/workflows/build-and-scan.yaml | 132 ++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/.github/workflows/build-and-scan.yaml b/.github/workflows/build-and-scan.yaml index 9e1ce3619..bf2b426ed 100644 --- a/.github/workflows/build-and-scan.yaml +++ b/.github/workflows/build-and-scan.yaml @@ -42,6 +42,11 @@ on: required: false type: boolean default: true + auto_fix: + description: "Auto-fix app dependency vulnerabilities by updating packages on the PR branch" + required: false + type: boolean + default: false secrets: ORG_PAT_GITHUB: required: true @@ -390,6 +395,15 @@ jobs: fail_on_findings = os.environ.get("FAIL_ON_FINDINGS", "true").lower() == "true" + # Write fixable app dependencies for auto-fix step + fixable = [v for v in new_vulns if v["source_type"] == "app" and v["fixed"]] + with open("/tmp/fixable_packages.json", "w") as f: + json.dump(fixable, f) + if fixable: + print(f"Auto-fixable app dependencies: {len(fixable)}") + for v in fixable: + print(f" {v['package']}@{v['installed']} -> {v['fixed']}") + blockers = new_vulns + expired if blockers: status = "Failed" if fail_on_findings else "Warning" @@ -423,6 +437,15 @@ jobs: print(f"GATE PASSED: All {len(unique)} vulnerabilities are allowlisted") PYEOF + - name: Upload fixable packages + if: always() + uses: actions/upload-artifact@v4 + continue-on-error: true + with: + name: fixable-packages + path: /tmp/fixable_packages.json + retention-days: 1 + - name: Comment on PR if: always() && github.event_name == 'pull_request' uses: actions/github-script@v7 @@ -457,3 +480,112 @@ jobs: body: body, }); } + + # ── Auto-fix: update vulnerable app dependencies on the PR branch ────────── + auto-fix: + name: Auto-fix Vulnerabilities + needs: [security-gate] + if: always() && inputs.auto_fix && github.event_name == 'pull_request' && needs.security-gate.result == 'failure' + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: write + steps: + - name: Checkout PR branch + uses: actions/checkout@v4 + with: + ref: ${{ github.head_ref }} + token: ${{ secrets.ORG_PAT_GITHUB }} + + - name: Download fixable packages + uses: actions/download-artifact@v4 + with: + name: fixable-packages + path: /tmp + + - name: Install uv + uses: astral-sh/setup-uv@v4 + + - name: Auto-fix vulnerable packages + id: fix + run: | + python3 - <<'PYEOF' + import json, subprocess, sys + + with open("/tmp/fixable_packages.json") as f: + fixable = json.load(f) + + if not fixable: + print("No fixable app dependencies found") + sys.exit(0) + + fixed = [] + failed = [] + + for v in fixable: + pkg = v["package"] + target = v["fixed"].split(",")[0].strip() + print(f"Updating {pkg} -> {target}...") + + result = subprocess.run( + ["uv", "lock", "--upgrade-package", pkg], + capture_output=True, text=True + ) + + if result.returncode == 0: + fixed.append(f"{pkg} -> {target}") + print(f" Updated {pkg}") + else: + failed.append(f"{pkg}: {result.stderr[:200]}") + print(f" Failed to update {pkg}: {result.stderr[:200]}") + + import os + with open(os.environ["GITHUB_OUTPUT"], "a") as f: + f.write(f"fixed_count={len(fixed)}\n") + f.write(f"failed_count={len(failed)}\n") + + if fixed: + summary = "### Auto-fixed packages\n" + "\n".join(f"- {p}" for p in fixed) + if failed: + summary += "\n\n### Failed to update\n" + "\n".join(f"- {p}" for p in failed) + with open("/tmp/fix_summary.md", "w") as f: + f.write(summary) + + print(f"\nDone: {len(fixed)} fixed, {len(failed)} failed") + PYEOF + + - name: Commit fixes + if: steps.fix.outputs.fixed_count > 0 + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add uv.lock pyproject.toml 2>/dev/null || true + if ! git diff --staged --quiet; then + git commit -m "fix(security): auto-update vulnerable dependencies + + Updated packages with known CRITICAL/HIGH CVEs to their fixed versions. + This commit was created automatically by the security scan workflow." + git push + echo "Fixes committed and pushed — checks will re-run automatically" + else + echo "No lockfile changes to commit" + fi + + - name: Comment fix summary on PR + if: steps.fix.outputs.fixed_count > 0 && github.event_name == 'pull_request' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + let body; + try { + body = fs.readFileSync('/tmp/fix_summary.md', 'utf8'); + } catch { + return; + } + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: body, + }); From 74c0039f5457cb51d971c0061269fc679b40caeb Mon Sep 17 00:00:00 2001 From: mananjain99 Date: Tue, 14 Apr 2026 14:22:57 +0530 Subject: [PATCH 2/4] fix: add source_type to all parsed vulnerabilities for auto-fix filtering --- .github/workflows/build-and-scan.yaml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-and-scan.yaml b/.github/workflows/build-and-scan.yaml index bf2b426ed..91f9f56e1 100644 --- a/.github/workflows/build-and-scan.yaml +++ b/.github/workflows/build-and-scan.yaml @@ -287,6 +287,9 @@ jobs: with open("/tmp/trivy_results.json") as f: trivy = json.load(f) for result in trivy.get("Results", []): + result_type = result.get("Type", "") + # Python lockfiles = app dependency, everything else = base image + is_app = result_type in ("pip", "pipenv", "poetry", "uv", "python-pkg") for vuln in result.get("Vulnerabilities", []): vid = vuln.get("VulnerabilityID") if vid and vid not in all_vulns: @@ -297,6 +300,7 @@ jobs: "installed": vuln.get("InstalledVersion", ""), "fixed": vuln.get("FixedVersion", ""), "source": "trivy", + "source_type": "app" if is_app else "base_image", } trivy_count += 1 except Exception as e: @@ -308,6 +312,7 @@ jobs: with open("/tmp/snyk_results.json") as f: snyk = json.load(f) if not snyk.get("error"): + # Snyk container scan: top-level vulns are from the image (base_image) for vuln in snyk.get("vulnerabilities", []): vid = vuln.get("id") cves = vuln.get("identifiers", {}).get("CVE", []) @@ -320,8 +325,10 @@ jobs: "installed": vuln.get("version", ""), "fixed": ", ".join(vuln.get("fixedIn", [])) if vuln.get("fixedIn") else "", "source": "snyk", + "source_type": "base_image", } snyk_count += 1 + # Snyk applications section = app dependencies for app in snyk.get("applications", []): for vuln in app.get("vulnerabilities", []): vid = vuln.get("id") @@ -333,6 +340,7 @@ jobs: "installed": vuln.get("version", ""), "fixed": ", ".join(vuln.get("fixedIn", [])) if vuln.get("fixedIn") else "", "source": "snyk", + "source_type": "app", } snyk_count += 1 except Exception as e: @@ -396,7 +404,7 @@ jobs: fail_on_findings = os.environ.get("FAIL_ON_FINDINGS", "true").lower() == "true" # Write fixable app dependencies for auto-fix step - fixable = [v for v in new_vulns if v["source_type"] == "app" and v["fixed"]] + fixable = [v for v in new_vulns if v.get("source_type", "app") == "app" and v.get("fixed")] with open("/tmp/fixable_packages.json", "w") as f: json.dump(fixable, f) if fixable: From 3805ea4b992d2ef1cd73971a587eb1417ac8ddc8 Mon Sep 17 00:00:00 2001 From: mananjain99 Date: Tue, 14 Apr 2026 14:43:52 +0530 Subject: [PATCH 3/4] fix: filter out Go/Rust packages from auto-fix, only fix Python deps --- .github/workflows/build-and-scan.yaml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-and-scan.yaml b/.github/workflows/build-and-scan.yaml index 91f9f56e1..9d521fc52 100644 --- a/.github/workflows/build-and-scan.yaml +++ b/.github/workflows/build-and-scan.yaml @@ -404,7 +404,14 @@ jobs: fail_on_findings = os.environ.get("FAIL_ON_FINDINGS", "true").lower() == "true" # Write fixable app dependencies for auto-fix step - fixable = [v for v in new_vulns if v.get("source_type", "app") == "app" and v.get("fixed")] + # Only fix Python packages (skip Go/Rust/system packages) + go_prefixes = ("github.com/", "golang.org/", "google.golang.org/", "go.opentelemetry.io/", "go.") + fixable = [ + v for v in new_vulns + if v.get("source_type", "app") == "app" + and v.get("fixed") + and not v.get("package", "").startswith(go_prefixes) + ] with open("/tmp/fixable_packages.json", "w") as f: json.dump(fixable, f) if fixable: From 75c4a6f5502a59b8716a07e3f9a76a0c81ce069c Mon Sep 17 00:00:00 2001 From: mananjain99 Date: Tue, 14 Apr 2026 17:27:30 +0530 Subject: [PATCH 4/4] fix: create new comment per scan run (audit trail) + fix auto-fix comment permissions --- .github/workflows/build-and-scan.yaml | 26 ++++++++------------------ 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/.github/workflows/build-and-scan.yaml b/.github/workflows/build-and-scan.yaml index 9d521fc52..8d70817ea 100644 --- a/.github/workflows/build-and-scan.yaml +++ b/.github/workflows/build-and-scan.yaml @@ -473,28 +473,14 @@ jobs: } catch { body = '### 🛡️ Security Gate\n\nScan results unavailable.'; } - const marker = '### 🛡️ Security Gate'; - const { data: comments } = await github.rest.issues.listComments({ + const time = new Date().toUTCString(); + body += `\n\nRun: ${context.runId} | ${time}`; + await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo, issue_number: context.issue.number, + body: body, }); - const existing = comments.find(c => c.body.startsWith(marker)); - if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id, - body: body, - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: body, - }); - } # ── Auto-fix: update vulnerable app dependencies on the PR branch ────────── auto-fix: @@ -505,6 +491,7 @@ jobs: timeout-minutes: 10 permissions: contents: write + pull-requests: write steps: - name: Checkout PR branch uses: actions/checkout@v4 @@ -590,6 +577,7 @@ jobs: if: steps.fix.outputs.fixed_count > 0 && github.event_name == 'pull_request' uses: actions/github-script@v7 with: + github-token: ${{ github.token }} script: | const fs = require('fs'); let body; @@ -598,6 +586,8 @@ jobs: } catch { return; } + const time = new Date().toUTCString(); + body += `\n\nRun: ${context.runId} | ${time}`; await github.rest.issues.createComment({ owner: context.repo.owner, repo: context.repo.repo,