From 55b88d660bf79645334164c1fb87a0e4e10d024f Mon Sep 17 00:00:00 2001 From: Matt Hartigan Date: Tue, 24 Feb 2026 22:41:47 +0000 Subject: [PATCH] feat: add audit_security() method for security dependency auditing Adds `app.audit_security()` that returns a list of all routes with no SecurityBase dependency anywhere in their dependency tree. Recursively walks nested dependencies to detect security schemes at any depth. Detects all security types: OAuth2PasswordBearer, HTTPBearer, HTTPBasic, APIKeyHeader/Query/Cookie, OpenIdConnect, and Security() with scopes. Also detects router-level security dependencies. Use case: security reviews and compliance audits. There is currently no way to statically determine which FastAPI endpoints are unprotected without manually traversing the full dependency graph of every route. Example: unprotected = app.audit_security() for route in unprotected: print(f"UNPROTECTED: {route['methods']} {route['path']}") --- fastapi/applications.py | 59 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/fastapi/applications.py b/fastapi/applications.py index e7e816c2e9..377f0e2acf 100644 --- a/fastapi/applications.py +++ b/fastapi/applications.py @@ -1099,6 +1099,65 @@ class FastAPI(Starlette): ) return self.openapi_schema + def audit_security( + self, + ) -> list[dict[str, Any]]: + """ + Audit all routes for security dependencies. + + Returns a list of route info dicts for every route that has **no** + security dependency (i.e. no ``SecurityBase`` instance) anywhere in + its dependency tree. Useful for security reviews and compliance + audits. + + Each dict contains: + + * ``path`` – the route path (e.g. ``/users/{user_id}``) + * ``methods`` – the HTTP methods (e.g. ``{"GET"}``) + * ``name`` – the route/endpoint name + * ``endpoint`` – the endpoint callable + + Example:: + + unprotected = app.audit_security() + for route_info in unprotected: + print(f"UNPROTECTED: {route_info['methods']} {route_info['path']}") + """ + from fastapi.routing import APIRoute, APIWebSocketRoute + + def _has_security(dependant: Any) -> bool: + """Recursively check if a Dependant tree contains any SecurityBase.""" + if dependant._is_security_scheme: + return True + for sub in dependant.dependencies: + if _has_security(sub): + return True + return False + + unprotected: list[dict[str, Any]] = [] + for route in self.routes: + if isinstance(route, APIRoute): + if not _has_security(route.dependant): + unprotected.append( + { + "path": route.path, + "methods": route.methods, + "name": route.name, + "endpoint": route.endpoint, + } + ) + elif isinstance(route, APIWebSocketRoute): + if not _has_security(route.dependant): + unprotected.append( + { + "path": route.path, + "methods": {"WS"}, + "name": route.name, + "endpoint": route.endpoint, + } + ) + return unprotected + def setup(self) -> None: if self.openapi_url: