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.
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.0.dist-info/LICENSE +21 -0
  39. pywebguard-0.0.0.dist-info/METADATA +368 -0
  40. pywebguard-0.0.0.dist-info/RECORD +43 -0
  41. pywebguard-0.0.0.dist-info/WHEEL +5 -0
  42. pywebguard-0.0.0.dist-info/entry_points.txt +2 -0
  43. pywebguard-0.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,455 @@
1
+ """
2
+ Configuration management for PyWebGuard.
3
+
4
+ This module provides configuration classes for various security features
5
+ of PyWebGuard, including IP filtering, rate limiting, user agent filtering,
6
+ penetration detection, CORS, logging, and storage.
7
+ """
8
+
9
+ import os
10
+ from typing import List, Dict, Any, Optional, Union, Set
11
+ from pydantic import BaseModel, Field, field_validator
12
+
13
+
14
+ class IPFilterConfig(BaseModel):
15
+ """Configuration for IP filtering.
16
+
17
+ Attributes:
18
+ enabled: Whether IP filtering is enabled
19
+ whitelist: List of allowed IP addresses
20
+ blacklist: List of blocked IP addresses
21
+ block_cloud_providers: Whether to block known cloud provider IPs
22
+ geo_restrictions: Dictionary mapping country codes to allow/block status
23
+ """
24
+
25
+ enabled: bool = True
26
+ whitelist: List[str] = Field(default_factory=list)
27
+ blacklist: List[str] = Field(default_factory=list)
28
+ block_cloud_providers: bool = False
29
+ geo_restrictions: Dict[str, bool] = Field(default_factory=dict)
30
+
31
+ @field_validator("whitelist", "blacklist")
32
+ def validate_ip_addresses(cls, v: List[str]) -> List[str]:
33
+ """Validate IP addresses in whitelist and blacklist.
34
+
35
+ Args:
36
+ v: List of IP addresses to validate
37
+
38
+ Returns:
39
+ List of validated IP addresses
40
+
41
+ Raises:
42
+ ValueError: If any IP address is invalid
43
+ """
44
+ import ipaddress
45
+
46
+ for ip in v:
47
+ try:
48
+ # Handle CIDR notation
49
+ if "/" in ip:
50
+ ipaddress.ip_network(ip, strict=False)
51
+ else:
52
+ ipaddress.ip_address(ip)
53
+ except ValueError as e:
54
+ raise ValueError(f"Invalid IP address: {ip}") from e
55
+ return v
56
+
57
+
58
+ class RateLimitConfig(BaseModel):
59
+ """Configuration for rate limiting.
60
+
61
+ Attributes:
62
+ enabled: Whether rate limiting is enabled
63
+ requests_per_minute: Maximum number of requests allowed per minute
64
+ burst_size: Maximum number of requests allowed in burst
65
+ auto_ban_threshold: Number of violations before auto-ban
66
+ auto_ban_duration_minutes: Duration of auto-ban in minutes
67
+ """
68
+
69
+ enabled: bool = True
70
+ requests_per_minute: int = Field(default=60, ge=1)
71
+ burst_size: int = Field(default=10, ge=0)
72
+ auto_ban_threshold: int = Field(default=100, ge=1)
73
+ auto_ban_duration_minutes: int = Field(default=60, ge=1)
74
+
75
+
76
+ class UserAgentConfig(BaseModel):
77
+ """Configuration for user agent filtering.
78
+
79
+ Attributes:
80
+ enabled: Whether user agent filtering is enabled
81
+ blocked_agents: List of blocked user agent strings
82
+ """
83
+
84
+ enabled: bool = True
85
+ blocked_agents: List[str] = Field(default_factory=list)
86
+
87
+
88
+ class PenetrationDetectionConfig(BaseModel):
89
+ """Configuration for penetration attempt detection.
90
+
91
+ Attributes:
92
+ enabled: Whether penetration detection is enabled
93
+ log_suspicious: Whether to log suspicious activities
94
+ suspicious_patterns: List of patterns to detect suspicious activities
95
+ """
96
+
97
+ enabled: bool = True
98
+ log_suspicious: bool = True
99
+ suspicious_patterns: List[str] = Field(default_factory=list)
100
+
101
+
102
+ class CORSConfig(BaseModel):
103
+ """Configuration for CORS (Cross-Origin Resource Sharing).
104
+
105
+ Attributes:
106
+ enabled: Whether CORS is enabled
107
+ allow_origins: List of allowed origins
108
+ allow_methods: List of allowed HTTP methods
109
+ allow_headers: List of allowed headers
110
+ allow_credentials: Whether to allow credentials
111
+ max_age: Maximum age of preflight requests in seconds
112
+ """
113
+
114
+ enabled: bool = True
115
+ allow_origins: List[str] = Field(default=["*"])
116
+ allow_methods: List[str] = Field(
117
+ default=["GET", "POST", "PUT", "DELETE", "OPTIONS"]
118
+ )
119
+ allow_headers: List[str] = Field(default=["*"])
120
+ allow_credentials: bool = False
121
+ max_age: int = Field(default=600, ge=0)
122
+
123
+
124
+ class LoggingConfig(BaseModel):
125
+ """Configuration for logging.
126
+
127
+ Attributes:
128
+ enabled: Whether logging is enabled
129
+ log_file: Path to log file (None for stdout)
130
+ log_level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
131
+ stream: Whether to log to stdout
132
+ stream_levels: List of levels to log to stdout
133
+ max_log_size: Maximum log file size in bytes
134
+ max_log_files: Maximum number of log files to keep
135
+ log_format: Log format string
136
+ log_date_format: Log date format string
137
+ log_rotation: Log rotation interval
138
+ log_backup_count: Number of backup log files to keep
139
+ log_encoding: Log file encoding
140
+ meilisearch: Meilisearch backend configuration
141
+ elasticsearch: Elasticsearch backend configuration
142
+ mongodb: MongoDB backend configuration
143
+ """
144
+
145
+ enabled: bool = True
146
+ log_file: Optional[str] = None
147
+ log_level: str = Field(default="INFO")
148
+ stream: bool = False
149
+ stream_levels: List[str] = Field(default_factory=list)
150
+ max_log_size: int = Field(default=10 * 1024 * 1024) # 10MB
151
+ max_log_files: int = Field(default=2)
152
+ log_format: str = Field(default="{asctime} {levelname} {message}")
153
+ log_date_format: str = Field(default="%Y-%m-%d %H:%M:%S")
154
+ log_rotation: str = Field(default="midnight")
155
+ log_backup_count: int = Field(default=3)
156
+ log_encoding: str = Field(default="utf-8")
157
+
158
+ # Backend configurations
159
+ meilisearch: Optional[Dict[str, Any]] = None
160
+ elasticsearch: Optional[Dict[str, Any]] = None
161
+ mongodb: Optional[Dict[str, Any]] = None
162
+
163
+ @field_validator("log_level")
164
+ def validate_log_level(cls, v: str) -> str:
165
+ """Validate logging level.
166
+
167
+ Args:
168
+ v: Logging level to validate
169
+
170
+ Returns:
171
+ Validated logging level
172
+
173
+ Raises:
174
+ ValueError: If logging level is invalid
175
+ """
176
+ valid_levels = {"DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"}
177
+ if v.upper() not in valid_levels:
178
+ raise ValueError(f"Invalid log level: {v}. Must be one of {valid_levels}")
179
+ return v.upper()
180
+
181
+ @field_validator("meilisearch")
182
+ def validate_meilisearch(
183
+ cls, v: Optional[Dict[str, Any]]
184
+ ) -> Optional[Dict[str, Any]]:
185
+ """Validate Meilisearch configuration.
186
+
187
+ Args:
188
+ v: Meilisearch configuration to validate
189
+
190
+ Returns:
191
+ Validated Meilisearch configuration
192
+
193
+ Raises:
194
+ ValueError: If configuration is invalid
195
+ """
196
+ if v is None:
197
+ return None
198
+
199
+ required_fields = {"url", "api_key", "index_name"}
200
+ missing_fields = required_fields - set(v.keys())
201
+ if missing_fields:
202
+ raise ValueError(f"Missing required Meilisearch fields: {missing_fields}")
203
+ return v
204
+
205
+ @field_validator("elasticsearch")
206
+ def validate_elasticsearch(
207
+ cls, v: Optional[Dict[str, Any]]
208
+ ) -> Optional[Dict[str, Any]]:
209
+ """Validate Elasticsearch configuration.
210
+
211
+ Args:
212
+ v: Elasticsearch configuration to validate
213
+
214
+ Returns:
215
+ Validated Elasticsearch configuration
216
+
217
+ Raises:
218
+ ValueError: If configuration is invalid
219
+ """
220
+ if v is None:
221
+ return None
222
+
223
+ required_fields = {"hosts"}
224
+ missing_fields = required_fields - set(v.keys())
225
+ if missing_fields:
226
+ raise ValueError(f"Missing required Elasticsearch fields: {missing_fields}")
227
+ return v
228
+
229
+ @field_validator("mongodb")
230
+ def validate_mongodb(cls, v: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
231
+ """Validate MongoDB configuration.
232
+
233
+ Args:
234
+ v: MongoDB configuration to validate
235
+
236
+ Returns:
237
+ Validated MongoDB configuration
238
+
239
+ Raises:
240
+ ValueError: If configuration is invalid
241
+ """
242
+ if v is None:
243
+ return None
244
+
245
+ # Check if URI is provided or if host/port are provided
246
+ if "uri" not in v and not all(
247
+ k in v for k in ["host", "port", "database", "collection"]
248
+ ):
249
+ raise ValueError(
250
+ "MongoDB configuration must include either 'uri' or all of 'host', 'port', 'database', and 'collection'"
251
+ )
252
+ return v
253
+
254
+
255
+ class StorageConfig(BaseModel):
256
+ """Configuration for storage.
257
+
258
+ Attributes:
259
+ type: Storage type ("memory", "redis", "sqlite", "tinydb", "mongodb", "postgresql")
260
+ url: Connection URL for the storage backend
261
+ prefix: Prefix for storage keys
262
+ ttl: Time-to-live for stored data in seconds
263
+ table_name: Table or collection name (for SQL/MongoDB)
264
+ """
265
+
266
+ type: str = Field(default="memory")
267
+ url: Optional[str] = None
268
+ prefix: str = Field(default="pywebguard:")
269
+ ttl: int = Field(default=3600, ge=0)
270
+ table_name: str = Field(default="pywebguard")
271
+
272
+ @field_validator("type")
273
+ def validate_storage_type(cls, v: str) -> str:
274
+ """Validate storage type.
275
+
276
+ Args:
277
+ v: Storage type to validate
278
+
279
+ Returns:
280
+ Validated storage type
281
+
282
+ Raises:
283
+ ValueError: If storage type is invalid
284
+ """
285
+ valid_types = {"memory", "redis", "sqlite", "tinydb", "mongodb", "postgresql"}
286
+ if v.lower() not in valid_types:
287
+ raise ValueError(f"Invalid storage type: {v}. Must be one of {valid_types}")
288
+ return v.lower()
289
+
290
+ @field_validator("url")
291
+ def validate_url(cls, v: Optional[str], values: Dict[str, Any]) -> Optional[str]:
292
+ """Validate storage URL.
293
+
294
+ Args:
295
+ v: Storage URL to validate
296
+ values: Other field values
297
+
298
+ Returns:
299
+ Validated storage URL
300
+
301
+ Raises:
302
+ ValueError: If URL is required but not provided
303
+ """
304
+ storage_type = values.get("type", "memory")
305
+
306
+ # Memory storage doesn't need a URL
307
+ if storage_type == "memory":
308
+ return None
309
+
310
+ # For other storage types, URL is required
311
+ if not v and storage_type != "memory":
312
+ if storage_type == "redis":
313
+ return "redis://localhost:6379/0"
314
+ elif storage_type == "sqlite":
315
+ return "pywebguard.db"
316
+ elif storage_type == "tinydb":
317
+ return "pywebguard.json"
318
+ elif storage_type == "mongodb":
319
+ return "mongodb://localhost:27017/pywebguard"
320
+ elif storage_type == "postgresql":
321
+ return "postgresql://postgres:postgres@localhost:5432/pywebguard"
322
+
323
+ return v
324
+
325
+
326
+ class GuardConfig(BaseModel):
327
+ """Main configuration for PyWebGuard.
328
+
329
+ This class combines all configuration components into a single configuration
330
+ object that can be used to initialize the WebGuard instance.
331
+
332
+ Attributes:
333
+ ip_filter: IP filtering configuration
334
+ rate_limit: Rate limiting configuration
335
+ user_agent: User agent filtering configuration
336
+ penetration: Penetration detection configuration
337
+ cors: CORS configuration
338
+ logging: Logging configuration
339
+ storage: Storage configuration
340
+ """
341
+
342
+ ip_filter: IPFilterConfig = Field(default_factory=IPFilterConfig)
343
+ rate_limit: RateLimitConfig = Field(default_factory=RateLimitConfig)
344
+ user_agent: UserAgentConfig = Field(default_factory=UserAgentConfig)
345
+ penetration: PenetrationDetectionConfig = Field(
346
+ default_factory=PenetrationDetectionConfig
347
+ )
348
+ cors: CORSConfig = Field(default_factory=CORSConfig)
349
+ logging: LoggingConfig = Field(default_factory=LoggingConfig)
350
+ storage: StorageConfig = Field(default_factory=StorageConfig)
351
+
352
+ @classmethod
353
+ def from_env(cls) -> "GuardConfig":
354
+ """Create a GuardConfig instance from environment variables.
355
+
356
+ Environment variables:
357
+ PYWEBGUARD_STORAGE_TYPE: Storage type (memory, redis, sqlite, tinydb, mongodb, postgresql)
358
+ PYWEBGUARD_STORAGE_URL: Connection URL for the storage backend
359
+ PYWEBGUARD_STORAGE_PREFIX: Prefix for storage keys
360
+ PYWEBGUARD_STORAGE_TTL: Time-to-live for stored data in seconds
361
+ PYWEBGUARD_STORAGE_TABLE: Table or collection name
362
+
363
+ PYWEBGUARD_RATE_LIMIT_ENABLED: Whether rate limiting is enabled (true/false)
364
+ PYWEBGUARD_RATE_LIMIT_RPM: Requests per minute
365
+ PYWEBGUARD_RATE_LIMIT_BURST: Burst size
366
+ PYWEBGUARD_RATE_LIMIT_BAN_THRESHOLD: Auto-ban threshold
367
+ PYWEBGUARD_RATE_LIMIT_BAN_DURATION: Auto-ban duration in minutes
368
+
369
+ PYWEBGUARD_IP_FILTER_ENABLED: Whether IP filtering is enabled (true/false)
370
+ PYWEBGUARD_IP_WHITELIST: Comma-separated list of whitelisted IPs
371
+ PYWEBGUARD_IP_BLACKLIST: Comma-separated list of blacklisted IPs
372
+
373
+ PYWEBGUARD_LOG_ENABLED: Whether logging is enabled (true/false)
374
+ PYWEBGUARD_LOG_FILE: Path to log file
375
+ PYWEBGUARD_LOG_LEVEL: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
376
+
377
+ Returns:
378
+ GuardConfig: Configuration instance with values from environment variables
379
+ """
380
+ # Create default config
381
+ config = cls()
382
+
383
+ # Storage configuration
384
+ storage_type = os.environ.get("PYWEBGUARD_STORAGE_TYPE")
385
+ if storage_type:
386
+ config.storage.type = storage_type.lower()
387
+
388
+ storage_url = os.environ.get("PYWEBGUARD_STORAGE_URL")
389
+ if storage_url:
390
+ config.storage.url = storage_url
391
+
392
+ storage_prefix = os.environ.get("PYWEBGUARD_STORAGE_PREFIX")
393
+ if storage_prefix:
394
+ config.storage.prefix = storage_prefix
395
+
396
+ storage_ttl = os.environ.get("PYWEBGUARD_STORAGE_TTL")
397
+ if storage_ttl and storage_ttl.isdigit():
398
+ config.storage.ttl = int(storage_ttl)
399
+
400
+ storage_table = os.environ.get("PYWEBGUARD_STORAGE_TABLE")
401
+ if storage_table:
402
+ config.storage.table_name = storage_table
403
+
404
+ # Rate limit configuration
405
+ rate_limit_enabled = os.environ.get("PYWEBGUARD_RATE_LIMIT_ENABLED")
406
+ if rate_limit_enabled is not None:
407
+ config.rate_limit.enabled = rate_limit_enabled.lower() in (
408
+ "true",
409
+ "1",
410
+ "yes",
411
+ )
412
+
413
+ rate_limit_rpm = os.environ.get("PYWEBGUARD_RATE_LIMIT_RPM")
414
+ if rate_limit_rpm and rate_limit_rpm.isdigit():
415
+ config.rate_limit.requests_per_minute = int(rate_limit_rpm)
416
+
417
+ rate_limit_burst = os.environ.get("PYWEBGUARD_RATE_LIMIT_BURST")
418
+ if rate_limit_burst and rate_limit_burst.isdigit():
419
+ config.rate_limit.burst_size = int(rate_limit_burst)
420
+
421
+ rate_limit_ban_threshold = os.environ.get("PYWEBGUARD_RATE_LIMIT_BAN_THRESHOLD")
422
+ if rate_limit_ban_threshold and rate_limit_ban_threshold.isdigit():
423
+ config.rate_limit.auto_ban_threshold = int(rate_limit_ban_threshold)
424
+
425
+ rate_limit_ban_duration = os.environ.get("PYWEBGUARD_RATE_LIMIT_BAN_DURATION")
426
+ if rate_limit_ban_duration and rate_limit_ban_duration.isdigit():
427
+ config.rate_limit.auto_ban_duration_minutes = int(rate_limit_ban_duration)
428
+
429
+ # IP filter configuration
430
+ ip_filter_enabled = os.environ.get("PYWEBGUARD_IP_FILTER_ENABLED")
431
+ if ip_filter_enabled is not None:
432
+ config.ip_filter.enabled = ip_filter_enabled.lower() in ("true", "1", "yes")
433
+
434
+ ip_whitelist = os.environ.get("PYWEBGUARD_IP_WHITELIST")
435
+ if ip_whitelist:
436
+ config.ip_filter.whitelist = [ip.strip() for ip in ip_whitelist.split(",")]
437
+
438
+ ip_blacklist = os.environ.get("PYWEBGUARD_IP_BLACKLIST")
439
+ if ip_blacklist:
440
+ config.ip_filter.blacklist = [ip.strip() for ip in ip_blacklist.split(",")]
441
+
442
+ # Logging configuration
443
+ log_enabled = os.environ.get("PYWEBGUARD_LOG_ENABLED")
444
+ if log_enabled is not None:
445
+ config.logging.enabled = log_enabled.lower() in ("true", "1", "yes")
446
+
447
+ log_file = os.environ.get("PYWEBGUARD_LOG_FILE")
448
+ if log_file:
449
+ config.logging.log_file = log_file
450
+
451
+ log_level = os.environ.get("PYWEBGUARD_LOG_LEVEL")
452
+ if log_level:
453
+ config.logging.log_level = log_level.upper()
454
+
455
+ return config
File without changes
@@ -0,0 +1,44 @@
1
+ """
2
+ Base filter interfaces for PyWebGuard with both sync and async support.
3
+ """
4
+
5
+ from abc import ABC, abstractmethod
6
+ from typing import Dict, Any, Union
7
+
8
+
9
+ class BaseFilter(ABC):
10
+ """
11
+ Abstract base class for synchronous filters.
12
+ """
13
+
14
+ @abstractmethod
15
+ def is_allowed(self, value: Any) -> Dict[str, Union[bool, str]]:
16
+ """
17
+ Check if a value is allowed by the filter.
18
+
19
+ Args:
20
+ value: The value to check
21
+
22
+ Returns:
23
+ Dict with allowed status and reason
24
+ """
25
+ pass
26
+
27
+
28
+ class AsyncBaseFilter(ABC):
29
+ """
30
+ Abstract base class for asynchronous filters.
31
+ """
32
+
33
+ @abstractmethod
34
+ async def is_allowed(self, value: Any) -> Dict[str, Union[bool, str]]:
35
+ """
36
+ Check if a value is allowed by the filter asynchronously.
37
+
38
+ Args:
39
+ value: The value to check
40
+
41
+ Returns:
42
+ Dict with allowed status and reason
43
+ """
44
+ pass