fastapi-guard 0.1.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.
config/__init__.py ADDED
File without changes
config/ip2/__init__.py ADDED
File without changes
@@ -0,0 +1,162 @@
1
+ import asyncio
2
+ from datetime import datetime, timezone
3
+ from IP2Location import IP2Location
4
+ import logging
5
+ import os
6
+ import requests
7
+ import zipfile
8
+
9
+ from typing import TYPE_CHECKING
10
+
11
+ if TYPE_CHECKING:
12
+ from guard.models import SecurityConfig
13
+
14
+ IP2_CONFIG_PATH = "./config/ip2/files"
15
+ DB_FILENAME = "IP2LOCATION-LITE-DB1.IPV6.BIN"
16
+ DOWNLOAD_URL = f"https://download.ip2location.com/lite/{DB_FILENAME}.ZIP"
17
+ VERSION_FILE = f"{IP2_CONFIG_PATH}/ip2location_version.txt"
18
+
19
+ ip2location_db = None
20
+
21
+
22
+ def get_ip2location_database(
23
+ config: "SecurityConfig"
24
+ ) -> IP2Location:
25
+ """
26
+ Get the IP2Location database object.
27
+ """
28
+ global ip2location_db
29
+ if ip2location_db is None:
30
+ db_path = config.ip2location_db_path or os.path.join(
31
+ IP2_CONFIG_PATH, DB_FILENAME
32
+ )
33
+ try:
34
+ ip2location_db = IP2Location(db_path)
35
+ except Exception as e:
36
+ message = "Error loading IP2Location database"
37
+ reason_message = f"Reason: {str(e)}"
38
+ logging.error(f"{message} - {reason_message}")
39
+ ip2location_db = None
40
+ return ip2location_db
41
+
42
+
43
+ def check_for_updates() -> bool:
44
+ """
45
+ Check if there's a new version
46
+ of the IP2Location database available.
47
+
48
+ Returns:
49
+ bool: True if an update is
50
+ available, False otherwise.
51
+ """
52
+ response = requests.head(DOWNLOAD_URL)
53
+ if response.status_code != 200:
54
+ message = "Failed to check for updates"
55
+ reason_message = f"Status code: {response.status_code}"
56
+ logging.error(f"{message} - {reason_message}")
57
+ return False
58
+
59
+ last_modified = datetime.strptime(
60
+ response.headers[
61
+ "Last-Modified"
62
+ ], "%a, %d %b %Y %H:%M:%S GMT"
63
+ ).replace(tzinfo=timezone.utc)
64
+
65
+ if os.path.exists(VERSION_FILE):
66
+ with open(VERSION_FILE, "r") as f:
67
+ current_version = datetime.fromisoformat(
68
+ f.read().strip()
69
+ ).replace(
70
+ tzinfo=timezone.utc
71
+ )
72
+
73
+ if last_modified <= current_version:
74
+ print("Database is up to date.")
75
+ return False
76
+
77
+ print("New version available. Updating...")
78
+ return True
79
+
80
+
81
+ def download_ip2location_database(
82
+ config: "SecurityConfig"
83
+ ):
84
+ """
85
+ Download and extract the latest IP2Location
86
+ database if an update is available.
87
+ """
88
+ if not config.ip2location_auto_download:
89
+ return
90
+
91
+ if not check_for_updates():
92
+ return
93
+
94
+ response = requests.get(DOWNLOAD_URL)
95
+
96
+ if response.status_code == 200:
97
+ zip_path = os.path.join(IP2_CONFIG_PATH, f"{DB_FILENAME}.ZIP")
98
+ with open(zip_path, "wb") as f:
99
+ f.write(response.content)
100
+
101
+ with zipfile.ZipFile(zip_path, "r") as zip_ref:
102
+ zip_ref.extractall(IP2_CONFIG_PATH)
103
+
104
+ os.remove(zip_path)
105
+
106
+ with open(VERSION_FILE, "w") as f:
107
+ f.write(
108
+ datetime.now(
109
+ timezone.utc
110
+ ).isoformat()
111
+ )
112
+
113
+ logging.info("IP2Location db downloaded successfully.")
114
+ else:
115
+ message = "Failed to download IP2Location database"
116
+ reason_message = f"Status code: {response.status_code}"
117
+ logging.error(f"{message} - {reason_message}")
118
+
119
+
120
+ async def periodic_update_check(
121
+ interval_hours: int = 24
122
+ ):
123
+ """
124
+ Periodically check for updates
125
+ and download the new database if available.
126
+ """
127
+ while True:
128
+ try:
129
+ await asyncio.sleep(interval_hours * 3600)
130
+ if check_for_updates():
131
+ download_ip2location_database()
132
+
133
+ global ip2location_db
134
+ ip2location_db = None
135
+ except asyncio.CancelledError:
136
+ break
137
+
138
+
139
+ async def start_periodic_update_check(
140
+ config: "SecurityConfig"
141
+ ):
142
+ """
143
+ Start the periodic update
144
+ check in the background.
145
+ """
146
+ if not config.ip2location_auto_update:
147
+ return None
148
+
149
+ task = asyncio.create_task(
150
+ periodic_update_check(
151
+ config.ip2location_update_interval
152
+ )
153
+ )
154
+
155
+ def handle_task_done(future):
156
+ try:
157
+ future.result()
158
+ except asyncio.CancelledError:
159
+ pass
160
+
161
+ task.add_done_callback(handle_task_done)
162
+ return task
config/sus_patterns.py ADDED
@@ -0,0 +1,261 @@
1
+ import re
2
+ from typing import Set, List
3
+
4
+
5
+ class SusPatterns:
6
+ """
7
+ A singleton class that manages suspicious
8
+ patterns for security checks.
9
+
10
+ This class maintains two sets of patterns:
11
+ default patterns and custom patterns.
12
+ It provides methods to add, remove,
13
+ and retrieve patterns.
14
+ """
15
+
16
+ _instance = None
17
+
18
+ custom_patterns: Set[str] = set()
19
+
20
+ patterns: List[str] = [
21
+ # XSS
22
+ r"<script.*?>.*?</script.*?>",
23
+ r"javascript:",
24
+ r"onerror=",
25
+ r"onload=",
26
+ r"alert\(",
27
+ r"document\.cookie",
28
+ r"document\.write",
29
+ r"window\.location",
30
+ # SQL Injection
31
+ r"SELECT\s+.*\s+FROM\s+.*",
32
+ r"UNION\s+SELECT\s+.*",
33
+ r"'.*?OR.*?=.*?'",
34
+ r"'.*?AND.*?=.*?'",
35
+ r"INSERT\s+INTO\s+.*",
36
+ r"UPDATE\s+.*\s+SET\s+.*",
37
+ r"DELETE\s+FROM\s+.*",
38
+ r"DROP\s+TABLE\s+.*",
39
+ r"CREATE\s+TABLE\s+.*",
40
+ r"ALTER\s+TABLE\s+.*",
41
+ r"EXEC\s+.*",
42
+ r"CAST\s*\(.*\s+AS\s+.*\)",
43
+ r"CONVERT\s*\(.*\s+USING\s+.*\)",
44
+ # Directory Traversal
45
+ r"\.\./",
46
+ r"\.\.\\",
47
+ r"/etc/passwd",
48
+ r"/etc/shadow",
49
+ r"/etc/group",
50
+ r"/proc/self/environ",
51
+ r"/windows/win.ini",
52
+ r"/boot.ini",
53
+ # Command Injection
54
+ r"\b(?:ls|cat|rm|mv|cp|chmod|chown|sudo|su)\b",
55
+ r"\b(?:wget|curl|nc|ncat|telnet|ssh|ftp)\b",
56
+ r"\b(?:ping|traceroute|nslookup|dig)\b",
57
+ r"\b(?:ifconfig|ipconfig|netstat)\b",
58
+ r"\b(?:uname|whoami|id|pwd)\b",
59
+ # Sensitive File Access
60
+ r"\b(?:passwd|shadow|group)\b",
61
+ r"\b(?:\.env|\.git|\.svn|\.hg|\.DS_Store)\b",
62
+ r"\b(?:phpinfo|setup\.php|config\.php|admin\.php)\b",
63
+ r"\b(?:sitemap\.xml|robots\.txt|security\.txt)\b",
64
+ # Common Admin Paths
65
+ r"\b(?:solr|admin|cgi-bin|wp-admin|wp-login)\b",
66
+ # Common Query Parameters
67
+ r"\b(?:query|show|diagnostics|status|action)\b",
68
+ r"\b(?:format=json|wt=json)\b",
69
+ # HTTP Method Tampering
70
+ r"OPTIONS",
71
+ r"TRACE",
72
+ r"CONNECT",
73
+ # Path Traversal
74
+ r"\.\./",
75
+ r"\.\.\\",
76
+ # File Inclusion
77
+ r"file://",
78
+ r"php://",
79
+ r"data://",
80
+ r"zip://",
81
+ r"rar://",
82
+ r"expect://",
83
+ # LDAP Injection
84
+ r"\(\|\(.*?\=\*\)\)",
85
+ r"\(\&\(.*?\=\*\)\)",
86
+ # XML Injection
87
+ r"<!DOCTYPE\s+.*?>",
88
+ r"<\?xml\s+.*?>",
89
+ r"<!ENTITY\s+.*?>",
90
+ # SSRF (Server-Side Request Forgery)
91
+ r"http://localhost",
92
+ r"http://127\.0\.0\.1",
93
+ r"http://169\.254\.169\.254",
94
+ r"http://metadata\.google\.internal",
95
+ # Open Redirect
96
+ r"//",
97
+ r"/\.\./",
98
+ r"/\.\.\\",
99
+ # CRLF Injection
100
+ r"%0d%0a",
101
+ r"%0d",
102
+ r"%0a",
103
+ # Path Manipulation
104
+ r"\.\./",
105
+ r"\.\.\\",
106
+ # Shell Injection
107
+ r";",
108
+ r"&",
109
+ r"\|",
110
+ r"`",
111
+ r"\$\(.*?\)",
112
+ r"\$\{.*?\}",
113
+ # NoSQL Injection
114
+ r"\{\s*['\"]?\$.*?['\"]?\s*:\s*.*?\s*\}",
115
+ # JSON Injection
116
+ r"\{\s*\"\$.*?\"\s*:\s*.*?\s*\}",
117
+ # HTTP Header Injection
118
+ r"\r\n",
119
+ r"\n",
120
+ # File Upload
121
+ r"Content-Disposition: form-data; name=\".*?\"; filename=\".*?\."
122
+ r"(php|exe|sh|bat)\"",
123
+ # Other
124
+ r"eval\(",
125
+ r"base64_decode\(",
126
+ r"system\(",
127
+ r"shell_exec\(",
128
+ r"exec\(",
129
+ r"popen\(",
130
+ r"proc_open\(",
131
+ ]
132
+
133
+ def __new__(cls):
134
+ """
135
+ Ensure only one instance of SusPatterns
136
+ is created (singleton pattern).
137
+
138
+ Returns:
139
+ SusPatterns: The single instance
140
+ of the SusPatterns class.
141
+ """
142
+ if cls._instance is None:
143
+ cls._instance = super(
144
+ SusPatterns,
145
+ cls
146
+ ).__new__(cls)
147
+ cls._instance.compiled_patterns = [
148
+ re.compile(
149
+ pattern,
150
+ re.IGNORECASE
151
+ )
152
+ for pattern in cls.patterns
153
+ ]
154
+ cls._instance.compiled_custom_patterns = set()
155
+ return cls._instance
156
+
157
+ @classmethod
158
+ async def add_pattern(
159
+ cls,
160
+ pattern: str,
161
+ custom: bool = False
162
+ ) -> None:
163
+ """
164
+ Add a new pattern to either the custom or
165
+ default patterns list.
166
+
167
+ Args:
168
+ pattern (str): The pattern to be added.
169
+ custom (bool, optional): If True, add
170
+ to custom patterns; otherwise, add to
171
+ default patterns. Defaults to False.
172
+ """
173
+ compiled_pattern = re.compile(
174
+ pattern,
175
+ re.IGNORECASE
176
+ )
177
+ if custom:
178
+ cls._instance.compiled_custom_patterns.add(
179
+ compiled_pattern
180
+ )
181
+ cls._instance.custom_patterns.add(
182
+ pattern
183
+ )
184
+ else:
185
+ cls._instance.compiled_patterns.append(
186
+ compiled_pattern
187
+ )
188
+ cls._instance.patterns.append(
189
+ pattern
190
+ )
191
+
192
+ @classmethod
193
+ async def remove_pattern(
194
+ cls,
195
+ pattern: str,
196
+ custom: bool = False
197
+ ) -> None:
198
+ """
199
+ Remove a pattern from either the
200
+ custom or default patterns list.
201
+
202
+ Args:
203
+ pattern (str): The pattern to be removed.
204
+ custom (bool, optional): If True, remove
205
+ from custom patterns; otherwise, remove
206
+ from default patterns. Defaults to False.
207
+ """
208
+ compiled_pattern = re.compile(
209
+ pattern,
210
+ re.IGNORECASE
211
+ )
212
+ if custom:
213
+ cls._instance.compiled_custom_patterns.discard(
214
+ compiled_pattern
215
+ )
216
+ cls._instance.custom_patterns.discard(
217
+ pattern
218
+ )
219
+ else:
220
+ cls._instance.compiled_patterns = [
221
+ p
222
+ for p in cls._instance.compiled_patterns
223
+ if p.pattern != pattern
224
+ ]
225
+ cls._instance.patterns = [
226
+ p
227
+ for p in cls._instance.patterns
228
+ if p != pattern
229
+ ]
230
+
231
+ @classmethod
232
+ async def get_all_patterns(
233
+ cls
234
+ ) -> List[str]:
235
+ """
236
+ Retrieve all patterns, including
237
+ both default and custom patterns.
238
+
239
+ Returns:
240
+ List[str]: A list containing
241
+ all default and custom patterns.
242
+ """
243
+ return cls._instance.patterns + list(
244
+ cls._instance.custom_patterns
245
+ )
246
+
247
+ @classmethod
248
+ async def get_all_compiled_patterns(
249
+ cls
250
+ ) -> List[re.Pattern]:
251
+ """
252
+ Retrieve all compiled patterns,
253
+ including both default and custom patterns.
254
+
255
+ Returns:
256
+ List[re.Pattern]: A list containing
257
+ all default and custom compiled patterns.
258
+ """
259
+ return cls._instance.compiled_patterns + list(
260
+ cls._instance.compiled_custom_patterns
261
+ )
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Renzo F
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.