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
3 changes: 2 additions & 1 deletion .mike.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -64,4 +65,4 @@ versions:
- 0.3.2
- latest
aliases:
latest: 8.1.0
latest: 8.1.1
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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

- 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)
-------------------

Expand Down
2 changes: 1 addition & 1 deletion docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

___
Expand Down
20 changes: 20 additions & 0 deletions docs/release-notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 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

- 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)
-------------------

Expand Down
3 changes: 2 additions & 1 deletion docs/versions/versions.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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"
}
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"}
Expand Down Expand Up @@ -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",
]
Expand Down
31 changes: 31 additions & 0 deletions tests/live_smoke/scenarios/detection_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
)
69 changes: 69 additions & 0 deletions tests/live_smoke/scenarios/framework_gaps_detection_limits.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
)
Loading