Spaces:
Sleeping
Sleeping
File size: 6,215 Bytes
fbe9dad ebd50f7 fbe9dad ebd50f7 fbe9dad ebd50f7 fbe9dad ebd50f7 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 | """Mock data fixtures for the control plane demo.
WHY THIS EXISTS
---------------
The control plane governs *actions agents take against systems* β read an asset,
draft a remediation, create a ticket, stop a container. To demonstrate that
end to end without touching any real SOC tooling, we ship a small, static,
**non-sensitive** dataset that stands in for the systems an agent would call:
* alerts.json β SIEM detections (the "why we're here" for each incident)
* assets.json β hosts/workstations (and the containers running on them)
* cves.json β vulnerabilities referenced by alerts and remediation drafts
* tickets.json β the ticketing system's seed records (and its writable store)
* users.json β the identities referenced by alerts and tickets
All data is invented. There are no real hostnames, credentials, IPs, or PII β
the only realistic values are *public* CVE identifiers, kept for recognisability.
This module is the single, typed entry point the execution backends (Phases 5.2β
5.4) and demo scenarios load their data through, so there is exactly one place
that knows where the JSON lives and how it is shaped.
DESIGN NOTES
------------
* The JSON files are pure data (JSON has no comments) β the narrative and the
contract live here, in code, where they can be read and tested.
* :data:`SCENARIO_IDENTIFIERS` pins the specific IDs each acceptance
scenario depends on. ``test_mock_data`` asserts every one of them resolves, so
if a fixture is ever edited in a way that would break a demo scenario, the
test fails immediately rather than the demo failing live.
"""
from __future__ import annotations
import json
from functools import lru_cache
from pathlib import Path
from typing import Any
# Directory holding the JSON fixtures β this package's own directory.
DATA_DIR = Path(__file__).resolve().parent
# Logical dataset name -> filename. The keys double as the public API surface
# (see :func:`load_dataset`) and as the set of datasets the test iterates over.
_DATASETS: dict[str, str] = {
"alerts": "alerts.json",
"assets": "assets.json",
"cves": "cves.json",
"tickets": "tickets.json",
"users": "users.json",
}
# The primary-key field for each dataset, so callers can index records by ID
# without hard-coding "which field is the id" at every call site.
_ID_FIELD: dict[str, str] = {
"alerts": "alert_id",
"assets": "asset_id",
"cves": "cve_id",
"tickets": "ticket_id",
"users": "user_id",
}
# Identifiers each acceptance scenario relies on existing in the data.
# This is the contract the demo runs against; the test enforces it. Scenarios 5
# (dangerous action denied) and 6 (kill switch) are pure governance outcomes and
# need no specific record, so they are intentionally absent here.
SCENARIO_IDENTIFIERS: dict[str, dict[str, str]] = {
# 1. L0 Direct API read β an asset must exist to look up.
"scenario_1_l0_read": {"assets": "ASSET-001"},
# 2. L1 Function draft β a CVE must exist to draft remediation against.
"scenario_2_l1_draft": {"cves": "CVE-2024-3094"},
# 3. L2 low-risk ticket β an alert/asset must exist for the new ticket to reference.
"scenario_3_l2_ticket": {"alerts": "ALERT-2001", "assets": "ASSET-001"},
# 4. L3 safe-CLI container stop β the suspicious container lives on this asset
# (the asset record carries it; verified explicitly in the test).
"scenario_4_l3_stop": {"assets": "ASSET-001"},
# 7. Identity verification β the user attributed to the incident must exist.
"scenario_7_identity": {"users": "USR-301"},
}
# Container name the L3 "stop a suspicious container" scenario acts on.
SUSPICIOUS_CONTAINER_NAME = "suspicious-nginx"
@lru_cache(maxsize=None)
def load_dataset(name: str) -> list[dict[str, Any]]:
"""Load one fixture by logical name (e.g. ``"assets"``) as a list of records.
Results are cached: the fixtures are static for the life of the process, so
we read and parse each file at most once. (Backends that *mutate* state β
e.g. creating a ticket β must copy what they need into their own store; see
:func:`load_records`, which hands back a fresh copy.)
"""
if name not in _DATASETS:
raise KeyError(f"Unknown dataset {name!r}; expected one of {sorted(_DATASETS)}")
path = DATA_DIR / _DATASETS[name]
with path.open(encoding="utf-8") as fh:
data = json.load(fh)
if not isinstance(data, list):
raise ValueError(f"Fixture {path.name} must be a JSON array of records")
return data
def load_records(name: str) -> list[dict[str, Any]]:
"""Return a **deep copy** of a dataset, safe for callers that mutate it.
Use this from execution backends that add or change records (e.g. creating a
ticket) so the in-memory mock store can diverge from the on-disk seed without
corrupting the shared cached copy.
"""
return json.loads(json.dumps(load_dataset(name)))
def index_by_id(name: str) -> dict[str, dict[str, Any]]:
"""Return a dataset keyed by its primary-id field, for O(1) lookups by ID."""
id_field = _ID_FIELD[name]
return {record[id_field]: record for record in load_dataset(name)}
def get_by_id(name: str, record_id: str) -> dict[str, Any] | None:
"""Fetch a single record by its ID, or ``None`` if no such record exists."""
return index_by_id(name).get(record_id)
# Convenience accessors β thin, self-documenting wrappers the backends and
# scenarios can import directly (``from data import load_assets``).
def load_alerts() -> list[dict[str, Any]]:
return load_dataset("alerts")
def load_assets() -> list[dict[str, Any]]:
return load_dataset("assets")
def load_cves() -> list[dict[str, Any]]:
return load_dataset("cves")
def load_tickets() -> list[dict[str, Any]]:
return load_dataset("tickets")
def load_users() -> list[dict[str, Any]]:
return load_dataset("users")
__all__ = [
"DATA_DIR",
"SCENARIO_IDENTIFIERS",
"SUSPICIOUS_CONTAINER_NAME",
"load_dataset",
"load_records",
"index_by_id",
"get_by_id",
"load_alerts",
"load_assets",
"load_cves",
"load_tickets",
"load_users",
]
|