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