Claude Skill

dependency-check

Audit project dependencies for known-vulnerable versions and risky pinning. Parses requirements.txt and package.json, matches a bundled offline advisory DB, optionally queries OSV.dev live, and warns about unpinned versions. Use when the user asks to "check dependencies for vulne

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download novacode37-claude-security-skills-skills_dependency-check-dd4e44f.zip · 6 KB
Part of novacode37/claude-security-skills — 5 skills

Install

skills CLI npx skills add https://github.com/NovaCode37/claude-security-skills/tree/main/skills/dependency-check
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install novacode37-claude-security-skills@llmmart
Git git clone https://github.com/NovaCode37/claude-security-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole novacode37/claude-security-skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Dependency Check

Scans Python (requirements.txt) and npm (package.json) manifests for known-vulnerable versions and supply-chain risks. Offline by default — it ships a bundled advisory database so it runs in air-gapped CI — with an optional live OSV.dev lookup. Pure standard library.

When to use this skill

  • "Are any of my dependencies vulnerable?"
  • "Audit requirements.txt / package.json."
  • "Check for vulnerable / outdated packages before release."

What it reports

  • Known vulnerabilities — version matches against the bundled advisory DB (or OSV.dev with --online), with CVE/ID, severity and summary.
  • Unpinned dependencies — ranges (^, ~, >=) or missing pins that make builds non-reproducible and widen supply-chain exposure.

How to run it

# Offline scan (bundled advisory DB)
python skills/dependency-check/checker.py requirements.txt
python skills/dependency-check/checker.py package.json

# Scan a directory (auto-discovers both manifest types)
python skills/dependency-check/checker.py .

# Live advisory lookup via OSV.dev
python skills/dependency-check/checker.py requirements.txt --online

# JSON output
python skills/dependency-check/checker.py . --json

# Only report MEDIUM or higher findings (unpinned warnings are LOW)
python skills/dependency-check/checker.py . --min-severity medium

Exit codes: 0 clean · 1 findings reported · 2 no manifest / usage error. Unpinned dependencies are reported, so they fail the build too; suppress them with --no-unpinned, or raise --min-severity to filter advisory findings out of both the report and the exit code.

Recommended workflow for Claude

  1. Run offline first for a fast baseline, then --online for full coverage if the user has network access.
  2. For each vulnerable package, recommend the minimum fixed version and note breaking-change risk.
  3. Encourage exact pins (== / lockfiles) for reproducible, auditable builds.

Note

The bundled DB is intentionally small (well-known historical CVEs) so the tool is self-contained and testable. For comprehensive coverage use --online (OSV.dev) or integrate a dedicated scanner; treat the offline DB as a fast first pass.

