Repository navigation
Releases: Guard-Core/djapi-guard
Release list
v5.0.0
v5.0.0 (2026-10-15)
Engine routing parity with fastapi-guard, required GUARD_SECURITY_CONFIG (v5.0.0)
-
Breaking changes - A missing
settings.GUARD_SECURITY_CONFIGnow raisesImproperlyConfigured(#39, closes #38).DjangoAPIGuardused to fall back silently toSecurityConfig()defaults when the setting was absent orNone, which ran the guard with a configuration you never chose. It now fails at startup with a message naming the setting. -
Breaking changes - Upgrade action - Define
GUARD_SECURITY_CONFIG = SecurityConfig(...)in your Django settings before upgrading. If you relied on the defaults, setGUARD_SECURITY_CONFIG = SecurityConfig()explicitly. -
Fixed - Global behavioral return rules now run.
global_behavior_rulesare passed to the engine response pass at both response-pass call sites, as fastapi-guard does. -
Documentation and tests - The README examples now build a real
SecurityConfiginstead of relying on the fallback, and the advanced example uses the engine request and response protocols and thestatus:404return pattern syntax. -
Documentation and tests -
tests/test_engine_parity.pydrives the middleware over Django's real URL resolution and views with the real engine bypass handler and response factory. The test that asserted the fallback now asserts the error. -
Compatibility - The
guard-corefloor rises from>=4.1.0to>=4.4.0, so upgrading djapi-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_urldrops/latest/so canonical URLs resolve, the README markdown lint errors that blocked the docs deploy are fixed and the README was brought to content parity, a CODEOWNERS file was added, the family Discord relay workflow was added, and theupload-sarifaction was bumped (#29, #30, #31, #32, #34, #36, #37).
v4.3.3
v4.3.2
v4.3.2 (2026-09-26)
Block response Content-Type, guard-core 4.1.0 tracking (v4.3.2)
- Fixed - Blocked responses now declare
Content-Type: text/plain(#22): the Django response factory left the content type unset, so middleware-generated 403/429/400 pages went out without an explicit content type and clients were left guessing. The factory now sets an explicittext/plaincontent type, matching the engine's reference response factory and the fastapi-guard (#144) and flaskapi-guard (#25) fixes. - Compatibility - The
guard-corefloor moves from>=4.0.0to>=4.1.0inpyproject.toml(no upper bound), so this adapter tracks the guard-core 4.1.0 engine (the exempt_ips skip-list, the recon leading-separator gate, the raw-view recon scan, the cross-engine interop harness). The adapter delegates all security behavior to the engine through the unasync-generated sync mirror; no sync API djapi-guard uses changed shape.
v4.3.1
guard-core 4.0.x tracking: dependency floor to >=4.0.0, agent docs (v4.3.1)
- Compatibility - The
guard-corefloor moves from>=3.15.0to>=4.0.0inpyproject.toml(no upper bound), so this adapter tracks the guard-core 4.0.x engine line and is verified against guard-core 4.0.4. No adapter code changed: the delta since 4.3.0 is docs only, hence the patch bump. Since the adapter delegates all security behavior to the engine, no other change is required for the 4.0.x line; guard-core 4.0.0's own breaking changes (grammar-based secret redaction, exact-valuerequire_headers()enforcement,excluded_detection_headerssemantics) are documented in the guard-core changelog. - Documentation - Added
AGENTS.md/CLAUDE.mdagent guidance and the djapi-guard package skill, and noted in the access-control decorator docs that an unknown bypass token now warns. - Fixed (tests) -
test_blocked_user_agent_patternconfiguredblocked_user_agents=[r".*crawler.*"], which guard-core 4.x's ReDoS validator rejects (adjacent broad unbounded quantifiers), so the suite cannot construct aSecurityConfigagainst the 4.0.x line. The test now blocks through[Cc]rawler/\d+\.\d+, which still exercises regex-pattern matching against a real user agent while satisfying the validator.
v4.3.0
v4.3.0 (2026-08-31)
guard-core 3.15.0 compatibility: lazy Redis initialization, fail-secure client-address rejection (v4.3.0)
- Fixed -
DjangoAPIGuard.__init__()calledHandlerInitializer.initialize_redis_handlers()eagerly. Django'sload_middleware()runs this beforeconvert_exception_to_responseis installed, so a Redis outage at process start raisedGuardRedisErroras a raw unhandled exception at the WSGI boundary instead of a styled response, and since__init__runs once per worker process, recovery needed a restart. Initialization is now lazy:__call__runs it once, on the first request, through the new_ensure_handlers_initialized(). AGuardRedisErroron that first attempt is caught, logged, and answered withHttpResponse(status=503)andRetry-After: 5; the initialized flag stays unset, so the next request retries instead of waiting on a restart.redis_fail_openandfail_securekeep their existing guard-core meaning throughout; no new policy was introduced. - Compatibility (GHSA-634g-4wr8-xwxv) - A request with no client address is now rejected by default (
fail_secure=True) instead of running the security pipeline with an "unknown" identity; this is guard-core 3.15.0's own fix, this adapter's test suite is brought in line with it here.test_passthrough_request_no_client_hostnow expects403instead of200. - Fixed (tests) -
test_rate_limiter_redis_errorsasserted that a Redis error during rate limiting always falls back to the in-memory window. guard-core 3.15.0'scheck_rate_limitnow honorsredis_fail_openlike every other check: with the defaultredis_fail_open=Falseit raisesGuardRedisErrorinstead of returningNonesilently, and only falls back in-memory onceredis_fail_open=Trueis set. The test now asserts the raise on the default and the fallback only after opting in. - Internal - The no-client-host 403 above is returned before
call_nextis ever reached, so thewrapped_call_nextclosure in__call__lost the only test path that invoked it. It is not dead:handle_security_bypassstill calls it when a route bypasses all checks (@security.bypass(["all"])) outsidepassive_mode. Coverage is restored with a test that exercises that route instead of removing the closure. Splitting the lazy-init check into_ensure_handlers_initialized()and the CORS-preflight block into_handle_preflight()also keeps__call__at cyclomatic complexity rank B. - Compatibility -
guard-coremoves from an unconstrained dependency toguard-core>=3.15.0inpyproject.toml; no upper bound, the floor moves with each lockstep release. No public API of this adapter changed.
v4.2.0
guard-core 3.13.0 compatibility: call_next bypass contract, bounded body readers, behavior-scan lockstep.
- Fixed: guard-core 3.12.0 (30f43944) made BypassHandler.handle_passthrough and handle_security_bypass require a call_next callable, so calls without it raised TypeError on every request and cascaded to 500. DjangoAPIGuard.call now builds a wrapped_call_next closure over self.get_response(request) and passes it to both bypass methods. Django can supply a real downstream callable because its middleware exposes get_response.
- Added: DjangoGuardRequest/DjangoGuardResponse.read_body_prefix(max_bytes) implement guard-core 3.12.0 SyncBoundedBodyReader/SyncBoundedResponseBodyReader protocols (StreamingHttpResponse returns b"").
- Fixed (tests): behavior_scan_response_body fixture, detection-cache seam patch (utils.detect_penetration_attempt), VALID_CLOUD_PROVIDERS expansion, honeypot read_body_prefix, --cov-branch enforced.
- Compatibility: requires guard-core 3.13.0. No public API change.
See CHANGELOG.md for full details.
v4.1.0
Unresolvable paths are reported to guard-core (v4.1.0)
- Added —
_populate_guard_state()swallowedResolver404and leftrequest.stateuntouched, which is indistinguishable from a view that simply carries no decorators. guard-core reads both as "nothing to enforce", so a resolution failure silently skips every per-route check — the fail-open half of GHSA-f2vm-w8gq-h378, reported against the Starlette adapter. Django resolves URLs itself, so this adapter has no equivalent matching bug, but it now reports the failure by settingrequest.state.guard_route_unresolved. With guard-core >= 3.7.0 andSecurityConfig.route_resolution_strict=True, those requests are logged, emit aroute_unresolvedevent, and are blocked with500. Behaviour is unchanged by default and on older guard-core, which ignores the attribute. - Fixed (tests) — The IPv6 and mixed IPv4/IPv6 allow-list tests still encoded pre-3.2.0 guard-core semantics, where the blacklist was evaluated before the whitelist. Since guard-core 3.2.0 an explicit whitelist match overrides the blacklist, so an address inside both a whitelist CIDR and a blacklist entry is now served rather than blocked. CI floats guard-core to the latest release and had not run here since 2026-05-28, so these only surfaced now; the adapter itself was correct throughout.
- Fixed (CI) — The greetings workflow passed
issue-message/pr-messagetoactions/first-interaction@v3, which expectsissue_message/pr_message, so the job failed on every first-time issue and pull request.
What's Changed
- Bump actions/checkout from 6 to 7 by @dependabot[bot] in #13
- Bump actions/setup-python from 6 to 7 by @dependabot[bot] in #14
- Bump actions/labeler from 6 to 7 by @dependabot[bot] in #15
- feat(middleware): report an unresolvable path to guard-core (v4.1.0) by @rennf93 in #16
Full Changelog: 4.0.1...4.1.0
4.0.1
4.0.0
v4.0.0 (2026-04-29)
Fail-secure by default (upstream), agent-stats surface, version reporting (v4.0.0)
- Breaking (upstream) —
SecurityConfig.fail_securenow defaults toTrue(inherited fromguard-core >= 3.0.0). When any security check raises an unhandled exception, the request is now blocked with HTTP 500 instead of logging and falling through. Bugs in checks that previously slipped past as silent fail-open responses now surface immediately. Restore the old behavior on deployments that depend on it viaSecurityConfig(fail_secure=False). Recommended migration: keep the new default, surface any check exceptions in your monitoring, and fix them — the previous default could mask serious bugs. The djapi-guard major bump tracks this upstream change so deployments see a clear signal. - Added —
DjangoAPIGuard.agent_statsread-only@propertyreturning the agent's telemetry buffer state. Returns{"enabled": False}when no agent is wired; otherwise returns{"enabled": True, **agent_handler.get_stats()}exposingevents_dropped,metrics_dropped,circuit_breaker_state, and other agent counters. No caching — fresh on each call. Lets app teams build health endpoints that surface agent-side drops and circuit-breaker trips without scraping the agent directly. - Added —
from djangoapi_guard import __version__— package version is now exported viaimportlib.metadata.version("djapi-guard")with a"0.0.0+unknown"fallback if the package is not installed (development from source). Pairs withguard-core >= 3.0.0'sSecurityConfig.agent_guard_versionso application code can wire the djapi-guard version through to the agent for SaaS-side telemetry attribution:SecurityConfig(agent_guard_version=__version__). - Compatibility —
DjangoAPIGuard.agent_statsis purely additive; no existing API was changed.__version__was previously absent; reading it before this release returnedNonevia missing-attribute fallback in some integrations.
What's Changed
New Contributors
- @dependabot[bot] made their first contribution in #9
Full Changelog: 3.0.0...4.0.0
3.0.0
Hardening release routing CORS through the security pipeline.
Companion to guard-core 2.2.0 and guard-agent 2.3.0. Closes the audit's preflight-bypass finding for the Django adapter.
Breaking change — preflight OPTIONS requests are now subject to the security pipeline. Migrate by ensuring you're on guard-core>=2.2.0 (no other code changes required).
Pipeline / CORS
- The middleware no longer short-circuits
OPTIONSpreflights ahead of the security pipeline. Banned IPs, rate-limited clients, and suspicious patterns now apply to preflight just like every other request. - CORS preflight construction and response-header injection moved to the shared
guard_core.sync.handlers.cors_handler.CorsHandlermodule — single source of truth across all adapters. - Blocked responses now carry CORS headers when the origin is allowed, so cross-origin clients see the real status code (403, 429, etc.) rather than an opaque CORS error.
_handle_preflightremoved; replaced by_build_preflight_responseand_attach_cors_headershelpers that delegate toCorsHandler.
Internal — defects surfaced by removing suppressions
The pre-existing [[tool.mypy.overrides]] ignore_missing_imports = true and follow_imports = "skip" blocks for guard_core.* and friends had been hiding real defects:
middleware.pywas importingBaseSecurityDecoratorandRouteConfigfromguard_core.decorators.base(the async path) when it should have been usingguard_core.sync.decorators.base. Sync/async protocol mismatches cascaded from there.cloud_handler.refresh(ttl=...)was being called with attlkwarg that the method does not accept; correct call isrefresh_async(). Also the call was missingawait.GuardResponsewas being assigned to aDjangoGuardResponse-typed slot; bridged withcast.- 4 tests passed
agent_model="..."toSecurityConfig— that field does not exist on the model.
All fixed at root.
Suppression cleanup
- Removed all six
[[tool.mypy.overrides]]suppression blocks. Installeddjango-stubsas dev dependency.redis7.x shipspy.typednatively;guard-core2.2.0 shipspy.typed;guard-agent2.2.0 shipspy.typed. Examples excluded via[tool.mypy] excluderather than per-module overrides. - Removed 8
# type: ignoremarkers from edited test files. Replaced with propercast(...),MagicMock(spec=...), or properly typed callback functions. - Stripped
[tool.uv.sources] guard-core = { path = ..., editable = true }from committed pyproject.toml. Useuv pip install -e ../guard-coreafteruv syncfor local-dev override.
Test coverage
- 258 tests, 100% line + 100% branch coverage across
djangoapi_guard/(397 statements, 106 branches, 0 missed). Zero warnings under-W error.
Compatibility
- Python 3.10–3.14.
- Requires
guard-core>=2.2.0(declared as unconstrainedguard-corein pyproject.toml; documented here for upgrade guidance). - guard-agent integration via
enable_agent=Truecontinues to work;guard-agentis a dev dependency and an optional runtime extra.
Migration
Before:
class GuardConfig(SecurityConfig):
pass
# Adapter handled OPTIONS preflight via internal short-circuitAfter:
config = SecurityConfig(
enable_cors=True,
cors_allow_origins=["https://app.example.com"],
cors_allow_methods=["GET", "POST"],
cors_allow_headers=["X-Custom"],
cors_allow_credentials=True,
cors_max_age=600,
cors_expose_headers=["X-Trace"],
)The cors_* fields drive everything; preflight is now subject to all security checks; CORS headers are emitted on success, on preflight, and on blocked responses for legitimate origins.
What's Changed
Full Changelog: 2.2.0...3.0.0