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
15 changes: 15 additions & 0 deletions .github/workflows/deps-locked.yml
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,18 @@ jobs:
run: |
/tmp/locked/bin/python -m pip install --no-deps -e .
/tmp/locked/bin/python -c "import mcp_warden; from mcp_warden import cli; import mcp, rfc8785, pydantic, typer, rich, yaml, anyio, pytest, hypothesis; print('OK: mcp_warden + hash-locked dev/CI closure import clean')"

# --------------------------------------------------------------------
# 4. CVE gate (pip-audit, fail-closed).
# Audits the RESOLVED dependency closure against the PyPI/OSV advisory
# databases and fails the job on ANY known advisory. A supply-chain
# integrity tool must not ship its own vulnerable transitive deps — this
# is the standing evidence that we don't. --skip-editable excludes the
# editable mcp-warden-cli checkout itself (no published advisories for a
# local source tree). Mirrors conclave's fail-closed pip-audit job.
# --------------------------------------------------------------------
- name: Install pip-audit (pinned)
run: /tmp/locked/bin/python -m pip install "pip-audit==2.9.0"

- name: CVE audit the locked closure (fail on any advisory)
run: /tmp/locked/bin/python -m pip_audit --skip-editable
51 changes: 47 additions & 4 deletions .github/workflows/integrity-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,24 @@ jobs:
python-version: "3.11"
cache: pip

- name: Install package + dev deps
run: pip install -e ".[dev]"
# Install the sigstore extra too so the trust-root suite (test_signing*.py)
# exercises signing.py fully and the coverage number is reproducible run to
# run — without it the version-probe branches skip and coverage drifts.
- name: Install package + dev deps (with sigstore extra)
run: pip install -e ".[dev,sigstore]"

- name: Run pytest
run: pytest -q
# Coverage floor gate. --cov-fail-under=80 pins the whole-project floor
# below the current ~86% actual so it can only rise, never silently
# regress. COVERAGE_PROCESS_START makes the coverage .pth hook fire inside
# the guard child that tests/fixtures/fault_guard_launcher.py spawns as a
# real subprocess (see [tool.coverage.run] parallel=true in pyproject.toml),
# so guard_list_gate.py — which only ever executes in-subprocess — is
# measured instead of reporting a 0% visibility artifact. pytest-cov
# combines the parallel data files automatically.
- name: Run pytest with coverage (floor 80%)
env:
COVERAGE_PROCESS_START: pyproject.toml
run: pytest -q --cov=mcp_warden --cov-report=term-missing --cov-fail-under=80

# Re-run the property-fuzz suite deterministically with a FIXED hypothesis
# seed so any counterexample is replayable from the workflow log alone
Expand All @@ -53,6 +66,36 @@ jobs:
- name: Run property-fuzz suite (deterministic, seed=0)
run: pytest tests/fuzz -p no:randomly --hypothesis-seed=0

# --------------------------------------------------------------------------
# Job 1b: lint gate (ruff)
#
# ruff is the fast lint gate: pyflakes (F, dead imports/vars), pycodestyle
# (E/W), isort (I, import order) and bugbear (B, common bug patterns). Config
# lives in [tool.ruff] in pyproject.toml (line-length=100; E501 + the typer
# B008 default-call idiom are ignored). A single real violation fails CI.
# --------------------------------------------------------------------------
lint:
name: Lint (ruff)
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- name: Set up Python 3.11
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6
with:
python-version: "3.11"
cache: pip

# Pinned ruff — this is the supply-chain tool's own CI, so the linter is
# version-pinned too (bump deliberately, never float).
- name: Install ruff (pinned)
run: pip install "ruff==0.15.21"

- name: Ruff lint check
run: ruff check .

# --------------------------------------------------------------------------
# Job 2: MCP integrity gate demo
#
Expand Down
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,14 @@ site/
.pytest_cache/
.ruff_cache/
.mypy_cache/
.hypothesis/
uv.lock

# Coverage data (parallel data files + combined)
.coverage
.coverage.*
coverage.xml
htmlcov/

# OS
.DS_Store
23 changes: 23 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,29 @@ CI. The v0.3 `guard` proxy adds deterministic runtime *result* inspection

### Added

