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.
@@ -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
+ ]