Skip to content

Commit 15169a0

Browse files
authored
Merge pull request #2 from litemars/add-features
added persistance, file loggin and whitelist enforcement
2 parents 2cf71b3 + 2c39107 commit 15169a0

9 files changed

Lines changed: 918 additions & 7 deletions

File tree

config/config.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,14 @@ logging:
151151
# Number of backup log files to keep
152152
backup_count: 5
153153

154+
# =============================================================================
155+
# PERSISTENCE CONFIGURATION
156+
# =============================================================================
157+
persistence:
158+
# SQLite database path for persisting alerts and beacons
159+
# Set to null to disable persistence (in-memory only)
160+
db_path: "./beacon_detect.db"
161+
154162
# =============================================================================
155163
# WHITELIST CONFIGURATION
156164
# =============================================================================

control_plane/alerter.py

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,19 @@ def convert_value(v):
9090
def to_json(self):
9191
return json.dumps(self.to_dict(), indent=2)
9292

93+
@classmethod
94+
def from_dict(cls, data):
95+
return cls(
96+
alert_id=data["alert_id"],
97+
title=data["title"],
98+
description=data["description"],
99+
severity=AlertSeverity(data["severity"]),
100+
source=data["source"],
101+
details=data.get("details", {}),
102+
timestamp=data.get("timestamp", ""),
103+
tags=data.get("tags", []),
104+
)
105+
93106
def to_syslog_message(self):
94107
return (
95108
f"[{self.severity.value.upper()}] {self.title} | "
@@ -293,9 +306,10 @@ def send(self, alert: Alert):
293306

294307
class AlertManager:
295308

296-
def __init__(self, config=None):
309+
def __init__(self, config=None, persistence=None):
297310

298311
self.config = config or AlertingConfig()
312+
self._persistence = persistence
299313

300314
# Initialize handlers
301315
self._syslog = SyslogHandler(self.config)
@@ -406,6 +420,13 @@ def _deliver_alert(self, alert: Alert):
406420
if len(self._recent_alerts) > self._max_recent_alerts:
407421
self._recent_alerts = self._recent_alerts[-self._max_recent_alerts :]
408422

423+
# Persist to database
424+
if self._persistence:
425+
try:
426+
self._persistence.save_alert(alert.to_dict())
427+
except Exception as e:
428+
logger.error(f"Failed to persist alert: {e}")
429+
409430
def send_alert(self, alert: Alert):
410431

411432
if not self.config.enabled:
@@ -456,6 +477,28 @@ def get_recent_alerts(self, limit: int = 50, severity=None):
456477

457478
return [a.to_dict() for a in reversed(alerts)]
458479

480+
def load_historical_alerts(self):
481+
"""Load alerts from persistence on startup."""
482+
if not self._persistence:
483+
return
484+
485+
try:
486+
alert_dicts = self._persistence.load_alerts(limit=self._max_recent_alerts)
487+
for alert_dict in reversed(alert_dicts):
488+
try:
489+
alert = Alert.from_dict(alert_dict)
490+
self._recent_alerts.append(alert)
491+
except Exception as e:
492+
logger.warning(
493+
f"Failed to restore alert {alert_dict.get('alert_id')}: {e}"
494+
)
495+
496+
logger.info(
497+
f"Loaded {len(self._recent_alerts)} historical alerts from database"
498+
)
499+
except Exception as e:
500+
logger.error(f"Failed to load historical alerts: {e}")
501+
459502
@property
460503
def statistics(self):
461504

control_plane/analyzer.py

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ def __init__(self, run_id: str):
3939
self.pairs_analyzed = 0
4040
self.beacons_detected = 0
4141
self.alerts_generated = 0
42+
self.pairs_skipped = 0
4243
self.errors = 0
4344
self.results: List[DetectionResult] = []
4445

@@ -61,6 +62,7 @@ def to_dict(self):
6162
"pairs_analyzed": int(self.pairs_analyzed),
6263
"beacons_detected": int(self.beacons_detected),
6364
"alerts_generated": int(self.alerts_generated),
65+
"pairs_skipped": int(self.pairs_skipped),
6466
"errors": int(self.errors),
6567
}
6668

