From de7c33fc143a46801e699b126833ba7a0227db09 Mon Sep 17 00:00:00 2001 From: Renn F Date: Thu, 15 Oct 2026 12:01:15 +0200 Subject: [PATCH 1/3] release(8.1.1): pure-ASGI request metrics --- .mike.yml | 3 ++- CHANGELOG.md | 20 ++++++++++++++++++++ docs/index.md | 2 +- docs/release-notes.md | 20 ++++++++++++++++++++ docs/versions/versions.json | 3 ++- pyproject.toml | 2 +- 6 files changed, 46 insertions(+), 4 deletions(-) diff --git a/.mike.yml b/.mike.yml index 3308c35f..24965921 100644 --- a/.mike.yml +++ b/.mike.yml @@ -2,6 +2,7 @@ version_selector: true title_switch: true versions_file: docs/versions/versions.json versions: + - 8.1.1 - 8.1.0 - 8.0.3 - 8.0.2 @@ -64,4 +65,4 @@ versions: - 0.3.2 - latest aliases: - latest: 8.1.0 + latest: 8.1.1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 9de15518..a3e11621 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,26 @@ Release Notes ___ +v8.1.1 (2026-10-15) +------------------- + +The pure-ASGI metrics release: the pass-through path records request metrics like the classic middleware (v8.1.1) +----------------------------------------------------------------------------------------------------------------- + +### Fixed + +- **`PureASGISecurityMiddleware` records request metrics on the pass-through path** (fastapi-guard #163, #164). The pure-ASGI middleware now records request metrics when the response starts, matching the classic middleware: `response_time` stops at the response start, and the body is still streamed, never buffered. Tests cover allowed, error and streaming responses. + +### Note + +- The `guard-core` dependency floor stays at `>=4.1.0`. The detection and dynamic rule fixes in guard-core 4.4.0 (Cookie header scanned per pair, the per-request scan budget, the semantic-only header opt-out and dynamic rule versions applied over the base config) reach this adapter by upgrading guard-core, for example `pip install -U guard-core`, with no change to this package. + +### Chore + +- The sponsor button points at the maintainer's Sponsors page, the mkdocs `site_url` drops `/latest/` so canonical URLs resolve, the family Discord relay workflow was added, and the `upload-sarif` action was bumped (fastapi-guard #158, #159, #161, #162). + +___ + v8.1.0 (2026-10-10) ------------------- diff --git a/docs/index.md b/docs/index.md index 902d1792..2f15a429 100644 --- a/docs/index.md +++ b/docs/index.md @@ -75,7 +75,7 @@ You can also download the example app as a Docker container from [GitHub Contain docker pull ghcr.io/guard-core/fastapi-guard-example:latest # Or pull a specific version (matches library releases) -docker pull ghcr.io/guard-core/fastapi-guard-example:v8.1.0 +docker pull ghcr.io/guard-core/fastapi-guard-example:v8.1.1 ``` ___ diff --git a/docs/release-notes.md b/docs/release-notes.md index edeea497..8c212e74 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -10,6 +10,26 @@ Release Notes ___ +v8.1.1 (2026-10-15) +------------------- + +The pure-ASGI metrics release: the pass-through path records request metrics like the classic middleware (v8.1.1) +----------------------------------------------------------------------------------------------------------------- + +### Fixed + +- **`PureASGISecurityMiddleware` records request metrics on the pass-through path** (fastapi-guard #163, #164). The pure-ASGI middleware now records request metrics when the response starts, matching the classic middleware: `response_time` stops at the response start, and the body is still streamed, never buffered. Tests cover allowed, error and streaming responses. + +### Note + +- The `guard-core` dependency floor stays at `>=4.1.0`. The detection and dynamic rule fixes in guard-core 4.4.0 (Cookie header scanned per pair, the per-request scan budget, the semantic-only header opt-out and dynamic rule versions applied over the base config) reach this adapter by upgrading guard-core, for example `pip install -U guard-core`, with no change to this package. + +### Chore + +- The sponsor button points at the maintainer's Sponsors page, the mkdocs `site_url` drops `/latest/` so canonical URLs resolve, the family Discord relay workflow was added, and the `upload-sarif` action was bumped (fastapi-guard #158, #159, #161, #162). + +___ + v8.1.0 (2026-10-10) ------------------- diff --git a/docs/versions/versions.json b/docs/versions/versions.json index 7c191daf..15eb9b52 100644 --- a/docs/versions/versions.json +++ b/docs/versions/versions.json @@ -1,4 +1,5 @@ { + "8.1.1": "8.1.1", "8.1.0": "8.1.0", "8.0.3": "8.0.3", "8.0.2": "8.0.2", @@ -60,5 +61,5 @@ "0.3.4": "0.3.4", "0.3.3": "0.3.3", "0.3.2": "0.3.2", - "latest": "8.1.0" + "latest": "8.1.1" } diff --git a/pyproject.toml b/pyproject.toml index f6b99a21..1e274ce4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "fastapi_guard" -version = "8.1.0" +version = "8.1.1" description = "Production-ready security middleware for FastAPI — IP filtering, rate limiting, penetration detection, and 20+ per-route security decorators." authors = [ {name = "Renzo Franceschini", email = "rennf93@guard-core.com"} From 92d3925fe2a0867d5b9069e4394561db3d32cb3c Mon Sep 17 00:00:00 2001 From: Renn F Date: Thu, 15 Oct 2026 14:32:16 +0200 Subject: [PATCH 2/3] build(deps): require guard-core>=4.4.0 guard-core 4.4.0 is on PyPI, so the floor moves up and upgrading this adapter brings the engine fixes. The release notes say so instead of asking users to upgrade guard-core separately. --- CHANGELOG.md | 2 +- docs/release-notes.md | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3e11621..c52b63a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ The pure-ASGI metrics release: the pass-through path records request metrics lik ### Note -- The `guard-core` dependency floor stays at `>=4.1.0`. The detection and dynamic rule fixes in guard-core 4.4.0 (Cookie header scanned per pair, the per-request scan budget, the semantic-only header opt-out and dynamic rule versions applied over the base config) reach this adapter by upgrading guard-core, for example `pip install -U guard-core`, with no change to this package. +- The `guard-core` dependency floor rises from `>=4.1.0` to `>=4.4.0`, so upgrading fastapi-guard also brings the guard-core 4.4.0 detection and dynamic rule fixes (Cookie header scanned per pair, the per-request scan budget, the semantic-only header opt-out and dynamic rule versions applied over the base config). ### Chore diff --git a/docs/release-notes.md b/docs/release-notes.md index 8c212e74..9ae811b1 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -22,7 +22,7 @@ The pure-ASGI metrics release: the pass-through path records request metrics lik ### Note -- The `guard-core` dependency floor stays at `>=4.1.0`. The detection and dynamic rule fixes in guard-core 4.4.0 (Cookie header scanned per pair, the per-request scan budget, the semantic-only header opt-out and dynamic rule versions applied over the base config) reach this adapter by upgrading guard-core, for example `pip install -U guard-core`, with no change to this package. +- The `guard-core` dependency floor rises from `>=4.1.0` to `>=4.4.0`, so upgrading fastapi-guard also brings the guard-core 4.4.0 detection and dynamic rule fixes (Cookie header scanned per pair, the per-request scan budget, the semantic-only header opt-out and dynamic rule versions applied over the base config). ### Chore diff --git a/pyproject.toml b/pyproject.toml index 1e274ce4..fa91b167 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ keywords = [ ] dependencies = [ "fastapi>=0.109.0", - "guard-core>=4.1.0", + "guard-core>=4.4.0", "starlette>=0.35.0", "uvicorn", ] From a276c8e62249a166a74dc13813b760d41a0f0b66 Mon Sep 17 00:00:00 2001 From: Renn F Date: Thu, 15 Oct 2026 14:45:54 +0200 Subject: [PATCH 3/3] test(live-smoke): cover detection_max_scan_seconds and excluded_semantic_detection_headers guard-core 4.4.0 adds both fields and the live-smoke completeness check requires a scenario for every SecurityConfig field. The two scenarios are the ones guard-core #167 added to its own live-smoke suite. --- .../live_smoke/scenarios/detection_engine.py | 31 +++++++++ .../framework_gaps_detection_limits.py | 69 +++++++++++++++++++ 2 files changed, 100 insertions(+) diff --git a/tests/live_smoke/scenarios/detection_engine.py b/tests/live_smoke/scenarios/detection_engine.py index 8d9ef8ab..75818a7a 100644 --- a/tests/live_smoke/scenarios/detection_engine.py +++ b/tests/live_smoke/scenarios/detection_engine.py @@ -448,3 +448,34 @@ def detection_binary_min_run_length_lowered_restores_fragment_detection( "detection_binary_min_run_length=4 did not restore detection of a " f"short fragment inside a binary-dense upload: {response.status_code}" ) + + +_JWT_COOKIE = ( + "session=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." + "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ." + "SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c" +) + + +@scenario( + covers={"excluded_semantic_detection_headers"}, + config={ + **_BASE, + "detection_semantic_threshold": 0.3, + "excluded_semantic_detection_headers": ["cookie"], + }, +) +def excluded_semantic_detection_headers_skip_only_semantic_scoring( + ctx: ScenarioContext, +) -> None: + listed = ctx.client.get("/basic/ip", headers={"cookie": _JWT_COOKIE}) + assert listed.status_code == 200, ( + "excluded_semantic_detection_headers=['cookie'] still flagged a JWT " + f"session cookie semantically: {listed.status_code}" + ) + + unlisted = ctx.client.get("/basic/ip", headers={"x-session": _JWT_COOKIE}) + assert unlisted.status_code == 400, ( + "a header not in excluded_semantic_detection_headers lost its " + f"semantic analysis: {unlisted.status_code}" + ) diff --git a/tests/live_smoke/scenarios/framework_gaps_detection_limits.py b/tests/live_smoke/scenarios/framework_gaps_detection_limits.py index 4fb85468..2aa9b6c0 100644 --- a/tests/live_smoke/scenarios/framework_gaps_detection_limits.py +++ b/tests/live_smoke/scenarios/framework_gaps_detection_limits.py @@ -177,3 +177,72 @@ def sync_body_read_max_concurrent_serializes_reads(ctx: ScenarioContext) -> None f"max_concurrent=1 should serialize the two reads; B started at " f"{b_start:.2f}s, before A finished at {a_end:.2f}s: {events}" ) + + +_SCAN_BUDGET_PROBE = """ +import asyncio +import logging +import types + +logging.disable(logging.CRITICAL) + +from guard_core._utils import detection_scan +from guard_core.models import SecurityConfig +from guard_core.utils import detect_penetration_attempt + + +class _Clock: + now = 0.0 + + @classmethod + def tick(cls): + cls.now += 1.0 + return cls.now + + +detection_scan._thread_cpu_seconds = _Clock.tick + + +class _Request: + headers = {} + url_path = "/" + url_scheme = "http" + url_full = "http://smoke/" + method = "GET" + client_host = "203.0.113.9" + scope = {} + state = types.SimpleNamespace() + query_params = {"q": "select name from the users table where the quick brown fox"} + + async def body(self): + return b"" + + +async def main(): + config = SecurityConfig(detection_max_scan_seconds=0.1) + result = await detect_penetration_attempt(_Request(), config) + print(f"SMOKE_SCAN_BUDGET_PROBE threat={result.is_threat} " + f"info={result.trigger_info!r}", flush=True) + + +asyncio.run(main()) +""" + + +@scenario( + covers={"detection_max_scan_seconds"}, + config={**COMPILER_TIMEOUT_CONFIG, "detection_max_scan_seconds": 0.1}, +) +def detection_max_scan_seconds_flags_a_request_that_exhausts_the_budget( + ctx: ScenarioContext, +) -> None: + result = run_in_app_container(_SCAN_BUDGET_PROBE, timeout=20.0) + output = result.stdout + result.stderr + + assert "SMOKE_SCAN_BUDGET_PROBE" in output, ( + f"the detection_max_scan_seconds probe did not complete; output: {output!r}" + ) + assert "threat=True" in output and "scan_time_budget_exhausted" in output, ( + "detection_max_scan_seconds=0.1 did not flag a request whose regex CPU " + f"time exceeded the budget; probe output: {output!r}" + )