pywebguard 0.0.1__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.
Files changed (43) hide show
  1. pywebguard/__init__.py +87 -0
  2. pywebguard/cli.py +1022 -0
  3. pywebguard/core/__init__.py +0 -0
  4. pywebguard/core/base.py +792 -0
  5. pywebguard/core/config.py +455 -0
  6. pywebguard/filters/__init__.py +0 -0
  7. pywebguard/filters/base.py +44 -0
  8. pywebguard/filters/ip_filter.py +233 -0
  9. pywebguard/filters/user_agent.py +102 -0
  10. pywebguard/frameworks/__init__.py +0 -0
  11. pywebguard/frameworks/_fastapi.py +191 -0
  12. pywebguard/frameworks/_flask.py +182 -0
  13. pywebguard/limiters/__init__.py +0 -0
  14. pywebguard/limiters/base.py +46 -0
  15. pywebguard/limiters/rate_limit.py +415 -0
  16. pywebguard/logging/__init__.py +0 -0
  17. pywebguard/logging/backends/__init__.py +17 -0
  18. pywebguard/logging/backends/_elasticsearch.py +307 -0
  19. pywebguard/logging/backends/_meilisearch.py +310 -0
  20. pywebguard/logging/backends/_mongodb.py +303 -0
  21. pywebguard/logging/base.py +117 -0
  22. pywebguard/logging/logger.py +408 -0
  23. pywebguard/security/__init__.py +0 -0
  24. pywebguard/security/base.py +44 -0
  25. pywebguard/security/cors.py +204 -0
  26. pywebguard/security/penetration.py +265 -0
  27. pywebguard/storage/__init__.py +0 -0
  28. pywebguard/storage/_mongodb.py +396 -0
  29. pywebguard/storage/_postgresql.py +539 -0
  30. pywebguard/storage/_redis.py +314 -0
  31. pywebguard/storage/_sqlite.py +374 -0
  32. pywebguard/storage/_tinydb.py +271 -0
  33. pywebguard/storage/base.py +327 -0
  34. pywebguard/storage/memory.py +245 -0
  35. pywebguard/utils/__init__.py +0 -0
  36. pywebguard/utils/ip.py +84 -0
  37. pywebguard/utils/request.py +65 -0
  38. pywebguard-0.0.1.dist-info/LICENSE +21 -0
  39. pywebguard-0.0.1.dist-info/METADATA +434 -0
  40. pywebguard-0.0.1.dist-info/RECORD +43 -0
  41. pywebguard-0.0.1.dist-info/WHEEL +5 -0
  42. pywebguard-0.0.1.dist-info/entry_points.txt +2 -0
  43. pywebguard-0.0.1.dist-info/top_level.txt +1 -0
