repomatic.deps.audit 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.audit.AUDIT_HEADER_DEFS: tuple[ColumnSpec, ...] = (ColumnSpec(id='package', label='Package', description='', max_width=None, optional=False, sortable=True), ColumnSpec(id='version', label='Version', description='', max_width=None, optional=False, sortable=True), ColumnSpec(id='advisory', label='Advisory', description='', max_width=None, optional=False, sortable=True), ColumnSpec(id='fixed', label='Fixed', description='', max_width=None, optional=False, sortable=True), ColumnSpec(id='sources', label='Sources', description='', max_width=None, optional=False, sortable=True))¶

Column definitions for the repomatic audit table.

Lives beside VulnerablePackage, whose row fills the columns, so the two cannot drift apart. The CLI derives its --sort-by choices from it.

class repomatic.deps.audit.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 (GHSA frequently lists a CVE before the PyPA Advisory Database mirrors it, for one). 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).

classmethod from_config(values)[source]¶

Read [tool.repomatic] vulnerable-deps.sources.

An unknown name is logged and skipped. The list stays empty when the configuration names no known source: an empty list queries nothing, where None would mean every source.

Parameters:

values (Iterable[str]) – The configured source names.

Return type:

list[AdvisorySource]

Returns:

The known sources, in configuration order.

class repomatic.deps.audit.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 (like 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 against GitHub’s /advisories/ page), so the rendered table can link the source name to the database that actually surfaced it.

property row: tuple[str, str, str, str, str]¶

The advisory as a row of AUDIT_HEADER_DEFS.

repomatic.deps.audit.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.audit.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.audit.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.

class repomatic.deps.audit.AuditFix(vulnerabilities=<factory>, changes=<factory>, lock=<factory>, release_notes=<factory>)[source]¶

Bases: object

What audit --fix found, and what it upgraded.

vulnerabilities: list[VulnerablePackage]¶

Every advisory the sources reported.

changes: list[tuple[str, str, str]]¶

The (name, old, new) version moves the upgrade made.

lock: LockFile¶

The upgraded lock.

release_notes: dict[str, tuple[str, list[tuple[str, str]]]]¶

Release notes of the upgraded packages, as repomatic.deps.report.fetch_release_notes() returns them.

property has_fixes: bool¶

Whether the upgrade moved at least one package.

render(reference_date)[source]¶

Render the advisories, the version moves and the release notes.

Parameters:

reference_date (date) – The day the “Released” hints count back from.

Return type:

str

Returns:

The markdown report, sections separated by a blank line.

repomatic.deps.audit.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 (from the sync-uv-lock job, say) 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 below.

Parameters:
Return type:

AuditFix

Returns:

The advisories, and the upgrade they led to. No change when nothing was fixable.

repomatic.deps.audit.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).