Files
2026-03-24 15:04:21 +03:00

236 lines
8.4 KiB
Python

"""
Communications Router - Proxies to graph-query service
"""
import logging
import httpx
from typing import List, Optional
from fastapi import APIRouter, HTTPException, Query
from app.config import settings
logger = logging.getLogger(__name__)
router = APIRouter(tags=["Communications"])
async def proxy_to_graph_query(path: str, params: dict = None, json_body: dict = None) -> dict:
"""
Proxy request to graph-query service (GET or POST)
"""
url = f"{settings.graph_query_url}{path}"
if params:
params = {k: v for k, v in params.items() if v is not None}
try:
async with httpx.AsyncClient(timeout=30.0) as client:
if json_body is not None:
response = await client.post(url, json=json_body)
else:
response = await client.get(url, params=params)
response.raise_for_status()
content_type = response.headers.get("content-type", "")
if "text/plain" in content_type:
return {"content": response.text, "format": "text"}
return response.json()
except httpx.HTTPStatusError as e:
logger.error(f"Graph query error: {e.response.status_code} - {e.response.text}")
raise HTTPException(status_code=e.response.status_code, detail=str(e))
except httpx.RequestError as e:
logger.error(f"Graph query connection error: {e}")
raise HTTPException(status_code=503, detail="Graph query service unavailable")
@router.get("/communications")
async def get_communications(
cluster_id: int = Query(..., description="Cluster ID"),
analysis_id: Optional[int] = Query(None, description="Filter by analysis ID"),
namespace: Optional[str] = Query(None, description="Filter by namespace"),
protocol: Optional[str] = Query(None, description="Filter by protocol"),
limit: int = Query(100, description="Limit results")
):
"""
Get communications list
"""
params = {
"cluster_id": cluster_id,
"analysis_id": analysis_id,
"namespace": namespace,
"protocol": protocol,
"limit": limit
}
return await proxy_to_graph_query("/communications", params)
@router.get("/communications/graph")
async def get_dependency_graph(
cluster_id: Optional[int] = Query(None, description="Cluster ID (optional for multi-cluster)"),
analysis_id: Optional[int] = Query(None, description="Filter by analysis ID"),
namespace: Optional[str] = Query(None, description="Filter by namespace"),
depth: int = Query(2, description="Traversal depth")
):
"""
Get dependency graph for visualization
Returns nodes and edges for the Live Map.
For multi-cluster analyses, only analysis_id is required.
For single-cluster, cluster_id can be provided for additional filtering.
"""
params = {
"cluster_id": cluster_id,
"analysis_id": analysis_id,
"namespace": namespace,
"depth": depth
}
return await proxy_to_graph_query("/dependencies/graph", params)
@router.get("/communications/cross-namespace")
async def get_cross_namespace_communications(
cluster_id: int = Query(..., description="Cluster ID"),
analysis_id: Optional[int] = Query(None, description="Filter by analysis ID"),
limit: int = Query(50, description="Limit results")
):
"""
Get cross-namespace communications (potential security risk)
"""
params = {
"cluster_id": cluster_id,
"analysis_id": analysis_id,
"limit": limit
}
return await proxy_to_graph_query("/communications/cross-namespace", params)
@router.get("/communications/high-risk")
async def get_high_risk_communications(
cluster_id: int = Query(..., description="Cluster ID"),
risk_threshold: float = Query(0.5, description="Risk threshold"),
limit: int = Query(50, description="Limit results")
):
"""
Get high-risk communications
"""
params = {
"cluster_id": cluster_id,
"risk_threshold": risk_threshold,
"limit": limit
}
return await proxy_to_graph_query("/communications/high-risk", params)
@router.get("/communications/external")
async def get_external_communications(
cluster_id: int = Query(..., description="Cluster ID"),
analysis_id: Optional[int] = Query(None, description="Filter by analysis ID"),
limit: int = Query(50, description="Limit results")
):
"""
Get external communications (to endpoints outside the cluster)
"""
params = {
"cluster_id": cluster_id,
"analysis_id": analysis_id,
"limit": limit
}
return await proxy_to_graph_query("/communications/external", params)
@router.get("/communications/stats")
async def get_communication_stats(
cluster_id: Optional[int] = Query(None, description="Cluster ID (optional for multi-cluster)"),
analysis_id: Optional[int] = Query(None, description="Filter by analysis ID")
):
"""
Get communication statistics.
For multi-cluster analyses, only analysis_id is required.
"""
params = {
"cluster_id": cluster_id,
"analysis_id": analysis_id
}
return await proxy_to_graph_query("/communications/stats", params)
@router.get("/communications/dependencies/stream")
async def find_pod_dependencies(
analysis_id: Optional[int] = Query(None),
cluster_id: Optional[int] = Query(None),
pod_name: Optional[str] = Query(None),
namespace: Optional[str] = Query(None),
owner_name: Optional[str] = Query(None),
label_key: Optional[str] = Query(None),
label_value: Optional[str] = Query(None),
annotation_key: Optional[str] = Query(None),
annotation_value: Optional[str] = Query(None),
ip: Optional[str] = Query(None),
depth: int = Query(1, ge=1, le=5),
format: Optional[str] = Query("json"),
):
"""Proxy: find pod dependencies (upstream/downstream/callers)"""
params = {
"analysis_id": analysis_id, "cluster_id": cluster_id,
"pod_name": pod_name, "namespace": namespace,
"owner_name": owner_name, "label_key": label_key,
"label_value": label_value, "annotation_key": annotation_key,
"annotation_value": annotation_value, "ip": ip,
"depth": depth, "format": format,
}
return await proxy_to_graph_query("/dependencies/stream", params)
@router.post("/communications/dependencies/batch")
async def batch_find_dependencies(request: dict):
"""Proxy: batch find dependencies for multiple services"""
return await proxy_to_graph_query("/dependencies/batch", json_body=request)
@router.get("/communications/dependencies/diff")
async def diff_dependencies(
analysis_id_before: str = Query(...),
analysis_id_after: str = Query(...),
pod_name: Optional[str] = Query(None),
namespace: Optional[str] = Query(None),
owner_name: Optional[str] = Query(None),
cluster_id: Optional[int] = Query(None),
):
"""Proxy: diff dependencies between two analysis runs"""
params = {
"analysis_id_before": analysis_id_before,
"analysis_id_after": analysis_id_after,
"pod_name": pod_name, "namespace": namespace,
"owner_name": owner_name, "cluster_id": cluster_id,
}
return await proxy_to_graph_query("/dependencies/diff", params)
@router.get("/communications/dependencies/summary")
async def get_dependency_summary(
analysis_ids: List[int] = Query(...),
cluster_id: Optional[int] = Query(None),
pod_name: Optional[str] = Query(None),
namespace: Optional[str] = Query(None),
owner_name: Optional[str] = Query(None),
label_key: Optional[str] = Query(None),
label_value: Optional[str] = Query(None),
annotation_key: Optional[str] = Query(None),
annotation_value: Optional[str] = Query(None),
ip: Optional[str] = Query(None),
depth: int = Query(1, ge=1, le=5),
):
"""Proxy: AI-agent-friendly dependency summary grouped by category"""
params = {
"analysis_ids": [str(a) for a in analysis_ids],
"cluster_id": cluster_id, "pod_name": pod_name,
"namespace": namespace, "owner_name": owner_name,
"label_key": label_key, "label_value": label_value,
"annotation_key": annotation_key, "annotation_value": annotation_value,
"ip": ip, "depth": depth,
}
return await proxy_to_graph_query("/dependencies/summary", params)
# NOTE: /communications/dependencies/impact is backend-only (requires blast_radius logic).
# External clients should call the backend API directly for impact assessments.