pywebguard/cli.py ADDED
@@ -0,0 +1,1022 @@
1
+ """
2
+ Command-line interface for PyWebGuard.
3
+
4
+ This module provides a command-line interface for managing PyWebGuard configurations,
5
+ testing connections, managing banned IPs, and more.
6
+ """
7
+
8
+ import argparse
9
+ import sys
10
+ import json
11
+ import os
12
+ import time
13
+ import ipaddress
14
+ from typing import Dict, Any, List, Optional, Union, Tuple, NoReturn
15
+ from pathlib import Path
16
+
17
+ from pywebguard import __version__
18
+ from pywebguard.core.config import GuardConfig
19
+ from pywebguard.storage.memory import MemoryStorage
20
+ from pywebguard.logging.logger import SecurityLogger, AsyncSecurityLogger
21
+
22
+ try:
23
+ from pywebguard.storage._redis import RedisStorage
24
+ except ImportError:
25
+ RedisStorage = None
26
+ try:
27
+ from pywebguard.storage._sqlite import SQLiteStorage
28
+ except ImportError:
29
+ SQLiteStorage = None
30
+ try:
31
+ from pywebguard.storage._tinydb import TinyDBStorage
32
+ except ImportError:
33
+ TinyDBStorage = None
34
+
35
+
36
+ def main() -> int:
37
+ """
38
+ Main entry point for the CLI.
39
+
40
+ Parses command-line arguments and dispatches to the appropriate command handler.
41
+
42
+ Returns:
43
+ int: Exit code (0 for success, non-zero for errors)
44
+ """
45
+ parser = argparse.ArgumentParser(
46
+ description="PyWebGuard - A comprehensive security library for Python web applications"
47
+ )
48
+
49
+ parser.add_argument(
50
+ "--version", "-v", action="store_true", help="Show version and exit"
51
+ )
52
+
53
+ subparsers = parser.add_subparsers(dest="command", help="Command to run")
54
+
55
+ # Init command
56
+ init_parser = subparsers.add_parser("init", help="Initialize a configuration file")
57
+ init_parser.add_argument(
58
+ "--output", "-o", default="pywebguard.json", help="Output file path"
59
+ )
60
+ init_parser.add_argument(
61
+ "--framework", "-f", choices=["fastapi", "flask"], help="Framework to use"
62
+ )
63
+
64
+ # Interactive init command
65
+ subparsers.add_parser(
66
+ "interactive", help="Interactively create a configuration file"
67
+ )
68
+
69
+ # Validate command
70
+ validate_parser = subparsers.add_parser(
71
+ "validate", help="Validate a configuration file"
72
+ )
73
+ validate_parser.add_argument("config_file", help="Path to configuration file")
74
+
75
+ # Test connection command
76
+ test_parser = subparsers.add_parser(
77
+ "test", help="Test connection to a storage backend"
78
+ )
79
+ test_parser.add_argument(
80
+ "storage_type",
81
+ choices=["memory", "redis", "sqlite", "tinydb"],
82
+ help="Storage type to test",
83
+ )
84
+ test_parser.add_argument(
85
+ "--connection", "-c", help="Connection string for the storage backend"
86
+ )
87
+
88
+ # Ban IP command
89
+ ban_parser = subparsers.add_parser("ban", help="Ban an IP address")
90
+ ban_parser.add_argument("config_file", help="Path to configuration file")
91
+ ban_parser.add_argument("ip_address", help="IP address to ban")
92
+ ban_parser.add_argument(
93
+ "--duration", "-d", type=int, default=60, help="Duration of the ban in minutes"
94
+ )
95
+
96
+ # Unban IP command
97
+ unban_parser = subparsers.add_parser("unban", help="Unban an IP address")
98
+ unban_parser.add_argument("config_file", help="Path to configuration file")
99
+ unban_parser.add_argument("ip_address", help="IP address to unban")
100
+
101
+ # Clear storage command
102
+ clear_parser = subparsers.add_parser("clear", help="Clear all data from storage")
103
+ clear_parser.add_argument("config_file", help="Path to configuration file")
104
+ clear_parser.add_argument(
105
+ "--yes", "-y", action="store_true", help="Skip confirmation prompt"
106
+ )
107
+
108
+ # Status command
109
+ status_parser = subparsers.add_parser("status", help="Show status information")
110
+ status_parser.add_argument("config_file", help="Path to configuration file")
111
+
112
+ args = parser.parse_args()
113
+
114
+ if args.version:
115
+ print(f"PyWebGuard v{__version__}")
116
+ return 0
117
+
118
+ if args.command == "init":
119
+ return cmd_init(args.output, args.framework)
120
+ elif args.command == "interactive":
121
+ return cmd_interactive_init()
122
+ elif args.command == "validate":
123
+ return cmd_validate(args.config_file)
124
+ elif args.command == "test":
125
+ return cmd_test_connection(args.storage_type, args.connection)
126
+ elif args.command == "ban":
127
+ return cmd_ban_ip(args.config_file, args.ip_address, args.duration)
128
+ elif args.command == "unban":
129
+ return cmd_unban_ip(args.config_file, args.ip_address)
130
+ elif args.command == "clear":
131
+ return cmd_clear_storage(args.config_file, args.yes)
132
+ elif args.command == "status":
133
+ return cmd_status(args.config_file)
134
+ else:
135
+ parser.print_help()
136
+ return 0
137
+
138
+
139
+ def cmd_init(output_path: str, framework: Optional[str] = None) -> int:
140
+ """
141
+ Initialize a configuration file with default settings.
142
+
143
+ Args:
144
+ output_path: Path to output file
145
+ framework: Framework to use (fastapi, flask, or None)
146
+
147
+ Returns:
148
+ int: Exit code (0 for success, 1 for error)
149
+
150
+ Raises:
151
+ IOError: If the file cannot be written
152
+ """
153
+ # Create default configuration
154
+ config: Dict[str, Any] = {
155
+ "ip_filter": {
156
+ "enabled": True,
157
+ "whitelist": [],
158
+ "blacklist": [],
159
+ "block_cloud_providers": False,
160
+ "geo_restrictions": {},
161
+ },
162
+ "rate_limit": {
163
+ "enabled": True,
164
+ "requests_per_minute": 60,
165
+ "burst_size": 10,
166
+ "auto_ban_threshold": 100,
167
+ "auto_ban_duration_minutes": 60,
168
+ },
169
+ "user_agent": {
170
+ "enabled": True,
171
+ "blocked_agents": ["curl", "wget", "python-requests"],
172
+ },
173
+ "penetration": {
174
+ "enabled": True,
175
+ "log_suspicious": True,
176
+ "suspicious_patterns": [],
177
+ },
178
+ "cors": {
179
+ "enabled": True,
180
+ "allow_origins": ["*"],
181
+ "allow_methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
182
+ "allow_headers": ["*"],
183
+ "allow_credentials": False,
184
+ "max_age": 600,
185
+ },
186
+ "logging": {
187
+ "enabled": True,
188
+ "log_file": "pywebguard.log",
189
+ "log_level": "INFO",
190
+ "stream": True,
191
+ "stream_levels": ["ERROR", "CRITICAL"],
192
+ "max_log_size": 10 * 1024 * 1024, # 10MB
193
+ "max_log_files": 2,
194
+ "log_format": "{asctime} {levelname} {message}",
195
+ "log_date_format": "%Y-%m-%d %H:%M:%S",
196
+ "log_rotation": "midnight",
197
+ "log_backup_count": 3,
198
+ "log_encoding": "utf-8",
199
+ "meilisearch": None,
200
+ "elasticsearch": None,
201
+ "mongodb": None,
202
+ },
203
+ "storage": {
204
+ "type": "memory",
205
+ "redis_url": None,
206
+ "redis_prefix": "pywebguard:",
207
+ "ttl": 3600,
208
+ },
209
+ }
210
+
211
+ # Add framework-specific configuration
212
+ if framework == "fastapi":
213
+ config["framework"] = {
214
+ "type": "fastapi",
215
+ "route_rate_limits": [
216
+ {"endpoint": "/api/limited", "requests_per_minute": 5, "burst_size": 2}
217
+ ],
218
+ }
219
+ elif framework == "flask":
220
+ config["framework"] = {
221
+ "type": "flask",
222
+ "route_rate_limits": [
223
+ {"endpoint": "/api/limited", "requests_per_minute": 5, "burst_size": 2}
224
+ ],
225
+ }
226
+
227
+ # Write configuration to file
228
+ try:
229
+ with open(output_path, "w") as f:
230
+ json.dump(config, f, indent=4)
231
+ print(f"Configuration written to {output_path}")
232
+ return 0
233
+ except IOError as e:
234
+ print(f"Error writing configuration: {e}", file=sys.stderr)
235
+ return 1
236
+
237
+
238
+ def cmd_validate(config_path: str) -> int:
239
+ """
240
+ Validate a configuration file against the PyWebGuard schema.
241
+
242
+ Args:
243
+ config_path: Path to configuration file
244
+
245
+ Returns:
246
+ int: Exit code (0 for success, 1 for error)
247
+
248
+ Raises:
249
+ FileNotFoundError: If the configuration file does not exist
250
+ json.JSONDecodeError: If the configuration file is not valid JSON
251
+ ValueError: If the configuration is not valid
252
+ """
253
+ try:
254
+ # Read configuration from file
255
+ with open(config_path, "r") as f:
256
+ config_data = json.load(f)
257
+
258
+ # Validate configuration
259
+ GuardConfig(**config_data)
260
+
261
+ print(f"Configuration file {config_path} is valid")
262
+ return 0
263
+ except FileNotFoundError:
264
+ print(f"Configuration file {config_path} not found")
265
+ return 1
266
+ except json.JSONDecodeError:
267
+ print(f"Configuration file {config_path} is not valid JSON")
268
+ return 1
269
+ except Exception as e:
270
+ print(f"Configuration file {config_path} is not valid: {e}")
271
+ return 1
272
+
273
+
274
+ def cmd_test_connection(
275
+ storage_type: str, connection_string: Optional[str] = None
276
+ ) -> int:
277
+ """
278
+ Test connection to a storage backend.
279
+
280
+ Args:
281
+ storage_type: Type of storage backend to test (memory, redis, sqlite, tinydb)
282
+ connection_string: Connection string for the storage backend
283
+
284
+ Returns:
285
+ int: Exit code (0 for success, 1 for error)
286
+
287
+ Raises:
288
+ ImportError: If the required storage backend is not installed
289
+ ConnectionError: If the connection to the storage backend fails
290
+ """
291
+ print(f"Testing connection to {storage_type} storage...")
292
+
293
+ try:
294
+ if storage_type == "memory":
295
+ storage = MemoryStorage()
296
+ storage.set("test_key", "test_value")
297
+ value = storage.get("test_key")
298
+ storage.delete("test_key")
299
+ print("Memory storage test successful")
300
+ return 0
301
+
302
+ elif storage_type == "redis":
303
+ if RedisStorage is None:
304
+ print(
305
+ "Redis storage is not available. Install it with 'pip install pywebguard[redis]'"
306
+ )
307
+ return 1
308
+
309
+ if not connection_string:
310
+ connection_string = "redis://localhost:6379/0"
311
+
312
+ storage = RedisStorage(url=connection_string)
313
+ storage.set("test_key", "test_value")
314
+ value = storage.get("test_key")
315
+ storage.delete("test_key")
316
+ print(f"Redis connection to {connection_string} successful")
317
+ return 0
318
+
319
+ elif storage_type == "sqlite":
320
+ if SQLiteStorage is None:
321
+ print(
322
+ "SQLite storage is not available. Install it with 'pip install pywebguard[sqlite]'"
323
+ )
324
+ return 1
325
+
326
+ if not connection_string:
327
+ connection_string = ":memory:"
328
+
329
+ storage = SQLiteStorage(db_path=connection_string)
330
+ storage.set("test_key", "test_value")
331
+ value = storage.get("test_key")
332
+ storage.delete("test_key")
333
+ print(f"SQLite connection to {connection_string} successful")
334
+ return 0
335
+
336
+ elif storage_type == "tinydb":
337
+ if TinyDBStorage is None:
338
+ print(
339
+ "TinyDB storage is not available. Install it with 'pip install pywebguard[tinydb]'"
340
+ )
341
+ return 1
342
+
343
+ if not connection_string:
344
+ connection_string = "pywebguard_test.json"
345
+
346
+ storage = TinyDBStorage(db_path=connection_string)
347
+ storage.set("test_key", "test_value")
348
+ value = storage.get("test_key")
349
+ storage.delete("test_key")
350
+
351
+ # Clean up test file if created
352
+ if (
353
+ os.path.exists(connection_string)
354
+ and connection_string != "pywebguard.json"
355
+ ):
356
+ os.remove(connection_string)
357
+
358
+ print(f"TinyDB connection test successful")
359
+ return 0
360
+
361
+ else:
362
+ print(f"Unknown storage type: {storage_type}")
363
+ return 1
364
+
365
+ except Exception as e:
366
+ print(f"Connection test failed: {e}")
367
+ return 1
368
+
369
+
370
+ def cmd_ban_ip(config_path: str, ip_address: str, duration_minutes: int = 60) -> int:
371
+ """
372
+ Ban an IP address for a specified duration.
373
+
374
+ Args:
375
+ config_path: Path to configuration file
376
+ ip_address: IP address to ban
377
+ duration_minutes: Duration of the ban in minutes
378
+
379
+ Returns:
380
+ int: Exit code (0 for success, 1 for error)
381
+
382
+ Raises:
383
+ FileNotFoundError: If the configuration file does not exist
384
+ json.JSONDecodeError: If the configuration file is not valid JSON
385
+ ValueError: If the IP address is invalid
386
+ """
387
+ try:
388
+ # Validate IP address
389
+ try:
390
+ ipaddress.ip_address(ip_address)
391
+ except ValueError:
392
+ print(f"Invalid IP address: {ip_address}")
393
+ return 1
394
+
395
+ # Read configuration from file
396
+ with open(config_path, "r") as f:
397
+ config_data = json.load(f)
398
+
399
+ # Create GuardConfig
400
+ config = GuardConfig(**config_data)
401
+
402
+ # Initialize storage
403
+ storage = _get_storage_from_config(config, config_data)
404
+
405
+ # Ban the IP
406
+ ban_key = f"banned_ip:{ip_address}"
407
+ storage.set(
408
+ ban_key,
409
+ {"reason": "Manually banned via CLI", "timestamp": time.time()},
410
+ duration_minutes * 60,
411
+ )
412
+
413
+ print(f"IP address {ip_address} banned for {duration_minutes} minutes")
414
+ return 0
415
+
416
+ except FileNotFoundError:
417
+ print(f"Configuration file {config_path} not found")
418
+ return 1
419
+ except json.JSONDecodeError:
420
+ print(f"Configuration file {config_path} is not valid JSON")
421
+ return 1
422
+ except Exception as e:
423
+ print(f"Error banning IP address: {e}")
424
+ return 1
425
+
426
+
427
+ def cmd_unban_ip(config_path: str, ip_address: str) -> int:
428
+ """
429
+ Unban a previously banned IP address.
430
+
431
+ Args:
432
+ config_path: Path to configuration file
433
+ ip_address: IP address to unban
434
+
435
+ Returns:
436
+ int: Exit code (0 for success, 1 for error)
437
+
438
+ Raises:
439
+ FileNotFoundError: If the configuration file does not exist
440
+ json.JSONDecodeError: If the configuration file is not valid JSON
441
+ ValueError: If the IP address is invalid
442
+ """
443
+ try:
444
+ # Validate IP address
445
+ try:
446
+ ipaddress.ip_address(ip_address)
447
+ except ValueError:
448
+ print(f"Invalid IP address: {ip_address}")
449
+ return 1
450
+
451
+ # Read configuration from file
452
+ with open(config_path, "r") as f:
453
+ config_data = json.load(f)
454
+
455
+ # Create GuardConfig
456
+ config = GuardConfig(**config_data)
457
+
458
+ # Initialize storage
459
+ storage = _get_storage_from_config(config, config_data)
460
+
461
+ # Unban the IP
462
+ ban_key = f"banned_ip:{ip_address}"
463
+ if storage.exists(ban_key):
464
+ storage.delete(ban_key)
465
+ print(f"IP address {ip_address} unbanned")
466
+ else:
467
+ print(f"IP address {ip_address} is not banned")
468
+
469
+ return 0
470
+
471
+ except FileNotFoundError:
472
+ print(f"Configuration file {config_path} not found")
473
+ return 1
474
+ except json.JSONDecodeError:
475
+ print(f"Configuration file {config_path} is not valid JSON")
476
+ return 1
477
+ except Exception as e:
478
+ print(f"Error unbanning IP address: {e}")
479
+ return 1
480
+
481
+
482
+ def cmd_clear_storage(config_path: str, confirm: bool = False) -> int:
483
+ """
484
+ Clear all data from storage.
485
+
486
+ Args:
487
+ config_path: Path to configuration file
488
+ confirm: Whether to skip confirmation prompt
489
+
490
+ Returns:
491
+ int: Exit code (0 for success, 1 for error)
492
+
493
+ Raises:
494
+ FileNotFoundError: If the configuration file does not exist
495
+ json.JSONDecodeError: If the configuration file is not valid JSON
496
+ """
497
+ try:
498
+ # Read configuration from file
499
+ with open(config_path, "r") as f:
500
+ config_data = json.load(f)
501
+
502
+ # Create GuardConfig
503
+ config = GuardConfig(**config_data)
504
+
505
+ # Initialize storage and get description
506
+ storage, storage_desc = _get_storage_and_desc_from_config(config, config_data)
507
+
508
+ # Confirm action
509
+ if not confirm:
510
+ response = input(
511
+ f"Are you sure you want to clear all data from {storage_desc} storage? [y/N] "
512
+ )
513
+ if response.lower() not in ["y", "yes"]:
514
+ print("Operation cancelled")
515
+ return 0
516
+
517
+ # Clear storage
518
+ storage.clear()
519
+ print(f"All data cleared from {storage_desc} storage")
520
+ return 0
521
+
522
+ except FileNotFoundError:
523
+ print(f"Configuration file {config_path} not found")
524
+ return 1
525
+ except json.JSONDecodeError:
526
+ print(f"Configuration file {config_path} is not valid JSON")
527
+ return 1
528
+ except Exception as e:
529
+ print(f"Error clearing storage: {e}")
530
+ return 1
531
+
532
+
533
+ def cmd_status(config_path: str) -> int:
534
+ """
535
+ Show status information about PyWebGuard configuration and storage.
536
+
537
+ Args:
538
+ config_path: Path to configuration file
539
+
540
+ Returns:
541
+ int: Exit code (0 for success, 1 for error)
542
+
543
+ Raises:
544
+ FileNotFoundError: If the configuration file does not exist
545
+ json.JSONDecodeError: If the configuration file is not valid JSON
546
+ """
547
+ try:
548
+ # Read configuration from file
549
+ with open(config_path, "r") as f:
550
+ config_data = json.load(f)
551
+
552
+ # Create GuardConfig
553
+ config = GuardConfig(**config_data)
554
+
555
+ # Initialize storage and get description
556
+ storage, storage_desc = _get_storage_and_desc_from_config(config, config_data)
557
+
558
+ # Print status information
559
+ print(f"PyWebGuard v{__version__}")
560
+ print(f"Storage: {storage_desc}")
561
+ print(f"IP Filter: {'Enabled' if config.ip_filter.enabled else 'Disabled'}")
562
+ print(f"Rate Limit: {'Enabled' if config.rate_limit.enabled else 'Disabled'}")
563
+ print(
564
+ f"User Agent Filter: {'Enabled' if config.user_agent.enabled else 'Disabled'}"
565
+ )
566
+ print(
567
+ f"Penetration Detection: {'Enabled' if config.penetration.enabled else 'Disabled'}"
568
+ )
569
+ print(f"CORS: {'Enabled' if config.cors.enabled else 'Disabled'}")
570
+ print(f"Logging: {'Enabled' if config.logging.enabled else 'Disabled'}")
571
+
572
+ # Try to get some statistics
573
+ try:
574
+ # Count banned IPs
575
+ banned_ips = []
576
+ if config.storage.type == "redis" and RedisStorage is not None:
577
+ # For Redis, we can use pattern matching
578
+ import redis
579
+
580
+ r = redis.from_url(
581
+ config.storage.redis_url or "redis://localhost:6379/0"
582
+ )
583
+ banned_keys = r.keys(f"{config.storage.redis_prefix}banned_ip:*")
584
+ banned_ips = [key.decode().split(":")[-1] for key in banned_keys]
585
+ else:
586
+ # For other storage types, we can't easily get this information
587
+ pass
588
+
589
+ if banned_ips:
590
+ print(f"\nBanned IPs: {len(banned_ips)}")
591
+ for ip in banned_ips[:5]: # Show first 5
592
+ print(f" - {ip}")
593
+ if len(banned_ips) > 5:
594
+ print(f" ... and {len(banned_ips) - 5} more")
595
+ except Exception:
596
+ # Ignore errors in getting statistics
597
+ pass
598
+
599
+ return 0
600
+
601
+ except FileNotFoundError:
602
+ print(f"Configuration file {config_path} not found")
603
+ return 1
604
+ except json.JSONDecodeError:
605
+ print(f"Configuration file {config_path} is not valid JSON")
606
+ return 1
607
+ except Exception as e:
608
+ print(f"Error getting status: {e}")
609
+ return 1
610
+
611
+
612
+ def cmd_interactive_init() -> int:
613
+ """
614
+ Interactively create a configuration file by prompting the user for input.
615
+
616
+ Returns:
617
+ int: Exit code (0 for success, 1 for error)
618
+ """
619
+ print("PyWebGuard Interactive Configuration")
620
+ print("===================================")
621
+
622
+ # Initialize default config
623
+ config: Dict[str, Any] = {
624
+ "ip_filter": {
625
+ "enabled": True,
626
+ "whitelist": [],
627
+ "blacklist": [],
628
+ "block_cloud_providers": False,
629
+ "geo_restrictions": {},
630
+ },
631
+ "rate_limit": {
632
+ "enabled": True,
633
+ "requests_per_minute": 60,
634
+ "burst_size": 10,
635
+ "auto_ban_threshold": 100,
636
+ "auto_ban_duration_minutes": 60,
637
+ },
638
+ "user_agent": {
639
+ "enabled": True,
640
+ "blocked_agents": ["curl", "wget", "python-requests"],
641
+ },
642
+ "penetration": {
643
+ "enabled": True,
644
+ "log_suspicious": True,
645
+ "suspicious_patterns": [],
646
+ },
647
+ "cors": {
648
+ "enabled": True,
649
+ "allow_origins": ["*"],
650
+ "allow_methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
651
+ "allow_headers": ["*"],
652
+ "allow_credentials": False,
653
+ "max_age": 600,
654
+ },
655
+ "logging": {
656
+ "enabled": True,
657
+ "log_file": "pywebguard.log",
658
+ "log_level": "INFO",
659
+ "stream": True,
660
+ "stream_levels": ["ERROR", "CRITICAL"],
661
+ "max_log_size": 10 * 1024 * 1024, # 10MB
662
+ "max_log_files": 2,
663
+ "log_format": "{asctime} {levelname} {message}",
664
+ "log_date_format": "%Y-%m-%d %H:%M:%S",
665
+ "log_rotation": "midnight",
666
+ "log_backup_count": 3,
667
+ "log_encoding": "utf-8",
668
+ "meilisearch": None,
669
+ "elasticsearch": None,
670
+ "mongodb": None,
671
+ },
672
+ "storage": {
673
+ "type": "memory",
674
+ "redis_url": None,
675
+ "redis_prefix": "pywebguard:",
676
+ "ttl": 3600,
677
+ },
678
+ }
679
+
680
+ # IP Filter
681
+ print("\nIP Filter Configuration")
682
+ config["ip_filter"]["enabled"] = input(
683
+ "Enable IP filtering? [Y/n]: "
684
+ ).strip().lower() not in ["n", "no", "0", "false"]
685
+
686
+ if config["ip_filter"]["enabled"]:
687
+ whitelist = input("Enter whitelisted IPs (comma-separated): ").strip()
688
+ if whitelist:
689
+ config["ip_filter"]["whitelist"] = [
690
+ ip.strip() for ip in whitelist.split(",")
691
+ ]
692
+
693
+ blacklist = input("Enter blacklisted IPs (comma-separated): ").strip()
694
+ if blacklist:
695
+ config["ip_filter"]["blacklist"] = [
696
+ ip.strip() for ip in blacklist.split(",")
697
+ ]
698
+
699
+ config["ip_filter"]["block_cloud_providers"] = input(
700
+ "Block cloud provider IPs? [y/N]: "
701
+ ).strip().lower() in ["y", "yes", "1", "true"]
702
+
703
+ # Rate Limit
704
+ print("\nRate Limit Configuration")
705
+ config["rate_limit"]["enabled"] = input(
706
+ "Enable rate limiting? [Y/n]: "
707
+ ).strip().lower() not in ["n", "no", "0", "false"]
708
+
709
+ if config["rate_limit"]["enabled"]:
710
+ rpm = input("Enter requests per minute [60]: ").strip()
711
+ if rpm:
712
+ config["rate_limit"]["requests_per_minute"] = int(rpm)
713
+
714
+ burst = input("Enter burst size [10]: ").strip()
715
+ if burst:
716
+ config["rate_limit"]["burst_size"] = int(burst)
717
+
718
+ ban_threshold = input("Enter auto-ban threshold [100]: ").strip()
719
+ if ban_threshold:
720
+ config["rate_limit"]["auto_ban_threshold"] = int(ban_threshold)
721
+
722
+ ban_duration = input("Enter auto-ban duration in minutes [60]: ").strip()
723
+ if ban_duration:
724
+ config["rate_limit"]["auto_ban_duration_minutes"] = int(ban_duration)
725
+
726
+ # User Agent
727
+ print("\nUser Agent Configuration")
728
+ config["user_agent"]["enabled"] = input(
729
+ "Enable user agent filtering? [Y/n]: "
730
+ ).strip().lower() not in ["n", "no", "0", "false"]
731
+
732
+ if config["user_agent"]["enabled"]:
733
+ blocked_agents = input(
734
+ "Enter blocked user agents (comma-separated) [curl,wget,python-requests]: "
735
+ ).strip()
736
+ if blocked_agents:
737
+ config["user_agent"]["blocked_agents"] = [
738
+ agent.strip() for agent in blocked_agents.split(",")
739
+ ]
740
+
741
+ # Penetration Detection
742
+ print("\nPenetration Detection Configuration")
743
+ config["penetration"]["enabled"] = input(
744
+ "Enable penetration detection? [Y/n]: "
745
+ ).strip().lower() not in ["n", "no", "0", "false"]
746
+
747
+ if config["penetration"]["enabled"]:
748
+ config["penetration"]["log_suspicious"] = input(
749
+ "Log suspicious activities? [Y/n]: "
750
+ ).strip().lower() not in ["n", "no", "0", "false"]
751
+
752
+ patterns = input("Enter suspicious patterns (comma-separated): ").strip()
753
+ if patterns:
754
+ config["penetration"]["suspicious_patterns"] = [
755
+ pattern.strip() for pattern in patterns.split(",")
756
+ ]
757
+
758
+ # CORS
759
+ print("\nCORS Configuration")
760
+ config["cors"]["enabled"] = input("Enable CORS? [Y/n]: ").strip().lower() not in [
761
+ "n",
762
+ "no",
763
+ "0",
764
+ "false",
765
+ ]
766
+
767
+ if config["cors"]["enabled"]:
768
+ origins = input("Enter allowed origins (comma-separated) [*]: ").strip()
769
+ if origins:
770
+ config["cors"]["allow_origins"] = [
771
+ origin.strip() for origin in origins.split(",")
772
+ ]
773
+
774
+ methods = input(
775
+ "Enter allowed methods (comma-separated) [GET,POST,PUT,DELETE,OPTIONS]: "
776
+ ).strip()
777
+ if methods:
778
+ config["cors"]["allow_methods"] = [
779
+ method.strip() for method in methods.split(",")
780
+ ]
781
+
782
+ headers = input("Enter allowed headers (comma-separated) [*]: ").strip()
783
+ if headers:
784
+ config["cors"]["allow_headers"] = [
785
+ header.strip() for header in headers.split(",")
786
+ ]
787
+
788
+ config["cors"]["allow_credentials"] = input(
789
+ "Allow credentials? [y/N]: "
790
+ ).strip().lower() in ["y", "yes", "1", "true"]
791
+
792
+ max_age = input("Enter max age in seconds [600]: ").strip()
793
+ if max_age:
794
+ config["cors"]["max_age"] = int(max_age)
795
+
796
+ # Storage
797
+ print("\nStorage Configuration")
798
+ print("Storage types:")
799
+ print("1. Memory (default)")
800
+ print("2. Redis")
801
+ print("3. SQLite")
802
+ print("4. TinyDB")
803
+
804
+ storage_choice = input("Choose storage type [1]: ").strip() or "1"
805
+
806
+ if storage_choice == "2":
807
+ config["storage"]["type"] = "redis"
808
+ redis_url = (
809
+ input("Enter Redis URL [redis://localhost:6379/0]: ").strip()
810
+ or "redis://localhost:6379/0"
811
+ )
812
+ config["storage"]["redis_url"] = redis_url
813
+ redis_prefix = (
814
+ input("Enter Redis key prefix [pywebguard:]: ").strip() or "pywebguard:"
815
+ )
816
+ config["storage"]["redis_prefix"] = redis_prefix
817
+ elif storage_choice == "3":
818
+ config["storage"]["type"] = "sqlite"
819
+ db_path = (
820
+ input("Enter SQLite database path [pywebguard.db]: ").strip()
821
+ or "pywebguard.db"
822
+ )
823
+ config["storage"]["db_path"] = db_path
824
+ elif storage_choice == "4":
825
+ config["storage"]["type"] = "tinydb"
826
+ db_path = (
827
+ input("Enter TinyDB database path [pywebguard.json]: ").strip()
828
+ or "pywebguard.json"
829
+ )
830
+ config["storage"]["db_path"] = db_path
831
+
832
+ # Logging
833
+ print("\nLogging Configuration")
834
+ config["logging"]["enabled"] = input(
835
+ "Enable logging? [Y/n]: "
836
+ ).strip().lower() not in ["n", "no", "0", "false"]
837
+
838
+ if config["logging"]["enabled"]:
839
+ log_file = (
840
+ input(
841
+ "Enter log file path (leave empty for stdout) [pywebguard.log]: "
842
+ ).strip()
843
+ or "pywebguard.log"
844
+ )
845
+ config["logging"]["log_file"] = log_file
846
+
847
+ print("Log levels: DEBUG, INFO, WARNING, ERROR, CRITICAL")
848
+ log_level = input("Enter log level [INFO]: ").strip().upper() or "INFO"
849
+ config["logging"]["log_level"] = log_level
850
+
851
+ # Configure logging backends
852
+ print("\nLogging Backends")
853
+ print("Available backends:")
854
+ print("1. None (default)")
855
+ print("2. Meilisearch")
856
+ print("3. Elasticsearch")
857
+ print("4. MongoDB")
858
+
859
+ backend_choice = input("Choose logging backend [1]: ").strip() or "1"
860
+
861
+ if backend_choice == "2":
862
+ config["logging"]["meilisearch"] = {
863
+ "url": input("Enter Meilisearch URL [http://localhost:7700]: ").strip()
864
+ or "http://localhost:7700",
865
+ "api_key": input("Enter Meilisearch API key: ").strip(),
866
+ "index_name": input("Enter index name [pywebguard_logs]: ").strip()
867
+ or "pywebguard_logs",
868
+ }
869
+ elif backend_choice == "3":
870
+ config["logging"]["elasticsearch"] = {
871
+ "hosts": [
872
+ input("Enter Elasticsearch host [http://localhost:9200]: ").strip()
873
+ or "http://localhost:9200"
874
+ ],
875
+ "index_prefix": input("Enter index prefix [pywebguard]: ").strip()
876
+ or "pywebguard",
877
+ "username": input("Enter username (optional): ").strip() or None,
878
+ "password": input("Enter password (optional): ").strip() or None,
879
+ }
880
+ elif backend_choice == "4":
881
+ use_uri = input("Use connection URI? [y/N]: ").strip().lower() in [
882
+ "y",
883
+ "yes",
884
+ "1",
885
+ "true",
886
+ ]
887
+ if use_uri:
888
+ config["logging"]["mongodb"] = {
889
+ "uri": input("Enter MongoDB URI: ").strip(),
890
+ "database": input("Enter database name [pywebguard]: ").strip()
891
+ or "pywebguard",
892
+ "collection": input("Enter collection name [logs]: ").strip()
893
+ or "logs",
894
+ }
895
+ else:
896
+ config["logging"]["mongodb"] = {
897
+ "host": input("Enter MongoDB host [localhost]: ").strip()
898
+ or "localhost",
899
+ "port": int(
900
+ input("Enter MongoDB port [27017]: ").strip() or "27017"
901
+ ),
902
+ "database": input("Enter database name [pywebguard]: ").strip()
903
+ or "pywebguard",
904
+ "collection": input("Enter collection name [logs]: ").strip()
905
+ or "logs",
906
+ "username": input("Enter username (optional): ").strip() or None,
907
+ "password": input("Enter password (optional): ").strip() or None,
908
+ }
909
+
910
+ # Output file
911
+ output_path = (
912
+ input("\nEnter output file path [pywebguard.json]: ").strip()
913
+ or "pywebguard.json"
914
+ )
915
+
916
+ # Write configuration to file
917
+ try:
918
+ with open(output_path, "w") as f:
919
+ json.dump(config, f, indent=4)
920
+ print(f"Configuration written to {output_path}")
921
+ return 0
922
+ except IOError as e:
923
+ print(f"Error writing configuration: {e}", file=sys.stderr)
924
+ return 1
925
+
926
+
927
+ def _get_storage_from_config(config: GuardConfig, config_data: Dict[str, Any]) -> Any:
928
+ """
929
+ Get a storage instance from a configuration.
930
+
931
+ Args:
932
+ config: GuardConfig object
933
+ config_data: Raw configuration data
934
+
935
+ Returns:
936
+ Storage instance
937
+
938
+ Raises:
939
+ ImportError: If the required storage backend is not installed
940
+ ValueError: If the storage type is unknown
941
+ """
942
+ if config.storage.type == "redis":
943
+ if RedisStorage is None:
944
+ raise ImportError(
945
+ "Redis storage is not available. Install it with 'pip install pywebguard[redis]'"
946
+ )
947
+ return RedisStorage(
948
+ url=config.storage.redis_url or "redis://localhost:6379/0",
949
+ prefix=config.storage.redis_prefix,
950
+ )
951
+ elif config.storage.type == "sqlite":
952
+ if SQLiteStorage is None:
953
+ raise ImportError(
954
+ "SQLite storage is not available. Install it with 'pip install pywebguard[sqlite]'"
955
+ )
956
+ return SQLiteStorage(
957
+ db_path=config_data.get("storage", {}).get("db_path", "pywebguard.db")
958
+ )
959
+ elif config.storage.type == "tinydb":
960
+ if TinyDBStorage is None:
961
+ raise ImportError(
962
+ "TinyDB storage is not available. Install it with 'pip install pywebguard[tinydb]'"
963
+ )
964
+ return TinyDBStorage(
965
+ db_path=config_data.get("storage", {}).get("db_path", "pywebguard.json")
966
+ )
967
+ else:
968
+ return MemoryStorage()
969
+
970
+
971
+ def _get_storage_and_desc_from_config(
972
+ config: GuardConfig, config_data: Dict[str, Any]
973
+ ) -> Tuple[Any, str]:
974
+ """
975
+ Get a storage instance and description from a configuration.
976
+
977
+ Args:
978
+ config: GuardConfig object
979
+ config_data: Raw configuration data
980
+
981
+ Returns:
982
+ Tuple of (storage instance, storage description)
983
+
984
+ Raises:
985
+ ImportError: If the required storage backend is not installed
986
+ ValueError: If the storage type is unknown
987
+ """
988
+ if config.storage.type == "redis":
989
+ if RedisStorage is None:
990
+ raise ImportError(
991
+ "Redis storage is not available. Install it with 'pip install pywebguard[redis]'"
992
+ )
993
+ storage = RedisStorage(
994
+ url=config.storage.redis_url or "redis://localhost:6379/0",
995
+ prefix=config.storage.redis_prefix,
996
+ )
997
+ storage_desc = f"Redis ({config.storage.redis_url})"
998
+ elif config.storage.type == "sqlite":
999
+ if SQLiteStorage is None:
1000
+ raise ImportError(
1001
+ "SQLite storage is not available. Install it with 'pip install pywebguard[sqlite]'"
1002
+ )
1003
+ db_path = config_data.get("storage", {}).get("db_path", "pywebguard.db")
1004
+ storage = SQLiteStorage(db_path=db_path)
1005
+ storage_desc = f"SQLite ({db_path})"
1006
+ elif config.storage.type == "tinydb":
1007
+ if TinyDBStorage is None:
1008
+ raise ImportError(
1009
+ "TinyDB storage is not available. Install it with 'pip install pywebguard[tinydb]'"
1010
+ )
1011
+ db_path = config_data.get("storage", {}).get("db_path", "pywebguard.json")
1012
+ storage = TinyDBStorage(db_path=db_path)
1013
+ storage_desc = f"TinyDB ({db_path})"
1014
+ else:
1015
+ storage = MemoryStorage()
1016
+ storage_desc = "Memory"
1017
+
1018
+ return storage, storage_desc
1019
+
1020
+
1021
+ if __name__ == "__main__":
1022
+ sys.exit(main())