From b714acd24396caecc4396c368512eb8cb27eb7a2 Mon Sep 17 00:00:00 2001 From: Lucky Abolorunke Date: Mon, 30 Mar 2026 22:07:14 +0000 Subject: [PATCH] automation setup for the dependency vulnerability robot --- .github/workflows/auto-vuln-fix.yml | 35 ++++++ go.mod | 12 +- go.sum | 28 ++--- vuln_robot.py | 182 ++++++++++++++++++++++++++++ 4 files changed, 237 insertions(+), 20 deletions(-) create mode 100644 .github/workflows/auto-vuln-fix.yml create mode 100644 vuln_robot.py diff --git a/.github/workflows/auto-vuln-fix.yml b/.github/workflows/auto-vuln-fix.yml new file mode 100644 index 000000000..682c08f15 --- /dev/null +++ b/.github/workflows/auto-vuln-fix.yml @@ -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 }} diff --git a/go.mod b/go.mod index d5b4e8c28..8a878ee96 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index f149b75a8..e4f6dd373 100644 --- a/go.sum +++ b/go.sum @@ -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= @@ -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= diff --git a/vuln_robot.py b/vuln_robot.py new file mode 100644 index 000000000..3540d3380 --- /dev/null +++ b/vuln_robot.py @@ -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()