fix(publicenv): redact runtime host data
All checks were successful
Code Review / ai-code-review (push) Successful in 13s
CD Pipeline / tests (push) Successful in 1m26s
CD Pipeline / build-and-deploy (push) Successful in 4m11s
CD Pipeline / post-deploy-checks (push) Successful in 1m53s

This commit is contained in:
Your Name
2026-06-13 05:06:23 +08:00
parent 2c1271d264
commit d3970a9b2e
9 changed files with 224 additions and 24 deletions

View File

@@ -181,6 +181,7 @@ from src.services.package_supply_chain_inventory import (
from src.services.runtime_surface_inventory import (
load_latest_runtime_surface_inventory,
)
from src.services.public_redaction import redact_public_lan_topology
from src.services.service_health_failure_notification_policy import (
load_latest_service_health_failure_notification_policy,
)
@@ -541,7 +542,8 @@ async def get_market_governance_snapshot() -> dict[str, Any]:
async def get_automation_inventory_snapshot() -> dict[str, Any]:
"""Return the latest read-only AI Agent automation inventory snapshot."""
try:
return await asyncio.to_thread(load_latest_ai_agent_automation_inventory_snapshot)
payload = await asyncio.to_thread(load_latest_ai_agent_automation_inventory_snapshot)
return redact_public_lan_topology(payload)
except FileNotFoundError as exc:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@@ -1400,7 +1402,8 @@ async def get_agent_host_stateful_version_inventory() -> dict[str, Any]:
async def get_runtime_surface_inventory() -> dict[str, Any]:
"""Return the latest read-only runtime surface inventory."""
try:
return await asyncio.to_thread(load_latest_runtime_surface_inventory)
payload = await asyncio.to_thread(load_latest_runtime_surface_inventory)
return redact_public_lan_topology(payload)
except FileNotFoundError as exc:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
@@ -1510,7 +1513,8 @@ async def get_ai_provider_route_matrix() -> dict[str, Any]:
async def get_service_health_gap_matrix() -> dict[str, Any]:
"""Return the latest read-only service health gap matrix."""
try:
return await asyncio.to_thread(load_latest_service_health_gap_matrix)
payload = await asyncio.to_thread(load_latest_service_health_gap_matrix)
return redact_public_lan_topology(payload)
except FileNotFoundError as exc:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,

View File

@@ -27,6 +27,23 @@ router = APIRouter(prefix="/monitoring", tags=["Monitoring"])
TIMEOUT = 3.0
PUBLIC_TOOL_URLS = {
"Sentry": "https://sentry.wooo.work",
"Langfuse": "https://langfuse.wooo.work",
"SigNoz": "https://signoz.wooo.work",
"Gitea": "https://gitea.wooo.work",
}
def public_monitoring_tool_payload(tool: dict) -> dict:
"""Drop internal probe URLs before returning tool status to browsers."""
payload = dict(tool)
payload.pop("url", None)
public_url = PUBLIC_TOOL_URLS.get(str(payload.get("name") or ""))
if public_url:
payload["url"] = public_url
return payload
# =============================================================================
# Probes
@@ -243,7 +260,7 @@ async def get_monitoring_status() -> dict:
if isinstance(r, Exception):
logger.error("monitoring_probe_exception", error=str(r))
continue
tools.append({**r, "checked_at": now})
tools.append({**public_monitoring_tool_payload(r), "checked_at": now})
return {
"tools": tools,

View File

@@ -0,0 +1,66 @@
"""
Public response redaction helpers.
These helpers preserve committed evidence semantics while preventing internal
LAN topology from being returned to browser-facing API responses.
"""
from __future__ import annotations
import re
from typing import Any
_ENDPOINT_ALIASES = {
"192.168.0.110:3001": "host:public-gateway/gitea",
"192.168.0.110:3002": "host:public-gateway/grafana",
"192.168.0.110:3100": "host:public-gateway/langfuse",
"192.168.0.110:5000": "host:public-gateway/registry",
"192.168.0.110:9000": "host:public-gateway/sentry",
"192.168.0.112:8080": "host:kali-readonly/scanner",
"192.168.0.188:11434": "host:observability-a/ollama",
"192.168.0.188:8088": "host:observability-a/openclaw-legacy",
"192.168.0.188:8089": "host:observability-a/openclaw",
"192.168.0.188:3301": "host:observability-a/signoz",
"192.168.0.188:5432": "host:observability-a/postgres",
"192.168.0.188:6380": "host:observability-a/redis",
}
_HOST_ALIASES = {
"192.168.0.110": "host:public-gateway",
"192.168.0.111": "host:dev-111",
"192.168.0.112": "host:kali-112",
"192.168.0.120": "host:k3s-control-a",
"192.168.0.121": "host:k3s-control-b",
"192.168.0.125": "host:edge-vip",
"192.168.0.168": "host:dev-168",
"192.168.0.188": "host:observability-a",
}
_PRIVATE_LAN_RE = re.compile(r"192\.168\.0\.\d{1,3}(?::\d{1,5})?")
def redact_public_lan_text(value: str) -> str:
"""Replace internal LAN addresses with public-safe asset aliases."""
redacted = value
for endpoint, alias in _ENDPOINT_ALIASES.items():
redacted = redacted.replace(f"http://{endpoint}", alias)
redacted = redacted.replace(f"https://{endpoint}", alias)
redacted = redacted.replace(endpoint, alias)
for host, alias in _HOST_ALIASES.items():
redacted = redacted.replace(f"http://{host}", alias)
redacted = redacted.replace(f"https://{host}", alias)
redacted = redacted.replace(host, alias)
return _PRIVATE_LAN_RE.sub("host:internal-node", redacted)
def redact_public_lan_topology(value: Any) -> Any:
"""Recursively redact internal LAN topology from JSON-compatible values."""
if isinstance(value, str):
return redact_public_lan_text(value)
if isinstance(value, list):
return [redact_public_lan_topology(item) for item in value]
if isinstance(value, dict):
return {key: redact_public_lan_topology(nested) for key, nested in value.items()}
return value