test_network_security.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789
  1. # -*- coding: utf-8 -*-
  2. """
  3. Tests für Network Security Module.
  4. Testet Access Control, Rate Limiting und DDoS Protection.
  5. """
  6. import asyncio
  7. import time
  8. import pytest
  9. from datetime import datetime, timedelta
  10. from trixy_core.network.security.access_control import (
  11. AccessController,
  12. AccessRule,
  13. AccessDecision,
  14. IPMatcher,
  15. CIDRMatcher,
  16. MACMatcher,
  17. WildcardMatcher,
  18. )
  19. from trixy_core.network.security.rate_limiter import (
  20. ConnectionRateLimiter,
  21. RateLimitConfig,
  22. RateLimitResult,
  23. IPRateLimiter,
  24. GlobalRateLimiter,
  25. TokenBucket,
  26. )
  27. from trixy_core.network.security.ddos import (
  28. DDoSProtection,
  29. DDoSConfig,
  30. ThreatLevel,
  31. ConnectionTracker,
  32. SuspiciousActivityDetector,
  33. )
  34. # =============================================================================
  35. # Access Control Tests
  36. # =============================================================================
  37. class TestIPMatcher:
  38. """Tests für IPMatcher."""
  39. def test_ipv4_exact_match(self):
  40. """IPv4 exakter Match."""
  41. matcher = IPMatcher("192.168.1.100")
  42. assert matcher.matches("192.168.1.100") is True
  43. assert matcher.matches("192.168.1.101") is False
  44. def test_ipv6_exact_match(self):
  45. """IPv6 exakter Match."""
  46. matcher = IPMatcher("::1")
  47. assert matcher.matches("::1") is True
  48. assert matcher.matches("::2") is False
  49. def test_invalid_ip_no_match(self):
  50. """Ungültige IP matcht nicht."""
  51. matcher = IPMatcher("192.168.1.100")
  52. assert matcher.matches("invalid") is False
  53. assert matcher.matches("") is False
  54. class TestCIDRMatcher:
  55. """Tests für CIDRMatcher."""
  56. def test_cidr_in_range(self):
  57. """IP im CIDR-Bereich."""
  58. matcher = CIDRMatcher("192.168.1.0/24")
  59. assert matcher.matches("192.168.1.0") is True
  60. assert matcher.matches("192.168.1.100") is True
  61. assert matcher.matches("192.168.1.255") is True
  62. def test_cidr_out_of_range(self):
  63. """IP außerhalb des CIDR-Bereichs."""
  64. matcher = CIDRMatcher("192.168.1.0/24")
  65. assert matcher.matches("192.168.2.1") is False
  66. assert matcher.matches("10.0.0.1") is False
  67. def test_cidr_slash_32(self):
  68. """CIDR /32 = einzelne IP."""
  69. matcher = CIDRMatcher("192.168.1.100/32")
  70. assert matcher.matches("192.168.1.100") is True
  71. assert matcher.matches("192.168.1.101") is False
  72. def test_cidr_slash_16(self):
  73. """CIDR /16 = Class B Netzwerk."""
  74. matcher = CIDRMatcher("10.0.0.0/16")
  75. assert matcher.matches("10.0.0.1") is True
  76. assert matcher.matches("10.0.255.255") is True
  77. assert matcher.matches("10.1.0.1") is False
  78. class TestMACMatcher:
  79. """Tests für MACMatcher."""
  80. def test_mac_colon_format(self):
  81. """MAC mit Doppelpunkt-Format."""
  82. matcher = MACMatcher("AA:BB:CC:DD:EE:FF")
  83. assert matcher.matches("AA:BB:CC:DD:EE:FF") is True
  84. assert matcher.matches("aa:bb:cc:dd:ee:ff") is True # Case-insensitive
  85. def test_mac_dash_format(self):
  86. """MAC mit Bindestrich-Format."""
  87. matcher = MACMatcher("AA-BB-CC-DD-EE-FF")
  88. assert matcher.matches("AA:BB:CC:DD:EE:FF") is True
  89. def test_mac_no_separator(self):
  90. """MAC ohne Trennzeichen."""
  91. matcher = MACMatcher("AABBCCDDEEFF")
  92. assert matcher.matches("AA:BB:CC:DD:EE:FF") is True
  93. def test_mac_no_match(self):
  94. """MAC matcht nicht."""
  95. matcher = MACMatcher("AA:BB:CC:DD:EE:FF")
  96. assert matcher.matches("11:22:33:44:55:66") is False
  97. class TestWildcardMatcher:
  98. """Tests für WildcardMatcher."""
  99. def test_wildcard_single_segment(self):
  100. """Wildcard für ein Segment."""
  101. matcher = WildcardMatcher("192.168.1.*")
  102. assert matcher.matches("192.168.1.0") is True
  103. assert matcher.matches("192.168.1.255") is True
  104. assert matcher.matches("192.168.2.1") is False
  105. def test_wildcard_multiple_segments(self):
  106. """Wildcard für mehrere Segmente."""
  107. matcher = WildcardMatcher("192.168.*.*")
  108. assert matcher.matches("192.168.0.1") is True
  109. assert matcher.matches("192.168.255.255") is True
  110. assert matcher.matches("192.169.0.1") is False
  111. def test_wildcard_prefix(self):
  112. """Wildcard am Anfang."""
  113. matcher = WildcardMatcher("*.168.1.100")
  114. assert matcher.matches("192.168.1.100") is True
  115. assert matcher.matches("10.168.1.100") is True
  116. class TestAccessRule:
  117. """Tests für AccessRule."""
  118. def test_rule_creation(self):
  119. """Regel erstellen."""
  120. rule = AccessRule(
  121. pattern="192.168.1.0/24",
  122. decision=AccessDecision.ALLOW,
  123. description="Lokales Netzwerk"
  124. )
  125. assert rule.pattern == "192.168.1.0/24"
  126. assert rule.decision == AccessDecision.ALLOW
  127. assert rule.hit_count == 0
  128. def test_rule_expiry(self):
  129. """Regel-Ablauf prüfen."""
  130. # Nicht abgelaufene Regel
  131. rule = AccessRule(
  132. pattern="test",
  133. decision=AccessDecision.DENY,
  134. expires_at=datetime.now() + timedelta(hours=1)
  135. )
  136. assert rule.is_expired() is False
  137. # Abgelaufene Regel
  138. rule_expired = AccessRule(
  139. pattern="test",
  140. decision=AccessDecision.DENY,
  141. expires_at=datetime.now() - timedelta(hours=1)
  142. )
  143. assert rule_expired.is_expired() is True
  144. def test_rule_hit_count(self):
  145. """Treffer zählen."""
  146. rule = AccessRule(pattern="test", decision=AccessDecision.ALLOW)
  147. assert rule.hit_count == 0
  148. rule.record_hit()
  149. assert rule.hit_count == 1
  150. rule.record_hit()
  151. assert rule.hit_count == 2
  152. class TestAccessController:
  153. """Tests für AccessController."""
  154. def test_default_deny(self):
  155. """Standard: Deny."""
  156. controller = AccessController(default_allow=False)
  157. assert controller.check_ip("192.168.1.100") == AccessDecision.DENY
  158. def test_default_allow(self):
  159. """Standard: Allow."""
  160. controller = AccessController(default_allow=True)
  161. assert controller.check_ip("192.168.1.100") == AccessDecision.ALLOW
  162. def test_whitelist_ip(self):
  163. """Whitelist einzelne IP."""
  164. controller = AccessController(default_allow=False)
  165. controller.whitelist("192.168.1.100", "Erlaubte IP")
  166. assert controller.check_ip("192.168.1.100") == AccessDecision.ALLOW
  167. assert controller.check_ip("192.168.1.101") == AccessDecision.DENY
  168. def test_whitelist_cidr(self):
  169. """Whitelist CIDR-Bereich."""
  170. controller = AccessController(default_allow=False)
  171. controller.whitelist("192.168.1.0/24", "Lokales Netzwerk")
  172. assert controller.check_ip("192.168.1.50") == AccessDecision.ALLOW
  173. assert controller.check_ip("192.168.2.50") == AccessDecision.DENY
  174. def test_blacklist_overrides_whitelist(self):
  175. """Blacklist hat Vorrang vor Whitelist."""
  176. controller = AccessController(default_allow=False)
  177. controller.whitelist("192.168.1.0/24", "Netzwerk")
  178. controller.blacklist("192.168.1.100", "Gesperrte IP")
  179. assert controller.check_ip("192.168.1.50") == AccessDecision.ALLOW
  180. assert controller.check_ip("192.168.1.100") == AccessDecision.DENY
  181. def test_mac_whitelist(self):
  182. """MAC-Whitelist."""
  183. controller = AccessController(default_allow=False)
  184. controller.whitelist_mac("AA:BB:CC:DD:EE:FF", "Erlaubtes Gerät")
  185. assert controller.check_mac("AA:BB:CC:DD:EE:FF") == AccessDecision.ALLOW
  186. assert controller.check_mac("11:22:33:44:55:66") == AccessDecision.DENY
  187. def test_combined_check(self):
  188. """Kombinierte IP + MAC Prüfung."""
  189. controller = AccessController(default_allow=False)
  190. controller.whitelist("192.168.1.0/24")
  191. controller.whitelist_mac("AA:BB:CC:DD:EE:FF")
  192. # Beide erlaubt
  193. assert controller.check("192.168.1.100", "AA:BB:CC:DD:EE:FF") == AccessDecision.ALLOW
  194. # IP erlaubt, MAC nicht
  195. assert controller.check("192.168.1.100", "11:22:33:44:55:66") == AccessDecision.DENY
  196. # IP nicht erlaubt
  197. assert controller.check("10.0.0.1", "AA:BB:CC:DD:EE:FF") == AccessDecision.DENY
  198. def test_expired_rules_ignored(self):
  199. """Abgelaufene Regeln werden ignoriert."""
  200. controller = AccessController(default_allow=False)
  201. controller.whitelist(
  202. "192.168.1.100",
  203. expires_at=datetime.now() - timedelta(hours=1)
  204. )
  205. assert controller.check_ip("192.168.1.100") == AccessDecision.DENY
  206. def test_remove_from_whitelist(self):
  207. """Regel von Whitelist entfernen."""
  208. controller = AccessController(default_allow=False)
  209. controller.whitelist("192.168.1.100")
  210. assert controller.check_ip("192.168.1.100") == AccessDecision.ALLOW
  211. controller.remove_from_whitelist("192.168.1.100")
  212. assert controller.check_ip("192.168.1.100") == AccessDecision.DENY
  213. def test_clear_expired(self):
  214. """Abgelaufene Regeln bereinigen."""
  215. controller = AccessController(default_allow=False)
  216. controller.whitelist("192.168.1.1", expires_at=datetime.now() - timedelta(hours=1))
  217. controller.whitelist("192.168.1.2", expires_at=datetime.now() + timedelta(hours=1))
  218. controller.whitelist("192.168.1.3") # Kein Ablauf
  219. count = controller.clear_expired()
  220. assert count == 1
  221. def test_statistics(self):
  222. """Statistiken abrufen."""
  223. controller = AccessController(default_allow=False)
  224. controller.whitelist("192.168.1.0/24")
  225. controller.blacklist("10.0.0.0/8")
  226. stats = controller.get_statistics()
  227. assert stats["whitelist_count"] == 1
  228. assert stats["blacklist_count"] == 1
  229. def test_serialization(self):
  230. """Serialisierung und Deserialisierung."""
  231. controller = AccessController(default_allow=False)
  232. controller.whitelist("192.168.1.0/24", "Netzwerk")
  233. controller.blacklist("10.0.0.1", "Gesperrt")
  234. data = controller.to_dict()
  235. restored = AccessController.from_dict(data)
  236. assert restored.check_ip("192.168.1.100") == AccessDecision.ALLOW
  237. assert restored.check_ip("10.0.0.1") == AccessDecision.DENY
  238. # =============================================================================
  239. # Rate Limiter Tests
  240. # =============================================================================
  241. class TestTokenBucket:
  242. """Tests für TokenBucket."""
  243. def test_initial_capacity(self):
  244. """Initiale Kapazität."""
  245. bucket = TokenBucket(capacity=10, tokens=10.0, refill_rate=1.0)
  246. assert bucket.tokens == 10.0
  247. def test_consume_tokens(self):
  248. """Tokens verbrauchen."""
  249. bucket = TokenBucket(capacity=10, tokens=10.0, refill_rate=1.0)
  250. assert bucket.consume(5) is True
  251. # Tokens können durch Refill leicht höher sein
  252. assert 4.5 <= bucket.tokens <= 5.5
  253. assert bucket.consume(5) is True
  254. # Fast 0, aber Refill kann kleine Menge hinzufügen
  255. assert bucket.tokens < 0.5
  256. assert bucket.consume(1) is False # Nicht genug Tokens
  257. def test_refill(self):
  258. """Token-Refill."""
  259. bucket = TokenBucket(capacity=10, tokens=0.0, refill_rate=100.0)
  260. time.sleep(0.05) # 50ms = 5 Tokens bei 100/s
  261. bucket.refill()
  262. assert bucket.tokens >= 4.0 # Mindestens 4 Tokens
  263. def test_capacity_limit(self):
  264. """Kapazitätslimit."""
  265. bucket = TokenBucket(capacity=10, tokens=10.0, refill_rate=100.0)
  266. time.sleep(0.1)
  267. bucket.refill()
  268. assert bucket.tokens == 10.0 # Nicht über Kapazität
  269. def test_blocking(self):
  270. """Bucket blockieren."""
  271. bucket = TokenBucket(capacity=10, tokens=10.0, refill_rate=1.0)
  272. assert bucket.is_blocked() is False
  273. bucket.block(1.0)
  274. assert bucket.is_blocked() is True
  275. time.sleep(1.1)
  276. assert bucket.is_blocked() is False
  277. class TestIPRateLimiter:
  278. """Tests für IPRateLimiter."""
  279. def test_allow_under_limit(self):
  280. """Erlauben unter Limit."""
  281. config = RateLimitConfig(requests_per_second=10.0, burst_size=5)
  282. limiter = IPRateLimiter(config)
  283. for _ in range(5):
  284. result = limiter.check("192.168.1.100")
  285. assert result == RateLimitResult.ALLOWED
  286. def test_limit_exceeded(self):
  287. """Limit überschritten."""
  288. config = RateLimitConfig(requests_per_second=1.0, burst_size=2)
  289. limiter = IPRateLimiter(config)
  290. assert limiter.check("192.168.1.100") == RateLimitResult.ALLOWED
  291. assert limiter.check("192.168.1.100") == RateLimitResult.ALLOWED
  292. assert limiter.check("192.168.1.100") == RateLimitResult.LIMITED
  293. def test_different_ips_separate_limits(self):
  294. """Verschiedene IPs haben separate Limits."""
  295. config = RateLimitConfig(requests_per_second=1.0, burst_size=2)
  296. limiter = IPRateLimiter(config)
  297. # IP 1 ausschöpfen
  298. limiter.check("192.168.1.1")
  299. limiter.check("192.168.1.1")
  300. assert limiter.check("192.168.1.1") == RateLimitResult.LIMITED
  301. # IP 2 noch erlaubt
  302. assert limiter.check("192.168.1.2") == RateLimitResult.ALLOWED
  303. def test_blocking_after_threshold(self):
  304. """Blockierung nach Schwellenwert."""
  305. config = RateLimitConfig(
  306. requests_per_second=1.0,
  307. burst_size=1,
  308. block_threshold=3,
  309. block_duration=10.0
  310. )
  311. limiter = IPRateLimiter(config)
  312. # Erste Anfrage OK
  313. limiter.check("192.168.1.100")
  314. # Violations sammeln
  315. for _ in range(3):
  316. limiter.check("192.168.1.100")
  317. # Sollte jetzt blockiert sein
  318. result = limiter.check("192.168.1.100")
  319. assert result == RateLimitResult.BLOCKED
  320. def test_unblock_ip(self):
  321. """IP entsperren."""
  322. config = RateLimitConfig(
  323. requests_per_second=1.0,
  324. burst_size=1,
  325. block_threshold=2,
  326. block_duration=10.0
  327. )
  328. limiter = IPRateLimiter(config)
  329. # Blockieren
  330. limiter.check("192.168.1.100")
  331. limiter.check("192.168.1.100")
  332. limiter.check("192.168.1.100")
  333. assert limiter.check("192.168.1.100") == RateLimitResult.BLOCKED
  334. # Entsperren
  335. limiter.unblock("192.168.1.100")
  336. assert limiter.check("192.168.1.100") == RateLimitResult.ALLOWED
  337. def test_get_status(self):
  338. """Status abrufen."""
  339. config = RateLimitConfig(requests_per_second=10.0, burst_size=5)
  340. limiter = IPRateLimiter(config)
  341. limiter.check("192.168.1.100")
  342. limiter.check("192.168.1.100")
  343. status = limiter.get_status("192.168.1.100")
  344. assert status["exists"] is True
  345. assert status["tokens"] < 5.0
  346. def test_reset(self):
  347. """Reset einer IP."""
  348. config = RateLimitConfig(requests_per_second=1.0, burst_size=2)
  349. limiter = IPRateLimiter(config)
  350. limiter.check("192.168.1.100")
  351. limiter.check("192.168.1.100")
  352. limiter.reset("192.168.1.100")
  353. status = limiter.get_status("192.168.1.100")
  354. assert status["exists"] is False
  355. def test_cleanup(self):
  356. """Cleanup inaktiver Buckets."""
  357. config = RateLimitConfig(requests_per_second=10.0, burst_size=5)
  358. limiter = IPRateLimiter(config)
  359. limiter.check("192.168.1.100")
  360. # Bucket sollte existieren
  361. assert limiter.get_status("192.168.1.100")["exists"] is True
  362. # Cleanup (wird normalerweise nach Timeout aufgerufen)
  363. count = limiter.cleanup()
  364. # Bei kurzer Inaktivität wird nichts entfernt
  365. assert count == 0
  366. class TestGlobalRateLimiter:
  367. """Tests für GlobalRateLimiter."""
  368. def test_allow_under_limit(self):
  369. """Erlauben unter globalem Limit."""
  370. limiter = GlobalRateLimiter(max_requests_per_second=100.0, burst_size=10)
  371. for _ in range(10):
  372. result = limiter.check()
  373. assert result == RateLimitResult.ALLOWED
  374. def test_limit_exceeded(self):
  375. """Globales Limit überschritten."""
  376. limiter = GlobalRateLimiter(max_requests_per_second=1.0, burst_size=3)
  377. limiter.check()
  378. limiter.check()
  379. limiter.check()
  380. assert limiter.check() == RateLimitResult.LIMITED
  381. def test_get_status(self):
  382. """Status abrufen."""
  383. limiter = GlobalRateLimiter(max_requests_per_second=10.0, burst_size=10)
  384. limiter.check()
  385. limiter.check()
  386. status = limiter.get_status()
  387. assert status["total_requests"] == 2
  388. assert status["tokens_available"] < 10.0
  389. class TestConnectionRateLimiter:
  390. """Tests für kombinierten Rate-Limiter."""
  391. def test_combined_limits(self):
  392. """Kombinierte IP- und Global-Limits."""
  393. ip_config = RateLimitConfig(requests_per_second=10.0, burst_size=5)
  394. limiter = ConnectionRateLimiter(
  395. ip_config=ip_config,
  396. global_rps=100.0,
  397. global_burst=50
  398. )
  399. # Unter beiden Limits
  400. result = limiter.check("192.168.1.100")
  401. assert result == RateLimitResult.ALLOWED
  402. def test_ip_limit_triggers_first(self):
  403. """IP-Limit greift zuerst."""
  404. ip_config = RateLimitConfig(requests_per_second=1.0, burst_size=2)
  405. limiter = ConnectionRateLimiter(
  406. ip_config=ip_config,
  407. global_rps=1000.0,
  408. global_burst=1000
  409. )
  410. limiter.check("192.168.1.100")
  411. limiter.check("192.168.1.100")
  412. result = limiter.check("192.168.1.100")
  413. assert result == RateLimitResult.LIMITED
  414. def test_statistics(self):
  415. """Kombinierte Statistiken."""
  416. ip_config = RateLimitConfig(requests_per_second=10.0, burst_size=5)
  417. limiter = ConnectionRateLimiter(ip_config=ip_config)
  418. limiter.check("192.168.1.100")
  419. stats = limiter.get_statistics()
  420. assert "ip" in stats
  421. assert "global" in stats
  422. # =============================================================================
  423. # DDoS Protection Tests
  424. # =============================================================================
  425. class TestConnectionTracker:
  426. """Tests für ConnectionTracker."""
  427. def test_register_connection(self):
  428. """Verbindung registrieren."""
  429. config = DDoSConfig(max_connections_per_ip=5)
  430. tracker = ConnectionTracker(config)
  431. conn_id, allowed = tracker.register_connection("192.168.1.100")
  432. assert allowed is True
  433. assert conn_id != ""
  434. def test_max_connections_per_ip(self):
  435. """Maximale Verbindungen pro IP."""
  436. config = DDoSConfig(max_connections_per_ip=2)
  437. tracker = ConnectionTracker(config)
  438. conn1, allowed1 = tracker.register_connection("192.168.1.100")
  439. conn2, allowed2 = tracker.register_connection("192.168.1.100")
  440. _, allowed3 = tracker.register_connection("192.168.1.100")
  441. assert allowed1 is True
  442. assert allowed2 is True
  443. assert allowed3 is False
  444. def test_unregister_connection(self):
  445. """Verbindung entfernen."""
  446. config = DDoSConfig(max_connections_per_ip=2)
  447. tracker = ConnectionTracker(config)
  448. conn_id, _ = tracker.register_connection("192.168.1.100")
  449. tracker.register_connection("192.168.1.100")
  450. assert tracker.get_connection_count("192.168.1.100") == 2
  451. tracker.unregister_connection(conn_id)
  452. assert tracker.get_connection_count("192.168.1.100") == 1
  453. def test_ban_ip(self):
  454. """IP sperren."""
  455. config = DDoSConfig()
  456. tracker = ConnectionTracker(config)
  457. tracker.ban_ip("192.168.1.100", duration=10.0)
  458. assert tracker.is_banned("192.168.1.100") is True
  459. _, allowed = tracker.register_connection("192.168.1.100")
  460. assert allowed is False
  461. def test_unban_ip(self):
  462. """IP entsperren."""
  463. config = DDoSConfig()
  464. tracker = ConnectionTracker(config)
  465. tracker.ban_ip("192.168.1.100")
  466. assert tracker.is_banned("192.168.1.100") is True
  467. tracker.unban_ip("192.168.1.100")
  468. assert tracker.is_banned("192.168.1.100") is False
  469. def test_global_connection_limit(self):
  470. """Globales Verbindungslimit."""
  471. config = DDoSConfig(max_global_connections=3)
  472. tracker = ConnectionTracker(config)
  473. tracker.register_connection("192.168.1.1")
  474. tracker.register_connection("192.168.1.2")
  475. tracker.register_connection("192.168.1.3")
  476. _, allowed = tracker.register_connection("192.168.1.4")
  477. assert allowed is False
  478. def test_statistics(self):
  479. """Statistiken abrufen."""
  480. config = DDoSConfig()
  481. tracker = ConnectionTracker(config)
  482. tracker.register_connection("192.168.1.100")
  483. tracker.register_connection("192.168.1.101")
  484. stats = tracker.get_statistics()
  485. assert stats["total_connections"] == 2
  486. assert stats["unique_ips"] == 2
  487. class TestSuspiciousActivityDetector:
  488. """Tests für SuspiciousActivityDetector."""
  489. def test_record_event(self):
  490. """Event aufzeichnen."""
  491. config = DDoSConfig(monitoring_window=60.0)
  492. detector = SuspiciousActivityDetector(config)
  493. detector.record_event("192.168.1.100", "connection_denied")
  494. level = detector.analyze("192.168.1.100")
  495. assert level == ThreatLevel.LOW
  496. def test_threat_level_escalation(self):
  497. """Bedrohungsstufe eskaliert."""
  498. config = DDoSConfig(monitoring_window=60.0)
  499. detector = SuspiciousActivityDetector(config)
  500. # Viele Events = höhere Bedrohung
  501. for _ in range(50):
  502. detector.record_event("192.168.1.100", "connection_denied")
  503. level = detector.analyze("192.168.1.100")
  504. assert level >= ThreatLevel.MEDIUM
  505. def test_get_suspicious_ips(self):
  506. """Verdächtige IPs abrufen."""
  507. config = DDoSConfig(monitoring_window=60.0)
  508. detector = SuspiciousActivityDetector(config)
  509. for _ in range(20):
  510. detector.record_event("192.168.1.100", "connection_denied")
  511. suspicious = detector.get_suspicious_ips(ThreatLevel.LOW)
  512. assert len(suspicious) > 0
  513. assert suspicious[0][0] == "192.168.1.100"
  514. class TestDDoSProtection:
  515. """Tests für DDoSProtection."""
  516. @pytest.mark.asyncio
  517. async def test_on_connect_allowed(self):
  518. """Verbindung erlaubt."""
  519. config = DDoSConfig(max_connections_per_ip=5)
  520. protection = DDoSProtection(config)
  521. conn_id, allowed = await protection.on_connect("192.168.1.100")
  522. assert allowed is True
  523. assert conn_id != ""
  524. @pytest.mark.asyncio
  525. async def test_on_connect_denied(self):
  526. """Verbindung verweigert."""
  527. config = DDoSConfig(max_connections_per_ip=1)
  528. protection = DDoSProtection(config)
  529. await protection.on_connect("192.168.1.100")
  530. _, allowed = await protection.on_connect("192.168.1.100")
  531. assert allowed is False
  532. @pytest.mark.asyncio
  533. async def test_on_disconnect(self):
  534. """Verbindung getrennt."""
  535. config = DDoSConfig(max_connections_per_ip=2)
  536. protection = DDoSProtection(config)
  537. conn_id, _ = await protection.on_connect("192.168.1.100")
  538. await protection.on_connect("192.168.1.100")
  539. await protection.on_disconnect(conn_id)
  540. # Jetzt sollte wieder Platz sein
  541. _, allowed = await protection.on_connect("192.168.1.100")
  542. assert allowed is True
  543. @pytest.mark.asyncio
  544. async def test_ban_unban(self):
  545. """IP sperren und entsperren."""
  546. protection = DDoSProtection()
  547. protection.ban_ip("192.168.1.100")
  548. assert protection.is_allowed("192.168.1.100") is False
  549. protection.unban_ip("192.168.1.100")
  550. assert protection.is_allowed("192.168.1.100") is True
  551. @pytest.mark.asyncio
  552. async def test_threat_callback(self):
  553. """Threat-Callback wird aufgerufen."""
  554. config = DDoSConfig(max_connections_per_ip=1)
  555. protection = DDoSProtection(config)
  556. threats_detected = []
  557. @protection.on_threat
  558. def handle_threat(ip: str, level: ThreatLevel):
  559. threats_detected.append((ip, level))
  560. # Viele fehlgeschlagene Verbindungsversuche
  561. await protection.on_connect("192.168.1.100")
  562. for _ in range(10):
  563. await protection.on_connect("192.168.1.100")
  564. # Nach vielen Versuchen sollte Callback aufgerufen werden
  565. # (abhängig von der Threat-Level-Logik)
  566. @pytest.mark.asyncio
  567. async def test_statistics(self):
  568. """Statistiken abrufen."""
  569. protection = DDoSProtection()
  570. await protection.on_connect("192.168.1.100")
  571. stats = protection.get_statistics()
  572. assert "connections" in stats
  573. assert "threats" in stats
  574. assert "config" in stats
  575. # =============================================================================
  576. # Async Tests
  577. # =============================================================================
  578. class TestAsyncRateLimiter:
  579. """Tests für async Rate-Limiter-Methoden."""
  580. @pytest.mark.asyncio
  581. async def test_ip_rate_limiter_async(self):
  582. """Async IP Rate-Limiter."""
  583. config = RateLimitConfig(requests_per_second=10.0, burst_size=5)
  584. limiter = IPRateLimiter(config)
  585. result = await limiter.check_async("192.168.1.100")
  586. assert result == RateLimitResult.ALLOWED
  587. @pytest.mark.asyncio
  588. async def test_global_rate_limiter_async(self):
  589. """Async Global Rate-Limiter."""
  590. limiter = GlobalRateLimiter(max_requests_per_second=10.0, burst_size=5)
  591. result = await limiter.check_async()
  592. assert result == RateLimitResult.ALLOWED
  593. @pytest.mark.asyncio
  594. async def test_connection_rate_limiter_async(self):
  595. """Async kombinierter Rate-Limiter."""
  596. ip_config = RateLimitConfig(requests_per_second=10.0, burst_size=5)
  597. limiter = ConnectionRateLimiter(ip_config=ip_config)
  598. result = await limiter.check_async("192.168.1.100")
  599. assert result == RateLimitResult.ALLOWED