poc3
This commit is contained in:
@@ -56,6 +56,55 @@ class _WebHTTPServer(ThreadingHTTPServer):
|
||||
super().handle_error(request, client_address)
|
||||
|
||||
|
||||
class MetricsAccessControl:
|
||||
"""Pre-parsed ACL for the lightweight Prometheus endpoint."""
|
||||
|
||||
def __init__(self, config: Config) -> None:
|
||||
networks = []
|
||||
for item in config.metrics_allowed_ips.split(","):
|
||||
item = item.strip()
|
||||
if not item:
|
||||
continue
|
||||
try:
|
||||
networks.append(ipaddress.ip_network(item, strict=False))
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"invalid METRICS_ALLOWED_IPS entry: {item}") from exc
|
||||
self.networks = tuple(networks)
|
||||
self.username = config.metrics_basic_auth_username
|
||||
self.password = config.metrics_basic_auth_password
|
||||
if bool(self.username) != bool(self.password):
|
||||
raise ValueError(
|
||||
"METRICS_BASIC_AUTH_USERNAME and METRICS_BASIC_AUTH_PASSWORD "
|
||||
"must either both be set or both be empty"
|
||||
)
|
||||
|
||||
@property
|
||||
def basic_auth_enabled(self) -> bool:
|
||||
return bool(self.username and self.password)
|
||||
|
||||
def ip_allowed(self, client_ip: str) -> bool:
|
||||
try:
|
||||
address = ipaddress.ip_address(client_ip)
|
||||
except ValueError:
|
||||
return False
|
||||
if isinstance(address, ipaddress.IPv6Address) and address.ipv4_mapped is not None:
|
||||
address = address.ipv4_mapped
|
||||
return any(address in network for network in self.networks if network.version == address.version)
|
||||
|
||||
def basic_auth_allowed(self, authorization: str) -> bool:
|
||||
if not self.basic_auth_enabled:
|
||||
return True
|
||||
scheme, separator, encoded = authorization.partition(" ")
|
||||
if not separator or scheme.lower() != "basic" or not encoded.strip():
|
||||
return False
|
||||
try:
|
||||
supplied = base64.b64decode(encoded.strip().encode("ascii"), validate=True)
|
||||
except (ValueError, UnicodeEncodeError):
|
||||
return False
|
||||
expected = f"{self.username}:{self.password}".encode("utf-8")
|
||||
return hmac.compare_digest(supplied, expected)
|
||||
|
||||
|
||||
class WebServer:
|
||||
def __init__(
|
||||
self,
|
||||
@@ -73,6 +122,7 @@ class WebServer:
|
||||
ndr_analyzer: NDRAnalyzer | None = None,
|
||||
backup_manager: BackupManager | None = None,
|
||||
forensic_pcap: ForensicPcapRing | None = None,
|
||||
metrics_provider: Callable[[], str] | None = None,
|
||||
) -> None:
|
||||
self.config = config
|
||||
self.store = store
|
||||
@@ -87,6 +137,8 @@ class WebServer:
|
||||
self.threat_intel = threat_intel
|
||||
self.ndr_analyzer = ndr_analyzer
|
||||
self.forensic_pcap = forensic_pcap
|
||||
self.metrics_provider = metrics_provider
|
||||
self.metrics_access = MetricsAccessControl(config) if metrics_provider is not None else None
|
||||
self.backup_manager = backup_manager or BackupManager(config.db_path, os.path.dirname(config.db_path) or ".")
|
||||
self.auth = SessionAuth(config, store)
|
||||
self._login_lock = threading.Lock()
|
||||
@@ -143,6 +195,8 @@ class WebServer:
|
||||
ndr_analyzer = self.ndr_analyzer
|
||||
backup_manager = self.backup_manager
|
||||
forensic_pcap = self.forensic_pcap
|
||||
metrics_provider = self.metrics_provider
|
||||
metrics_access = self.metrics_access
|
||||
auth = self.auth
|
||||
|
||||
class Handler(BaseHTTPRequestHandler):
|
||||
@@ -155,6 +209,35 @@ class WebServer:
|
||||
if parsed.path == "/ws/live":
|
||||
self._websocket(parsed)
|
||||
return
|
||||
if parsed.path == "/metrics":
|
||||
if metrics_provider is None or metrics_access is None:
|
||||
self._send(404, b"metrics unavailable\n", "text/plain; charset=utf-8")
|
||||
return
|
||||
client_ip = self.client_address[0] if self.client_address else ""
|
||||
if not metrics_access.ip_allowed(client_ip):
|
||||
self._send(403, b"metrics access denied\n", "text/plain; charset=utf-8")
|
||||
return
|
||||
if not metrics_access.basic_auth_allowed(self.headers.get("Authorization", "")):
|
||||
self._send(
|
||||
401,
|
||||
b"metrics authentication required\n",
|
||||
"text/plain; charset=utf-8",
|
||||
extra_headers={
|
||||
"WWW-Authenticate": 'Basic realm="metrics", charset="UTF-8"'
|
||||
},
|
||||
)
|
||||
return
|
||||
try:
|
||||
payload = metrics_provider().encode("utf-8")
|
||||
except Exception:
|
||||
self._send(500, b"metrics scrape failed\n", "text/plain; charset=utf-8")
|
||||
return
|
||||
self._send(
|
||||
200,
|
||||
payload,
|
||||
"text/plain; version=0.0.4; charset=utf-8",
|
||||
)
|
||||
return
|
||||
if view_path in {"/", "/live", "/security", "/intelligence", "/blocks", "/reports", "/feeds", "/rules", "/system"}:
|
||||
self._send(200, DASHBOARD.encode("utf-8"), "text/html; charset=utf-8")
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user