guard-agent 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.
- guard_agent/__init__.py +66 -0
- guard_agent/buffer.py +322 -0
- guard_agent/client.py +353 -0
- guard_agent/encryption.py +192 -0
- guard_agent/models.py +231 -0
- guard_agent/protocols.py +55 -0
- guard_agent/py.typed +0 -0
- guard_agent/transport.py +381 -0
- guard_agent/utils.py +195 -0
- guard_agent-2.0.0.dist-info/METADATA +335 -0
- guard_agent-2.0.0.dist-info/RECORD +14 -0
- guard_agent-2.0.0.dist-info/WHEEL +5 -0
- guard_agent-2.0.0.dist-info/licenses/LICENSE +21 -0
- guard_agent-2.0.0.dist-info/top_level.txt +1 -0
guard_agent/__init__.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Guard Agent — framework-agnostic telemetry agent for the Guard ecosystem.
|
|
3
|
+
|
|
4
|
+
Provides telemetry capabilities for the Guard adapters (``fastapi-guard``,
|
|
5
|
+
``flaskapi-guard``, ``djangoapi-guard``, ``tornadoapi-guard``), enabling
|
|
6
|
+
monitoring, analytics, and dynamic rule management through a centralized
|
|
7
|
+
management platform.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from guard_agent.buffer import EventBuffer
|
|
11
|
+
from guard_agent.client import GuardAgentHandler, guard_agent
|
|
12
|
+
from guard_agent.models import (
|
|
13
|
+
AgentConfig,
|
|
14
|
+
AgentStatus,
|
|
15
|
+
DynamicRules,
|
|
16
|
+
EventBatch,
|
|
17
|
+
SecurityEvent,
|
|
18
|
+
SecurityMetric,
|
|
19
|
+
)
|
|
20
|
+
from guard_agent.protocols import (
|
|
21
|
+
AgentHandlerProtocol,
|
|
22
|
+
BufferProtocol,
|
|
23
|
+
RedisHandlerProtocol,
|
|
24
|
+
TransportProtocol,
|
|
25
|
+
)
|
|
26
|
+
from guard_agent.transport import HTTPTransport
|
|
27
|
+
from guard_agent.utils import (
|
|
28
|
+
CircuitBreaker,
|
|
29
|
+
RateLimiter,
|
|
30
|
+
generate_batch_id,
|
|
31
|
+
get_current_timestamp,
|
|
32
|
+
hash_ip,
|
|
33
|
+
sanitize_headers,
|
|
34
|
+
setup_agent_logging,
|
|
35
|
+
truncate_payload,
|
|
36
|
+
validate_config,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
__version__ = "2.0.0"
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"guard_agent",
|
|
43
|
+
"GuardAgentHandler",
|
|
44
|
+
"AgentConfig",
|
|
45
|
+
"SecurityEvent",
|
|
46
|
+
"SecurityMetric",
|
|
47
|
+
"DynamicRules",
|
|
48
|
+
"AgentStatus",
|
|
49
|
+
"EventBatch",
|
|
50
|
+
"EventBuffer",
|
|
51
|
+
"HTTPTransport",
|
|
52
|
+
"AgentHandlerProtocol",
|
|
53
|
+
"TransportProtocol",
|
|
54
|
+
"BufferProtocol",
|
|
55
|
+
"RedisHandlerProtocol",
|
|
56
|
+
"generate_batch_id",
|
|
57
|
+
"get_current_timestamp",
|
|
58
|
+
"hash_ip",
|
|
59
|
+
"sanitize_headers",
|
|
60
|
+
"truncate_payload",
|
|
61
|
+
"validate_config",
|
|
62
|
+
"setup_agent_logging",
|
|
63
|
+
"RateLimiter",
|
|
64
|
+
"CircuitBreaker",
|
|
65
|
+
"__version__",
|
|
66
|
+
]
|
guard_agent/buffer.py
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import time
|
|
4
|
+
from collections import deque
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from guard_agent.models import AgentConfig, SecurityEvent, SecurityMetric
|
|
8
|
+
from guard_agent.protocols import BufferProtocol, RedisHandlerProtocol
|
|
9
|
+
from guard_agent.utils import (
|
|
10
|
+
safe_json_deserialize,
|
|
11
|
+
safe_json_serialize,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class EventBuffer(BufferProtocol):
|
|
16
|
+
"""
|
|
17
|
+
Event buffer with Redis persistence and automatic flushing.
|
|
18
|
+
Follows fastapi-guard handler patterns.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def __init__(self, config: AgentConfig):
|
|
22
|
+
self.config = config
|
|
23
|
+
self.logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
self.event_buffer: deque[SecurityEvent] = deque(maxlen=config.buffer_size)
|
|
26
|
+
self.metric_buffer: deque[SecurityMetric] = deque(maxlen=config.buffer_size)
|
|
27
|
+
|
|
28
|
+
self.redis_handler: RedisHandlerProtocol | None = None
|
|
29
|
+
|
|
30
|
+
self._flush_task: asyncio.Task | None = None
|
|
31
|
+
self._running = False
|
|
32
|
+
|
|
33
|
+
self.events_buffered = 0
|
|
34
|
+
self.metrics_buffered = 0
|
|
35
|
+
self.events_flushed = 0
|
|
36
|
+
self.metrics_flushed = 0
|
|
37
|
+
self.last_flush_time: float | None = None
|
|
38
|
+
|
|
39
|
+
async def initialize_redis(self, redis_handler: RedisHandlerProtocol) -> None:
|
|
40
|
+
"""Initialize Redis connection for persistent buffering."""
|
|
41
|
+
self.redis_handler = redis_handler
|
|
42
|
+
await self._load_from_redis()
|
|
43
|
+
|
|
44
|
+
async def start_auto_flush(self) -> None:
|
|
45
|
+
"""Start automatic buffer flushing."""
|
|
46
|
+
if self._flush_task and not self._flush_task.done():
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
self._running = True
|
|
50
|
+
self._flush_task = asyncio.create_task(self._auto_flush_loop())
|
|
51
|
+
|
|
52
|
+
async def stop_auto_flush(self) -> None:
|
|
53
|
+
"""Stop automatic buffer flushing."""
|
|
54
|
+
self._running = False
|
|
55
|
+
if self._flush_task and not self._flush_task.done():
|
|
56
|
+
self._flush_task.cancel()
|
|
57
|
+
try:
|
|
58
|
+
await self._flush_task
|
|
59
|
+
except asyncio.CancelledError:
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
async def add_event(self, event: SecurityEvent) -> None:
|
|
63
|
+
"""Add security event to buffer."""
|
|
64
|
+
try:
|
|
65
|
+
self.event_buffer.append(event)
|
|
66
|
+
self.events_buffered += 1
|
|
67
|
+
|
|
68
|
+
if self.redis_handler:
|
|
69
|
+
await self._persist_event_to_redis(event)
|
|
70
|
+
|
|
71
|
+
if len(self.event_buffer) >= self.config.buffer_size:
|
|
72
|
+
asyncio.create_task(self._flush_if_needed())
|
|
73
|
+
|
|
74
|
+
except Exception as e:
|
|
75
|
+
self.logger.error(f"Failed to buffer event: {str(e)}")
|
|
76
|
+
|
|
77
|
+
async def add_metric(self, metric: SecurityMetric) -> None:
|
|
78
|
+
"""Add metric to buffer."""
|
|
79
|
+
try:
|
|
80
|
+
self.metric_buffer.append(metric)
|
|
81
|
+
self.metrics_buffered += 1
|
|
82
|
+
|
|
83
|
+
if self.redis_handler:
|
|
84
|
+
await self._persist_metric_to_redis(metric)
|
|
85
|
+
|
|
86
|
+
if len(self.metric_buffer) >= self.config.buffer_size:
|
|
87
|
+
asyncio.create_task(self._flush_if_needed())
|
|
88
|
+
|
|
89
|
+
except Exception as e:
|
|
90
|
+
self.logger.error(f"Failed to buffer metric: {str(e)}")
|
|
91
|
+
|
|
92
|
+
async def flush_events(self) -> list[SecurityEvent]:
|
|
93
|
+
"""Flush and return all buffered events."""
|
|
94
|
+
events = list(self.event_buffer)
|
|
95
|
+
self.event_buffer.clear()
|
|
96
|
+
self.events_flushed += len(events)
|
|
97
|
+
|
|
98
|
+
if self.redis_handler and events:
|
|
99
|
+
await self._clear_events_from_redis(len(events))
|
|
100
|
+
|
|
101
|
+
self.last_flush_time = time.time()
|
|
102
|
+
return events
|
|
103
|
+
|
|
104
|
+
async def flush_metrics(self) -> list[SecurityMetric]:
|
|
105
|
+
"""Flush and return all buffered metrics."""
|
|
106
|
+
metrics = list(self.metric_buffer)
|
|
107
|
+
self.metric_buffer.clear()
|
|
108
|
+
self.metrics_flushed += len(metrics)
|
|
109
|
+
|
|
110
|
+
if self.redis_handler and metrics:
|
|
111
|
+
await self._clear_metrics_from_redis(len(metrics))
|
|
112
|
+
|
|
113
|
+
self.last_flush_time = time.time()
|
|
114
|
+
return metrics
|
|
115
|
+
|
|
116
|
+
async def get_buffer_size(self) -> int:
|
|
117
|
+
"""Get current total buffer size."""
|
|
118
|
+
return len(self.event_buffer) + len(self.metric_buffer)
|
|
119
|
+
|
|
120
|
+
async def clear_buffer(self) -> None:
|
|
121
|
+
"""Clear all buffers."""
|
|
122
|
+
self.event_buffer.clear()
|
|
123
|
+
self.metric_buffer.clear()
|
|
124
|
+
|
|
125
|
+
if self.redis_handler:
|
|
126
|
+
await self._clear_redis_buffers()
|
|
127
|
+
|
|
128
|
+
async def _auto_flush_loop(self) -> None:
|
|
129
|
+
"""Automatic flush loop."""
|
|
130
|
+
while self._running:
|
|
131
|
+
try:
|
|
132
|
+
await asyncio.sleep(self.config.flush_interval)
|
|
133
|
+
if self._running:
|
|
134
|
+
await self._flush_if_needed()
|
|
135
|
+
except asyncio.CancelledError:
|
|
136
|
+
break
|
|
137
|
+
except Exception as e:
|
|
138
|
+
self.logger.error(f"Error in auto flush loop: {str(e)}")
|
|
139
|
+
|
|
140
|
+
async def _flush_if_needed(self) -> None:
|
|
141
|
+
"""Check if flush is needed and perform it."""
|
|
142
|
+
current_time = time.time()
|
|
143
|
+
|
|
144
|
+
time_since_last_flush = (
|
|
145
|
+
current_time - self.last_flush_time
|
|
146
|
+
if self.last_flush_time
|
|
147
|
+
else self.config.flush_interval + 1
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
buffer_size = await self.get_buffer_size()
|
|
151
|
+
should_flush = (
|
|
152
|
+
buffer_size >= self.config.buffer_size * 0.8
|
|
153
|
+
or time_since_last_flush >= self.config.flush_interval
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
if should_flush and buffer_size > 0:
|
|
157
|
+
self.logger.debug(f"Triggering buffer flush - size: {buffer_size}")
|
|
158
|
+
# NOTE: This method doesn't actually send data, just marks it ready.
|
|
159
|
+
# The actual sending is handled by the transport layer.
|
|
160
|
+
|
|
161
|
+
async def _persist_event_to_redis(self, event: SecurityEvent) -> None:
|
|
162
|
+
"""Persist event to Redis for durability."""
|
|
163
|
+
if not self.redis_handler:
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
try:
|
|
167
|
+
key = f"event_{int(time.time() * 1000)}"
|
|
168
|
+
data = event.model_dump() if hasattr(event, "model_dump") else vars(event)
|
|
169
|
+
serialized = await safe_json_serialize(data)
|
|
170
|
+
await self.redis_handler.set_key(
|
|
171
|
+
"agent_events",
|
|
172
|
+
key,
|
|
173
|
+
serialized,
|
|
174
|
+
ttl=3600,
|
|
175
|
+
)
|
|
176
|
+
except Exception as e:
|
|
177
|
+
self.logger.warning(f"Failed to persist event to Redis: {str(e)}")
|
|
178
|
+
|
|
179
|
+
async def _persist_metric_to_redis(self, metric: SecurityMetric) -> None:
|
|
180
|
+
"""Persist metric to Redis for durability."""
|
|
181
|
+
if not self.redis_handler:
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
try:
|
|
185
|
+
key = f"metric_{int(time.time() * 1000)}"
|
|
186
|
+
if hasattr(metric, "model_dump"):
|
|
187
|
+
data = metric.model_dump()
|
|
188
|
+
else:
|
|
189
|
+
data = vars(metric)
|
|
190
|
+
serialized = await safe_json_serialize(data)
|
|
191
|
+
await self.redis_handler.set_key(
|
|
192
|
+
"agent_metrics",
|
|
193
|
+
key,
|
|
194
|
+
serialized,
|
|
195
|
+
ttl=3600,
|
|
196
|
+
)
|
|
197
|
+
except Exception as e:
|
|
198
|
+
self.logger.warning(f"Failed to persist metric to Redis: {str(e)}")
|
|
199
|
+
|
|
200
|
+
async def _load_from_redis(self) -> None:
|
|
201
|
+
"""Load any persisted events/metrics from Redis on startup."""
|
|
202
|
+
if not self.redis_handler:
|
|
203
|
+
return
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
event_keys = await self.redis_handler.keys("agent_events:*") or []
|
|
207
|
+
for key in event_keys:
|
|
208
|
+
try:
|
|
209
|
+
event_data = await self.redis_handler.get_key(
|
|
210
|
+
"agent_events", key.split(":")[-1]
|
|
211
|
+
)
|
|
212
|
+
if event_data:
|
|
213
|
+
event_dict = await safe_json_deserialize(event_data)
|
|
214
|
+
if event_dict:
|
|
215
|
+
event = SecurityEvent(**event_dict)
|
|
216
|
+
self.event_buffer.append(event)
|
|
217
|
+
self.events_buffered += 1
|
|
218
|
+
else:
|
|
219
|
+
message = f"Failed to load event from Redis key {key}"
|
|
220
|
+
self.logger.warning(f"{message}: No data found for key")
|
|
221
|
+
except Exception as e:
|
|
222
|
+
self.logger.warning(
|
|
223
|
+
f"Failed to load event from Redis key {key}: {e}"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
metric_keys = await self.redis_handler.keys("agent_metrics:*") or []
|
|
227
|
+
for key in metric_keys:
|
|
228
|
+
try:
|
|
229
|
+
metric_data = await self.redis_handler.get_key(
|
|
230
|
+
"agent_metrics", key.split(":")[-1]
|
|
231
|
+
)
|
|
232
|
+
if metric_data:
|
|
233
|
+
metric_dict = await safe_json_deserialize(metric_data)
|
|
234
|
+
if metric_dict:
|
|
235
|
+
metric = SecurityMetric(**metric_dict)
|
|
236
|
+
self.metric_buffer.append(metric)
|
|
237
|
+
self.metrics_buffered += 1
|
|
238
|
+
else:
|
|
239
|
+
message = f"Failed to load metric from Redis key {key}"
|
|
240
|
+
self.logger.warning(f"{message}: No data found for key")
|
|
241
|
+
except Exception as e:
|
|
242
|
+
self.logger.warning(
|
|
243
|
+
f"Failed to load metric from Redis key {key}: {e}"
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
if self.event_buffer or self.metric_buffer:
|
|
247
|
+
loaded_events = f"Loaded {len(self.event_buffer)} events"
|
|
248
|
+
loaded_metrics = f"Loaded {len(self.metric_buffer)} metrics"
|
|
249
|
+
self.logger.info(f"{loaded_events} and {loaded_metrics} from Redis")
|
|
250
|
+
|
|
251
|
+
except Exception as e:
|
|
252
|
+
self.logger.warning(f"Failed to load from Redis: {str(e)}")
|
|
253
|
+
|
|
254
|
+
async def _clear_events_from_redis(self, count: int) -> None:
|
|
255
|
+
"""Clear flushed events from Redis."""
|
|
256
|
+
if not self.redis_handler:
|
|
257
|
+
return
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
event_keys = await self.redis_handler.keys("agent_events:*") or []
|
|
261
|
+
sorted_keys = sorted(event_keys)
|
|
262
|
+
|
|
263
|
+
for i, key in enumerate(sorted_keys):
|
|
264
|
+
if i >= count:
|
|
265
|
+
break
|
|
266
|
+
key_name = key.split(":")[-1]
|
|
267
|
+
await self.redis_handler.delete("agent_events", key_name)
|
|
268
|
+
|
|
269
|
+
except Exception as e:
|
|
270
|
+
self.logger.warning(f"Failed to clear events from Redis: {str(e)}")
|
|
271
|
+
|
|
272
|
+
async def _clear_metrics_from_redis(self, count: int) -> None:
|
|
273
|
+
"""Clear flushed metrics from Redis."""
|
|
274
|
+
if not self.redis_handler:
|
|
275
|
+
return
|
|
276
|
+
|
|
277
|
+
try:
|
|
278
|
+
metric_keys = await self.redis_handler.keys("agent_metrics:*") or []
|
|
279
|
+
sorted_keys = sorted(metric_keys)
|
|
280
|
+
|
|
281
|
+
for i, key in enumerate(sorted_keys):
|
|
282
|
+
if i >= count:
|
|
283
|
+
break
|
|
284
|
+
key_name = key.split(":")[-1]
|
|
285
|
+
await self.redis_handler.delete("agent_metrics", key_name)
|
|
286
|
+
|
|
287
|
+
except Exception as e:
|
|
288
|
+
self.logger.warning(f"Failed to clear metrics from Redis: {str(e)}")
|
|
289
|
+
|
|
290
|
+
async def _clear_redis_buffers(self) -> None:
|
|
291
|
+
"""Clear all Redis buffers."""
|
|
292
|
+
if not self.redis_handler:
|
|
293
|
+
return
|
|
294
|
+
|
|
295
|
+
try:
|
|
296
|
+
event_keys = await self.redis_handler.keys("agent_events:*") or []
|
|
297
|
+
for key in event_keys:
|
|
298
|
+
key_name = key.split(":")[-1]
|
|
299
|
+
await self.redis_handler.delete("agent_events", key_name)
|
|
300
|
+
|
|
301
|
+
metric_keys = await self.redis_handler.keys("agent_metrics:*") or []
|
|
302
|
+
for key in metric_keys:
|
|
303
|
+
key_name = key.split(":")[-1]
|
|
304
|
+
await self.redis_handler.delete("agent_metrics", key_name)
|
|
305
|
+
|
|
306
|
+
self.logger.info("Cleared all Redis buffers")
|
|
307
|
+
|
|
308
|
+
except Exception as e:
|
|
309
|
+
self.logger.warning(f"Failed to clear Redis buffers: {str(e)}")
|
|
310
|
+
|
|
311
|
+
def get_stats(self) -> dict[str, Any]:
|
|
312
|
+
"""Get buffer statistics."""
|
|
313
|
+
return {
|
|
314
|
+
"events_buffered": self.events_buffered,
|
|
315
|
+
"metrics_buffered": self.metrics_buffered,
|
|
316
|
+
"events_flushed": self.events_flushed,
|
|
317
|
+
"metrics_flushed": self.metrics_flushed,
|
|
318
|
+
"current_event_buffer_size": len(self.event_buffer),
|
|
319
|
+
"current_metric_buffer_size": len(self.metric_buffer),
|
|
320
|
+
"last_flush_time": self.last_flush_time,
|
|
321
|
+
"auto_flush_running": self._running,
|
|
322
|
+
}
|