Skip to content
Draft
Show file tree
Hide file tree
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
35 changes: 35 additions & 0 deletions .github/workflows/auto-vuln-fix.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Auto Vulnerability Fixer

on:
schedule:
- cron: '0 2 * * *' # Run daily at 2 AM UTC
workflow_dispatch: # Allow manual trigger

jobs:
fix-vulnerabilities:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.26.1'

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.13'

- name: Install Python dependencies
run: pip install requests packaging

- name: Run Vulnerability Robot
run: python3 vuln_robot.py --create-pr
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
12 changes: 6 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -72,17 +72,17 @@ require (
go.uber.org/zap v1.27.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/oauth2 v0.32.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/oauth2 v0.34.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/term v0.37.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/term v0.38.0 // indirect
golang.org/x/text v0.32.0 // indirect
golang.org/x/time v0.13.0 // indirect
gomodules.xyz/jsonpatch/v2 v2.5.0 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
google.golang.org/grpc v1.77.0 // indirect
google.golang.org/grpc v1.79.3 // indirect
google.golang.org/protobuf v1.36.10 // indirect
gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
Expand Down
28 changes: 14 additions & 14 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -162,34 +162,34 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
Expand All @@ -202,8 +202,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
Expand Down
182 changes: 182 additions & 0 deletions vuln_robot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
import subprocess
import json
import requests
import sys
import os
import re
import argparse
from packaging.version import parse as parse_version

def get_go_dependencies():
result = subprocess.run(['go', 'list', '-m', 'all'], capture_output=True, text=True)
if result.returncode != 0:
return []

deps = []
lines = result.stdout.strip().split('\n')
for line in lines[1:]:
parts = line.split()
if len(parts) >= 2:
name = parts[0]
version = parts[1]
deps.append({'name': name, 'version': version, 'ecosystem': 'Go'})
return deps

def get_python_dependencies(req_file):
deps = []
if not os.path.exists(req_file):
return []

with open(req_file, 'r') as f:
for line in f:
line = line.strip()
if not line or line.startswith('#'):
continue
# Basic regex to match package==version
match = re.match(r'^([a-zA-Z0-9\-_]+)==([a-zA-Z0-9\.\-_]+)', line)
if match:
deps.append({'name': match.group(1), 'version': match.group(2), 'ecosystem': 'PyPI'})
return deps

def check_vulnerability(package_name, version, ecosystem):
clean_version = version.lstrip('v')
url = "https://api.osv.dev/v1/query"
payload = {
"version": clean_version,
"package": {
"name": package_name,
"ecosystem": ecosystem
}
}
try:
response = requests.post(url, json=payload, timeout=10)
if response.status_code == 200:
return response.json()
except Exception as e:
print(f"Error querying OSV for {package_name}: {e}")
return None

def apply_go_fix(package, version):
if not version.startswith('v'):
version = f'v{version}'
print(f"Updating Go package {package} to {version}...")
result = subprocess.run(['go', 'get', f'{package}@{version}'], capture_output=True, text=True)
if result.returncode != 0:
print(f"Error updating {package}: {result.stderr}")
return False
return True

def apply_python_fix(req_file, package, old_version, new_version):
print(f"Updating Python package {package} to {new_version} in {req_file}...")
with open(req_file, 'r') as f:
content = f.read()

new_content = content.replace(f"{package}=={old_version}", f"{package}=={new_version}")
with open(req_file, 'w') as f:
f.write(new_content)
return True

def create_github_pr(fix_details):
print("Detected changes. Creating GitHub PR...")

# Check if we are in a git repo
if subprocess.run(['git', 'rev-parse', '--is-inside-work-tree'], capture_output=True).returncode != 0:
print("Not a git repository. Skipping PR creation.")
return

# Check for changes
status = subprocess.run(['git', 'status', '--porcelain'], capture_output=True, text=True)
if not status.stdout.strip():
print("No changes to commit. Skipping PR creation.")
return

# Create a unique branch name
import time
branch_name = f"vuln-fix-{int(time.time())}"

subprocess.run(['git', 'checkout', '-b', branch_name])
subprocess.run(['git', 'add', '.'])

commit_msg = "Security: automated vulnerability fixes\n\nFixed vulnerabilities in:\n"
for fix in fix_details:
commit_msg += f"- {fix['name']} ({fix['old_version']} -> {fix['new_version']})\n"

subprocess.run(['git', 'commit', '-m', commit_msg])

# Use gh CLI to create PR (pre-installed in GH Actions)
print(f"Pushing branch {branch_name} and opening PR...")
push_res = subprocess.run(['git', 'push', 'origin', branch_name], capture_output=True, text=True)
if push_res.returncode != 0:
print(f"Error pushing branch: {push_res.stderr}")
return

pr_title = "[Security] Automated dependency vulnerability fixes"
pr_body = "The vulnerability robot has detected and fixed the following vulnerabilities:\n\n"
for fix in fix_details:
pr_body += f"* **{fix['name']}**: {fix['old_version']} -> {fix['new_version']} ({fix['ecosystem']})\n"
pr_body += "\nThis PR was automatically generated."

subprocess.run(['gh', 'pr', 'create', '--title', pr_title, '--body', pr_body])

def main():
parser = argparse.ArgumentParser(description='Vulnerability Fixing Robot')
parser.add_argument('--create-pr', action='store_true', help='Create a GitHub PR if fixes are applied')
args = parser.parse_args()

all_deps = []

# Check for Go dependencies
if os.path.exists('go.mod'):
print("Fetching Go dependencies...")
all_deps.extend(get_go_dependencies())

# Check for Python dependencies (recursive search)
for root, dirs, files in os.walk('.'):
for file in files:
if file == 'requirements.txt':
full_path = os.path.join(root, file)
print(f"Fetching Python dependencies from {full_path}...")
all_deps.extend([ {**d, 'file': full_path} for d in get_python_dependencies(full_path) ])

print(f"Found {len(all_deps)} total dependencies.")

fix_applied = []

for dep in all_deps:
result = check_vulnerability(dep['name'], dep['version'], dep['ecosystem'])
if result and 'vulns' in result:
print(f"!!! Found vulnerability in {dep['ecosystem']} package {dep['name']}@{dep['version']}")
latest_fixed = None
for vuln in result['vulns']:
for affected in vuln.get('affected', []):
if affected.get('package', {}).get('name') == dep['name']:
for range in affected.get('ranges', []):
if range.get('type') == 'SEMVER':
for event in range.get('events', []):
if 'fixed' in event:
fixed_ver = event['fixed']
if latest_fixed is None or parse_version(fixed_ver) > parse_version(latest_fixed):
latest_fixed = fixed_ver

if latest_fixed:
if dep['ecosystem'] == 'Go':
if apply_go_fix(dep['name'], latest_fixed):
fix_applied.append({'name': dep['name'], 'old_version': dep['version'], 'new_version': latest_fixed, 'ecosystem': 'Go'})
elif dep['ecosystem'] == 'PyPI':
if apply_python_fix(dep['file'], dep['name'], dep['version'], latest_fixed):
fix_applied.append({'name': dep['name'], 'old_version': dep['version'], 'new_version': latest_fixed, 'ecosystem': 'PyPI'})

if not fix_applied:
print("No fixes applied.")
else:
if os.path.exists('go.mod'):
print("Running go mod tidy...")
subprocess.run(['go', 'mod', 'tidy'], capture_output=True)

print(f"Successfully fixed {len(fix_applied)} vulnerabilities.")

if args.create_pr:
create_github_pr(fix_applied)

if __name__ == "__main__":
main()