DEV Community

Ayi NEDJIMI
Ayi NEDJIMI

Posted on

Building an Automated Dependency Update Bot with Security Checks

Building an Automated Dependency Update Bot with Security Checks

Outdated dependencies are behind a significant chunk of real-world breaches — Log4Shell, the 2021 Codecov incident, and countless smaller compromises all involved packages that weren't updated or audited. The fix seems obvious, but most teams don't update regularly because doing it by hand is slow, test coverage is never quite right, and nobody owns the task.

This article walks through building a bot that does it automatically: scan your Python project for outdated packages, check each one against a public vulnerability database, and open a pull request with the update when a CVE is found in the installed version.

Why Manual Dependency Management Fails

The standard approach is to run pip list --outdated or safety check once in a while, feel mild concern, then move on. It fails for three reasons:

  1. No one is accountable. Updating dependencies is infrastructure work with no immediate user value, so it gets deprioritized indefinitely.
  2. Batch updates are risky. Skip six months of updates and one pip install -U can break half your app. Small, frequent updates are safer but require more process than a team will sustain manually.
  3. Vulnerability knowledge is external. You can't know which packages have CVEs just by looking at your requirements.txt. You need a live feed.

An automated bot solves all three: it runs on a schedule, creates one PR per package rather than a giant batch, and pulls CVE data fresh each time.

Architecture Overview

The bot has three stages:

  1. Scan — list all outdated packages in the project
  2. Check — query the OSV API for known CVEs in the currently installed version
  3. Act — if a CVE exists, create a Git branch, install the update, and open a GitHub PR

For CVE data I use the OSV API (osv.dev — Google's open-source vulnerability database). It covers PyPI, npm, Go, Rust, Maven, and more. No authentication required, no rate-limit headaches.

Stage 1: Scanning and Checking CVEs

# checker.py
import subprocess
import json
import requests
from dataclasses import dataclass, field

@dataclass
class PackageStatus:
    name: str
    current_version: str
    latest_version: str
    vulnerabilities: list[dict] = field(default_factory=list)

def get_outdated_packages() -> list[dict]:
    result = subprocess.run(
        ["pip", "list", "--outdated", "--format=json"],
        capture_output=True, text=True, check=True
    )
    return json.loads(result.stdout)

def check_osv(package_name: str, version: str) -> list[dict]:
    payload = {
        "package": {"name": package_name, "ecosystem": "PyPI"},
        "version": version
    }
    resp = requests.post(
        "https://api.osv.dev/v1/query",
        json=payload,
        timeout=10
    )
    resp.raise_for_status()
    return resp.json().get("vulns", [])

def audit_project() -> list[PackageStatus]:
    outdated = get_outdated_packages()
    results = []
    for pkg in outdated:
        vulns = check_osv(pkg["name"], pkg["version"])
        results.append(PackageStatus(
            name=pkg["name"],
            current_version=pkg["version"],
            latest_version=pkg["latest_version"],
            vulnerabilities=vulns
        ))
    return results
Enter fullscreen mode Exit fullscreen mode

A few things worth noting here:

  • pip list --outdated --format=json gives you structured output you can parse without regex. Much more reliable than scraping the text format.
  • The OSV API returns CVE identifiers plus CVSS severity scores, so you can filter by severity if you only want to act on critical or high findings.
  • Wrap results in a dataclass rather than passing raw dicts around — it makes the downstream code cleaner and catches typos at write time.

You can test the scan step in isolation before wiring up the GitHub integration:

for pkg in audit_project():
    if pkg.vulnerabilities:
        ids = [v["id"] for v in pkg.vulnerabilities]
        print(f"{pkg.name} {pkg.current_version} → {pkg.latest_version} | CVEs: {', '.join(ids)}")
Enter fullscreen mode Exit fullscreen mode

Stage 2: Opening Pull Requests Automatically

Once you have a list of packages with CVEs in the installed version, the bot creates a branch per package, runs the update, and opens a PR.

# pr_creator.py
import os
import subprocess
import requests
from datetime import datetime

GITHUB_TOKEN = os.environ["GITHUB_TOKEN"]
REPO = os.environ["GITHUB_REPO"]  # format: "owner/repo"

def update_package_and_open_pr(
    pkg_name: str,
    old_version: str,
    new_version: str,
    vuln_ids: list[str]
) -> str:
    branch = f"deps/{pkg_name}-{new_version}-{datetime.now().strftime('%Y%m%d')}"

    subprocess.run(["git", "checkout", "-b", branch], check=True)
    subprocess.run(["pip", "install", f"{pkg_name}=={new_version}"], check=True)
    subprocess.run("pip freeze > requirements.txt", shell=True, check=True)
    subprocess.run(["git", "add", "requirements.txt"], check=True)
    subprocess.run(
        ["git", "commit", "-m", f"deps: update {pkg_name} to {new_version}"],
        check=True
    )
    subprocess.run(["git", "push", "origin", branch], check=True)

    cve_list = "\n".join(f"- {v}" for v in vuln_ids)
    body = (
        f"## Automated dependency update\n\n"
        f"**Package:** `{pkg_name}`\n"
        f"**Current version:** `{old_version}` (vulnerable)\n"
        f"**Updated to:** `{new_version}`\n\n"
        f"**CVEs patched:**\n{cve_list}\n\n"
        f"> This PR was opened automatically. Run the test suite before merging."
    )
    headers = {
        "Authorization": f"Bearer {GITHUB_TOKEN}",
        "Accept": "application/vnd.github+json",
    }
    resp = requests.post(
        f"https://api.github.com/repos/{REPO}/pulls",
        headers=headers,
        json={
            "title": f"deps: update {pkg_name} {old_version} → {new_version}",
            "body": body,
            "head": branch,
            "base": "main"
        },
        timeout=15
    )
    resp.raise_for_status()
    return resp.json()["html_url"]
Enter fullscreen mode Exit fullscreen mode

Practical notes:

  • Run this inside CI (GitHub Actions, GitLab CI) rather than locally. You want a clean virtualenv each run so pip freeze reflects only project dependencies, not whatever you have installed on your laptop.
  • Before creating a PR, check whether one already exists for that branch with a GET /repos/{REPO}/pulls?head={branch} call. Otherwise you will get a 422 on the second run.
  • For projects using pyproject.toml or poetry.lock, swap the freeze step for poetry update {pkg_name} and commit the lockfile instead.

Running it on a Schedule

The simplest deployment is a GitHub Actions cron workflow:

# .github/workflows/dep-audit.yml
name: Dependency Security Audit
on:
  schedule:
    - cron: "0 8 * * 1"  # every Monday at 08:00 UTC
  workflow_dispatch:

jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          token: ${{ secrets.GITHUB_TOKEN }}
      - uses: actions/setup-python@v5
        with:
          python-version: "3.12"
      - run: pip install -r requirements.txt requests
      - run: python bot.py
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          GITHUB_REPO: ${{ github.repository }}
Enter fullscreen mode Exit fullscreen mode

The workflow_dispatch trigger is useful when a high-profile CVE drops mid-week and you do not want to wait until Monday. One click and the bot runs immediately.

Before automating, it is worth having a solid security baseline for your project. We publish free security hardening checklists for different environments if you want a structured starting point.

The Takeaway

Automated dependency updates are not just a convenience — they are a security control. The window between a CVE publication and an active exploit in the wild has shrunk to days, sometimes hours. Manual quarterly reviews do not cut it anymore.

The design here is intentional about scope: one PR per package, triggered by a confirmed CVE rather than updating everything blindly, and always through a pull request so a human reviews before merging. That balance keeps the bot useful without making it a liability that silently bumps a major version and breaks production.

The OSV API is free, well-maintained, and covers most ecosystems. There is no good reason not to query it in CI.


I run AYI NEDJIMI Consultants, a cybersecurity consulting firm. We publish free security hardening checklists — PDF and Excel.

Top comments (0)