pywebguard 0.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.
- pywebguard/__init__.py +87 -0
- pywebguard/cli.py +1022 -0
- pywebguard/core/__init__.py +0 -0
- pywebguard/core/base.py +792 -0
- pywebguard/core/config.py +455 -0
- pywebguard/filters/__init__.py +0 -0
- pywebguard/filters/base.py +44 -0
- pywebguard/filters/ip_filter.py +233 -0
- pywebguard/filters/user_agent.py +102 -0
- pywebguard/frameworks/__init__.py +0 -0
- pywebguard/frameworks/_fastapi.py +191 -0
- pywebguard/frameworks/_flask.py +182 -0
- pywebguard/limiters/__init__.py +0 -0
- pywebguard/limiters/base.py +46 -0
- pywebguard/limiters/rate_limit.py +415 -0
- pywebguard/logging/__init__.py +0 -0
- pywebguard/logging/backends/__init__.py +17 -0
- pywebguard/logging/backends/_elasticsearch.py +307 -0
- pywebguard/logging/backends/_meilisearch.py +310 -0
- pywebguard/logging/backends/_mongodb.py +303 -0
- pywebguard/logging/base.py +117 -0
- pywebguard/logging/logger.py +408 -0
- pywebguard/security/__init__.py +0 -0
- pywebguard/security/base.py +44 -0
- pywebguard/security/cors.py +204 -0
- pywebguard/security/penetration.py +265 -0
- pywebguard/storage/__init__.py +0 -0
- pywebguard/storage/_mongodb.py +396 -0
- pywebguard/storage/_postgresql.py +539 -0
- pywebguard/storage/_redis.py +314 -0
- pywebguard/storage/_sqlite.py +374 -0
- pywebguard/storage/_tinydb.py +271 -0
- pywebguard/storage/base.py +327 -0
- pywebguard/storage/memory.py +245 -0
- pywebguard/utils/__init__.py +0 -0
- pywebguard/utils/ip.py +84 -0
- pywebguard/utils/request.py +65 -0
- pywebguard-0.0.0.dist-info/LICENSE +21 -0
- pywebguard-0.0.0.dist-info/METADATA +368 -0
- pywebguard-0.0.0.dist-info/RECORD +43 -0
- pywebguard-0.0.0.dist-info/WHEEL +5 -0
- pywebguard-0.0.0.dist-info/entry_points.txt +2 -0
- pywebguard-0.0.0.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())
|