10xgraph-api 0.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 10xgraph_api-0.7.0.dist-info/METADATA +618 -0
- 10xgraph_api-0.7.0.dist-info/RECORD +203 -0
- 10xgraph_api-0.7.0.dist-info/WHEEL +5 -0
- 10xgraph_api-0.7.0.dist-info/entry_points.txt +3 -0
- 10xgraph_api-0.7.0.dist-info/licenses/LICENSE +21 -0
- 10xgraph_api-0.7.0.dist-info/top_level.txt +2 -0
- agentflow_cli/__init__.py +69 -0
- tenxgraph_api/__init__.py +23 -0
- tenxgraph_api/cli/__init__.py +6 -0
- tenxgraph_api/cli/capabilities.py +127 -0
- tenxgraph_api/cli/commands/__init__.py +49 -0
- tenxgraph_api/cli/commands/api.py +262 -0
- tenxgraph_api/cli/commands/audit.py +213 -0
- tenxgraph_api/cli/commands/build.py +305 -0
- tenxgraph_api/cli/commands/config.py +71 -0
- tenxgraph_api/cli/commands/demo.py +93 -0
- tenxgraph_api/cli/commands/eval.py +909 -0
- tenxgraph_api/cli/commands/init.py +531 -0
- tenxgraph_api/cli/commands/skills.py +532 -0
- tenxgraph_api/cli/commands/test.py +113 -0
- tenxgraph_api/cli/commands/version.py +47 -0
- tenxgraph_api/cli/config_editor/__init__.py +15 -0
- tenxgraph_api/cli/config_editor/schema.py +566 -0
- tenxgraph_api/cli/config_editor/server.py +240 -0
- tenxgraph_api/cli/config_editor/static/app.css +2 -0
- tenxgraph_api/cli/config_editor/static/app.js +3 -0
- tenxgraph_api/cli/config_editor/static/favicon.svg +11 -0
- tenxgraph_api/cli/config_editor/static/index.html +20 -0
- tenxgraph_api/cli/config_editor/store.py +120 -0
- tenxgraph_api/cli/config_editor/validation.py +377 -0
- tenxgraph_api/cli/constants.py +112 -0
- tenxgraph_api/cli/context.py +46 -0
- tenxgraph_api/cli/core/__init__.py +0 -0
- tenxgraph_api/cli/core/animations.py +550 -0
- tenxgraph_api/cli/core/config.py +279 -0
- tenxgraph_api/cli/core/output.py +625 -0
- tenxgraph_api/cli/core/prompts.py +220 -0
- tenxgraph_api/cli/core/screen.py +287 -0
- tenxgraph_api/cli/core/steps.py +621 -0
- tenxgraph_api/cli/core/theme.py +242 -0
- tenxgraph_api/cli/core/validation.py +251 -0
- tenxgraph_api/cli/exceptions.py +129 -0
- tenxgraph_api/cli/logger.py +84 -0
- tenxgraph_api/cli/main.py +936 -0
- tenxgraph_api/cli/templates/__init__.py +0 -0
- tenxgraph_api/cli/templates/defaults.py +844 -0
- tenxgraph_api/cli/templates/dev/.env.example +1 -0
- tenxgraph_api/cli/templates/dev/10xgraph.json +16 -0
- tenxgraph_api/cli/templates/dev/graph/__init__.py +0 -0
- tenxgraph_api/cli/templates/dev/graph/agent.py +63 -0
- tenxgraph_api/cli/templates/prod/.env.example +75 -0
- tenxgraph_api/cli/templates/prod/.pre-commit-config.yaml +50 -0
- tenxgraph_api/cli/templates/prod/.python-version +1 -0
- tenxgraph_api/cli/templates/prod/10xgraph.json +42 -0
- tenxgraph_api/cli/templates/prod/auth/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/auth/agent_auth.py +46 -0
- tenxgraph_api/cli/templates/prod/evals/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/evals/user_simulator_eval.py +76 -0
- tenxgraph_api/cli/templates/prod/evals/weather_agents_eval.py +53 -0
- tenxgraph_api/cli/templates/prod/graph/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/graph/agent.py +50 -0
- tenxgraph_api/cli/templates/prod/graph/state.py +5 -0
- tenxgraph_api/cli/templates/prod/graph/thread_name_generator.py +7 -0
- tenxgraph_api/cli/templates/prod/graph/tools/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/graph/tools/weather_tool.py +43 -0
- tenxgraph_api/cli/templates/prod/graph/validators/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/graph/validators/lifecyle.py +107 -0
- tenxgraph_api/cli/templates/prod/graph/validators/manager.py +9 -0
- tenxgraph_api/cli/templates/prod/graph/validators/validators.py +21 -0
- tenxgraph_api/cli/templates/prod/pyproject.toml +79 -0
- tenxgraph_api/cli/templates/prod/tests/__init__.py +0 -0
- tenxgraph_api/cli/templates/prod/tests/conftest.py +6 -0
- tenxgraph_api/cli/templates/prod/tests/test_agent_eval.py +78 -0
- tenxgraph_api/cli/templates/prod/tests/test_catalog_tools.py +47 -0
- tenxgraph_api/cli/templates/prod/tests/test_graph_nodes.py +57 -0
- tenxgraph_api/cli/templates/skills/10xgraph/SKILL.md +92 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/agents-and-tools.md +389 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/api-client.md +61 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/api-configuration.md +75 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/api-settings-and-middleware.md +89 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/architecture.md +91 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/auth-and-authorization.md +149 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/callbacks-and-command.md +283 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/checkpointing-and-threads.md +63 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/cli-commands.md +73 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/client-auth-and-errors.md +94 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/client-messages-invoke-stream.md +115 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/client-threads-memory-files.md +80 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/context-id-background.md +80 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/dependency-injection.md +58 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/evaluation.md +500 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/id-and-thread-name-generators.md +69 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/media-and-files.md +66 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/memory-and-store.md +70 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/prebuilt-agents-and-tools.md +394 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/production-runtime.md +56 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/providers-and-adapters.md +89 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/publishers-and-runtime-protocols.md +84 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/rate-limiting.md +194 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/realtime.md +172 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/remote-tools.md +55 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/rest-api-and-errors.md +97 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/security-and-validators.md +86 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/state-and-messages.md +68 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/state-graph.md +232 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/stream-emitter.md +144 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/streaming.md +53 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/testing-and-evaluation.md +64 -0
- tenxgraph_api/cli/templates/skills/10xgraph/references/unit-testing.md +202 -0
- tenxgraph_api/cli/templates/skills/copilot/10xgraph.instructions.md +108 -0
- tenxgraph_api/py.typed +0 -0
- tenxgraph_api/src/__init__.py +0 -0
- tenxgraph_api/src/app/__init__.py +13 -0
- tenxgraph_api/src/app/core/__init__.py +15 -0
- tenxgraph_api/src/app/core/auth/__init__.py +0 -0
- tenxgraph_api/src/app/core/auth/auth_backend.py +38 -0
- tenxgraph_api/src/app/core/auth/authorization.py +379 -0
- tenxgraph_api/src/app/core/auth/base_auth.py +27 -0
- tenxgraph_api/src/app/core/auth/jwt_auth.py +122 -0
- tenxgraph_api/src/app/core/auth/ownership_resolver.py +170 -0
- tenxgraph_api/src/app/core/auth/permissions.py +354 -0
- tenxgraph_api/src/app/core/auth/request_config.py +130 -0
- tenxgraph_api/src/app/core/auth/route_guard.py +105 -0
- tenxgraph_api/src/app/core/config/__init__.py +0 -0
- tenxgraph_api/src/app/core/config/graph_config.py +532 -0
- tenxgraph_api/src/app/core/config/media_settings.py +72 -0
- tenxgraph_api/src/app/core/config/sentry_config.py +60 -0
- tenxgraph_api/src/app/core/config/settings.py +202 -0
- tenxgraph_api/src/app/core/config/setup_logs.py +65 -0
- tenxgraph_api/src/app/core/config/setup_middleware.py +483 -0
- tenxgraph_api/src/app/core/config/worker_middleware.py +99 -0
- tenxgraph_api/src/app/core/exceptions/__init__.py +17 -0
- tenxgraph_api/src/app/core/exceptions/general_exception.py +58 -0
- tenxgraph_api/src/app/core/exceptions/handle_errors.py +363 -0
- tenxgraph_api/src/app/core/exceptions/resources_exceptions.py +72 -0
- tenxgraph_api/src/app/core/exceptions/user_exception.py +58 -0
- tenxgraph_api/src/app/core/middleware/__init__.py +25 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/__init__.py +15 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/base.py +27 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/factory.py +98 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/keying.py +133 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/memory.py +98 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/middleware.py +90 -0
- tenxgraph_api/src/app/core/middleware/rate_limit/redis.py +115 -0
- tenxgraph_api/src/app/core/middleware/request_limits.py +154 -0
- tenxgraph_api/src/app/core/middleware/security_headers.py +239 -0
- tenxgraph_api/src/app/core/utils/__init__.py +14 -0
- tenxgraph_api/src/app/core/utils/log_sanitizer.py +216 -0
- tenxgraph_api/src/app/loader.py +516 -0
- tenxgraph_api/src/app/main.py +180 -0
- tenxgraph_api/src/app/routers/__init__.py +4 -0
- tenxgraph_api/src/app/routers/ag_ui/__init__.py +10 -0
- tenxgraph_api/src/app/routers/ag_ui/converter.py +195 -0
- tenxgraph_api/src/app/routers/ag_ui/event_mapper.py +353 -0
- tenxgraph_api/src/app/routers/ag_ui/router.py +90 -0
- tenxgraph_api/src/app/routers/ag_ui/service.py +193 -0
- tenxgraph_api/src/app/routers/checkpointer/__init__.py +1 -0
- tenxgraph_api/src/app/routers/checkpointer/router.py +482 -0
- tenxgraph_api/src/app/routers/checkpointer/schemas/__init__.py +1 -0
- tenxgraph_api/src/app/routers/checkpointer/schemas/checkpointer_schemas.py +121 -0
- tenxgraph_api/src/app/routers/checkpointer/services/__init__.py +1 -0
- tenxgraph_api/src/app/routers/checkpointer/services/checkpointer_service.py +300 -0
- tenxgraph_api/src/app/routers/evals/__init__.py +6 -0
- tenxgraph_api/src/app/routers/evals/router.py +38 -0
- tenxgraph_api/src/app/routers/evals/services/__init__.py +1 -0
- tenxgraph_api/src/app/routers/evals/services/eval_report_service.py +354 -0
- tenxgraph_api/src/app/routers/graph/__init__.py +6 -0
- tenxgraph_api/src/app/routers/graph/realtime_guard.py +161 -0
- tenxgraph_api/src/app/routers/graph/router.py +821 -0
- tenxgraph_api/src/app/routers/graph/schemas/__init__.py +1 -0
- tenxgraph_api/src/app/routers/graph/schemas/graph_schemas.py +386 -0
- tenxgraph_api/src/app/routers/graph/services/__init__.py +1 -0
- tenxgraph_api/src/app/routers/graph/services/graph_service.py +1219 -0
- tenxgraph_api/src/app/routers/graph/services/multimodal_preprocessor.py +164 -0
- tenxgraph_api/src/app/routers/media/__init__.py +506 -0
- tenxgraph_api/src/app/routers/media/router.py +242 -0
- tenxgraph_api/src/app/routers/media/schemas.py +39 -0
- tenxgraph_api/src/app/routers/ping/__init__.py +4 -0
- tenxgraph_api/src/app/routers/ping/router.py +28 -0
- tenxgraph_api/src/app/routers/setup_router.py +60 -0
- tenxgraph_api/src/app/routers/store/__init__.py +4 -0
- tenxgraph_api/src/app/routers/store/router.py +200 -0
- tenxgraph_api/src/app/routers/store/schemas/__init__.py +28 -0
- tenxgraph_api/src/app/routers/store/schemas/store_schemas.py +200 -0
- tenxgraph_api/src/app/routers/store/services/__init__.py +4 -0
- tenxgraph_api/src/app/routers/store/services/store_service.py +203 -0
- tenxgraph_api/src/app/tasks/__init__.py +0 -0
- tenxgraph_api/src/app/tasks/user_tasks.py +38 -0
- tenxgraph_api/src/app/utils/__init__.py +12 -0
- tenxgraph_api/src/app/utils/callable_helper.py +26 -0
- tenxgraph_api/src/app/utils/media/__init__.py +15 -0
- tenxgraph_api/src/app/utils/media/extractor.py +172 -0
- tenxgraph_api/src/app/utils/media/pipeline.py +83 -0
- tenxgraph_api/src/app/utils/parse_output.py +17 -0
- tenxgraph_api/src/app/utils/response_helper.py +129 -0
- tenxgraph_api/src/app/utils/schemas/__init__.py +11 -0
- tenxgraph_api/src/app/utils/schemas/output_schemas.py +28 -0
- tenxgraph_api/src/app/utils/schemas/user_schemas.py +13 -0
- tenxgraph_api/src/app/utils/snowflake_id_generator.py +90 -0
- tenxgraph_api/src/app/utils/swagger_helper.py +160 -0
- tenxgraph_api/src/app/utils/telemetry_store.py +123 -0
- tenxgraph_api/src/app/utils/thread_name_generator.py +304 -0
- tenxgraph_api/src/app/worker.py +91 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Security Headers Middleware
|
|
3
|
+
|
|
4
|
+
This middleware adds standard security headers to HTTP responses to protect
|
|
5
|
+
against common web vulnerabilities.
|
|
6
|
+
|
|
7
|
+
Headers Added:
|
|
8
|
+
- X-Content-Type-Options: Prevents MIME-type sniffing
|
|
9
|
+
- X-Frame-Options: Prevents clickjacking attacks
|
|
10
|
+
- X-XSS-Protection: Enables XSS filtering (legacy browsers)
|
|
11
|
+
- Strict-Transport-Security: Enforces HTTPS (if HTTPS is detected)
|
|
12
|
+
- Content-Security-Policy: Controls resource loading
|
|
13
|
+
- Referrer-Policy: Controls referrer information
|
|
14
|
+
- Permissions-Policy: Controls browser features
|
|
15
|
+
|
|
16
|
+
Configuration:
|
|
17
|
+
Configure via environment variables or settings:
|
|
18
|
+
- SECURITY_HEADERS_ENABLED: Enable/disable middleware (default: true)
|
|
19
|
+
- HSTS_MAX_AGE: HSTS max-age in seconds (default: 31536000 = 1 year)
|
|
20
|
+
- CSP_POLICY: Custom CSP policy (default: strict policy)
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from collections.abc import Callable
|
|
24
|
+
|
|
25
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
26
|
+
from starlette.requests import Request
|
|
27
|
+
from starlette.responses import Response
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class SecurityHeadersMiddleware(BaseHTTPMiddleware):
|
|
31
|
+
"""
|
|
32
|
+
Middleware to add security headers to responses.
|
|
33
|
+
|
|
34
|
+
This middleware enhances security by adding standard security headers
|
|
35
|
+
that protect against common web vulnerabilities.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__( # noqa: PLR0913
|
|
39
|
+
self,
|
|
40
|
+
app,
|
|
41
|
+
enable_hsts: bool = True,
|
|
42
|
+
hsts_max_age: int = 31536000, # 1 year
|
|
43
|
+
hsts_include_subdomains: bool = True,
|
|
44
|
+
hsts_preload: bool = False,
|
|
45
|
+
frame_options: str = "DENY",
|
|
46
|
+
content_type_options: str = "nosniff",
|
|
47
|
+
xss_protection: str = "1; mode=block",
|
|
48
|
+
referrer_policy: str = "strict-origin-when-cross-origin",
|
|
49
|
+
permissions_policy: str | None = None,
|
|
50
|
+
csp_policy: str | None = None,
|
|
51
|
+
):
|
|
52
|
+
"""
|
|
53
|
+
Initialize security headers middleware.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
app: ASGI application
|
|
57
|
+
enable_hsts: Enable Strict-Transport-Security header
|
|
58
|
+
hsts_max_age: HSTS max-age in seconds (default: 1 year)
|
|
59
|
+
hsts_include_subdomains: Include subdomains in HSTS
|
|
60
|
+
hsts_preload: Enable HSTS preload
|
|
61
|
+
frame_options: X-Frame-Options value (DENY, SAMEORIGIN, or ALLOW-FROM)
|
|
62
|
+
content_type_options: X-Content-Type-Options value
|
|
63
|
+
xss_protection: X-XSS-Protection value
|
|
64
|
+
referrer_policy: Referrer-Policy value
|
|
65
|
+
permissions_policy: Permissions-Policy value (optional)
|
|
66
|
+
csp_policy: Content-Security-Policy value (optional)
|
|
67
|
+
"""
|
|
68
|
+
super().__init__(app)
|
|
69
|
+
self.enable_hsts = enable_hsts
|
|
70
|
+
self.hsts_max_age = hsts_max_age
|
|
71
|
+
self.hsts_include_subdomains = hsts_include_subdomains
|
|
72
|
+
self.hsts_preload = hsts_preload
|
|
73
|
+
self.frame_options = frame_options
|
|
74
|
+
self.content_type_options = content_type_options
|
|
75
|
+
self.xss_protection = xss_protection
|
|
76
|
+
self.referrer_policy = referrer_policy
|
|
77
|
+
self.permissions_policy = permissions_policy or self._default_permissions_policy()
|
|
78
|
+
self.csp_policy = csp_policy or self._default_csp_policy()
|
|
79
|
+
|
|
80
|
+
def _default_permissions_policy(self) -> str:
|
|
81
|
+
"""
|
|
82
|
+
Get default Permissions-Policy header value.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
Default permissions policy string
|
|
86
|
+
"""
|
|
87
|
+
return (
|
|
88
|
+
"geolocation=(), microphone=(), camera=(), payment=(), usb=(), "
|
|
89
|
+
"magnetometer=(), gyroscope=(), accelerometer=()"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def _default_csp_policy(self) -> str:
|
|
93
|
+
"""
|
|
94
|
+
Get default Content-Security-Policy header value.
|
|
95
|
+
|
|
96
|
+
Returns:
|
|
97
|
+
Default CSP policy string
|
|
98
|
+
"""
|
|
99
|
+
return (
|
|
100
|
+
"default-src 'self'; "
|
|
101
|
+
"script-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://unpkg.com; "
|
|
102
|
+
"style-src 'self' 'unsafe-inline' https://cdn.jsdelivr.net https://fonts.googleapis.com;"
|
|
103
|
+
"img-src 'self' data: https:; "
|
|
104
|
+
"font-src 'self' data: https://fonts.gstatic.com; "
|
|
105
|
+
"connect-src 'self'; "
|
|
106
|
+
"frame-ancestors 'none'; "
|
|
107
|
+
"base-uri 'self'; "
|
|
108
|
+
"form-action 'self'"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
def _build_hsts_header(self) -> str:
|
|
112
|
+
"""
|
|
113
|
+
Build HSTS header value.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
HSTS header string
|
|
117
|
+
"""
|
|
118
|
+
hsts_parts = [f"max-age={self.hsts_max_age}"]
|
|
119
|
+
|
|
120
|
+
if self.hsts_include_subdomains:
|
|
121
|
+
hsts_parts.append("includeSubDomains")
|
|
122
|
+
|
|
123
|
+
if self.hsts_preload:
|
|
124
|
+
hsts_parts.append("preload")
|
|
125
|
+
|
|
126
|
+
return "; ".join(hsts_parts)
|
|
127
|
+
|
|
128
|
+
def _is_https(self, request: Request) -> bool:
|
|
129
|
+
"""
|
|
130
|
+
Check if request is using HTTPS.
|
|
131
|
+
|
|
132
|
+
Checks both the request scheme and X-Forwarded-Proto header
|
|
133
|
+
(for proxied requests).
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
request: Starlette request object
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
True if HTTPS, False otherwise
|
|
140
|
+
"""
|
|
141
|
+
# Check request scheme
|
|
142
|
+
if request.url.scheme == "https":
|
|
143
|
+
return True
|
|
144
|
+
|
|
145
|
+
# Check X-Forwarded-Proto header (for proxied requests)
|
|
146
|
+
forwarded_proto = request.headers.get("X-Forwarded-Proto", "")
|
|
147
|
+
return forwarded_proto.lower() == "https"
|
|
148
|
+
|
|
149
|
+
async def dispatch(self, request: Request, call_next: Callable) -> Response:
|
|
150
|
+
"""
|
|
151
|
+
Process request and add security headers to response.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
request: Incoming request
|
|
155
|
+
call_next: Next middleware/handler
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Response with security headers
|
|
159
|
+
"""
|
|
160
|
+
# Process request
|
|
161
|
+
response = await call_next(request)
|
|
162
|
+
|
|
163
|
+
# Add X-Content-Type-Options header
|
|
164
|
+
response.headers["X-Content-Type-Options"] = self.content_type_options
|
|
165
|
+
|
|
166
|
+
# Add X-Frame-Options header
|
|
167
|
+
response.headers["X-Frame-Options"] = self.frame_options
|
|
168
|
+
|
|
169
|
+
# Add X-XSS-Protection header (for legacy browser support)
|
|
170
|
+
response.headers["X-XSS-Protection"] = self.xss_protection
|
|
171
|
+
|
|
172
|
+
# Add Referrer-Policy header
|
|
173
|
+
response.headers["Referrer-Policy"] = self.referrer_policy
|
|
174
|
+
|
|
175
|
+
# Add Permissions-Policy header
|
|
176
|
+
if self.permissions_policy:
|
|
177
|
+
response.headers["Permissions-Policy"] = self.permissions_policy
|
|
178
|
+
|
|
179
|
+
# Add Content-Security-Policy header
|
|
180
|
+
if self.csp_policy:
|
|
181
|
+
response.headers["Content-Security-Policy"] = self.csp_policy
|
|
182
|
+
|
|
183
|
+
# Add Strict-Transport-Security header (only for HTTPS)
|
|
184
|
+
if self.enable_hsts and self._is_https(request):
|
|
185
|
+
response.headers["Strict-Transport-Security"] = self._build_hsts_header()
|
|
186
|
+
|
|
187
|
+
return response
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def create_security_headers_middleware(
|
|
191
|
+
enable_hsts: bool = True,
|
|
192
|
+
hsts_max_age: int = 31536000,
|
|
193
|
+
hsts_include_subdomains: bool = True,
|
|
194
|
+
hsts_preload: bool = False,
|
|
195
|
+
frame_options: str = "DENY",
|
|
196
|
+
content_type_options: str = "nosniff",
|
|
197
|
+
xss_protection: str = "1; mode=block",
|
|
198
|
+
referrer_policy: str = "strict-origin-when-cross-origin",
|
|
199
|
+
permissions_policy: str | None = None,
|
|
200
|
+
csp_policy: str | None = None,
|
|
201
|
+
) -> type[SecurityHeadersMiddleware]:
|
|
202
|
+
"""
|
|
203
|
+
Factory function to create SecurityHeadersMiddleware with configuration.
|
|
204
|
+
|
|
205
|
+
This is a convenience function for creating middleware with custom settings.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
enable_hsts: Enable HSTS header
|
|
209
|
+
hsts_max_age: HSTS max-age in seconds
|
|
210
|
+
hsts_include_subdomains: Include subdomains in HSTS
|
|
211
|
+
hsts_preload: Enable HSTS preload
|
|
212
|
+
frame_options: X-Frame-Options value
|
|
213
|
+
content_type_options: X-Content-Type-Options value
|
|
214
|
+
xss_protection: X-XSS-Protection value
|
|
215
|
+
referrer_policy: Referrer-Policy value
|
|
216
|
+
permissions_policy: Permissions-Policy value
|
|
217
|
+
csp_policy: Content-Security-Policy value
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
Configured SecurityHeadersMiddleware class
|
|
221
|
+
"""
|
|
222
|
+
|
|
223
|
+
class ConfiguredSecurityHeadersMiddleware(SecurityHeadersMiddleware):
|
|
224
|
+
def __init__(self, app):
|
|
225
|
+
super().__init__(
|
|
226
|
+
app,
|
|
227
|
+
enable_hsts=enable_hsts,
|
|
228
|
+
hsts_max_age=hsts_max_age,
|
|
229
|
+
hsts_include_subdomains=hsts_include_subdomains,
|
|
230
|
+
hsts_preload=hsts_preload,
|
|
231
|
+
frame_options=frame_options,
|
|
232
|
+
content_type_options=content_type_options,
|
|
233
|
+
xss_protection=xss_protection,
|
|
234
|
+
referrer_policy=referrer_policy,
|
|
235
|
+
permissions_policy=permissions_policy,
|
|
236
|
+
csp_policy=csp_policy,
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
return ConfiguredSecurityHeadersMiddleware
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Core utilities for 10xGraph CLI."""
|
|
2
|
+
|
|
3
|
+
from tenxgraph_api.src.app.core.utils.log_sanitizer import (
|
|
4
|
+
SanitizingFormatter,
|
|
5
|
+
sanitize_for_logging,
|
|
6
|
+
sanitize_log_message,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"sanitize_for_logging",
|
|
12
|
+
"sanitize_log_message",
|
|
13
|
+
"SanitizingFormatter",
|
|
14
|
+
]
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Log sanitization utilities for 10xGraph CLI.
|
|
3
|
+
|
|
4
|
+
This module provides utilities to sanitize sensitive data before logging,
|
|
5
|
+
preventing tokens, passwords, and other credentials from appearing in logs.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import re
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# Patterns for detecting sensitive field names
|
|
14
|
+
SENSITIVE_PATTERNS = {
|
|
15
|
+
"token",
|
|
16
|
+
"password",
|
|
17
|
+
"secret",
|
|
18
|
+
"key",
|
|
19
|
+
"credential",
|
|
20
|
+
"authorization",
|
|
21
|
+
"api_key",
|
|
22
|
+
"access_token",
|
|
23
|
+
"refresh_token",
|
|
24
|
+
"auth",
|
|
25
|
+
"bearer",
|
|
26
|
+
"jwt",
|
|
27
|
+
"session",
|
|
28
|
+
"cookie",
|
|
29
|
+
"private_key",
|
|
30
|
+
"passphrase",
|
|
31
|
+
"pin",
|
|
32
|
+
"ssn",
|
|
33
|
+
"credit_card",
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# Regex pattern to detect JWT tokens (three base64url parts separated by dots)
|
|
37
|
+
JWT_PATTERN = re.compile(r"^[A-Za-z0-9-_=]+\.[A-Za-z0-9-_=]+\.?[A-Za-z0-9-_.+/=]*$")
|
|
38
|
+
|
|
39
|
+
# Regex pattern to detect bearer tokens
|
|
40
|
+
BEARER_PATTERN = re.compile(r"^Bearer\s+[A-Za-z0-9\-_.~+/]+=*$", re.IGNORECASE)
|
|
41
|
+
|
|
42
|
+
# Tokens embedded anywhere in a message (f-string logs carry no args to sanitize).
|
|
43
|
+
_INLINE_SECRET_PATTERNS = (
|
|
44
|
+
(re.compile(r"Bearer\s+[A-Za-z0-9\-_.~+/]+=*", re.IGNORECASE), "Bearer ***REDACTED***"),
|
|
45
|
+
(re.compile(r"\beyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]*"), "***JWT_TOKEN***"),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def redact_text(text: str) -> str:
|
|
50
|
+
"""Redact bearer tokens and JWTs that appear anywhere inside ``text``."""
|
|
51
|
+
for pattern, replacement in _INLINE_SECRET_PATTERNS:
|
|
52
|
+
text = pattern.sub(replacement, text)
|
|
53
|
+
return text
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def sanitize_for_logging(data: Any, max_depth: int = 10, _current_depth: int = 0) -> Any:
|
|
57
|
+
"""
|
|
58
|
+
Recursively sanitize sensitive data for safe logging.
|
|
59
|
+
|
|
60
|
+
This function walks through data structures and replaces sensitive values
|
|
61
|
+
with redaction markers. It detects:
|
|
62
|
+
- Dictionary keys containing sensitive patterns
|
|
63
|
+
- JWT tokens (by structure)
|
|
64
|
+
- Bearer tokens
|
|
65
|
+
- Authorization headers
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
data: The data to sanitize (dict, list, str, or any other type)
|
|
69
|
+
max_depth: Maximum recursion depth to prevent infinite loops
|
|
70
|
+
_current_depth: Internal parameter tracking current recursion depth
|
|
71
|
+
|
|
72
|
+
Returns:
|
|
73
|
+
Sanitized copy of the data with sensitive values redacted
|
|
74
|
+
|
|
75
|
+
Examples:
|
|
76
|
+
>>> sanitize_for_logging({"user_id": "123", "token": "abc123"})
|
|
77
|
+
{'user_id': '123', 'token': '***REDACTED***'}
|
|
78
|
+
|
|
79
|
+
>>> sanitize_for_logging({"Authorization": "Bearer eyJhbGc..."})
|
|
80
|
+
{'Authorization': '***REDACTED***'}
|
|
81
|
+
"""
|
|
82
|
+
# Prevent excessive recursion
|
|
83
|
+
if _current_depth >= max_depth:
|
|
84
|
+
return "***MAX_DEPTH_REACHED***"
|
|
85
|
+
|
|
86
|
+
if isinstance(data, dict):
|
|
87
|
+
return {k: _sanitize_value(k, v, max_depth, _current_depth + 1) for k, v in data.items()}
|
|
88
|
+
if isinstance(data, list):
|
|
89
|
+
return [sanitize_for_logging(item, max_depth, _current_depth + 1) for item in data]
|
|
90
|
+
if isinstance(data, tuple):
|
|
91
|
+
return tuple(sanitize_for_logging(item, max_depth, _current_depth + 1) for item in data)
|
|
92
|
+
if isinstance(data, str):
|
|
93
|
+
return _sanitize_string(data)
|
|
94
|
+
return data
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _sanitize_value(key: str, value: Any, max_depth: int, current_depth: int) -> Any:
|
|
98
|
+
"""
|
|
99
|
+
Sanitize a value based on its key name and content.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
key: The dictionary key
|
|
103
|
+
value: The value to sanitize
|
|
104
|
+
max_depth: Maximum recursion depth
|
|
105
|
+
current_depth: Current recursion depth
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Sanitized value
|
|
109
|
+
"""
|
|
110
|
+
# Check if key name contains sensitive patterns
|
|
111
|
+
key_lower = key.lower()
|
|
112
|
+
if any(pattern in key_lower for pattern in SENSITIVE_PATTERNS):
|
|
113
|
+
return "***REDACTED***"
|
|
114
|
+
|
|
115
|
+
# Recursively sanitize the value
|
|
116
|
+
return sanitize_for_logging(value, max_depth, current_depth)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _sanitize_string(value: str) -> str:
|
|
120
|
+
"""
|
|
121
|
+
Sanitize a string value if it appears to be sensitive.
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
value: String to check and potentially sanitize
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Original string or redaction marker
|
|
128
|
+
"""
|
|
129
|
+
max_value = 32
|
|
130
|
+
# Check for JWT token pattern
|
|
131
|
+
if len(value) > max_value and JWT_PATTERN.match(value):
|
|
132
|
+
return "***JWT_TOKEN***"
|
|
133
|
+
|
|
134
|
+
# Check for Bearer token pattern
|
|
135
|
+
if BEARER_PATTERN.match(value):
|
|
136
|
+
return "***BEARER_TOKEN***"
|
|
137
|
+
|
|
138
|
+
# Check if string looks like a long random token (>32 chars, alphanumeric)
|
|
139
|
+
if len(value) > max_value and value.replace("-", "").replace("_", "").isalnum():
|
|
140
|
+
# Could be an API key or token
|
|
141
|
+
return f"{value[:4]}...{value[-4:]}"
|
|
142
|
+
|
|
143
|
+
return value
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def sanitize_log_message(message: str, *args: Any, **kwargs: Any) -> tuple[str, tuple, dict]:
|
|
147
|
+
"""
|
|
148
|
+
Sanitize log message arguments.
|
|
149
|
+
|
|
150
|
+
This is useful for sanitizing arguments passed to logger.debug(), logger.info(), etc.
|
|
151
|
+
|
|
152
|
+
Args:
|
|
153
|
+
message: Log message format string
|
|
154
|
+
*args: Positional arguments for the log message
|
|
155
|
+
**kwargs: Keyword arguments for the log message
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Tuple of (message, sanitized_args, sanitized_kwargs)
|
|
159
|
+
|
|
160
|
+
Examples:
|
|
161
|
+
>>> msg, args, kwargs = sanitize_log_message(
|
|
162
|
+
... "User %s logged in", {"user_id": "123", "token": "secret"}
|
|
163
|
+
... )
|
|
164
|
+
>>> # args will have sanitized data
|
|
165
|
+
"""
|
|
166
|
+
sanitized_args = tuple(sanitize_for_logging(arg) for arg in args)
|
|
167
|
+
sanitized_kwargs = {k: sanitize_for_logging(v) for k, v in kwargs.items()}
|
|
168
|
+
return message, sanitized_args, sanitized_kwargs
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class SanitizingFormatter(logging.Formatter):
|
|
172
|
+
"""
|
|
173
|
+
A wrapper for log formatters that sanitizes sensitive data.
|
|
174
|
+
|
|
175
|
+
This can be used to wrap existing formatters to add sanitization. It is a
|
|
176
|
+
proper ``logging.Formatter`` subclass so it can be passed to
|
|
177
|
+
``Handler.setFormatter`` without type errors, while delegating the actual
|
|
178
|
+
formatting to the wrapped base formatter.
|
|
179
|
+
|
|
180
|
+
Example:
|
|
181
|
+
import logging
|
|
182
|
+
|
|
183
|
+
formatter = logging.Formatter('%(asctime)s - %(message)s')
|
|
184
|
+
sanitizing_formatter = SanitizingFormatter(formatter)
|
|
185
|
+
handler.setFormatter(sanitizing_formatter)
|
|
186
|
+
"""
|
|
187
|
+
|
|
188
|
+
def __init__(self, base_formatter: logging.Formatter) -> None:
|
|
189
|
+
"""
|
|
190
|
+
Initialize the sanitizing formatter.
|
|
191
|
+
|
|
192
|
+
Args:
|
|
193
|
+
base_formatter: The underlying formatter to wrap
|
|
194
|
+
"""
|
|
195
|
+
super().__init__()
|
|
196
|
+
self.base_formatter = base_formatter
|
|
197
|
+
|
|
198
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
199
|
+
"""
|
|
200
|
+
Format the log record with sanitization.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
record: LogRecord to format
|
|
204
|
+
|
|
205
|
+
Returns:
|
|
206
|
+
Formatted and sanitized log string
|
|
207
|
+
"""
|
|
208
|
+
# Sanitize the message arguments
|
|
209
|
+
if isinstance(record.args, tuple) and record.args:
|
|
210
|
+
record.args = tuple(sanitize_for_logging(arg) for arg in record.args)
|
|
211
|
+
elif isinstance(record.args, dict):
|
|
212
|
+
record.args = sanitize_for_logging(record.args)
|
|
213
|
+
|
|
214
|
+
# Format using the base formatter, then catch tokens already baked into the text
|
|
215
|
+
# (f-string messages, exception text, tracebacks).
|
|
216
|
+
return redact_text(self.base_formatter.format(record))
|