Skip to content
Back to skills

Dependency Cve Audit Patcher

ASecurity

Audits third-party open-source packages for known vulnerabilities (CVEs), evaluating breaking change risks and generating automated patch updates.

  • 2 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added September 27, 2026
developmentpythonbashnodeapibackend

Works with

  • cli
  • api

Security analysis

A100/100

Scanned September 27, 2026

npx -y skills add RMAzrim/opencode-artes --skill dependency-cve-audit-patcher --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Dependency Cve Audit Patcher?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Dependency Cve Audit Patcher
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/rmazrim-dependency-cve-audit-patcher/badge)](https://www.skillsdirectory.com/skills/rmazrim-dependency-cve-audit-patcher)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: Dependency CVE Audit & Patcher
description: Audits third-party open-source packages for known vulnerabilities (CVEs), evaluating breaking change risks and generating automated patch updates.
metadata:
  source: skills/dependency-cve-audit-patcher/dependency-cve-audit-patcher.md
---

# Dependency CVE Audit & Patcher

## 1. System Architecture & Prerequisites
- Python 3.10+ (stdlib only: `re`, `os`, `json`, `subprocess`, `argparse`, `pathlib`)
- Node.js 18+ with npm available on PATH (for the `npm audit --json` backend)
- Target manifest: `package.json` (JS) or `requirements.txt` (Python, PEP 508 syntax)
- Optional CVE list file: JSON array of `{package, versions[], cve_id, severity, patched[]}` for offline (Python-only) mode

## 2. Input/Output Data Contracts

**Input (CLI args):**
```json
{
  "type": "object",
  "properties": {
    "manifest": { "type": "string", "description": "Path to package.json or requirements.txt" },
    "wordlist-report": { "type": "string", "description": "Output CVE report JSON path", "default": "./cve_report.json" },
    "fix-mode": { "type": "boolean", "description": "Generate npm audit fix + safe update commands", "default": false },
    "dry-run": { "type": "boolean", "description": "Print dependency-lock diff plan without executing", "default": false }
  },
  "required": ["manifest"]
}
```

**Output artifacts:**
- `{wordlist-report}` — JSON CVE report (`dependency`, `cve`, `severity`, `range`, `patched`, `action`)
- `{cve_dir}/npm_audit_fix_commands.sh` — fix command list (when `--fix-mode`)
- `{cve_dir}/dependency_lock_diff.json` — dry-run lock diff plan (when `--dry-run`)

## 3. Production Reference Implementation

```python
#!/usr/bin/env python3
"""Dependency CVE Audit & Patcher — npm audit backend + offline CVE list fallback + patch planner."""

import re
import os
import json
import argparse
import subprocess
from pathlib import Path

SEMVER_OP = re.compile(r"^([<>=~^*]+)?\s*(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)")

# Offline fallback CVE list (sample — replace with a real advisory feed).
DEFAULT_CVE_LIST = [
    {
        "package": "lodash",
        "versions": ["<4.17.21"],
        "cve_id": "CVE-2021-23337",
        "severity": "high",
        "patched": ">=4.17.21",
        "source": "offline-sample",
    },
    {
        "package": "minimist",
        "versions": ["<1.2.6"],
        "cve_id": "CVE-2021-44906",
        "severity": "high",
        "patched": ">=1.2.6",
        "source": "offline-sample",
    },
    {
        "package": "axios",
        "versions": ["<0.21.2"],
        "cve_id": "CVE-2021-3749",
        "severity": "high",
        "patched": ">=0.21.2",
        "source": "offline-sample",
    },
]


def parse_package_json(path: Path) -> dict:
    """Parse package.json into {name, version, deps: [{name, range, kind}]}."""
    data = json.loads(path.read_text(encoding="utf-8", errors="replace"))
    deps = []
    for kind, section in (("dependencies", data.get("dependencies", {})),
                          ("devDependencies", data.get("devDependencies", {}))):
        for name, version_range in section.items():
            deps.append({"name": name, "range": version_range, "kind": kind})
    return {"name": data.get("name", "?"), "version": data.get("version", "0.0.0"), "deps": deps}


def parse_requirements_txt(path: Path) -> dict:
    """Parse requirements.txt (PEP 508) into the same manifest shape."""
    deps = []
    for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
        line = line.strip()
        if not line or line.startswith("#") or line.startswith("-"):
            continue
        marker = line.split(";")[0].strip()
        spec = re.split(r"\s*(===|==|>=|<=|!=|~=|>|<)\s*", marker)
        name = spec[0]
        bounds = spec[1] + spec[2] if len(spec) >= 3 else ""
        deps.append({"name": name, "range": bounds or "*", "kind": "dependencies"})
    return {"name": "requirements.txt", "version": "0.0.0", "deps": deps}


def parse_manifest(path: Path) -> dict:
    suffix = path.suffix.lower()
    if suffix == ".json":
        return parse_package_json(path)
    if suffix in (".txt", ""):
        return parse_requirements_txt(path)
    raise ValueError(f"Unsupported manifest type {suffix!r}; use package.json or requirements.txt")


def run_npm_audit(manifest_dir: Path) -> dict:
    """Run `npm audit --json` in the manifest directory. Returns parsed advisories."""
    cmd = ["npm", "audit", "--json"]
    try:
        proc = subprocess.run(cmd, cwd=str(manifest_dir), capture_output=True, text=True, timeout=180)
    except (subprocess.SubprocessError, OSError) as exc:
        return {"error": f"npm audit failed to run: {exc}"}
    try:
        data = json.loads(proc.stdout)
    except json.JSONDecodeError:
        return {"error": f"npm audit returned non-JSON output (exit {proc.returncode}): {proc.stdout[:300]}"}

    advisories = []
    if data.get("error"):
        advisories.append({"error": data["error"]["detail"]})
        return {"advisories": advisories, "exit_code": proc.returncode}

    vulns = data.get("vulnerabilities", {})
    for pkg_name, info in vulns.items():
        via = info.get("via", [])
        advisories.append({
            "dependency": pkg_name,
            "severity": info.get("severity", "unknown"),
            "range": info.get("range", "?"),
            "fix_available": info.get("fixAvailable", False),
            "is_direct": info.get("isDirect", False),
            "via_details": [a if isinstance(a, str) else (a.get("url", "?") if isinstance(a, dict) else "?") for a in via],
            "source": "npm-audit",
        })
    return {"advisories": advisories, "exit_code": proc.returncode, "vuln_count": len(vulns)}


def semver_compare(a: str, b: str) -> int:
    """Compare two semver strings like 1.2.3 (no prerelease support needed here)."""
    ma = [int(x) for x in re.findall(r"\d+", a)][:3]
    mb = [int(x) for x in re.findall(r"\d+", b)][:3]
    while len(ma) < 3:
        ma.append(0)
    while len(mb) < 3:
        mb.append(0)
    return (ma > mb) - (ma < mb)


def major_of(version: str) -> int:
    import re as _re
    m = _re.match(r"\s*v?(\d+)", version or "")
    return int(m.group(1)) if m else 0


def evaluate_patch(current_range: str, patched_range: str) -> str:
    """Classify a patch by breaking-change risk."""
    cur_major = major_of(current_range.lstrip("^~>=<=>="))
    pat_major = major_of(patched_range.lstrip("^~>=<=>="))
    if not current_range or not patched_range:
        return "review-manually"
    if cur_major and pat_major and cur_major == pat_major:
        return "safe-auto-fix"
    if pat_major and cur_major and pat_major > cur_major:
        return "major-bump-review"
    return "review-manually"


def check_offline_cves(manifest: dict, cve_list_path: str) -> list:
    """Python-only fallback: compare manifest deps against a CVE list file."""
    cve_list = DEFAULT_CVE_LIST
    if cve_list_path:
        cve_list = json.loads(Path(cve_list_path).read_text(encoding="utf-8"))
    lockfile_candidates = []
    package_lock = None
    for name in ("package-lock.json", "yarn.lock", "pnpm-lock.yaml"):
        cand = Path(name)
        if cand.exists():
            package_lock = cand
            break

    findings = []
    for dep in manifest["deps"]:
        for cve in cve_list:
            if cve.get("package") != dep["name"]:
                continue
            for ver_expr in cve.get("versions", []):
                patched_ok = any(semver_compare(dep["range"], p) >= 0 for p in cve.get("patched", []))
                if not patched_ok:
                    findings.append({
                        "dependency": dep["name"],
                        "declared_range": dep["range"],
                        "cve": cve.get("cve_id", "UNKNOWN"),
                        "severity": cve.get("severity", "unknown"),
                        "vulnerable_version_range": ver_expr,
                        "patched": cve.get("patched", []),
                        "action": evaluate_patch(dep["range"], ",".join(cve.get("patched", []))),
                        "is_direct": True,
                        "source": f"offline-cve-list{(' + ' + str(package_lock)) if package_lock else ''}",
                    })
    return findings


def build_report(manifest_path: Path, wordlist_report: str, fix_mode: bool, dry_run: bool, cve_list_path: str) -> dict:
    manifest = parse_manifest(manifest_path)
    manifest_dir = manifest_path.parent if manifest_path.suffix == ".json" else manifest_path.parent

    npm_result = {}
    if manifest_path.suffix == ".json":
        npm_result = run_npm_audit(manifest_dir)

    actions = []
    if npm_result.get("advisories"):
        unique_pkgs = {}
        for adv in npm_result["advisories"]:
            if "error" in adv:
                continue
            key = adv["dependency"]
            unique_pkgs.setdefault(key, adv)
        for adv in unique_pkgs.values():
            patched = "N/A"
            action = "npm audit fix" if adv.get("fix_available") else "manual-review"
            actions.append({
                "dependency": adv["dependency"],
                "cve": "npm-advisory",
                "severity": adv.get("severity"),
                "range": adv.get("range"),
                "patched": patched,
                "action": action,
                "is_direct": adv.get("is_direct"),
                "source": "npm-audit",
            })
    else:
        actions.extend(check_offline_cves(manifest, cve_list_path))

    deduped = {}
    for a in actions:
        deduped.setdefault(a["dependency"], a)

    report = {
        "manifest": str(manifest_path),
        "project": manifest["name"],
        "version": manifest["version"],
        "dependency_count": len(manifest["deps"]),
        "advisories": npm_result.get("advisories", []),
        "cve_findings": list(deduped.values()),
        "patch_plan": {},
    }

    fix_commands = []
    if fix_mode and manifest_path.suffix == ".json":
        for a in deduped.values():
            if a["action"].startswith("npm audit fix"):
                fix_commands.append(f"npm audit fix --package-lock-only --dry-run  # {a['dependency']}")
        if fix_commands:
            report["patch_plan"]["npm"] = fix_commands
        else:
            report["patch_plan"]["npm"] = ["npm audit fix"]  # apply all directly safe fixes

    if dry_run:
        lock_diff = {"plan": []}
        for a in deduped.values():
            lock_diff["plan"].append({
                "dependency": a["dependency"],
                "from": a["range"],
                "to": a.get("patched") if a.get("patched") != "N/A" else "review",
                "risk": "low" if a["action"] == "safe-auto-fix" else "medium" if a["action"] == "major-bump-review" else "high",
                "dry_run": True,
            })
        report["dry_run_diff"] = lock_diff

    return report


def main():
    parser = argparse.ArgumentParser(
        description="Dependency CVE Audit & Patcher — npm audit + offline CVE list + patch-risk evaluation.",
        formatter_class=argparse.RawDescriptionHelpFormatter,
    )
    parser.add_argument("--manifest", required=True, help="Path to package.json or requirements.txt")
    parser.add_argument("--wordlist-report", default="./cve_report.json", help="Output CVE report JSON path")
    parser.add_argument("--fix-mode", action="store_true", help="Generate npm audit fix + safe update command list")
    parser.add_argument("--dry-run", action="store_true", help="Print dependency-lock diff plan without executing")
    parser.add_argument("--cve-list", default=None, help="Optional offline CVE list JSON for the Python-only fallback")
    args = parser.parse_args()

    manifest_path = Path(args.manifest).resolve()
    if not manifest_path.is_file():
        print(f"[ERROR] Manifest does not exist: {manifest_path}")
        raise SystemExit(1)

    try:
        report = build_report(manifest_path, args.wordlist_report, args.fix_mode, args.dry_run, args.cve_list)
    except ValueError as exc:
        print(f"[ERROR] {exc}")
        raise SystemExit(1)

    report_path = Path(args.wordlist_report)
    report_path.parent.mkdir(parents=True, exist_ok=True)
    report_path.write_text(json.dumps(report, indent=2, default=str), encoding="utf-8")

    print(f"\nProject: {report['project']}@{report['version']}  (deps: {report['dependency_count']})")
    for f in report["cve_findings"]:
        print(f"  [{f['severity'].upper()}] {f['dependency']} -> {f['cve']}  action: {f['action']}")
    if report.get("patch_plan"):
        print("\nFix commands:")
        for cmd in report["patch_plan"].get("npm", []):
            print(f"  $ {cmd}")
    if report.get("dry_run_diff"):
        print("\nDry-run lock diff plan:")
        for item in report["dry_run_diff"]["plan"]:
            print(f"  {item['dependency']}: {item['from']} -> {item['to']}  (risk: {item['risk']})")
    print(f"\n[INFO] Report written to {report_path}")


if __name__ == "__main__":
    main()
```

## 4. Execution Protocol & Step-by-Step Workflow
1. Audit a `package.json` (npm backend):
   ```bash
   python dependency-cve-audit-patcher.md --manifest ./package.json --wordlist-report ./cve_report.json
   ```
2. Read `cve_report.json`: `cve_findings[]` gives dependency, CVE ID, severity, affected range, action.
3. For Python manifests, run against `requirements.txt` (offline CVE list fallback; supply `--cve-list cves.json` for your own feed).
4. Generate fix commands:
   ```bash
   python dependency-cve-audit-patcher.md --manifest ./package.json --fix-mode
   ```
   Execute the printed commands (each is either `npm audit fix` scoped to a package or the whole-run fix).
5. Simulate the dependency-lock change first:
   ```bash
   python dependency-cve-audit-patcher.md --manifest ./package.json --dry-run
   ```
6. Review the lock diff plan; `safe-auto-fix` entries are same-major bumps, `major-bump-review` entries need peer/breaking-change review.
7. Run `npm audit fix` for safe entries, then a full `npm audit` to confirm zero vulnerabilities.
8. For `review-manually` entries (no patch exists), document a risk decision or swap to the vendor-recommended alternative.

## 5. Edge Cases & Error Handling
- `npm` missing or failing → `error` record with the underlying message; fallback CVE list still runs so the report is never empty.
- Non-JSON `npm audit` output (npm version mismatch) is captured with the exit code rather than crashing.
- Transitive-only findings (`is_direct: false`) still get an advisory entry, flagged for manual `npm audit fix` propagation.
- Major-version bumps are never applied automatically — they are marked `major-bump-review` to catch breaking changes (peer deps, API drift).
- `--dry-run` executes no commands; it reads `npm audit --json` (read-only) and only writes the diff-plan JSON.
- Python `requirements.txt`: markers (`; python_version<"3.9"`), comments, and `-r file` lines are ignored; only name + version-spec pairs are parsed.
- Lockfile candidate lookup is best-effort; offline findings fall back to the declared range in the manifest.
- The shipped `DEFAULT_CVE_LIST` is a sample (lodash/minimist/axios) — replace via `--cve-list` with a current advisory feed for production use.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.