@@ -73,6 +75,8 @@ def __init__(
7375
detector: BeaconDetector,
7476
alert_manager: AlertManager,
7577
config=None,
78+
whitelist: dict = None,
79+
persistence=None,
7680
):
7781
"""
7882
Initialize the analyzer.
@@ -82,11 +86,15 @@ def __init__(
8286
detector: BeaconDetector instance
8387
alert_manager: AlertManager instance
8488
config: Analyzer configuration
89+
whitelist: Whitelist configuration for filtering pairs
90+
persistence: SQLiteStore instance for persisting beacons
8591
"""
8692
self.storage = storage
8793
self.detector = detector
8894
self.alert_manager = alert_manager
8995
self.config = config or AnalyzerConfig()
96+
self._whitelist = whitelist or {}
97+
self._persistence = persistence
9098

9199
# Thread safety lock for shared state
92100
self._lock = threading.RLock()
@@ -118,6 +126,29 @@ def __init__(
118126
f"ConnectionAnalyzer initialized: interval={self.config.analysis_interval}s"
119127
)
120128

129+
def _is_whitelisted(self, pair) -> bool:
130+
"""Check if a connection pair matches any whitelist rule."""
131+
if pair.src_ip in self._whitelist.get("source_ips", []):
132+
return True
133+
134+
if pair.dst_ip in self._whitelist.get("destination_ips", []):
135+
return True
136+
137+
if pair.dst_port in self._whitelist.get("ports", []):
138+
return True
139+
140+
pair_str = f"{pair.src_ip}:{pair.dst_ip}:{pair.dst_port}"
141+
if pair_str in self._whitelist.get("pairs", []):
142+
return True
143+
144+
return False
145+
146+
def update_whitelist(self, whitelist: dict):
147+
"""Update whitelist configuration (thread-safe)."""
148+
with self._lock:
149+
self._whitelist = whitelist or {}
150+
logger.info("Whitelist updated")
151+
121152
def start(self):
122153

123154
if self._running:
@@ -165,13 +196,23 @@ def run_analysis(self):
165196
logger.info(f"Starting analysis run: {run_id}")
166197

167198
try:
168-
# logger.info(f"self.config.min_connections {self.config.min_connections} and self.config.min_duration {self.config.min_duration}")
169199
# Get analyzable pairs from storage
170200
pairs = self.storage.get_analyzable_pairs(
171201
min_connections=self.config.min_connections,
172202
min_duration=self.config.min_duration,
173203
)
174-
# logger.info(f"pairs{pairs}")
204+
205+
# Filter out whitelisted pairs
206+
if self._whitelist:
207+
original_count = len(pairs)
208+
pairs = [p for p in pairs if not self._is_whitelisted(p)]
209+
run.pairs_skipped = original_count - len(pairs)
210+
if run.pairs_skipped > 0:
211+
logger.info(
212+
f"Whitelist filtered {run.pairs_skipped} pairs "
213+
f"({original_count} -> {len(pairs)})"
214+
)
215+
175216
# Limit pairs for performance
176217
if len(pairs) > self.config.max_pairs_per_run:
177218
logger.warning(
@@ -208,6 +249,17 @@ def run_analysis(self):
208249
with self._lock:
209250
self._known_beacons[result.pair_key] = result
210251

252+
# Persist beacon to database
253+
if self._persistence:
254+
try:
255+
self._persistence.save_beacon(
256+
result.pair_key, result.to_dict()
257+
)
258+
except Exception as e:
259+
logger.error(
260+
f"Failed to persist beacon {result.pair_key}: {e}"
261+
)
262+
211263
except Exception as e:
212264
logger.error(f"Error generating alert for {result.pair_key}: {e}")
213265
run.errors += 1
@@ -220,6 +272,13 @@ def run_analysis(self):
220272
]
221273
for key in stale_keys:
222274
del self._known_beacons[key]
275+
if self._persistence:
276+
try:
277+
self._persistence.remove_beacon(key)
278+
except Exception as e:
279+
logger.error(
280+
f"Failed to remove beacon {key} from database: {e}"
281+
)
223282

