Skip to content

Dependencies

dependencies

Dependency rules — vulnerability scanning and hygiene checks.

DependencyAuditRule dataclass

Bases: ProjectRule

Scan dependencies for known vulnerabilities via pip-audit.

Scoring: 100 - (vuln_count * 15), min 0.

Vulnerabilities reported against environment tools (pip, setuptools, wheel, uv, pip-audit) are excluded from the count. These tools are injected into the audit venv by uv run --with rather than declared as project dependencies, so their CVEs are not actionable from the project being audited. Matching is case-insensitive.

Source code in packages/axm-audit/src/axm_audit/core/rules/dependencies.py
Python
@dataclass
@register_rule("deps")
class DependencyAuditRule(ProjectRule):
    """Scan dependencies for known vulnerabilities via pip-audit.

    Scoring: 100 - (vuln_count * 15), min 0.

    Vulnerabilities reported against environment tools (``pip``, ``setuptools``,
    ``wheel``, ``uv``, ``pip-audit``) are excluded from the count. These tools
    are injected into the audit venv by ``uv run --with`` rather than declared
    as project dependencies, so their CVEs are not actionable from the project
    being audited. Matching is case-insensitive.
    """

    @property
    def rule_id(self) -> str:
        """Unique identifier for this rule."""
        return "DEPS_AUDIT"

    def check(self, project_path: Path) -> CheckResult:
        """Check dependencies for known CVEs."""
        try:
            data = _run_pip_audit(project_path)
        except FileNotFoundError:
            return CheckResult(
                rule_id=self.rule_id,
                passed=False,
                message="pip-audit not available",
                severity=Severity.ERROR,
                score=0,
                details={"vuln_count": 0},
                fix_hint="Install with: uv add --dev pip-audit",
            )
        except RuntimeError as exc:
            return CheckResult(
                rule_id=self.rule_id,
                passed=False,
                message=str(exc),
                severity=Severity.ERROR,
                score=0,
                details={"vuln_count": 0},
                fix_hint="Check pip-audit installation: uv run pip-audit --version",
            )

        vulns = _parse_vulns(data)
        vuln_count = len(vulns)
        score = max(0, 100 - vuln_count * 15)

        top_vulns = [_summarize_vuln(v) for v in vulns[:5]]
        text_lines = [_format_vuln_line(v) for v in top_vulns]

        return CheckResult(
            rule_id=self.rule_id,
            passed=score >= PASS_THRESHOLD,
            message=(
                "No known vulnerabilities"
                if vuln_count == 0
                else f"{vuln_count} vulnerable package(s) found"
            ),
            severity=Severity.WARNING if score < PASS_THRESHOLD else Severity.INFO,
            score=int(score),
            details={
                "vuln_count": vuln_count,
                "top_vulns": top_vulns,
            },
            text="\n".join(text_lines) if text_lines else None,
            fix_hint=("Run: pip-audit --fix to remediate" if vuln_count > 0 else None),
        )
rule_id property

Unique identifier for this rule.

check(project_path)

Check dependencies for known CVEs.

