repomatic.deps.vulnerable_deps module

Vulnerability audit and remediation for locked dependencies.

Backs the audit command and the fix-vulnerable-deps job: queries the advisory sources enabled in [tool.repomatic] vulnerable-deps.sources, unions and deduplicates their findings into VulnerablePackage records, and (--fix) upgrades each fixable package through uv.

Two advisory sources are consulted:

Coverage diverges in practice: GHSA frequently lists a CVE before the PyPA database mirrors it, and transitive lockfile vulnerabilities sometimes only surface in GHSA. By unioning both sources, audit catches CVEs that either database alone would miss.

repomatic.deps.vulnerable_deps.AUDIT_HEADER_DEFS: tuple[tuple[str, str], ...] = (('Package', 'package'), ('Version', 'version'), ('Advisory', 'advisory'), ('Fixed', 'fixed'), ('Sources', 'sources'))

Column definitions for the repomatic audit table.

Lives beside the rows’ domain model so the columns and the fields they render cannot drift apart; the CLI derives its --sort-by choices from it.

repomatic.deps.vulnerable_deps.MIN_UV_AUDIT_JSON_VERSION = <Version('0.11.15')>

Minimum uv version exposing uv audit --output-format json.

The structured JSON output landed in uv 0.11.15 as a preview feature. Below this, uv audit emits only human-readable text, so _run_uv_audit refuses to run rather than silently scanning nothing.

class repomatic.deps.vulnerable_deps.AdvisorySource(*values)[source]

Bases: StrEnum

Where a vulnerability advisory was detected.

Each source has a distinct upstream database and ingestion pipeline, so coverage diverges in practice (e.g., GHSA frequently lists a CVE before the PyPA Advisory Database mirrors it). Tracking the source per VulnerablePackage lets the union deduplicate by advisory ID while still attributing each entry to the database that produced it.

UV_AUDIT = 'uv-audit'

Detected by uv audit (PyPA Advisory Database, OSV-backed).

GITHUB_ADVISORIES = 'github-advisories'

Detected via the repository’s Dependabot alerts (GitHub Advisory Database).

class repomatic.deps.vulnerable_deps.VulnerablePackage(name, current_version, advisory_id, advisory_title, fixed_version, advisory_url, aliases=<factory>, sources=<factory>, source_urls=<factory>)[source]

Bases: object

A single vulnerability advisory for a Python package.

name: str

Package name.

current_version: str

Currently resolved version.

advisory_id: str

Advisory identifier (e.g., GHSA-xxxx-xxxx-xxxx).

advisory_title: str

Short description of the vulnerability.

fixed_version: str

Version that contains the fix, or empty string if unknown.

advisory_url: str

URL to the advisory details.

aliases: set[str]

Alternate identifiers for the same advisory (CVE, GHSA, PYSEC, OSV).

Advisory databases cross-reference each other: the PyPA database (via uv audit) keys records by OSV/PYSEC IDs while listing the matching GHSA/CVE IDs as aliases, and Dependabot keys by GHSA while listing the CVE. collect_vulnerable_packages() unions entries whose identifier sets overlap, so a shared alias deduplicates the same advisory reported under different primary IDs by different sources.

sources: set[AdvisorySource]

Advisory databases that surfaced this entry.

A set rather than a single value because the same advisory can be reported by multiple sources after deduplication. Empty only for entries built without source attribution (test fixtures); every production code path records at least one source.

source_urls: dict[AdvisorySource, str]

Per-source URL pointing to the advisory page in each database.

Each source has its own canonical URL even when reporting the same advisory ID (PyPA’s osv.dev page vs. GitHub’s /advisories/ page), so the rendered table can link the source name to the database that actually surfaced it.

repomatic.deps.vulnerable_deps.parse_uv_audit_json(output)[source]

Parse uv audit --output-format json output into vulnerability records.

The structured contract avoids the regex fragility of scraping human-readable lines, and exposes the advisory aliases (cross-referenced CVE/GHSA/PYSEC IDs) that let collect_vulnerable_packages() deduplicate the same advisory across sources.

