File size: 2,377 Bytes
d860535
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
"""
Dependency vulnerability scanner using pip-audit.
Scans requirements.txt / pyproject.toml for known CVEs.
Best-effort: returns empty list if pip-audit is not installed or fails.
"""
import json
import subprocess  # nosec B404
import os
from app.core.logger import get_logger

logger = get_logger(__name__)


def scan_dependencies(local_path: str) -> list[dict]:
    """
    Run pip-audit on the repo and return a list of vulnerability dicts.
    Each dict has: package, version, id, description, fix_versions.
    Returns empty list if pip-audit unavailable or no manifest found.
    """
    # Find dependency manifest
    manifest = None
    for candidate in ["requirements.txt", "pyproject.toml", "setup.cfg"]:
        full = os.path.join(local_path, candidate)
        if os.path.exists(full):
            manifest = full
            break

    if manifest is None:
        logger.info("No dependency manifest found — skipping vulnerability scan")
        return []

    try:
        result = subprocess.run(  # nosec B603 B607
            ["pip-audit", "--requirement", manifest, "--format", "json", "--no-deps"],
            capture_output=True,
            text=True,
            timeout=120,
        )
    except FileNotFoundError:
        logger.warning("pip-audit not installed — skipping dependency scan")
        return []
    except subprocess.TimeoutExpired:
        logger.warning("pip-audit timed out")
        return []
    except Exception as exc:
        logger.warning("pip-audit failed", extra={"error": str(exc)})
        return []

    if result.returncode not in (0, 1):  # 0=clean, 1=vulns found, others=error
        logger.warning("pip-audit error", extra={"stderr": result.stderr[:200]})
        return []

    try:
        data = json.loads(result.stdout)
    except json.JSONDecodeError:
        return []

    findings = []
    for dep in data.get("dependencies", []):
        for vuln in dep.get("vulns", []):
            findings.append({
                "package": dep.get("name", "unknown"),
                "version": dep.get("version", "unknown"),
                "id": vuln.get("id", ""),
                "description": vuln.get("description", ""),
                "fix_versions": vuln.get("fix_versions", []),
            })

    logger.info("Dependency scan complete", extra={"vulnerabilities": len(findings)})
    return findings