100 lines
3.9 KiB
Python
100 lines
3.9 KiB
Python
"""DLP scan dispatch and safe proposal rendering for egress requests."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import typing
|
|
|
|
from .types import Route, ScanResult
|
|
|
|
|
|
def build_outbound_scan_text(host: str, path: str, query: str,
|
|
headers: typing.Mapping[str, str], body: str) -> str:
|
|
parts = [host, path]
|
|
if query:
|
|
parts.append(query)
|
|
parts.extend(f"{name}: {value}" for name, value in headers.items())
|
|
if body:
|
|
parts.append(body)
|
|
return "\n".join(parts)
|
|
|
|
|
|
def outbound_scan_headers(route: Route, headers: typing.Mapping[str, str]) -> dict[str, str]:
|
|
"""Drop agent Authorization when the route injects gateway-owned auth."""
|
|
skip_auth = bool(route.auth_scheme and route.token_env)
|
|
return {name: value for name, value in headers.items()
|
|
if not (skip_auth and name.lower() == "authorization")}
|
|
|
|
|
|
def build_inbound_scan_text(headers: typing.Mapping[str, str], body: str) -> str:
|
|
parts = [f"{name}: {value}" for name, value in headers.items()]
|
|
if body:
|
|
parts.append(body)
|
|
return "\n".join(parts)
|
|
|
|
|
|
def _enabled(configured: tuple[str, ...] | None, name: str) -> bool:
|
|
return configured is None or name in configured
|
|
|
|
|
|
def scan_outbound(route: Route, body: str | bytes, environ: typing.Mapping[str, str], *,
|
|
safe_tokens: typing.AbstractSet[str] | None = None,
|
|
crlf_text: str | None = None) -> ScanResult | None:
|
|
if not route.inspect:
|
|
return None
|
|
try:
|
|
from dlp_detectors import ( # type: ignore[import-not-found]
|
|
scan_crlf_injection, scan_entropy, scan_known_secrets, scan_token_patterns)
|
|
except ImportError: # pragma: no cover - gateway's flat module path
|
|
from .dlp_detectors import (
|
|
scan_crlf_injection, scan_entropy, scan_known_secrets, scan_token_patterns)
|
|
if isinstance(body, bytes):
|
|
try:
|
|
text = body.decode("utf-8")
|
|
except UnicodeDecodeError:
|
|
text = body.decode("latin-1")
|
|
else:
|
|
text = body
|
|
result = scan_crlf_injection(text if crlf_text is None else crlf_text)
|
|
if result is not None:
|
|
return result
|
|
if _enabled(route.outbound_detectors, "token_patterns"):
|
|
result = scan_token_patterns(text, location="body", safe_tokens=safe_tokens)
|
|
if result is not None:
|
|
return result
|
|
if _enabled(route.outbound_detectors, "known_secrets"):
|
|
extra = tuple(prefix for prefix in environ.get(
|
|
"BOT_BOTTLE_SENSITIVE_PREFIXES", "").split(",") if prefix)
|
|
result = scan_known_secrets(text, location="body", env=environ,
|
|
sensitive_prefixes=("EGRESS_TOKEN_",) + extra,
|
|
safe_tokens=safe_tokens)
|
|
if result is not None:
|
|
return result
|
|
if route.outbound_detectors is not None and "entropy" in route.outbound_detectors:
|
|
return scan_entropy(text, location="body")
|
|
return None
|
|
|
|
|
|
def build_token_allow_payload(host: str, method: str, path: str, result: ScanResult) -> str:
|
|
"""Render redacted operator context; the raw matched secret is excluded."""
|
|
lines = [
|
|
"egress blocked an outbound request carrying a detected token",
|
|
f"host: {host}", f"method: {method}", f"path: {path}",
|
|
f"detector: {result.reason}",
|
|
]
|
|
if result.context:
|
|
lines.append(f"context: {result.context}")
|
|
return "\n".join(lines) + "\n"
|
|
|
|
|
|
def scan_inbound(route: Route, body: str | bytes) -> ScanResult | None:
|
|
if not route.inspect:
|
|
return None
|
|
try:
|
|
from dlp_detectors import scan_naive_injection # type: ignore[import-not-found]
|
|
except ImportError: # pragma: no cover - gateway's flat module path
|
|
from .dlp_detectors import scan_naive_injection
|
|
text = body if isinstance(body, str) else body.decode("utf-8", errors="replace")
|
|
if _enabled(route.inbound_detectors, "naive_injection_detection"):
|
|
return scan_naive_injection(text)
|
|
return None
|