Skip to content
Merged
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
63 changes: 63 additions & 0 deletions .claude/rules/dependency-vulnerabilities.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Dependency Vulnerability Handling

When `uv-secure` flags a vulnerability in CI or pre-commit, follow this process.

## Triage

1. **Check if a fix version exists** — look at the `Fix Versions` column in the uv-secure output
2. **Check if it's a direct or transitive dependency** — search `pyproject.toml` for the package name

## If a Fix Exists

Upgrade the dependency immediately:

```bash
# Direct dependency
uv add package-name --upgrade-package package-name

# Transitive dependency (not in pyproject.toml)
uv lock --upgrade-package package-name
```

No suppression needed — take the fix.

## If No Fix Exists

Suppress the specific vulnerability ID in `pyproject.toml`:

```toml
[tool.uv-secure.vulnerability_criteria]
ignore_vulnerabilities = [
"GHSA-xxxx-xxxx-xxxx", # package-name X.Y.Z — brief description. No fix available.
]
allow_unused_ignores = false
```

### Required Comment Format

Every entry in `ignore_vulnerabilities` must have an inline TOML comment with:
- **Package name and version** affected
- **Brief description** of the vulnerability (e.g., "ReDoS in AdlLexer")
- **"No fix available"** to explain why it's suppressed

### The Safety Net: `allow_unused_ignores = false`

This setting is mandatory. It makes CI fail when a suppressed vulnerability no longer matches any finding — which happens when:
- Renovate bumps the package to a patched version
- The vulnerability is withdrawn or reclassified

This forces cleanup of stale entries automatically through CI failure.

### Lifecycle

1. Vuln flagged with no fix -> add GHSA ID + comment to `ignore_vulnerabilities`
2. Maintainer ships a fix -> Renovate opens a PR to bump the package
3. After merge, the ignore becomes unused -> CI fails on `allow_unused_ignores`
4. Remove the stale GHSA ID from `ignore_vulnerabilities` -> CI passes

## What NOT to Do

- **Don't use `ignore_unfixed = true`** — it's a blanket suppression that hides future unfixed vulns
- **Don't use `ignore_packages`** without version specifiers — suppresses all vulns for that package
- **Don't suppress without a comment** — undocumented entries rot silently
- **Don't leave `allow_unused_ignores` unset or true** — stale entries will accumulate
19 changes: 19 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,25 @@ GitHub Actions runs these checks on every PR:
| docvet | `docvet check --all` (all 4 checks) |
| CodeQL | Static security analysis |

## Dependency Vulnerabilities

CI runs `uv-secure` to scan for known vulnerabilities in the lockfile. If it flags something:

- **Fix exists?** Upgrade the package: `uv lock --upgrade-package <pkg>`. No suppression needed.
- **No fix?** Add the specific GHSA/CVE ID to the ignore list in `pyproject.toml`:

```toml
[tool.uv-secure.vulnerability_criteria]
ignore_vulnerabilities = [
"GHSA-xxxx-xxxx-xxxx", # pkg X.Y.Z — description. No fix available.
]
allow_unused_ignores = false
```

The `allow_unused_ignores = false` setting ensures CI fails once the suppression becomes stale (e.g., after Renovate bumps the package to a patched version), forcing cleanup.

See `.claude/rules/dependency-vulnerabilities.md` for the full triage process.

## Key Constraints

- Never add runtime dependencies beyond `typer` without maintainer approval
Expand Down
8 changes: 8 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,14 @@ url = "https://test.pypi.org/simple/"
publish-url = "https://test.pypi.org/legacy/"
explicit = true

# --------------------------------------------------------------------------- #
# uv-secure
# --------------------------------------------------------------------------- #
[tool.uv-secure.vulnerability_criteria]
# Pygments 2.19.2 ReDoS in AdlLexer (GHSA low severity, local-only). No fix available.
ignore_vulnerabilities = ["GHSA-5239-wwwm-4pmq"]
allow_unused_ignores = false

# --------------------------------------------------------------------------- #
# Ruff
# --------------------------------------------------------------------------- #
Expand Down
9 changes: 6 additions & 3 deletions src/docvet/checks/griffe_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,10 @@ def _collect_object_findings(
) -> list[Finding]:
"""Collect docstring findings for a single griffe object.

Triggers docstring parsing on the object, captures any new warning
records added during parsing, and converts them to findings.
Returns early if the object has no docstring (callers guarantee one
exists, but the guard keeps production code safe). Otherwise, triggers
docstring parsing, captures any new warning records, and converts them
to findings.

Args:
obj: A griffe object with a docstring to parse.
Expand All @@ -183,8 +185,9 @@ def _collect_object_findings(
A list of findings from docstring compatibility warnings for
this object.
"""
if obj.docstring is None: # guaranteed by _walk_objects filter
return []
before = len(handler.records)
assert obj.docstring is not None # guaranteed by _walk_objects filter
_ = obj.docstring.parsed
after = len(handler.records)
findings: list[Finding] = []
Expand Down
12 changes: 12 additions & 0 deletions tests/unit/checks/test_griffe_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from docvet.checks.griffe_compat import (
_build_finding_from_record,
_classify_warning,
_collect_object_findings,
_resolve_file_set,
_walk_objects,
_WarningCollector,
Expand Down Expand Up @@ -341,6 +342,17 @@ def _setup_package_dir(tmp_path: Path, *pkg_names: str) -> Path:
return src_root


class TestCollectObjectFindings:
"""Tests for _collect_object_findings helper."""

def test_none_docstring_returns_empty(self) -> None:
"""_collect_object_findings returns [] when obj.docstring is None."""
obj = MagicMock()
obj.docstring = None
handler = _WarningCollector()
assert _collect_object_findings(obj, handler) == []


class TestCheckGriffeCompat:
"""Tests for check_griffe_compat orchestrator."""

Expand Down
7 changes: 4 additions & 3 deletions uv.lock

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

Loading