Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 25 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,17 @@ Custom pre-commit hooks for Datacation to check Python dependency vulnerabilitie

### `check-uv-lock-vulnerabilities`

Checks your `uv.lock` for vulnerabilities using [`pip-audit`](https://github.com/pypa/pip-audit).
Checks your `uv.lock` for vulnerabilities using [`uv audit`](https://docs.astral.sh/uv/reference/cli/#uv-audit).

- **Runs:** Every pre-commit
- **Script:** [`hooks/check-uv-lock-vulnerabilities.py`](hooks/check-uv-lock-vulnerabilities.py)
- **Script:** [`hooks/check_uv_lock_vulnerabilities.py`](hooks/check_uv_lock_vulnerabilities.py)

### `check-uv-lock-vulnerabilities-daily`

Checks your `uv.lock` for vulnerabilities, but only if the last successful run was more than 24 hours ago (configurable).

- **Runs:** Every pre-commit, but skips if run in the last interval
- **Script:** [`hooks/check-uv-lock-vulnerabilities-daily.py`](hooks/check-uv-lock-vulnerabilities-daily.py)
- **Script:** [`hooks/check_uv_lock_vulnerabilities_daily.py`](hooks/check_uv_lock_vulnerabilities_daily.py)
- **Interval:** Default 24 hours (can be set via argument)

## Usage
Expand All @@ -32,3 +32,25 @@ Add this repo to your `.pre-commit-config.yaml`:
- id: check-uv-lock-vulnerabilities-daily
args: ["24"] # change this number to adjust the interval in hours, ex. weekly: 168
```

## Dependency Cooldown (Supply Chain Attack Prevention)

To prevent supply chain attacks, this hook respects the `exclude-newer` setting in your `pyproject.toml`. This ensures that newly published packages are subject to a cooldown period before they can be resolved, giving the community time to identify malicious releases.

Add the following to your `pyproject.toml`:

```toml
[tool.uv]
exclude-newer = "P7D"
```

This enforces a 7-day cooldown on dependency resolution, meaning only packages published more than 7 days ago will be considered. The `uv audit` command respects this setting via `--frozen`, so developers will not be blocked from committing due to vulnerability fixes that are still within the cooldown period.

## Ignoring Vulnerabilities

To ignore specific vulnerability advisories, add the following to your `pyproject.toml`:

```toml
[tool.uv-audit]
ignore = ["GHSA-xxxx-xxxx-xxxx", "PYSEC-2024-xxxx"]
```
69 changes: 3 additions & 66 deletions hooks/check_uv_lock_vulnerabilities.py
Original file line number Diff line number Diff line change
@@ -1,73 +1,10 @@
import os
import subprocess
import sys
import tempfile
import tomllib

from pip_audit._cli import audit

try:
with open("pyproject.toml", "rb") as f:
config = tomllib.load(f)
except FileNotFoundError:
config = {}
# Extract ignore list (default to empty)
ignore_vuln_list = config.get("tool", {}).get("pip-audit", {}).get("ignore-vuln", [])
# Extract packages to ignore (default to empty)
ignore_package_list = config.get("tool", {}).get("pip-audit", {}).get("ignore-package", [])
ignore_package_arguments = []
for ignore_package in ignore_package_list:
ignore_package_arguments.extend(["--no-emit-package", ignore_package])


def check_vulnerabilities() -> int | str | None:
# Create a temporary requirements file
with tempfile.NamedTemporaryFile(
mode="w+", suffix=".txt", delete=False
) as req_file:
req_file_path = req_file.name
try:
# Export requirements using uv
subprocess.run(
[
"uv",
"export",
"--format=requirements-txt",
"--all-groups",
"--locked",
"--no-emit-local",
] + ignore_package_arguments,
stdout=req_file,
check=True,
)
req_file.flush()

# Build pip-audit arguments
args = [
"pip-audit",
"-r",
req_file_path,
"--disable-pip",
"--require-hashes",
]
# Add ignore-vuln flags if any
for vuln in ignore_vuln_list:
args.extend(["--ignore-vuln", vuln])

# Run pip-audit
sys.argv = args
try:
audit()
except SystemExit as e:
return e.code
return 0
finally:
if os.path.exists(req_file_path):
try:
os.remove(req_file_path)
except PermissionError:
pass
# Leak temp file on Windows
def check_vulnerabilities() -> int:
result = subprocess.run(["uv", "audit", "--frozen"])
return result.returncode


def main():
Expand Down
7 changes: 5 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
[project]
name = "pre-commit-hooks"
version = "0.1.0"
description = "Custom pre-commit hooks for checking Python dependency vulnerabilities with uv and pip-audit."
description = "Custom pre-commit hooks for checking Python dependency vulnerabilities with uv audit."
authors = [{ name = "Datacation", email = "info@datacation.com" }]
readme = "README.md"
requires-python = ">=3.12"
dependencies = ["pip-audit"]
dependencies = []

[project.urls]
Homepage = "https://github.com/datacation/pre-commit-hooks"
Expand All @@ -20,3 +20,6 @@ requires = ["hatchling"]

[tool.hatch.build.targets.wheel]
packages = ["hooks"]

[tool.uv]
exclude-newer = "P7D"
Empty file added tests/__init__.py
Empty file.
5 changes: 5 additions & 0 deletions tests/data/no_vulnerabilities/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[project]
name = "test"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = []
8 changes: 8 additions & 0 deletions tests/data/no_vulnerabilities/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions tests/data/unfixed_ignored/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[project]
name = "test"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["py==1.11.0"]

[tool.uv.audit]
ignore-until-fixed = ["PYSEC-2022-42969"]
23 changes: 23 additions & 0 deletions tests/data/unfixed_ignored/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions tests/data/vulnerable/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[project]
name = "test"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["setuptools==69.5.1"]
27 changes: 27 additions & 0 deletions tests/data/vulnerable/uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 29 additions & 0 deletions tests/test_check_uv_lock_vulnerabilities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import shutil
from pathlib import Path

from hooks.check_uv_lock_vulnerabilities import check_vulnerabilities

_DATA = Path(__file__).parent / "data"


def _setup(tmp_path: Path, fixture: str) -> Path:
src = _DATA / fixture
shutil.copy(src / "pyproject.toml", tmp_path / "pyproject.toml")
shutil.copy(src / "uv.lock", tmp_path / "uv.lock")
return tmp_path


def test_detects_vulnerabilities(tmp_path, monkeypatch):
monkeypatch.chdir(_setup(tmp_path, "vulnerable"))
assert check_vulnerabilities() == 1


def test_ignore_until_fixed_suppresses_unfixed_vulnerability(tmp_path, monkeypatch):
# PYSEC-2022-42969 has no fix available; ignore-until-fixed silences it
monkeypatch.chdir(_setup(tmp_path, "unfixed_ignored"))
assert check_vulnerabilities() == 0


def test_no_vulnerabilities(tmp_path, monkeypatch):
monkeypatch.chdir(_setup(tmp_path, "no_vulnerabilities"))
assert check_vulnerabilities() == 0
Loading