Files (claude-security-skills)
  • tests
    • test_checker.py 5.8 KB
      import json
      import os
      import sys
      
      import pytest
      
      sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
      
      import checker
      
      requires_tomllib = pytest.mark.skipif(
          sys.version_info < (3, 11),
          reason="tomllib is stdlib only on Python 3.11+",
      )
      
      
      def test_cmp():
          assert checker._cmp("1.2.3", "1.2.4") < 0
          assert checker._cmp("2.0.0", "1.9.9") > 0
          assert checker._cmp("1.0", "1.0.0") == 0
      
      
      def test_version_matches():
          assert checker.version_matches("0.12.2", "<0.12.3")
          assert not checker.version_matches("0.12.3", "<0.12.3")
          assert checker.version_matches("1.0.0", "==1.0.0")
          assert checker.version_matches("2.0.0", ">=1.0.0")
      
      
      def test_parse_requirements_pinned():
          deps = checker.parse_requirements("flask==0.12.2\nrequests>=2.0\n# comment\n")
          flask = next(d for d in deps if d.name == "flask")
          assert flask.pinned and flask.version == "0.12.2"
          req = next(d for d in deps if d.name == "requests")
          assert not req.pinned
      
      
      def test_parse_requirements_skips_blank_and_flags():
          deps = checker.parse_requirements("\n-r other.txt\n--index-url x\nflask==1.0\n")
          assert {d.name for d in deps} == {"flask"}
      
      
      @requires_tomllib
      def test_parse_pyproject_toml_dependencies():
          toml = """
          [project]
          dependencies = [
            "flask==2.0.0",
            "requests>=2.25",
            "pandas; python_version < '3.11'",
            "# ignored comment",
          ]
          """
          deps = checker.parse_pyproject_toml(toml)
          assert {d.name for d in deps} == {"flask", "requests", "pandas"}
          flask = next(d for d in deps if d.name == "flask")
          assert flask.pinned and flask.version == "2.0.0"
          requests = next(d for d in deps if d.name == "requests")
          assert not requests.pinned and requests.version == "2.25"
          pandas = next(d for d in deps if d.name == "pandas")
          assert not pandas.pinned and pandas.version is None
      
      
      def test_parse_pyproject_toml_invalid_returns_empty():
          assert checker.parse_pyproject_toml("[not a valid toml") == []
      
      
      def test_parse_package_json():
          pkg = json.dumps({
              "dependencies": {"lodash": "4.17.20", "axios": "^0.21.0"},
              "devDependencies": {"jest": "27.0.0"},
          })
          deps = checker.parse_package_json(pkg)
          names = {d.name for d in deps}
          assert {"lodash", "axios", "jest"} <= names
          lodash = next(d for d in deps if d.name == "lodash")
          assert lodash.pinned and lodash.version == "4.17.20"
          axios = next(d for d in deps if d.name == "axios")
          assert not axios.pinned
      
      
      def test_parse_package_json_invalid():
          assert checker.parse_package_json("{not json") == []
      
      
      def test_vulnerable_flask_detected():
          deps = checker.parse_requirements("flask==0.12.2\n")
          findings = checker.check_offline(deps)
          assert any(f.id == "CVE-2018-1000656" for f in findings)
      
      
      def test_patched_flask_not_detected():
          deps = checker.parse_requirements("flask==1.0.0\n")
          assert checker.check_offline(deps) == []
      
      
      def test_vulnerable_lodash_detected():
          deps = checker.parse_package_json(
              json.dumps({"dependencies": {"lodash": "4.17.20"}}))
          findings = checker.check_offline(deps)
          assert any(f.id == "CVE-2021-23337" for f in findings)
      
      
      def test_findings_sorted_by_severity():
          deps = checker.parse_requirements("django==2.0.0\njinja2==2.10.0\n")
          findings = checker.check_offline(deps)
          ranks = [checker.SEV_RANK[f.severity] for f in findings]
          assert ranks == sorted(ranks)
      
      
      def test_unpinned_warnings():
          deps = checker.parse_requirements("requests>=2.0\nflask==1.0\n")
          warns = checker.unpinned_warnings(deps)
          assert any(w.package == "requests" for w in warns)
          assert not any(w.package == "flask" for w in warns)
      
      
      def test_run_directory(tmp_path):
          (tmp_path / "requirements.txt").write_text("flask==0.12.2\n")
          result = checker.run(str(tmp_path))
          assert result["dependency_count"] == 1
          assert result["vulnerabilities"]
      
      
      @requires_tomllib
      def test_run_directory_with_pyproject_toml(tmp_path):
          (tmp_path / "pyproject.toml").write_text(
              """
              [project]
              dependencies = ["flask==0.12.2"]
              """
          )
          result = checker.run(str(tmp_path))
          assert result["dependency_count"] == 1
          assert len(result["vulnerabilities"]) == 1
      
      
      def test_cli_exit_code_vuln(tmp_path):
          f = tmp_path / "requirements.txt"
          f.write_text("flask==0.12.2\n")
          assert checker.main([str(f)]) == 1
      
      
      def test_cli_exit_code_clean(tmp_path):
          f = tmp_path / "requirements.txt"
          f.write_text("flask==2.0.0\n")
          assert checker.main([str(f)]) == 0
      
      
      def test_cli_no_manifest(tmp_path):
          assert checker.main([str(tmp_path)]) == 2
      
      
      def test_cli_json(tmp_path, capsys):
          f = tmp_path / "requirements.txt"
          f.write_text("flask==0.12.2\n")
          checker.main([str(f), "--json"])
          data = json.loads(capsys.readouterr().out)
          assert data["vulnerabilities"]
      
      
      def test_cli_unpinned_only_exits_one(tmp_path):
          """Unpinned deps are reported, so they must fail the build (issue #46)."""
          f = tmp_path / "requirements.txt"
          f.write_text("httpx\nrich>=13.0\n")
          assert checker.main([str(f)]) == 1
      
      
      def test_cli_no_unpinned_suppresses_report_and_exit(tmp_path):
          f = tmp_path / "requirements.txt"
          f.write_text("httpx\nrich>=13.0\n")
          assert checker.main([str(f), "--no-unpinned"]) == 0
      
      
      def test_cli_min_severity_filters_report_and_exit(tmp_path, capsys):
          f = tmp_path / "requirements.txt"
          f.write_text("httpx\nrich>=13.0\n")
          rc = checker.main([str(f), "--min-severity", "medium", "--json"])
          data = json.loads(capsys.readouterr().out)
          assert data["unpinned"] == [] and data["vulnerabilities"] == []
          assert rc == 0
      
      
      def test_run_min_severity_filters_vulnerabilities(tmp_path):
          f = tmp_path / "requirements.txt"
          f.write_text("jinja2==2.11.0\n")  # medium-severity advisory
          assert checker.run(str(f))["vulnerabilities"]
          assert checker.run(str(f), min_severity="high")["vulnerabilities"] == []
      
  • checker.py 11.1 KB
    from __future__ import annotations
    
    import argparse
    import json
    import os
    import re
    import sys
    from dataclasses import dataclass, asdict
    
    SEV_RANK = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
    
    ADVISORIES = {
        "pypi": {
            "flask": [("<0.12.3", "CVE-2018-1000656", "high",
                       "Flask < 0.12.3 DoS via crafted JSON.")],
            "django": [("<2.2.28", "CVE-2022-28346", "critical",
                        "Django SQL injection via QuerySet.annotate (<2.2.28).")],
            "requests": [("<2.20.0", "CVE-2018-18074", "high",
                          "requests < 2.20.0 leaks Authorization on redirect.")],
            "pyyaml": [("<5.4", "CVE-2020-14343", "critical",
                        "PyYAML < 5.4 arbitrary code execution via full_load.")],
            "jinja2": [("<2.11.3", "CVE-2020-28493", "medium",
                        "Jinja2 < 2.11.3 ReDoS in urlize filter.")],
            "urllib3": [("<1.26.5", "CVE-2021-33503", "high",
                         "urllib3 < 1.26.5 ReDoS via crafted URL authority.")],
            "cryptography": [("<3.3.2", "CVE-2020-36242", "high",
                              "cryptography < 3.3.2 buffer overflow in Fernet.")],
        },
        "npm": {
            "lodash": [("<4.17.21", "CVE-2021-23337", "high",
                        "lodash < 4.17.21 command injection via template.")],
            "minimist": [("<1.2.6", "CVE-2021-44906", "critical",
                          "minimist < 1.2.6 prototype pollution.")],
            "axios": [("<0.21.2", "CVE-2021-3749", "high",
                       "axios < 0.21.2 ReDoS via trim regex.")],
            "node-fetch": [("<2.6.7", "CVE-2022-0235", "medium",
                            "node-fetch < 2.6.7 leaks cookies/Authorization on redirect.")],
        },
    }
    
    @dataclass
    class Finding:
        ecosystem: str
        package: str
        version: str
        id: str
        severity: str
        summary: str
    
        def to_dict(self) -> dict:
            return asdict(self)
    
    def _norm(v: str) -> tuple:
        nums = re.findall(r"\d+", v)
        return tuple(int(n) for n in nums) if nums else (0,)
    
    def _cmp(a: str, b: str) -> int:
        ta, tb = _norm(a), _norm(b)
        length = max(len(ta), len(tb))
        ta += (0,) * (length - len(ta))
        tb += (0,) * (length - len(tb))
        return (ta > tb) - (ta < tb)
    
    def version_matches(version: str, spec: str) -> bool:
        m = re.match(r"\s*(<=|>=|==|<|>)\s*(.+)\s*$", spec)
        if not m:
            return False
        op, target = m.group(1), m.group(2).strip()
        c = _cmp(version, target)
        return {
            "<": c < 0, "<=": c <= 0, "==": c == 0,
            ">": c > 0, ">=": c >= 0,
        }[op]
    
    @dataclass
    class Dep:
        ecosystem: str
        name: str
        version: str | None
        raw: str
        pinned: bool
    
    def _parse_dependency_token(text: str) -> tuple[str | None, str | None, bool] | None:
        token = text.split(";", 1)[0].strip()
        token = token.split("#", 1)[0].strip()
        if not token:
            return None
        m = re.match(
            r"^([A-Za-z0-9_.\-]+(?:\[[^\]]+\])?)\s*(==|>=|<=|~=|>|<|!=)?\s*([^\s;,]+)?",
            token)
        if not m:
            return None
        name = m.group(1).split("[", 1)[0].lower()
        op, ver = m.group(2), m.group(3)
        pinned = op == "==" and bool(ver)
        version = ver if pinned or op else (ver if op and ver else None)
        return name, version, pinned
    
    def parse_requirements(text: str) -> list[Dep]:
        deps: list[Dep] = []
        for line in text.splitlines():
            line = line.split("#", 1)[0].strip()
            if not line or line.startswith("-"):
                continue
            parsed = _parse_dependency_token(line)
            if not parsed:
                continue
            name, version, pinned = parsed
            deps.append(Dep("pypi", name, version, line, pinned))
        return deps
    
    def parse_pyproject_toml(text: str) -> list[Dep]:
        try:
            import tomllib
        except Exception:
            return []
    
        try:
            data = tomllib.loads(text)
        except Exception:
            return []
    
        deps: list[Dep] = []
        project = data.get("project") or {}
        project_deps = project.get("dependencies") or []
        if not isinstance(project_deps, list):
            return []
    
        for dep in project_deps:
            if not isinstance(dep, str):
                continue
            parsed = _parse_dependency_token(dep)
            if not parsed:
                continue
            name, version, pinned = parsed
            deps.append(Dep("pypi", name, version, dep, pinned))
        return deps
    
    def parse_package_json(text: str) -> list[Dep]:
        try:
            data = json.loads(text)
        except json.JSONDecodeError:
            return []
        deps: list[Dep] = []
        for section in ("dependencies", "devDependencies", "optionalDependencies"):
            for name, spec in (data.get(section) or {}).items():
                spec = str(spec)
                pinned = bool(re.match(r"^\d+\.\d+\.\d+", spec)) and not any(
                    c in spec for c in "^~*x ><||")
                ver = re.sub(r"^[\^~>=<\s]+", "", spec)
                ver = ver if re.match(r"^\d", ver) else None
                deps.append(Dep("npm", name.lower(), ver, f"{name}: {spec}", pinned))
        return deps
    
    def check_offline(deps: list[Dep]) -> list[Finding]:
        findings: list[Finding] = []
        for dep in deps:
            if not dep.version:
                continue
            eco_db = ADVISORIES.get(dep.ecosystem, {})
            for spec, cve, sev, summary in eco_db.get(dep.name, []):
                if version_matches(dep.version, spec):
                    findings.append(Finding(dep.ecosystem, dep.name, dep.version,
                                            cve, sev, summary))
        findings.sort(key=lambda f: SEV_RANK.get(f.severity, 9))
        return findings
    
    def check_online_osv(deps: list[Dep], timeout: float = 10.0) -> list[Finding]:
        import urllib.request
    
        eco_map = {"pypi": "PyPI", "npm": "npm"}
        findings: list[Finding] = []
        for dep in deps:
            if not dep.version:
                continue
            body = json.dumps({
                "version": dep.version,
                "package": {"name": dep.name, "ecosystem": eco_map[dep.ecosystem]},
            }).encode()
            req = urllib.request.Request(
                "https://api.osv.dev/v1/query", data=body,
                headers={"Content-Type": "application/json"})
            try:
                with urllib.request.urlopen(req, timeout=timeout) as resp:
                    data = json.loads(resp.read())
            except Exception:
                continue
            for vuln in data.get("vulns", []):
                sev = _osv_severity(vuln)
                findings.append(Finding(dep.ecosystem, dep.name, dep.version,
                                        vuln.get("id", "OSV"), sev,
                                        (vuln.get("summary") or "")[:200]))
        findings.sort(key=lambda f: SEV_RANK.get(f.severity, 9))
        return findings
    
    def _osv_severity(vuln: dict) -> str:
        db = (vuln.get("database_specific") or {})
        sev = str(db.get("severity", "")).lower()
        if sev in SEV_RANK:
            return sev
        return "medium"
    
    def unpinned_warnings(deps: list[Dep]) -> list[Finding]:
        out: list[Finding] = []
        for dep in deps:
            if not dep.pinned:
                out.append(Finding(dep.ecosystem, dep.name, dep.version or "*",
                                   "unpinned", "low",
                                   "Dependency is not pinned to an exact version "
                                   "— non-reproducible builds and supply-chain risk."))
        return out
    
    def filter_by_severity(findings: list[Finding],
                           min_severity: str = "info") -> list[Finding]:
        threshold = SEV_RANK.get(min_severity, SEV_RANK["info"])
        return [f for f in findings if SEV_RANK.get(f.severity, 3) <= threshold]
    
    def parse_file(path: str) -> list[Dep]:
        with open(path, "r", encoding="utf-8", errors="ignore") as fh:
            text = fh.read()
        base = os.path.basename(path).lower()
        if base == "package.json":
            return parse_package_json(text)
        if base == "pyproject.toml":
            return parse_pyproject_toml(text)
        if base.endswith(".txt") or "requirements" in base:
            return parse_requirements(text)
        return parse_requirements(text)
    
    def discover(path: str) -> list[str]:
        if os.path.isfile(path):
            return [path]
        found = []
        for name in ("requirements.txt", "package.json", "pyproject.toml"):
            candidate = os.path.join(path, name)
            if os.path.isfile(candidate):
                found.append(candidate)
        return found
    
    def run(path: str, online: bool = False, show_unpinned: bool = True,
            min_severity: str = "info") -> dict:
        files = discover(path)
        deps: list[Dep] = []
        for f in files:
            deps.extend(parse_file(f))
        findings = check_offline(deps)
        if online:
            findings.extend(check_online_osv(deps))
        findings = filter_by_severity(findings, min_severity)
        warnings = (filter_by_severity(unpinned_warnings(deps), min_severity)
                    if show_unpinned else [])
        return {
            "files": files,
            "dependency_count": len(deps),
            "vulnerabilities": [f.to_dict() for f in findings],
            "unpinned": [w.to_dict() for w in warnings],
        }
    
    def main(argv: list[str] | None = None) -> int:
        p = argparse.ArgumentParser(
            prog="dependency-check",
            description="Check dependencies for known vulns and risky pinning.")
        p.add_argument("path", help="requirements.txt, package.json, or a directory")
        p.add_argument("--online", action="store_true",
                       help="also query the OSV.dev advisory API (network)")
        p.add_argument("--no-unpinned", action="store_true",
                       help="suppress unpinned-dependency warnings")
        p.add_argument("--json", action="store_true", help="emit JSON")
        p.add_argument("--min-severity", default="info", choices=list(SEV_RANK),
                       help="report findings at or above this severity "
                            "(default: info, i.e. report everything)")
        args = p.parse_args(argv)
    
        if not os.path.exists(args.path):
            print(f"error: path not found: {args.path}", file=sys.stderr)
            return 2
    
        result = run(args.path, online=args.online,
                     show_unpinned=not args.no_unpinned,
                     min_severity=args.min_severity)
    
        if not result["files"]:
            print("error: no requirements.txt or package.json found.",
                  file=sys.stderr)
            return 2
    
        if args.json:
            print(json.dumps(result, indent=2))
        else:
            vulns = result["vulnerabilities"]
            print(f"[dependency-check] scanned {result['dependency_count']} "
                  f"dependencies from {len(result['files'])} file(s)\n")
            if vulns:
                print(f"Known vulnerabilities ({len(vulns)}):")
                for v in vulns:
                    print(f"  [{v['severity'].upper():<8}] {v['package']} "
                          f"{v['version']}  {v['id']}: {v['summary']}")
            else:
                print("No known vulnerabilities in the offline DB. [OK]")
            if result["unpinned"]:
                print(f"\nUnpinned dependencies ({len(result['unpinned'])}):")
                for w in result["unpinned"]:
                    print(f"  - {w['package']} ({w['version']})")
    
        return 1 if result["vulnerabilities"] or result["unpinned"] else 0
    
    if __name__ == "__main__":
        try:
            sys.stdout.reconfigure(encoding="utf-8")
        except Exception:
            pass
        raise SystemExit(main())
    
  • SKILL.md 2.7 KB
    ---
    name: dependency-check
    description: >-
      Audit project dependencies for known-vulnerable versions and risky pinning.
      Parses requirements.txt and package.json, matches a bundled offline advisory
      DB, optionally queries OSV.dev live, and warns about unpinned versions. Use
      when the user asks to "check dependencies for vulnerabilities", "audit my
      requirements.txt / package.json", "scan for vulnerable packages", or "is my
      dependency tree secure".
    license: MIT
    ---
    
    # Dependency Check
    
    Scans Python (`requirements.txt`) and npm (`package.json`) manifests for
    known-vulnerable versions and supply-chain risks. **Offline by default** — it
    ships a bundled advisory database so it runs in air-gapped CI — with an optional
    live OSV.dev lookup. Pure standard library.
    
    ## When to use this skill
    
    - "Are any of my dependencies vulnerable?"
    - "Audit requirements.txt / package.json."
    - "Check for vulnerable / outdated packages before release."
    
    ## What it reports
    
    - **Known vulnerabilities** — version matches against the bundled advisory DB
      (or OSV.dev with `--online`), with CVE/ID, severity and summary.
    - **Unpinned dependencies** — ranges (`^`, `~`, `>=`) or missing pins that make
      builds non-reproducible and widen supply-chain exposure.
    
    ## How to run it
    
    ```bash
    # Offline scan (bundled advisory DB)
    python skills/dependency-check/checker.py requirements.txt
    python skills/dependency-check/checker.py package.json
    
    # Scan a directory (auto-discovers both manifest types)
    python skills/dependency-check/checker.py .
    
    # Live advisory lookup via OSV.dev
    python skills/dependency-check/checker.py requirements.txt --online
    
    # JSON output
    python skills/dependency-check/checker.py . --json
    
    # Only report MEDIUM or higher findings (unpinned warnings are LOW)
    python skills/dependency-check/checker.py . --min-severity medium
    ```
    
    **Exit codes:** `0` clean · `1` findings reported · `2` no manifest / usage
    error. Unpinned dependencies are reported, so they fail the build too; suppress
    them with `--no-unpinned`, or raise `--min-severity` to filter advisory
    findings out of both the report and the exit code.
    
    ## Recommended workflow for Claude
    
    1. Run offline first for a fast baseline, then `--online` for full coverage if
       the user has network access.
    2. For each vulnerable package, recommend the **minimum fixed version** and
       note breaking-change risk.
    3. Encourage exact pins (`==` / lockfiles) for reproducible, auditable builds.
    
    ## Note
    
    The bundled DB is intentionally small (well-known historical CVEs) so the tool
    is self-contained and testable. For comprehensive coverage use `--online`
    (OSV.dev) or integrate a dedicated scanner; treat the offline DB as a fast
    first pass.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related