Parameters:

output (str) – stdout from uv audit --output-format json.

Return type:

list[VulnerablePackage]

Returns:

A list of VulnerablePackage entries (empty when the audit found nothing).

Raises:

RuntimeError – when the output is unusable as JSON (empty, malformed, or carrying an unrecognized schema.version). Raising rather than returning an empty list keeps the scanner from silently passing when the preview schema changes under it.

repomatic.deps.vulnerable_deps.format_vulnerability_table(vulns)[source]

Format vulnerability data as a markdown table.

Includes a Sources column listing the advisory databases that surfaced each entry, so reviewers can see which database (PyPA Advisory DB, GitHub Advisory DB, or both) detected the vulnerability.

Parameters:

vulns (list[VulnerablePackage]) – List of VulnerablePackage entries.

Return type:

str

Returns:

A markdown string with a ## Vulnerabilities heading and table, or an empty string if no vulnerabilities are provided.

repomatic.deps.vulnerable_deps.collect_vulnerable_packages(lock_path, repo=None, sources=None)[source]

Collect vulnerability advisories from all configured sources.

Queries each enabled advisory database, then deduplicates entries per package by advisory identity: two entries merge when their identifier sets (advisory_id plus aliases) overlap, so the same advisory reported under a PYSEC/OSV ID by uv audit and a GHSA ID by Dependabot collapses into one. Merging preserves the union of sources so the rendered table credits both databases when they agree.

Current versions reported by uv audit take precedence over the empty placeholder produced by the GHSA path, since uv audit reads the actual locked version while Dependabot alerts only carry the vulnerable range. When the GHSA path encounters a package that uv audit did not surface, the current version is filled in from the lock file.

Parameters:
  • lock_path (Path) – Path to the uv.lock file.

  • repo (str | None) – Repository in owner/repo format. Required for the AdvisorySource.GITHUB_ADVISORIES source; pass None to skip it (the result then reflects uv audit only).

  • sources (list[AdvisorySource] | None) – Advisory databases to consult. Defaults to all known sources.

Return type:

list[VulnerablePackage]

Returns:

Deduplicated list of VulnerablePackage entries.

repomatic.deps.vulnerable_deps.fix_vulnerable_deps(lock_path, repo=None, sources=None)[source]

Detect vulnerable packages and upgrade them in the lock file.

Queries every advisory source enabled by sources (defaults to all), then upgrades each fixable package with uv lock --upgrade-package using --exclude-newer-package to bypass the exclude-newer cooldown for security fixes. Also persists the exemptions in pyproject.toml so that subsequent uv lock --upgrade runs (e.g. from the sync-uv-lock job) do not downgrade the fixed packages back within the cooldown window.

An upgrade that resolves to the versions already locked leaves the file byte-identical to how it was found, because uv writes the overrides it was handed into the lock’s [options] table even when they change nothing. See the restore in step 5.

Parameters:
Return type:

tuple[bool, str]

Returns:

A tuple of (has_fixes, diff_table). has_fixes is True when at least one vulnerable package was upgraded. diff_table is a markdown-formatted string with vulnerability details and version changes, or an empty string if no fixable vulnerabilities were found.

repomatic.deps.vulnerable_deps.fetch_dependabot_alerts(repo)[source]

Fetch open pip-ecosystem Dependabot alerts for a repository.

Calls GET /repos/{repo}/dependabot/alerts?state=open&ecosystem=pip via the gh CLI, then maps each alert into a VulnerablePackage tagged with AdvisorySource.GITHUB_ADVISORIES.

Returns an empty list when the API is unreachable, the token lacks the Dependabot alerts permission, or the repository has no open alerts. A network or auth failure must not break the autofix workflow: the uv audit source is still consulted independently.

Parameters:

repo (str) – Repository in owner/repo format.

Return type:

list[VulnerablePackage]

Returns:

List of VulnerablePackage entries with a known fixed version. Alerts without first_patched_version are skipped (no upgrade target).