- **CI coverage / lint / CVE gates.** The `CI` workflow now enforces three new
standing gates: (1) a **coverage floor** — `pytest --cov=mcp_warden
--cov-fail-under=80` (whole-project coverage is ~86%; the floor is pinned below
actual so it can only rise). Subprocess coverage of `guard_list_gate.py` (which
only executes inside the guard child spawned by the strict-abort tests) is now
captured via `[tool.coverage.run] parallel = true` + `COVERAGE_PROCESS_START`,
taking it from a 0% visibility artifact to ~89%. (2) A **ruff lint gate**
(`ruff check .`) with config in `[tool.ruff]` (pyflakes/pycodestyle/isort/
bugbear; `line-length = 100`). (3) A **pip-audit CVE gate** in `deps-locked`
that audits the resolved dependency closure and fails closed on any advisory.
- **Trust-root unit tests (`tests/test_signing_unit.py`).** 25 new tests drive
the internal `signing.py` sign/verify code paths directly (mocking the sigstore
boundary — no network, OIDC, or Fulcio/Rekor traffic), lifting `signing.py`
line coverage from 61% to 99%. Covers the sign path (ambient + explicit token),
the verify path (identity/issuer plumbing), and every fail-closed branch.

### Changed

- **Dependency refresh (security).** Bumped the hash-locked dev/CI closure:
`pydantic-settings` 2.14.1 → 2.14.2 (clears advisory **GHSA-4xgf-cpjx-pc3j**),
plus current minors of `mcp` (1.27.2 → 1.28.1), `cryptography` (→ 49.0.0),
`anyio` (4.13.0 → 4.14.2), and others. `pytest-cov` added to the `dev` extra.

- **Runtime DNS resolution SSRF bypass detection (`WRD-RES-EXFIL-DNS-SSRF`)** (#11):
the `guard` proxy now resolves URL hostnames from `tools/call` results at runtime
and blocks (error-replace) when any resolved IP falls in a deny range
Expand Down
52 changes: 52 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ dependencies = [
dev = [
"pytest>=9.0.3",
"pytest-asyncio>=0.23.0",
"pytest-cov>=6.0.0",
"hypothesis>=6.100.0",
]
# #16: Sigstore keyless signing + verification of warden.lock. OPT-IN — the base
Expand All @@ -55,7 +56,58 @@ build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/mcp_warden"]

# --------------------------------------------------------------------------
# Lint config (ruff). Modeled on conclave's config: pyflakes (F), pycodestyle
# (E/W), isort (I) and bugbear (B). line-length owns E501 so we don't fight the
# formatter over long lines; B008 is ignored because typer/FastAPI idiomatically
# put `typer.Option(...)` / `Depends(...)` calls in argument defaults, which B008
# flags as false positives.
# --------------------------------------------------------------------------
[tool.ruff]
line-length = 100
target-version = "py311"
src = ["src", "tests"]

[tool.ruff.lint]
select = ["E", "W", "F", "I", "B"]
ignore = ["E501", "B008"]

[tool.ruff.lint.per-file-ignores]
# Tests intentionally reach into private module internals (leading underscore
# names) and re-bind them via monkeypatch; that is the whole point of the
# trust-root wiring tests, so don't flag those imports/uses.
"tests/**" = ["E402"]

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
filterwarnings = ["ignore::DeprecationWarning"]

# --------------------------------------------------------------------------
# Coverage config (issue: coverage + lint/CVE gates).
#
# parallel = true so the guard child spawned via tests/fixtures/
# fault_guard_launcher.py (a real subprocess — see test_guard_strict.py) is
# measured too. That subprocess inherits COVERAGE_PROCESS_START from the parent
# env and the coverage .pth hook starts a second data file; `coverage combine`
# (run by pytest-cov automatically) merges them. Without this, guard_list_gate.py
# reports 0% purely as a visibility artifact — it only ever runs in-subprocess.
#
# CI sets COVERAGE_PROCESS_START=pyproject.toml before pytest so the .pth hook
# fires in child processes; see .github/workflows/integrity-gate.yml.
# --------------------------------------------------------------------------
[tool.coverage.run]
branch = false
parallel = true
source = ["mcp_warden"]

[tool.coverage.report]
show_missing = true
# Exclude the guarded lines that are unreachable by design (the ImportError
# graceful-degrade path is exercised only via monkeypatch, never a real absent
# import) plus the standard pragma.
exclude_lines = [
"pragma: no cover",
"if TYPE_CHECKING:",
"raise NotImplementedError",
]
Loading
Loading