diff --git a/.claude/rules/development.md b/.claude/rules/development.md index cca6fed..1ab9faf 100644 --- a/.claude/rules/development.md +++ b/.claude/rules/development.md @@ -81,7 +81,8 @@ Run `bash maintain.sh` from the repository root. The authoritative type gate is The security scanner's exit status is authoritative, including a failure after a success message. `tests/test_maintenance.py` checks clean, vulnerable, and broken scanner -runs through the real shell entry point. Recheck unresolved advisories using the +runs through the real shell entry point, and pins the CI `security` job to the +same scanner line with no `continue-on-error`. Recheck unresolved advisories using the procedure in `docs/development.md`; do not revive the old text-matching bypass. Boundary modules for cv2, Torch, and Diffusers may carry narrow per-file relaxations for unknown third-party types. Keep pure-logic files strict, preserve the local piexif stub, and fix real errors before widening a pragma. diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d370fe6..37c4a82 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -29,6 +29,19 @@ jobs: - name: Ruff format check run: uv run ruff format --check + security: + name: lockfile vulnerability scan + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v7 + - uses: astral-sh/setup-uv@v7 + with: + enable-cache: false + # Policy and triaged blocks: docs/development.md "Known security-gate blocks". + - name: Scan uv.lock with uv-secure + run: uvx uv-secure uv.lock + test: name: test ${{ matrix.os }} py${{ matrix.python-version }} runs-on: ${{ matrix.os }} diff --git a/README.md b/README.md index 13f694c..31cb28b 100644 --- a/README.md +++ b/README.md @@ -571,7 +571,8 @@ bash maintain.sh ``` See [module internals](docs/module-internals.md) before changing a subsystem -with documented invariants. +with documented invariants. Report vulnerabilities as described in +[SECURITY.md](SECURITY.md). ## License diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..272b5d8 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,25 @@ +# Security policy + +## Supported versions + +Security fixes go into the latest release. Older releases are not patched; +upgrade through the channel you installed from (PyPI or Homebrew). + +## Reporting a vulnerability + +Report vulnerabilities privately through +[GitHub private vulnerability reporting](https://github.com/wiltodelta/remove-ai-watermarks/security/advisories/new). +Do not open a public issue for an unpatched vulnerability. Include the affected +version, the command or API call, and a minimal input that reproduces it. + +Vulnerabilities in a dependency, such as a CVE in a package from `uv.lock`, can +be reported as a regular issue when the advisory is already public. + +## Dependency vulnerabilities + +Dependabot raises alerts and update pull requests for `uv.lock` and GitHub +Actions, and CI scans `uv.lock` with +[`uv-secure`](https://github.com/owenlamont/uv-secure) on every push and pull +request. A vulnerable dependency is upgraded to a fixed release; advisories +without one are tracked under +[Known security-gate blocks](docs/development.md#known-security-gate-blocks). diff --git a/docs/development.md b/docs/development.md index b46633a..9501e2f 100644 --- a/docs/development.md +++ b/docs/development.md @@ -29,7 +29,7 @@ Do not add advisory ignores or suppress the scanner's exit status. Run and report Ruff, Pyright scoped to `src/`, and tests separately when security blocks the gate. -Rechecked on 2026-09-10 against the locked versions and the advisory sources: +Rechecked on 2026-10-02 against the locked versions and the advisory sources: - `lightning` and `pytorch-lightning`, pulled by the optional `trustmark` extra, are pinned to 2.6.6. That release clears PYSEC-2026-3624 and @@ -37,18 +37,19 @@ Rechecked on 2026-09-10 against the locked versions and the advisory sources: - `accelerate`, required by the diffusion stack, is affected by [GHSA-4j2p-28q2-5m79](https://github.com/advisories/GHSA-4j2p-28q2-5m79) and PYSEC-2026-3804. The sharded-checkpoint loader accepts unsafe - `weight_map` paths; neither advisory lists a patched release. 1.15.0 is - on PyPI and is outside the advisory's `<= 1.14.0` range, but its + `weight_map` paths; neither advisory lists a patched release. The lock + carries 1.15.0, which is outside the advisory's `<= 1.14.0` range, so the + scanner no longer blocks the gate. Its `load_checkpoint_in_model` still joins `weight_map` entries with no containment check, and the sanitizer PRs ([#4138](https://github.com/huggingface/accelerate/pull/4138), - [#4214](https://github.com/huggingface/accelerate/pull/4214)) did not - land. Do not bump solely to silence the scanner. The project has no + [#4214](https://github.com/huggingface/accelerate/pull/4214)) were closed + without merging. Do not treat the passing scan as a fix. The project has no direct call to `load_checkpoint_in_model` or `load_checkpoint_and_dispatch`, but this does not prove indirect model-loading paths are unaffected. -Before carrying the remaining block forward, rerun `uvx uv-secure uv.lock` and check +When a new block appears, or before carrying one forward, rerun `uvx uv-secure uv.lock` and check PyPI for a fixed release. Upgrade a fixed package with `uv lock --upgrade-package ` and rerun the gate. Removing the current dependency would remove a supported optional feature, so that is not a @@ -98,6 +99,11 @@ run the full-clip video test. Diffusion and model-running tests skip in that matrix; metadata, identification, visible removal, the DWT-DCT decoder, and the OpenCV eraser remain covered across operating systems. +A separate `security` job runs the `maintain.sh` lockfile scan, so an advisory +without a released fix turns `main` red; see +[Known security-gate blocks](#known-security-gate-blocks). Vulnerability +reporting is described in [`SECURITY.md`](../SECURITY.md). + Keep `uv.lock` compatible with `uv sync --frozen`. Dependency pull-request checks use GitHub's merge result against current `main`; if `main` moves, merge it locally and rerun the full gate because a newer linter can expose stale directives in later code. `test.yml` runs one concurrency group per ref with `cancel-in-progress: true`, so a newer push cancels the previous run mid-flight. A cancelled run is not a failure verdict: the superseding run's head includes the cancelled commits as ancestors, so watch that run to completion instead of retrying the push. diff --git a/tests/test_maintenance.py b/tests/test_maintenance.py index ee1e586..0dd42ff 100644 --- a/tests/test_maintenance.py +++ b/tests/test_maintenance.py @@ -8,8 +8,10 @@ import subprocess from pathlib import Path import pytest +import yaml -GATE = Path(__file__).resolve().parents[1] / "maintain.sh" +ROOT = Path(__file__).resolve().parents[1] +GATE = ROOT / "maintain.sh" BASH = shutil.which("bash") pytestmark = pytest.mark.skipif(os.name != "posix" or BASH is None, reason="The maintenance entry point requires Bash") @@ -65,3 +67,13 @@ def test_security_exit_status_controls_the_gate(tmp_path, scanner_output, scanne else: assert result.returncode == 0 assert calls[-1] == "uv run pytest -n auto" + + +def test_ci_runs_the_gate_security_scan_unweakened(): + scan = next(line for line in GATE.read_text().splitlines() if line.startswith("uvx uv-secure")) + workflow = yaml.safe_load((ROOT / ".github/workflows/test.yml").read_text()) + job = workflow["jobs"]["security"] + runs = [step["run"].strip() for step in job["steps"] if "run" in step] + assert runs == [scan] + assert not job.get("continue-on-error") + assert not any(step.get("continue-on-error") for step in job["steps"])