224283
except Exception as e:
225284
logger.error(f"Analysis run error: {e}", exc_info=True)
@@ -320,6 +379,26 @@ def get_run_history(self, limit: int = 10):
320379
runs = self._run_history[-limit:]
321380
return [r.to_dict() for r in reversed(runs)]
322381

382+
def load_historical_beacons(self):
383+
"""Load known beacons from persistence on startup."""
384+
if not self._persistence:
385+
return
386+
387+
try:
388+
beacon_dicts = self._persistence.load_beacons()
389+
for pair_key, detection_dict in beacon_dicts.items():
390+
try:
391+
result = DetectionResult.from_dict(detection_dict)
392+
self._known_beacons[pair_key] = result
393+
except Exception as e:
394+
logger.warning(f"Failed to restore beacon {pair_key}: {e}")
395+
396+
logger.info(
397+
f"Loaded {len(self._known_beacons)} historical beacons from database"
398+
)
399+
except Exception as e:
400+
logger.error(f"Failed to load historical beacons: {e}")
401+
323402
@property
324403
def statistics(self):
325404
"""Get analyzer statistics"""
@@ -331,4 +410,10 @@ def statistics(self):
331410
"total_alerts_generated": self._total_alerts_generated,
332411
"current_known_beacons": len(self._known_beacons),
333412
"active_cooldowns": len(self._alert_cooldowns),
413+
"whitelist_rules": {
414+
"source_ips": len(self._whitelist.get("source_ips", [])),
415+
"destination_ips": len(self._whitelist.get("destination_ips", [])),
416+
"ports": len(self._whitelist.get("ports", [])),
417+
"pairs": len(self._whitelist.get("pairs", [])),
418+
},
334419
}

control_plane/detector.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,19 @@ def to_dict(self):
4646
"jitter": float(round(self.jitter, 3)),
4747
}
4848

49+
@classmethod
50+
def from_dict(cls, data):
51+
return cls(
52+
count=data["count"],
53+
mean=data["mean"],
54+
std_dev=data["std_dev"],
55+
cv=data["cv"],
56+
median=data["median"],
57+
min_interval=data["min_interval"],
58+
max_interval=data["max_interval"],
59+
jitter=data["jitter"],
60+
)
61+
4962

5063
@dataclass
5164
class PeriodicityResult:
@@ -68,6 +81,15 @@ def to_dict(self):
6881
],
6982
}
7083

84+
@classmethod
85+
def from_dict(cls, data):
86+
return cls(
87+
is_periodic=data["is_periodic"],
88+
dominant_period=data["dominant_period"] or 0.0,
89+
periodicity_score=data["periodicity_score"],
90+
frequency_peaks=[(f, m) for f, m in data["frequency_peaks"]],
91+
)
92+
7193

7294
@dataclass
7395
class DetectionResult:
@@ -125,6 +147,29 @@ def to_dict(self):
125147
"analysis_time": str(self.analysis_time),
126148
}
127149

150+
@classmethod
151+
def from_dict(cls, data):
152+
return cls(
153+
pair_key=data["pair_key"],
154+
src_ip=data["src_ip"],
155+
dst_ip=data["dst_ip"],
156+
dst_port=data["dst_port"],
157+
protocol=data["protocol"],
158+
cv_score=data["cv_score"],
159+
periodicity_score=data["periodicity_score"],
160+
jitter_score=data["jitter_score"],
161+
combined_score=data["combined_score"],
162+
is_beacon=data["is_beacon"],
163+
confidence=BeaconConfidence(data["confidence"]),
164+
interval_stats=IntervalStats.from_dict(data["interval_stats"]),
165+
periodicity_result=PeriodicityResult.from_dict(data["periodicity_result"]),
166+
connection_count=data["connection_count"],
167+
duration_seconds=data["duration_seconds"],
168+
first_seen=data["first_seen"],
169+
last_seen=data["last_seen"],
170+
analysis_time=data.get("analysis_time", ""),
171+
)
172+
128173

129174
@dataclass
130175
class DetectorConfig:

0 commit comments

Comments
 (0)