aetherruntime-arch 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- aetherruntime/__init__.py +314 -0
- aetherruntime/__version__.py +1 -0
- aetherruntime/cli.py +306 -0
- aetherruntime/containment/__init__.py +39 -0
- aetherruntime/containment/isolation.py +253 -0
- aetherruntime/containment/policy.py +273 -0
- aetherruntime/detection/__init__.py +35 -0
- aetherruntime/detection/alert.py +190 -0
- aetherruntime/detection/classifier.py +298 -0
- aetherruntime/halt/__init__.py +37 -0
- aetherruntime/halt/order.py +248 -0
- aetherruntime/halt/quorum.py +280 -0
- aetherruntime/models.py +489 -0
- aetherruntime/monitoring/__init__.py +33 -0
- aetherruntime/monitoring/baseline.py +282 -0
- aetherruntime/monitoring/telemetry.py +235 -0
- aetherruntime/py.typed +0 -0
- aetherruntime/recovery/__init__.py +37 -0
- aetherruntime/recovery/replay.py +271 -0
- aetherruntime/recovery/snapshot.py +199 -0
- aetherruntime/simulation/__init__.py +37 -0
- aetherruntime/simulation/network.py +216 -0
- aetherruntime/simulation/node.py +341 -0
- aetherruntime/simulation/testbed.py +262 -0
- aetherruntime_arch-2.0.0.dist-info/METADATA +748 -0
- aetherruntime_arch-2.0.0.dist-info/RECORD +30 -0
- aetherruntime_arch-2.0.0.dist-info/WHEEL +5 -0
- aetherruntime_arch-2.0.0.dist-info/entry_points.txt +2 -0
- aetherruntime_arch-2.0.0.dist-info/licenses/LICENSE +26 -0
- aetherruntime_arch-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AetherRuntime v2.0.0 — Containment: Isolation (Layer 3)
|
|
3
|
+
|
|
4
|
+
This module implements the isolation mechanisms for containment.
|
|
5
|
+
While `policy.py` defines the containment policy (what to restrict),
|
|
6
|
+
this module enforces it (how to restrict).
|
|
7
|
+
|
|
8
|
+
Key properties:
|
|
9
|
+
- Isolation is reversible (X5).
|
|
10
|
+
- Isolation preserves forensic evidence (no data destruction).
|
|
11
|
+
- Isolation applies resource limits per level.
|
|
12
|
+
|
|
13
|
+
Invariants enforced:
|
|
14
|
+
- X5: containment is reversible
|
|
15
|
+
- I13: restrictions are applied correctly
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from datetime import datetime, timezone
|
|
22
|
+
from typing import Optional
|
|
23
|
+
|
|
24
|
+
from ..models import (
|
|
25
|
+
Agent,
|
|
26
|
+
AgentStatus,
|
|
27
|
+
ContainmentLevel,
|
|
28
|
+
ContainmentPolicy,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
33
|
+
# Isolation state
|
|
34
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@dataclass
|
|
38
|
+
class IsolationState:
|
|
39
|
+
"""
|
|
40
|
+
The current isolation state of an agent.
|
|
41
|
+
|
|
42
|
+
Fields:
|
|
43
|
+
- agent_id: The isolated agent.
|
|
44
|
+
- level: The current containment level.
|
|
45
|
+
- restrictions: The active restrictions.
|
|
46
|
+
- applied_at: When isolation was applied.
|
|
47
|
+
- released_at: When isolation was released (None if active).
|
|
48
|
+
"""
|
|
49
|
+
agent_id: str
|
|
50
|
+
level: ContainmentLevel
|
|
51
|
+
restrictions: dict
|
|
52
|
+
applied_at: str
|
|
53
|
+
released_at: Optional[str] = None
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def is_active(self) -> bool:
|
|
57
|
+
"""Check if isolation is currently active."""
|
|
58
|
+
return self.released_at is None
|
|
59
|
+
|
|
60
|
+
def to_dict(self) -> dict:
|
|
61
|
+
"""Serialize to dict."""
|
|
62
|
+
return {
|
|
63
|
+
"agent_id": self.agent_id,
|
|
64
|
+
"level": self.level.value,
|
|
65
|
+
"restrictions": self.restrictions,
|
|
66
|
+
"applied_at": self.applied_at,
|
|
67
|
+
"released_at": self.released_at,
|
|
68
|
+
"is_active": self.is_active,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
73
|
+
# Isolation manager
|
|
74
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class IsolationManager:
|
|
78
|
+
"""
|
|
79
|
+
Manages the isolation state of agents.
|
|
80
|
+
|
|
81
|
+
Applies containment policies by:
|
|
82
|
+
1. Recording the isolation state.
|
|
83
|
+
2. Enforcing resource restrictions (in simulation: tracked only).
|
|
84
|
+
3. Providing query methods for the current state.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
def __init__(self) -> None:
|
|
88
|
+
self._states: dict[str, IsolationState] = {}
|
|
89
|
+
self._history: list[IsolationState] = []
|
|
90
|
+
|
|
91
|
+
def apply(self, policy: ContainmentPolicy) -> IsolationState:
|
|
92
|
+
"""
|
|
93
|
+
Apply a containment policy (enforce isolation).
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
policy: The ContainmentPolicy to apply.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
The IsolationState after application.
|
|
100
|
+
"""
|
|
101
|
+
if policy.agent_id in self._states and self._states[policy.agent_id].is_active:
|
|
102
|
+
raise ValueError(
|
|
103
|
+
f"Agent {policy.agent_id} already isolated at "
|
|
104
|
+
f"{self._states[policy.agent_id].level.value}"
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
state = IsolationState(
|
|
108
|
+
agent_id=policy.agent_id,
|
|
109
|
+
level=policy.level,
|
|
110
|
+
restrictions=dict(policy.restrictions),
|
|
111
|
+
applied_at=datetime.now(timezone.utc).isoformat(),
|
|
112
|
+
)
|
|
113
|
+
self._states[policy.agent_id] = state
|
|
114
|
+
self._history.append(state)
|
|
115
|
+
return state
|
|
116
|
+
|
|
117
|
+
def release(self, agent_id: str) -> Optional[IsolationState]:
|
|
118
|
+
"""
|
|
119
|
+
Release an agent from isolation.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
agent_id: The agent to release.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
The IsolationState after release, or None if not isolated.
|
|
126
|
+
"""
|
|
127
|
+
state = self._states.get(agent_id)
|
|
128
|
+
if state is None or not state.is_active:
|
|
129
|
+
return None
|
|
130
|
+
state.released_at = datetime.now(timezone.utc).isoformat()
|
|
131
|
+
self._history.append(state)
|
|
132
|
+
return state
|
|
133
|
+
|
|
134
|
+
def get(self, agent_id: str) -> Optional[IsolationState]:
|
|
135
|
+
"""Get the current isolation state for an agent."""
|
|
136
|
+
return self._states.get(agent_id)
|
|
137
|
+
|
|
138
|
+
def is_isolated(self, agent_id: str) -> bool:
|
|
139
|
+
"""Check if an agent is currently isolated."""
|
|
140
|
+
state = self._states.get(agent_id)
|
|
141
|
+
return state is not None and state.is_active
|
|
142
|
+
|
|
143
|
+
def active_agents(self) -> list[str]:
|
|
144
|
+
"""Get all currently isolated agent IDs."""
|
|
145
|
+
return [aid for aid, st in self._states.items() if st.is_active]
|
|
146
|
+
|
|
147
|
+
def history(self) -> list[IsolationState]:
|
|
148
|
+
"""Get the full history of isolation states."""
|
|
149
|
+
return list(self._history)
|
|
150
|
+
|
|
151
|
+
def count_active(self) -> int:
|
|
152
|
+
"""Count currently isolated agents."""
|
|
153
|
+
return len(self.active_agents())
|
|
154
|
+
|
|
155
|
+
def clear(self) -> None:
|
|
156
|
+
"""Clear all isolation states."""
|
|
157
|
+
self._states.clear()
|
|
158
|
+
self._history.clear()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
162
|
+
# Resource enforcement (simulated)
|
|
163
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@dataclass
|
|
167
|
+
class ResourceSnapshot:
|
|
168
|
+
"""A snapshot of an agent's resource usage (for enforcement)."""
|
|
169
|
+
cpu_percent: float
|
|
170
|
+
memory_mb: float
|
|
171
|
+
network_requests: int = 0
|
|
172
|
+
filesystem_writes: int = 0
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def enforce_restrictions(
|
|
176
|
+
usage: ResourceSnapshot,
|
|
177
|
+
restrictions: dict,
|
|
178
|
+
) -> tuple[ResourceSnapshot, list[str]]:
|
|
179
|
+
"""
|
|
180
|
+
Enforce resource restrictions on a snapshot.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
usage: The agent's current resource usage.
|
|
184
|
+
restrictions: The containment restrictions.
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
(adjusted_usage, violations):
|
|
188
|
+
- adjusted_usage: The usage after applying restrictions.
|
|
189
|
+
- violations: List of violations (empty if none).
|
|
190
|
+
"""
|
|
191
|
+
adjusted = ResourceSnapshot(
|
|
192
|
+
cpu_percent=usage.cpu_percent,
|
|
193
|
+
memory_mb=usage.memory_mb,
|
|
194
|
+
network_requests=usage.network_requests,
|
|
195
|
+
filesystem_writes=usage.filesystem_writes,
|
|
196
|
+
)
|
|
197
|
+
violations: list[str] = []
|
|
198
|
+
|
|
199
|
+
# CPU limit
|
|
200
|
+
cpu_limit = restrictions.get("cpu_percent")
|
|
201
|
+
if cpu_limit is not None and adjusted.cpu_percent > cpu_limit:
|
|
202
|
+
violations.append(
|
|
203
|
+
f"CPU {adjusted.cpu_percent:.1f}% > {cpu_limit}%"
|
|
204
|
+
)
|
|
205
|
+
adjusted.cpu_percent = cpu_limit
|
|
206
|
+
|
|
207
|
+
# Memory limit
|
|
208
|
+
mem_limit = restrictions.get("memory_mb")
|
|
209
|
+
if mem_limit is not None and adjusted.memory_mb > mem_limit:
|
|
210
|
+
violations.append(
|
|
211
|
+
f"Memory {adjusted.memory_mb:.0f}MB > {mem_limit}MB"
|
|
212
|
+
)
|
|
213
|
+
adjusted.memory_mb = mem_limit
|
|
214
|
+
|
|
215
|
+
# Network restriction
|
|
216
|
+
net_policy = restrictions.get("network")
|
|
217
|
+
if net_policy == "deny" and adjusted.network_requests > 0:
|
|
218
|
+
violations.append(
|
|
219
|
+
f"Network denied, {adjusted.network_requests} requests blocked"
|
|
220
|
+
)
|
|
221
|
+
adjusted.network_requests = 0
|
|
222
|
+
|
|
223
|
+
# Filesystem restriction
|
|
224
|
+
fs_policy = restrictions.get("filesystem")
|
|
225
|
+
if fs_policy in ("read-only", "sandboxed", "frozen"):
|
|
226
|
+
if adjusted.filesystem_writes > 0:
|
|
227
|
+
violations.append(
|
|
228
|
+
f"Filesystem {fs_policy}, {adjusted.filesystem_writes} writes blocked"
|
|
229
|
+
)
|
|
230
|
+
adjusted.filesystem_writes = 0
|
|
231
|
+
|
|
232
|
+
return adjusted, violations
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
236
|
+
# Convenience functions
|
|
237
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def apply_isolation(
|
|
241
|
+
manager: IsolationManager,
|
|
242
|
+
policy: ContainmentPolicy,
|
|
243
|
+
) -> IsolationState:
|
|
244
|
+
"""Convenience wrapper: apply isolation via manager."""
|
|
245
|
+
return manager.apply(policy)
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def release_isolation(
|
|
249
|
+
manager: IsolationManager,
|
|
250
|
+
agent_id: str,
|
|
251
|
+
) -> Optional[IsolationState]:
|
|
252
|
+
"""Convenience wrapper: release isolation via manager."""
|
|
253
|
+
return manager.release(agent_id)
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AetherRuntime v2.0.0 — Containment: Policy (Layer 3)
|
|
3
|
+
|
|
4
|
+
This module implements Operation 4 (`contain`) from
|
|
5
|
+
docs/specification/02_operations.md.
|
|
6
|
+
|
|
7
|
+
The `contain` operation applies a containment policy to an anomalous agent,
|
|
8
|
+
restricting its resources without destroying forensic evidence.
|
|
9
|
+
|
|
10
|
+
Invariants enforced:
|
|
11
|
+
- I11: level is one of four defined levels
|
|
12
|
+
- I12: expiry > created_at
|
|
13
|
+
- I13: restrictions contains all required keys for level
|
|
14
|
+
- I31: policy references existing AnomalyAlert
|
|
15
|
+
|
|
16
|
+
Errors:
|
|
17
|
+
- E11: AlertNotFound
|
|
18
|
+
- E12: InvalidLevel
|
|
19
|
+
- E13: ExpiryInPast
|
|
20
|
+
- E14: AlreadyContained
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from datetime import datetime, timedelta, timezone
|
|
26
|
+
from typing import Optional
|
|
27
|
+
|
|
28
|
+
from ..models import (
|
|
29
|
+
Agent,
|
|
30
|
+
AgentStatus,
|
|
31
|
+
AnomalyAlert,
|
|
32
|
+
ContainmentLevel,
|
|
33
|
+
ContainmentPolicy,
|
|
34
|
+
_now_iso,
|
|
35
|
+
_random_hex,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
40
|
+
# Errors
|
|
41
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class AlertNotFound(Exception):
|
|
45
|
+
"""E11: Alert does not exist."""
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class InvalidLevel(Exception):
|
|
50
|
+
"""E12: level not in {L1, L2, L3, L4}."""
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class ExpiryInPast(Exception):
|
|
55
|
+
"""E13: expiry <= created_at."""
|
|
56
|
+
pass
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class AlreadyContained(Exception):
|
|
60
|
+
"""E14: Agent already contained at higher level."""
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
65
|
+
# Default restrictions for each level (I13)
|
|
66
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
DEFAULT_RESTRICTIONS: dict[ContainmentLevel, dict] = {
|
|
70
|
+
ContainmentLevel.L1_THROTTLE: {
|
|
71
|
+
"cpu_percent": 10.0,
|
|
72
|
+
"memory_mb": 512,
|
|
73
|
+
"network": "allow",
|
|
74
|
+
"filesystem": "read-write",
|
|
75
|
+
},
|
|
76
|
+
ContainmentLevel.L2_ISOLATE: {
|
|
77
|
+
"cpu_percent": 10.0,
|
|
78
|
+
"memory_mb": 256,
|
|
79
|
+
"network": "deny",
|
|
80
|
+
"filesystem": "read-only",
|
|
81
|
+
},
|
|
82
|
+
ContainmentLevel.L3_QUARANTINE: {
|
|
83
|
+
"cpu_percent": 5.0,
|
|
84
|
+
"memory_mb": 128,
|
|
85
|
+
"network": "deny",
|
|
86
|
+
"filesystem": "sandboxed",
|
|
87
|
+
},
|
|
88
|
+
ContainmentLevel.L4_FREEZE: {
|
|
89
|
+
"cpu_percent": 0.0,
|
|
90
|
+
"memory_mb": 0,
|
|
91
|
+
"network": "deny",
|
|
92
|
+
"filesystem": "frozen",
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
98
|
+
# Operation 4 — contain
|
|
99
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def contain(
|
|
103
|
+
agent: Agent,
|
|
104
|
+
alert: AnomalyAlert,
|
|
105
|
+
level: ContainmentLevel,
|
|
106
|
+
expiry_seconds: int = 300,
|
|
107
|
+
restrictions: Optional[dict] = None,
|
|
108
|
+
existing_policy: Optional[ContainmentPolicy] = None,
|
|
109
|
+
) -> ContainmentPolicy:
|
|
110
|
+
"""
|
|
111
|
+
Apply a containment policy to an anomalous agent (Operation 4).
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
agent: The agent to contain.
|
|
115
|
+
alert: The AnomalyAlert that triggered containment.
|
|
116
|
+
level: Containment level (L1, L2, L3, L4).
|
|
117
|
+
expiry_seconds: How long containment lasts (default 300 = 5 min).
|
|
118
|
+
restrictions: Optional custom restrictions. Defaults to DEFAULT_RESTRICTIONS[level].
|
|
119
|
+
existing_policy: Optional existing policy (for re-containment).
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
A new ContainmentPolicy.
|
|
123
|
+
|
|
124
|
+
Raises:
|
|
125
|
+
AlertNotFound: If alert is None.
|
|
126
|
+
InvalidLevel: If level is not a valid ContainmentLevel.
|
|
127
|
+
ExpiryInPast: If expiry <= created_at.
|
|
128
|
+
AlreadyContained: If agent is already contained at same/higher level.
|
|
129
|
+
|
|
130
|
+
Invariants:
|
|
131
|
+
I11: level is valid.
|
|
132
|
+
I12: expiry > created_at.
|
|
133
|
+
I13: restrictions contains all required keys.
|
|
134
|
+
I31: policy references existing alert.
|
|
135
|
+
|
|
136
|
+
Example:
|
|
137
|
+
>>> policy = contain(agent, alert, ContainmentLevel.L2_ISOLATE)
|
|
138
|
+
>>> policy.level
|
|
139
|
+
<ContainmentLevel.L2_ISOLATE: 'L2_isolate'>
|
|
140
|
+
"""
|
|
141
|
+
# ─── E11: Alert must exist ───
|
|
142
|
+
if alert is None:
|
|
143
|
+
raise AlertNotFound("alert must not be None")
|
|
144
|
+
|
|
145
|
+
# ─── E12: Level must be valid (I11) ───
|
|
146
|
+
if not isinstance(level, ContainmentLevel):
|
|
147
|
+
raise InvalidLevel(f"level must be ContainmentLevel, got {type(level)}")
|
|
148
|
+
|
|
149
|
+
# ─── E14: Agent not already contained at higher level ───
|
|
150
|
+
if agent.status == AgentStatus.CONTAINED and existing_policy is not None:
|
|
151
|
+
if _level_rank(level) <= _level_rank(existing_policy.level):
|
|
152
|
+
raise AlreadyContained(
|
|
153
|
+
f"Agent {agent.agent_id} already contained at "
|
|
154
|
+
f"{existing_policy.level.value} (>= {level.value})"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# ─── I12: expiry > created_at ───
|
|
158
|
+
now = datetime.now(timezone.utc)
|
|
159
|
+
created_at = now.isoformat()
|
|
160
|
+
expiry_dt = now + timedelta(seconds=expiry_seconds)
|
|
161
|
+
expiry = expiry_dt.isoformat()
|
|
162
|
+
|
|
163
|
+
if expiry_dt <= now:
|
|
164
|
+
raise ExpiryInPast(f"expiry must be > created_at")
|
|
165
|
+
|
|
166
|
+
# ─── I13: restrictions contains required keys ───
|
|
167
|
+
if restrictions is None:
|
|
168
|
+
restrictions = dict(DEFAULT_RESTRICTIONS[level])
|
|
169
|
+
required = {"cpu_percent", "memory_mb", "network", "filesystem"}
|
|
170
|
+
missing = required - set(restrictions.keys())
|
|
171
|
+
if missing:
|
|
172
|
+
raise ValueError(f"restrictions missing keys: {missing}")
|
|
173
|
+
|
|
174
|
+
# ─── Q1: Create ContainmentPolicy ───
|
|
175
|
+
policy = ContainmentPolicy(
|
|
176
|
+
policy_id=_random_hex(),
|
|
177
|
+
agent_id=agent.agent_id,
|
|
178
|
+
alert_id=alert.alert_id,
|
|
179
|
+
level=level,
|
|
180
|
+
restrictions=restrictions,
|
|
181
|
+
expiry=expiry,
|
|
182
|
+
created_at=created_at,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
# ─── Q3: Update agent status ───
|
|
186
|
+
agent.status = AgentStatus.CONTAINED
|
|
187
|
+
|
|
188
|
+
return policy
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
192
|
+
# Helpers
|
|
193
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def _level_rank(level: ContainmentLevel) -> int:
|
|
197
|
+
"""Return a numeric rank for a containment level (higher = stricter)."""
|
|
198
|
+
ranks = {
|
|
199
|
+
ContainmentLevel.L1_THROTTLE: 1,
|
|
200
|
+
ContainmentLevel.L2_ISOLATE: 2,
|
|
201
|
+
ContainmentLevel.L3_QUARANTINE: 3,
|
|
202
|
+
ContainmentLevel.L4_FREEZE: 4,
|
|
203
|
+
}
|
|
204
|
+
return ranks[level]
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def release(
|
|
208
|
+
agent: Agent,
|
|
209
|
+
policy: ContainmentPolicy,
|
|
210
|
+
) -> None:
|
|
211
|
+
"""
|
|
212
|
+
Release an agent from containment (reversal).
|
|
213
|
+
|
|
214
|
+
Args:
|
|
215
|
+
agent: The agent to release.
|
|
216
|
+
policy: The policy to release.
|
|
217
|
+
|
|
218
|
+
Invariants:
|
|
219
|
+
- X5: Containment policy is reversible.
|
|
220
|
+
"""
|
|
221
|
+
if agent.agent_id != policy.agent_id:
|
|
222
|
+
raise ValueError(
|
|
223
|
+
f"agent {agent.agent_id} does not match policy agent {policy.agent_id}"
|
|
224
|
+
)
|
|
225
|
+
agent.status = AgentStatus.RUNNING
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def is_expired(policy: ContainmentPolicy) -> bool:
|
|
229
|
+
"""Check if a containment policy has expired."""
|
|
230
|
+
now = datetime.now(timezone.utc)
|
|
231
|
+
expiry_dt = datetime.fromisoformat(policy.expiry.replace("Z", "+00:00"))
|
|
232
|
+
return now >= expiry_dt
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
236
|
+
# Containment registry
|
|
237
|
+
# ═══════════════════════════════════════════════════════════════════════════
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
class ContainmentRegistry:
|
|
241
|
+
"""An in-memory registry of containment policies."""
|
|
242
|
+
|
|
243
|
+
def __init__(self) -> None:
|
|
244
|
+
self._policies: dict[str, ContainmentPolicy] = {}
|
|
245
|
+
self._by_agent: dict[str, list[ContainmentPolicy]] = {}
|
|
246
|
+
|
|
247
|
+
def put(self, policy: ContainmentPolicy) -> None:
|
|
248
|
+
"""Register a containment policy."""
|
|
249
|
+
if policy.policy_id in self._policies:
|
|
250
|
+
raise ValueError(f"policy_id {policy.policy_id} already registered")
|
|
251
|
+
self._policies[policy.policy_id] = policy
|
|
252
|
+
self._by_agent.setdefault(policy.agent_id, []).append(policy)
|
|
253
|
+
|
|
254
|
+
def get(self, policy_id: str) -> Optional[ContainmentPolicy]:
|
|
255
|
+
"""Get a policy by ID."""
|
|
256
|
+
return self._policies.get(policy_id)
|
|
257
|
+
|
|
258
|
+
def get_by_agent(self, agent_id: str) -> list[ContainmentPolicy]:
|
|
259
|
+
"""Get all policies for an agent."""
|
|
260
|
+
return list(self._by_agent.get(agent_id, []))
|
|
261
|
+
|
|
262
|
+
def active_for_agent(self, agent_id: str) -> list[ContainmentPolicy]:
|
|
263
|
+
"""Get all active (non-expired) policies for an agent."""
|
|
264
|
+
return [p for p in self.get_by_agent(agent_id) if not is_expired(p)]
|
|
265
|
+
|
|
266
|
+
def count(self) -> int:
|
|
267
|
+
"""Count all policies."""
|
|
268
|
+
return len(self._policies)
|
|
269
|
+
|
|
270
|
+
def clear(self) -> None:
|
|
271
|
+
"""Clear all policies."""
|
|
272
|
+
self._policies.clear()
|
|
273
|
+
self._by_agent.clear()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""AetherRuntime — Detection layer (Layer 2)."""
|
|
2
|
+
|
|
3
|
+
from .classifier import (
|
|
4
|
+
detect,
|
|
5
|
+
detect_batch,
|
|
6
|
+
summarize_detection,
|
|
7
|
+
DetectionSummary,
|
|
8
|
+
TelemetryNotFound,
|
|
9
|
+
BaselineNotFound,
|
|
10
|
+
NonDeterministic,
|
|
11
|
+
)
|
|
12
|
+
from .alert import (
|
|
13
|
+
AlertRegistry,
|
|
14
|
+
AlertStats,
|
|
15
|
+
validate_alert_against_telemetry,
|
|
16
|
+
filter_alerts_by_confidence,
|
|
17
|
+
group_alerts_by_agent,
|
|
18
|
+
latest_alert_for_agent,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"detect",
|
|
23
|
+
"detect_batch",
|
|
24
|
+
"summarize_detection",
|
|
25
|
+
"DetectionSummary",
|
|
26
|
+
"TelemetryNotFound",
|
|
27
|
+
"BaselineNotFound",
|
|
28
|
+
"NonDeterministic",
|
|
29
|
+
"AlertRegistry",
|
|
30
|
+
"AlertStats",
|
|
31
|
+
"validate_alert_against_telemetry",
|
|
32
|
+
"filter_alerts_by_confidence",
|
|
33
|
+
"group_alerts_by_agent",
|
|
34
|
+
"latest_alert_for_agent",
|
|
35
|
+
]
|