Source code in packages/axm-audit/src/axm_audit/core/rules/dependencies.py
Python
def check(self, project_path: Path) -> CheckResult:
    """Check dependencies for known CVEs."""
    try:
        data = _run_pip_audit(project_path)
    except FileNotFoundError:
        return CheckResult(
            rule_id=self.rule_id,
            passed=False,
            message="pip-audit not available",
            severity=Severity.ERROR,
            score=0,
            details={"vuln_count": 0},
            fix_hint="Install with: uv add --dev pip-audit",
        )
    except RuntimeError as exc:
        return CheckResult(
            rule_id=self.rule_id,
            passed=False,
            message=str(exc),
            severity=Severity.ERROR,
            score=0,
            details={"vuln_count": 0},
            fix_hint="Check pip-audit installation: uv run pip-audit --version",
        )

    vulns = _parse_vulns(data)
    vuln_count = len(vulns)
    score = max(0, 100 - vuln_count * 15)

    top_vulns = [_summarize_vuln(v) for v in vulns[:5]]
    text_lines = [_format_vuln_line(v) for v in top_vulns]

    return CheckResult(
        rule_id=self.rule_id,
        passed=score >= PASS_THRESHOLD,
        message=(
            "No known vulnerabilities"
            if vuln_count == 0
            else f"{vuln_count} vulnerable package(s) found"
        ),
        severity=Severity.WARNING if score < PASS_THRESHOLD else Severity.INFO,
        score=int(score),
        details={
            "vuln_count": vuln_count,
            "top_vulns": top_vulns,
        },
        text="\n".join(text_lines) if text_lines else None,
        fix_hint=("Run: pip-audit --fix to remediate" if vuln_count > 0 else None),
    )

DependencyHygieneRule dataclass

Bases: ProjectRule

Check for unused/missing/transitive dependencies via deptry.

Scoring: 100 - (issue_count * 10), min 0.

Source code in packages/axm-audit/src/axm_audit/core/rules/dependencies.py
Python
@dataclass
@register_rule("deps")
class DependencyHygieneRule(ProjectRule):
    """Check for unused/missing/transitive dependencies via deptry.

    Scoring: 100 - (issue_count * 10), min 0.
    """

    @property
    def rule_id(self) -> str:
        """Unique identifier for this rule."""
        return "DEPS_HYGIENE"

    def check(self, project_path: Path) -> CheckResult:
        """Check dependency hygiene with deptry."""
        members = resolve_workspace_members(project_path)
        if members is not None:
            return self._check_workspace(project_path, members)
        result = self._check_single(project_path)
        assert isinstance(result, CheckResult)
        return result

    def _run_deptry_safely(
        self, project_path: Path, *, member_name: str = ""
    ) -> tuple[list[dict[str, object]] | None, CheckResult | None]:
        """Invoke ``run_deptry`` and translate failures into a ``CheckResult``.

        Returns ``(issues, None)`` on success and ``(None, error_result)`` on
        failure.  When *member_name* is set, non-missing failures are logged so
        workspace callers keep their existing diagnostics.
        """
        try:
            return run_deptry(project_path), None
        except FileNotFoundError:
            return None, CheckResult(
                rule_id=self.rule_id,
                passed=False,
                message="deptry not available",
                severity=Severity.ERROR,
                score=0,
                details={"issue_count": 0},
                fix_hint="Install with: uv add --dev deptry",
            )
        except (RuntimeError, json.JSONDecodeError) as exc:
            if member_name:
                logger.warning("deptry failed for %s: %s", member_name, exc)
            is_runtime = isinstance(exc, RuntimeError)
            msg = f"deptry failed: {exc}" if is_runtime else "deptry output parse error"
            return None, CheckResult(
                rule_id=self.rule_id,
                passed=False,
                message=msg,
                severity=Severity.ERROR,
                score=0,
                details={"issue_count": 0},
                fix_hint="Check deptry installation: uv run deptry --version",
            )

    def _build_single_check_result(
        self, issues: list[dict[str, object]]
    ) -> CheckResult:
        """Build the success-path ``CheckResult`` for single-package mode."""
        issue_count = len(issues)
        score = max(0, 100 - issue_count * 10)

        formatted = [_format_issue(i) for i in issues[:5]]
        text_lines = [
            f"\u2022 {fi['code']} {fi['module']}:"
            f" {_DEPTRY_LABELS.get(fi['code'], fi['message'])}"
            for fi in formatted
        ]

        return CheckResult(
            rule_id=self.rule_id,
            passed=score >= PASS_THRESHOLD,
            message=(
                "Clean dependencies (0 issues)"
                if issue_count == 0
                else f"{issue_count} dependency issue(s) found"
            ),
            severity=Severity.WARNING if score < PASS_THRESHOLD else Severity.INFO,
            score=int(score),
            details={
                "issue_count": issue_count,
                "top_issues": formatted,
            },
            text="\n".join(text_lines) if text_lines else None,
            fix_hint=("Run: deptry . to see details" if issue_count > 0 else None),
        )

    def _check_single(
        self, project_path: Path, *, member_name: str = ""
    ) -> CheckResult | list[dict[str, object]]:
        """Run deptry on a single package and return a CheckResult or issue list.

        When *member_name* is set the method returns filtered issues (for
        workspace aggregation).  Otherwise it returns a full ``CheckResult``.
        """
        issues, error = self._run_deptry_safely(project_path, member_name=member_name)
        if error is not None:
            return [] if member_name else error

        filtered = _filter_false_positives(issues or [], project_path)
        if member_name:
            return filtered
        return self._build_single_check_result(filtered)

    def _collect_member_issues(
        self, members: list[Path]
    ) -> list[tuple[str, dict[str, object]]]:
        """Run deptry on each workspace member and collect tagged issues."""
        all_issues: list[tuple[str, dict[str, object]]] = []
        for member in members:
            member_name = member.name
            issues = self._check_single(member, member_name=member_name)
            if isinstance(issues, list):
                for issue in issues:
                    all_issues.append((member_name, issue))
        return all_issues

    def _build_workspace_result(
        self, all_issues: list[tuple[str, dict[str, object]]]
    ) -> CheckResult:
        """Score and format aggregated member issues into a CheckResult."""
        issue_count = len(all_issues)
        score = max(0, 100 - issue_count * 10)

        formatted = [
            _format_issue(issue, member=name) for name, issue in all_issues[:5]
        ]
        text_lines = [
            f"\u2022 {fi['code']} {fi['module']}:"
            f" {_DEPTRY_LABELS.get(fi['code'], fi['message'])}"
            + (f" ({fi['member']})" if fi.get("member") else "")
            for fi in formatted
        ]

        return CheckResult(
            rule_id=self.rule_id,
            passed=score >= PASS_THRESHOLD,
            message=(
                "Clean dependencies (0 issues)"
                if issue_count == 0
                else f"{issue_count} dependency issue(s) found"
            ),
            severity=Severity.WARNING if score < PASS_THRESHOLD else Severity.INFO,
            score=int(score),
            details={
                "issue_count": issue_count,
                "top_issues": formatted,
            },
            text="\n".join(text_lines) if text_lines else None,
            fix_hint=("Run: deptry . to see details" if issue_count > 0 else None),
        )

    def _check_workspace(self, project_path: Path, members: list[Path]) -> CheckResult:
        """Aggregate deptry results across workspace members."""
        all_issues = self._collect_member_issues(members)
        return self._build_workspace_result(all_issues)
