Files
tracer-cloud--opensre/surfaces/cli/investigation/payload.py
T
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

245 lines
8.6 KiB
Python

"""Helpers for loading alert payloads from various input sources."""
from __future__ import annotations
import json
import re
import sys
from pathlib import Path
from typing import Any
from surfaces.cli.constants import SAMPLE_ALERT_OPTIONS
_DEMO_ALERT_FILENAME = "alert.json"
def bundled_demo_alert_path() -> Path | None:
"""Return the packaged demo alert used by ``opensre investigate -i alert.json``."""
candidate = Path(__file__).resolve().parent / "sample_alerts" / _DEMO_ALERT_FILENAME
if candidate.is_file():
return candidate
return None
def resolve_alert_path(path_str: str) -> Path:
"""Resolve an alert path, using the bundled demo when ``alert.json`` is missing locally."""
path = Path(path_str)
if path.is_file():
return path
if path.name == _DEMO_ALERT_FILENAME and not path.is_absolute() and path.parent == Path("."):
bundled = bundled_demo_alert_path()
if bundled is not None:
return bundled
return path
def parse_payload_text(raw_text: str, source_label: str) -> dict[str, Any]:
"""Parse and validate a JSON object payload."""
try:
data: Any = json.loads(raw_text)
except json.JSONDecodeError as exc:
raise SystemExit(
f"Invalid alert JSON from {source_label}: {exc.msg} at line {exc.lineno}, column {exc.colno}."
) from exc
if not isinstance(data, dict):
raise SystemExit(f"Alert payload from {source_label} must be a JSON object.")
if not data:
raise SystemExit(f"Alert payload from {source_label} must be a non-empty JSON object.")
return data
def load_file(path_str: str) -> dict[str, Any]:
"""Load an alert payload from any text file.
- ``.json`` — parsed directly as JSON.
- ``.md`` / ``.txt`` / other — first ```json``` block is extracted and parsed;
if none is found, raw content is passed as ``{"raw_text": ...}`` for the agent.
"""
path = resolve_alert_path(path_str)
try:
raw_text = path.read_text(encoding="utf-8")
except FileNotFoundError as exc:
raise SystemExit(f"Alert file not found: {path_str}") from exc
except UnicodeDecodeError as exc:
raise SystemExit(f"Alert file must be UTF-8 text: {path_str}") from exc
except OSError as exc:
raise SystemExit(f"Could not read alert file {path_str}: {exc}") from exc
if path.suffix.lower() == ".json":
return parse_payload_text(raw_text, path_str)
# For .md, .txt, and everything else: try to pull a fenced JSON block
match = re.search(r"```json\s*(\{.*?\})\s*```", raw_text, re.DOTALL)
if match:
return parse_payload_text(match.group(1), path_str)
# No structured JSON — let the agent interpret the raw content
return {"raw_text": raw_text}
def load_stdin() -> dict[str, Any]:
"""Read a JSON payload from stdin."""
if sys.stdin.isatty():
raise SystemExit(
"No alert input provided on stdin. Use --interactive, --input <file>, or --input-json."
)
return parse_payload_text(sys.stdin.read(), "stdin")
def load_interactive() -> dict[str, Any]:
"""Prompt the user to paste an alert payload."""
if not sys.stdin.isatty():
print("Paste the alert JSON payload, then press Ctrl-D when finished.", file=sys.stderr)
raw_text = sys.stdin.read()
else:
print(
"Paste the alert JSON payload. It auto-submits once valid JSON is complete.",
file=sys.stderr,
)
lines: list[str] = []
while True:
try:
line = input()
except EOFError:
break
except KeyboardInterrupt as exc:
raise SystemExit(0) from exc
if not line.strip():
break
lines.append(line)
candidate = "\n".join(lines)
try:
parsed_candidate: Any = json.loads(candidate)
except json.JSONDecodeError:
continue
if not isinstance(parsed_candidate, dict):
raise SystemExit("Alert payload from interactive input must be a JSON object.")
if not parsed_candidate:
raise SystemExit(
"Alert payload from interactive input must be a non-empty JSON object."
)
return parsed_candidate
raw_text = "\n".join(lines)
if not raw_text.strip():
raise SystemExit("No alert JSON was provided in interactive mode.")
return parse_payload_text(raw_text, "interactive input")
def _render_guided_menu() -> list[tuple[int, str]]:
"""Render the bare investigate guided menu and return option mapping."""
options: list[tuple[int, str]] = [(1, f"demo:{_DEMO_ALERT_FILENAME}")]
print("No alert input provided. Choose an investigation input source:", file=sys.stderr)
print(f" 1) {_DEMO_ALERT_FILENAME} (bundled demo alert file)", file=sys.stderr)
next_index = 2
for template_name, label in SAMPLE_ALERT_OPTIONS:
options.append((next_index, f"template:{template_name}"))
print(f" {next_index}) {label}", file=sys.stderr)
next_index += 1
options.append((next_index, "custom_file"))
print(f" {next_index}) Custom file path", file=sys.stderr)
next_index += 1
options.append((next_index, "paste_json"))
print(f" {next_index}) Paste JSON now", file=sys.stderr)
next_index += 1
options.append((next_index, "cancel"))
print(f" {next_index}) Cancel", file=sys.stderr)
return options
def _guided_menu_choices() -> list[tuple[str, str]]:
"""Return guided menu targets and labels for inline picker UIs."""
choices: list[tuple[str, str]] = [
(f"demo:{_DEMO_ALERT_FILENAME}", f"{_DEMO_ALERT_FILENAME} (bundled demo alert file)"),
]
for template_name, label in SAMPLE_ALERT_OPTIONS:
choices.append((f"template:{template_name}", label))
choices.extend(
[
("custom_file", "Custom file path"),
("paste_json", "Paste JSON now"),
("cancel", "Cancel"),
]
)
return choices
def _supports_inline_picker() -> bool:
"""Return whether we can safely render an inline terminal picker."""
return bool(sys.stdin.isatty() and sys.stdout.isatty())
def _choose_guided_target() -> str:
"""Choose a guided menu target using inline picker when available."""
choices = _guided_menu_choices()
if _supports_inline_picker():
import questionary
selected = questionary.select(
"Choose an investigation input source:",
choices=[questionary.Choice(label, value=target) for target, label in choices],
).ask()
if selected is None:
raise SystemExit(0)
return str(selected)
while True:
options = _render_guided_menu()
valid_choices = {str(index): target for index, target in options}
try:
choice = input("Select an option: ").strip()
except (EOFError, KeyboardInterrupt) as exc:
raise SystemExit(0) from exc
target = valid_choices.get(choice)
if target is None:
print("Invalid selection. Enter one of the menu numbers.", file=sys.stderr)
continue
return target
def _choose_guided_payload() -> dict[str, Any]:
from tools.investigation.alert_templates import build_alert_template
while True:
target = _choose_guided_target()
if target.startswith("demo:"):
return load_file(target.split(":", maxsplit=1)[1])
if target.startswith("template:"):
return build_alert_template(target.split(":", maxsplit=1)[1])
if target == "custom_file":
try:
custom_path = input("Alert file path: ").strip()
except (EOFError, KeyboardInterrupt) as exc:
raise SystemExit(0) from exc
if not custom_path:
print("Alert file path cannot be empty.", file=sys.stderr)
continue
return load_file(custom_path)
if target == "paste_json":
return load_interactive()
if target == "cancel":
raise SystemExit(0)
raise SystemExit("No alert input selected.")
def load_payload(
input_path: str | None,
input_json: str | None,
interactive: bool,
) -> dict[str, Any]:
"""Dispatch to the right loader based on what the user passed."""
if input_json:
return parse_payload_text(input_json, "--input-json")
if interactive:
return load_interactive()
if input_path == "-":
return load_stdin()
if input_path:
return load_file(input_path)
if sys.stdin.isatty():
return _choose_guided_payload()
return load_stdin()