4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
112 lines
3.9 KiB
Python
112 lines
3.9 KiB
Python
"""Normalize investigation state into report-context inputs."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
from typing import Any
|
|
|
|
from core.state import InvestigationState
|
|
|
|
|
|
def safe_get(data: dict[str, Any] | None, *keys: str, default: Any = None) -> Any:
|
|
"""Safely navigate nested dictionaries without raising."""
|
|
if data is None:
|
|
return default
|
|
current: Any = data
|
|
for key in keys:
|
|
if not isinstance(current, dict):
|
|
return default
|
|
current = current.get(key)
|
|
if current is None:
|
|
return default
|
|
return current
|
|
|
|
|
|
def as_snippet(value: str | None, max_len: int = 140) -> str | None:
|
|
"""Compact a value to a short, brace-free snippet for display."""
|
|
if not value:
|
|
return None
|
|
compact = " ".join(str(value).split())
|
|
compact = compact.replace("{", "").replace("}", "").replace("[", "").replace("]", "")
|
|
return compact[:max_len]
|
|
|
|
|
|
def filter_valid_claims(claims: list[dict]) -> list[dict]:
|
|
"""Drop claims with empty text or the NON_ artifact prefix."""
|
|
return [
|
|
c
|
|
for c in claims
|
|
if c.get("claim", "").strip() and not c.get("claim", "").strip().startswith("NON_")
|
|
]
|
|
|
|
|
|
class NormalizedState:
|
|
"""All raw data extracted from InvestigationState in one place."""
|
|
|
|
def __init__(self, state: InvestigationState) -> None:
|
|
evidence = state.get("evidence", {}) or {}
|
|
available_sources = state.get("available_sources", {}) or {}
|
|
raw_alert_value = state.get("raw_alert", {})
|
|
|
|
self.evidence: dict[str, Any] = evidence
|
|
self.raw_alert: dict[str, Any] = (
|
|
raw_alert_value if isinstance(raw_alert_value, dict) else {}
|
|
)
|
|
self.s3: dict[str, Any] = evidence.get("s3", {}) or {}
|
|
self.available_sources: dict[str, dict[str, Any]] = available_sources
|
|
|
|
self.grafana_endpoint: str | None = (available_sources.get("grafana") or {}).get(
|
|
"grafana_endpoint"
|
|
)
|
|
self.datadog_site: str = (available_sources.get("datadog") or {}).get(
|
|
"site"
|
|
) or "datadoghq.com"
|
|
|
|
self.validated_claims: list[dict] = filter_valid_claims(state.get("validated_claims", []))
|
|
self.non_validated_claims: list[dict] = state.get("non_validated_claims", [])
|
|
|
|
(
|
|
self.cloudwatch_url,
|
|
self.cloudwatch_group,
|
|
self.cloudwatch_stream,
|
|
self.cloudwatch_region,
|
|
self.alert_id,
|
|
) = extract_cloudwatch_info(self.raw_alert)
|
|
|
|
started_at = state.get("investigation_started_at")
|
|
self.duration_seconds: int | None = (
|
|
max(0, int(round(time.monotonic() - float(started_at))))
|
|
if isinstance(started_at, int | float)
|
|
else None
|
|
)
|
|
|
|
self.state = state
|
|
|
|
|
|
def extract_cloudwatch_info(
|
|
raw_alert: dict[str, Any],
|
|
) -> tuple[str | None, str | None, str | None, str | None, str | None]:
|
|
"""Pull CloudWatch metadata from an alert dict.
|
|
|
|
Returns: (url, log_group, log_stream, region, alert_id)
|
|
"""
|
|
annotations = raw_alert.get("annotations", {}) or raw_alert.get("commonAnnotations", {})
|
|
if not annotations and raw_alert.get("alerts"):
|
|
first_alert = raw_alert.get("alerts", [{}])[0]
|
|
if isinstance(first_alert, dict):
|
|
annotations = first_alert.get("annotations", {}) or {}
|
|
|
|
url = (
|
|
raw_alert.get("cloudwatch_logs_url")
|
|
or raw_alert.get("cloudwatch_url")
|
|
or safe_get(annotations, "cloudwatch_logs_url")
|
|
or safe_get(annotations, "cloudwatch_url")
|
|
)
|
|
group = raw_alert.get("cloudwatch_log_group") or safe_get(annotations, "cloudwatch_log_group")
|
|
stream = raw_alert.get("cloudwatch_log_stream") or safe_get(
|
|
annotations, "cloudwatch_log_stream"
|
|
)
|
|
region = raw_alert.get("cloudwatch_region") or safe_get(annotations, "cloudwatch_region")
|
|
alert_id = raw_alert.get("alert_id")
|
|
return url, group, stream, region, alert_id
|