rule_id property

Unique identifier for this rule.

check(project_path)

Check dependency hygiene with deptry.

Source code in packages/axm-audit/src/axm_audit/core/rules/dependencies.py
Python
def check(self, project_path: Path) -> CheckResult:
    """Check dependency hygiene with deptry."""
    members = resolve_workspace_members(project_path)
    if members is not None:
        return self._check_workspace(project_path, members)
    result = self._check_single(project_path)
    assert isinstance(result, CheckResult)
    return result

detect_first_party_packages(project_path)

Auto-detect first-party package names from project layout.

Scans src/ for top-level package directories (including namespace packages). Falls back to scanning the project root when no src/ directory exists.

Returns an empty list if [tool.deptry] known_first_party is already configured in pyproject.toml — deptry's own config takes precedence.

Source code in packages/axm-audit/src/axm_audit/core/rules/dependencies.py
Python
def detect_first_party_packages(project_path: Path) -> list[str]:
    """Auto-detect first-party package names from project layout.

    Scans ``src/`` for top-level package directories (including namespace
    packages). Falls back to scanning the project root when no ``src/``
    directory exists.

    Returns an empty list if ``[tool.deptry] known_first_party`` is already
    configured in ``pyproject.toml`` — deptry's own config takes precedence.
    """
    if has_deptry_config(project_path):
        return []

    src_dir = project_path / "src"
    if src_dir.is_dir():
        scan_root = src_dir
        exclude = {"__pycache__"}
    else:
        scan_root = project_path
        exclude = _FLAT_LAYOUT_EXCLUDES

    return [
        entry.name
        for entry in sorted(scan_root.iterdir())
        if entry.is_dir()
        and not entry.name.startswith(".")
        and entry.name not in exclude
    ]

