Add stable v1 schema IDs and docs
This commit is contained in:
parent
bfef23fc4a
commit
7a176ea238
13 changed files with 409 additions and 8 deletions
|
|
@ -12,6 +12,7 @@ import signal
|
|||
import sys
|
||||
|
||||
from . import __version__, detectors
|
||||
from . import schemas
|
||||
from .config import Config
|
||||
from .daemon import Sentinel
|
||||
from .system import SystemState, scan_suid_binaries
|
||||
|
|
@ -376,6 +377,7 @@ def _cmd_incident(cfg: Config, action: str, iid: str | None, as_json: bool) -> i
|
|||
|
||||
if action == "export":
|
||||
bundle = {
|
||||
"schema": schemas.INCIDENT_BUNDLE_V1,
|
||||
"incident": inc,
|
||||
"snapshots": [r for name in inc.get("snapshots", [])
|
||||
if (r := _load_snapshot_report(cfg, name)) is not None],
|
||||
|
|
@ -386,7 +388,11 @@ def _cmd_incident(cfg: Config, action: str, iid: str | None, as_json: bool) -> i
|
|||
# action == "show"
|
||||
timeline = _incident_timeline(cfg, inc)
|
||||
if as_json:
|
||||
print(json.dumps({"incident": inc, "timeline": timeline}, indent=2))
|
||||
print(json.dumps({
|
||||
"schema": schemas.INCIDENT_VIEW_V1,
|
||||
"incident": inc,
|
||||
"timeline": timeline,
|
||||
}, indent=2))
|
||||
return 0
|
||||
print(f"Incident {inc['id']} [{inc.get('severity', '?')}] on {inc.get('host', '?')}")
|
||||
print(f" first seen: {inc.get('first_seen', '?')}")
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import time
|
|||
from collections.abc import Callable, Iterable
|
||||
from datetime import datetime
|
||||
|
||||
from . import schemas
|
||||
from .alert import Alert, Severity
|
||||
from .config import Config
|
||||
|
||||
|
|
@ -112,7 +113,12 @@ def index_path(cfg: Config):
|
|||
def load_index(cfg: Config) -> dict:
|
||||
try:
|
||||
data = json.loads(index_path(cfg).read_text())
|
||||
return data if isinstance(data, dict) else {}
|
||||
if not isinstance(data, dict):
|
||||
return {}
|
||||
for inc in data.values():
|
||||
if isinstance(inc, dict):
|
||||
inc.setdefault("schema", schemas.INCIDENT_V1)
|
||||
return data
|
||||
except (OSError, ValueError):
|
||||
return {}
|
||||
|
||||
|
|
@ -161,6 +167,7 @@ def record(cfg: Config, snapshot_name: str, alerts: list[Alert],
|
|||
if iid is None:
|
||||
iid = _new_id(when)
|
||||
index[iid] = {
|
||||
"schema": schemas.INCIDENT_V1,
|
||||
"id": iid, "host": host,
|
||||
"first_ts": when, "last_ts": when,
|
||||
"first_seen": iso, "last_seen": iso,
|
||||
|
|
@ -169,6 +176,7 @@ def record(cfg: Config, snapshot_name: str, alerts: list[Alert],
|
|||
"lineage": [], "snapshots": [], "alert_count": 0,
|
||||
}
|
||||
inc = index[iid]
|
||||
inc.setdefault("schema", schemas.INCIDENT_V1)
|
||||
inc["last_ts"] = when
|
||||
inc["last_seen"] = iso
|
||||
inc["severity"] = max_severity(inc.get("severity", severity), severity)
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ from datetime import datetime
|
|||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from . import schemas
|
||||
from .config import Config
|
||||
from .netutil import is_public_ip
|
||||
|
||||
|
|
@ -189,7 +190,7 @@ def build_plan(bundle: dict, cfg: Config) -> dict:
|
|||
numbered.append(d)
|
||||
|
||||
return {
|
||||
"schema": "enodia.response.plan.v1",
|
||||
"schema": schemas.RESPONSE_PLAN_V1,
|
||||
"plan_id": f"plan-{incident_id}",
|
||||
"incident_id": incident_id,
|
||||
"created_at": datetime.now().astimezone().isoformat(),
|
||||
|
|
@ -242,6 +243,7 @@ def persist_plan(cfg: Config, plan: dict, actor: str = "cli") -> dict[str, str]:
|
|||
|
||||
audit_path = response_audit_path(cfg)
|
||||
record = {
|
||||
"schema": schemas.RESPONSE_AUDIT_V1,
|
||||
"time": datetime.now().astimezone().isoformat(),
|
||||
"event": "response_plan_generated",
|
||||
"actor": actor,
|
||||
|
|
|
|||
16
enodia_sentinel/schemas.py
Normal file
16
enodia_sentinel/schemas.py
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
"""Stable JSON schema identifiers for operator-facing contracts.
|
||||
|
||||
These are lightweight schema IDs, not a full JSON Schema implementation. Tests
|
||||
pin the minimum v1 fields and types so downstream automation can rely on them
|
||||
while newer releases remain free to add fields.
|
||||
"""
|
||||
|
||||
ALERT_V1 = "enodia.alert.v1"
|
||||
ALERT_SNAPSHOT_V1 = "enodia.alert.snapshot.v1"
|
||||
INCIDENT_V1 = "enodia.incident.v1"
|
||||
INCIDENT_VIEW_V1 = "enodia.incident.view.v1"
|
||||
INCIDENT_BUNDLE_V1 = "enodia.incident.bundle.v1"
|
||||
STATUS_V1 = "enodia.status.v1"
|
||||
RESPONSE_PLAN_V1 = "enodia.response.plan.v1"
|
||||
RESPONSE_AUDIT_V1 = "enodia.response.audit.v1"
|
||||
|
|
@ -15,6 +15,7 @@ import time
|
|||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from . import schemas
|
||||
from .alert import Alert, Severity
|
||||
from .config import Config
|
||||
from .system import Process, SystemState
|
||||
|
|
@ -173,6 +174,7 @@ def capture(alerts: list[Alert], state: SystemState, cfg: Config) -> Path:
|
|||
cfg, base.with_suffix(".log").name, alerts, lineage, now.timestamp(), host)
|
||||
|
||||
report = {
|
||||
"schema": schemas.ALERT_SNAPSHOT_V1,
|
||||
"time": now.isoformat(),
|
||||
"host": host,
|
||||
"severity": str(severity),
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ from pathlib import Path
|
|||
from urllib.parse import parse_qs, unquote, urlparse
|
||||
|
||||
from . import __version__
|
||||
from . import schemas
|
||||
from .config import Config
|
||||
|
||||
_STATIC = Path(__file__).parent / "static"
|
||||
|
|
@ -199,6 +200,7 @@ def daemon_status(cfg: Config) -> dict:
|
|||
from .selfprotect import heartbeat_age
|
||||
age = heartbeat_age(cfg)
|
||||
return {
|
||||
"schema": schemas.STATUS_V1,
|
||||
"version": __version__,
|
||||
"running": pid_alive,
|
||||
"total_alerts": len(alerts),
|
||||
|
|
@ -246,7 +248,12 @@ def get_incident(cfg: Config, incident_id: str) -> dict | None:
|
|||
if isinstance(p.get("pid"), int)],
|
||||
})
|
||||
timeline.sort(key=lambda r: r.get("time", ""))
|
||||
return {"incident": inc, "timeline": timeline, "snapshots": snapshots}
|
||||
return {
|
||||
"schema": schemas.INCIDENT_VIEW_V1,
|
||||
"incident": inc,
|
||||
"timeline": timeline,
|
||||
"snapshots": snapshots,
|
||||
}
|
||||
|
||||
|
||||
def response_plan(cfg: Config, incident_id: str) -> dict | None:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue