Skip to content
This repository was archived by the owner on Jun 8, 2026. It is now read-only.
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 195 additions & 0 deletions .github/workflows/security-resolutions.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
name: Auto-fix vulnerable transitive dependencies

on:
schedule:
# Runs every Monday at 8:00 UTC
- cron: "0 8 * * 1"
workflow_dispatch: # Allow manual trigger

permissions:
contents: write
pull-requests: write
security-events: read

jobs:
fix-vulnerabilities:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: "24"

- name: Install dependencies
run: yarn install --frozen-lockfile

- name: Fetch Dependabot alerts and run yarn audit
id: fetch
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
echo "::group::Fetching Dependabot alerts (medium/high/critical)"
gh api \
"/repos/${{ github.repository }}/dependabot/alerts?state=open&severity=medium,high,critical&per_page=100" \
> /tmp/dependabot-alerts.json 2>/dev/null || echo "[]" > /tmp/dependabot-alerts.json
echo "::endgroup::"

echo "::group::Running yarn audit"
yarn audit --json > /tmp/yarn-audit.json 2>/dev/null || true
echo "::endgroup::"

- name: Analyze and update resolutions
id: audit
run: |
python3 << 'PYEOF'
import json, os

# "medium" = Dependabot terminology, "moderate" = yarn audit terminology
MIN_SEVERITIES = {"moderate", "medium", "high", "critical"}
advisories = {}

# --- Source 1: Dependabot alerts ---
try:
with open("/tmp/dependabot-alerts.json", "r") as f:
alerts = json.load(f)
if isinstance(alerts, list):
for alert in alerts:
vuln = alert.get("security_vulnerability", {})
name = vuln.get("package", {}).get("name", "")
severity = alert.get("security_advisory", {}).get("severity", "").lower()
if severity not in MIN_SEVERITIES or not name:
continue
patched_obj = vuln.get("first_patched_version")
if patched_obj and patched_obj.get("identifier"):
version = patched_obj["identifier"]
if name not in advisories or version > advisories[name]["version"]:
advisories[name] = {"version": version, "severity": severity, "source": "dependabot"}
print(f" Dependabot: {name} ({severity}) -> {version}")
except Exception as e:
print(f" Warning: Could not parse Dependabot alerts: {e}")

# --- Source 2: yarn audit ---
try:
with open("/tmp/yarn-audit.json", "r") as f:
for line in f:
try:
obj = json.loads(line)
if obj.get("type") == "auditAdvisory":
d = obj["data"]["advisory"]
name = d["module_name"]
patched = d.get("patched_versions", "")
severity = d.get("severity", "").lower()
if severity not in MIN_SEVERITIES:
continue
if patched.startswith(">="):
version = patched[2:].strip()
if name not in advisories or version > advisories[name]["version"]:
advisories[name] = {"version": version, "severity": severity, "source": "yarn-audit"}
print(f" yarn audit: {name} ({severity}) -> {version}")
except json.JSONDecodeError:
pass
except Exception as e:
print(f" Warning: Could not parse yarn audit: {e}")

print(f"\nFound {len(advisories)} vulnerable packages (moderate/high/critical)")

output_file = os.environ.get("GITHUB_OUTPUT", "/dev/null")

if not advisories:
print("No actionable vulnerabilities found")
with open(output_file, "a") as f:
f.write("has_updates=false\n")
raise SystemExit(0)

# Read current package.json
with open("package.json", "r") as f:
pkg = json.load(f)

resolutions = pkg.get("resolutions", {})
needed = {}

for name, info in advisories.items():
current = resolutions.get(name, "")
target = f"^{info['version']}"
if current != target:
needed[name] = {
"version": target,
"severity": info["severity"],
"current": current,
"source": info["source"],
}

if not needed:
print("All resolutions already up to date")
with open(output_file, "a") as f:
f.write("has_updates=false\n")
raise SystemExit(0)

# Build summary table
lines = []
lines.append("| Dependency | Before | After | Severity | Source |")
lines.append("|---|---|---|---|---|")
for name, info in sorted(needed.items()):
before = info["current"] if info["current"] else "_(none)_"
lines.append(
f"| **{name}** | {before} | {info['version']} | {info['severity']} | {info['source']} |"
)

summary = "\n".join(lines)
print(f"\nUpdates needed:\n{summary}")

# Apply resolutions to package.json
for name, info in needed.items():
resolutions[name] = info["version"]

pkg["resolutions"] = resolutions

with open("package.json", "w") as f:
json.dump(pkg, f, indent=2)
f.write("\n")

with open(output_file, "a") as f:
f.write("has_updates=true\n")
f.write(f"summary<<EOFSUM\n{summary}\nEOFSUM\n")

print(f"\nApplied {len(needed)} resolution updates to package.json")
PYEOF

- name: Reinstall with updated resolutions
if: steps.audit.outputs.has_updates == 'true'
run: yarn install

- name: Verify fixes
if: steps.audit.outputs.has_updates == 'true'
run: |
echo "Remaining vulnerabilities (if any):"
yarn audit --summary 2>/dev/null || true

- name: Create Pull Request
if: steps.audit.outputs.has_updates == 'true'
uses: peter-evans/create-pull-request@v7
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "fix: update resolutions for vulnerable transitive dependencies"
branch: automated/security-resolutions
delete-branch: true
title: "Security: Update transitive dependency resolutions"
body: |
## Summary
Automated update of `resolutions` in `package.json` to fix vulnerable transitive dependencies.
Sources: Dependabot alerts (medium/high/critical) + yarn audit.

### Changes
${{ steps.audit.outputs.summary }}

> **Note:** This only updates transitive dependencies via resolutions. Direct dependency upgrades should be done manually to avoid breaking changes.

### Verify
- [ ] `yarn install` succeeds
- [ ] `yarn build` succeeds
- [ ] App runs correctly
labels: |
dependencies
security
Loading