has_deptry_config(project_path)

Check if [tool.deptry] known_first_party is configured.

Source code in packages/axm-audit/src/axm_audit/core/rules/dependencies.py
Python
def has_deptry_config(project_path: Path) -> bool:
    """Check if ``[tool.deptry] known_first_party`` is configured."""
    pyproject = project_path / "pyproject.toml"
    if not pyproject.exists():
        return False
    try:
        import tomllib

        data = tomllib.loads(pyproject.read_text())
    except Exception:  # noqa: BLE001
        logger.debug("Failed to parse %s", pyproject, exc_info=True)
        return False
    return bool(data.get("tool", {}).get("deptry", {}).get("known_first_party"))

resolve_workspace_members(project_path)

Resolve workspace member paths from [tool.uv.workspace].members.

Returns None when the project is not a uv workspace. Directories without a pyproject.toml are silently skipped.

Source code in packages/axm-audit/src/axm_audit/core/rules/dependencies.py
Python
def resolve_workspace_members(project_path: Path) -> list[Path] | None:
    """Resolve workspace member paths from ``[tool.uv.workspace].members``.

    Returns ``None`` when the project is not a uv workspace.
    Directories without a ``pyproject.toml`` are silently skipped.
    """
    pyproject = project_path / "pyproject.toml"
    if not pyproject.exists():
        return None
    try:
        import tomllib

        data = tomllib.loads(pyproject.read_text())
    except Exception:  # noqa: BLE001
        return None

    workspace = data.get("tool", {}).get("uv", {}).get("workspace")
    if workspace is None:
        return None

    members: list[Path] = []
    for pattern in workspace.get("members", []):
        for match in sorted(project_path.glob(pattern)):
            if match.is_dir() and (match / "pyproject.toml").exists():
                members.append(match)
    return members

run_deptry(project_path)

Run deptry and return parsed JSON issues.

Raises:

Type Description
RuntimeError

If deptry exits with a non-zero return code and produces no JSON output file.

Source code in packages/axm-audit/src/axm_audit/core/rules/dependencies.py
Python
def run_deptry(project_path: Path) -> list[dict[str, object]]:
    """Run deptry and return parsed JSON issues.

    Raises:
        RuntimeError: If deptry exits with a non-zero return code and
            produces no JSON output file.
    """
    import tempfile

    with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as tmp:
        tmp_path = Path(tmp.name)

    first_party = detect_first_party_packages(project_path)
    cmd = ["deptry", ".", "--json-output", str(tmp_path)]
    for pkg in first_party:
        cmd.extend(["--known-first-party", pkg])

    try:
        result = run_in_project(
            cmd,
            project_path,
            with_packages=["deptry"],
            capture_output=True,
            text=True,
            check=False,
        )
        if tmp_path.exists() and tmp_path.stat().st_size > 0:
            parsed = json.loads(tmp_path.read_text())
            if isinstance(parsed, list):
                return [item for item in parsed if isinstance(item, dict)]
            return []

        if result.returncode != 0:
            stderr = result.stderr.strip() if result.stderr else "unknown error"
            msg = f"deptry failed (rc={result.returncode}): {stderr}"
            raise RuntimeError(msg)

        return []
    finally:
        if tmp_path.exists():
            tmp_path.unlink()