3
3
# \/imana 2016
4
4
# [|-ramewørk
5
5
#
6
- #
7
6
# Author: s4dhu
8
7
# Email: <s4dhul4bs[at]prontonmail[dot]ch
9
8
# Git: @s4dhulabs
17
16
18
17
from .base import BaseDetector
19
18
20
-
21
19
class SanicDetector (BaseDetector ):
22
- """Sanic-specific detection methods"""
20
+ """Sanic-specific detection methods (improved for specificity) """
23
21
24
22
FRAMEWORK = "Sanic"
25
23
26
- # Common Sanic paths to check
27
24
COMMON_PATHS = [
28
25
'/api/status' ,
29
- '/about' ,
30
26
'/error' ,
27
+ '/about' ,
31
28
'/health' ,
32
29
'/metrics' ,
33
30
'/docs' ,
34
31
'/openapi.json' ,
35
32
'/api/docs' ,
36
33
]
37
34
38
- # Sanic error patterns
39
35
ERROR_PATTERNS = [
40
- # Pattern, Description, Confidence
41
- (r'⚠️ \d+ — [^=]+' , 'Sanic error page format' , 25 ),
42
- (r'Traceback of \w+_test_app' , 'Sanic test app traceback' , 30 ),
43
- (r'Exception: [^=]+ while handling path' , 'Sanic exception handling' , 25 ),
44
- (r'File /usr/local/lib/python3\.9/site-packages/sanic/app\.py' , 'Sanic app.py reference' , 35 ),
45
- (r'==============================' , 'Sanic error separator' , 15 ),
36
+ (r'SanicException' , 'SanicException in error page' , 30 ),
37
+ (r'sanic_test_app' , 'Sanic test app traceback' , 30 ),
38
+ (r'Sanic\s+\d+\.\d+\.\d+' , 'Sanic version in error' , 20 ),
39
+ (r'sanic/app\.py' , 'Sanic app.py in traceback' , 25 ),
40
+ (r'sanic.server' , 'Sanic server in traceback' , 20 ),
46
41
(r'Exception: Test error for framework detection' , 'Sanic test error' , 20 ),
42
+ (r'(?i)sanic' , 'Sanic reference in error' , 10 ),
47
43
]
48
44
49
- # Sanic content patterns
50
45
CONTENT_PATTERNS = [
51
- # Pattern, Description, Confidence
52
46
(r'Hello from Sanic!' , 'Sanic greeting in content' , 20 ),
53
47
(r'<title>Sanic Test App</title>' , 'Sanic test app title' , 25 ),
54
48
(r'<h1>Hello from Sanic!</h1>' , 'Sanic heading in content' , 20 ),
55
- (r'This is a minimal Sanic application ' , 'Sanic app description ' , 15 ),
56
- (r'<a href="/api/status">API Status</a> ' , 'Sanic API status link ' , 10 ),
57
- (r'<a href="/about">About</a> ' , 'Sanic about link ' , 10 ),
49
+ (r'Sanic Framework Detection Test ' , 'Sanic detection test string ' , 20 ),
50
+ (r'Sanic is a Python 3.7\+ web server ' , 'Sanic description ' , 15 ),
51
+ (r'(?i)sanic ' , 'Sanic reference in HTML ' , 10 ),
58
52
]
59
53
60
- # Sanic header patterns (though Sanic doesn't have many unique headers)
61
54
HEADER_PATTERNS = [
62
- # Header name, Pattern, Description, Confidence
63
- ('content-type' , r'text/plain; charset=utf-8' , 'Sanic plain text response' , 5 ),
64
- ('Allow' , r'GET' , 'Sanic GET method allowance' , 3 ),
55
+ ('server' , r'uvicorn' , 'Uvicorn server header (common for Sanic)' , 5 ),
56
+ ('content-type' , r'application/json' , 'JSON API response' , 3 ),
65
57
]
66
58
67
- # Sanic API response patterns
68
59
API_PATTERNS = [
69
- # Pattern, Description, Confidence
70
- (r'"status": "running"' , 'Sanic API status response' , 20 ),
71
- (r'"framework": "sanic"' , 'Sanic framework identification' , 25 ),
72
- (r'"version": "\d+\.\d+\.\d+"' , 'Sanic version in API' , 15 ),
60
+ (r'"framework": ?"sanic"' , 'Sanic framework in API response' , 30 ),
61
+ (r'"status": ?"running"' , 'Status running in API response' , 10 ),
62
+ (r'"version": ?"[0-9.]+"' , 'Version in API response' , 10 ),
73
63
]
74
64
75
65
def detect (self ) -> None :
76
- """Run Sanic detection methods"""
77
- self ._check_headers ()
78
- self ._check_content_patterns ()
79
- self ._check_error_patterns ()
80
- self ._check_api_endpoints ()
81
- self ._check_common_paths ()
66
+ evidence = set ()
67
+ evidence_types = set ()
68
+ # Check error page for Sanic-specific patterns
69
+ if self ._check_error_patterns (evidence , evidence_types ):
70
+ pass
71
+ # Check API status endpoint for Sanic-specific JSON
72
+ if self ._check_api_status (evidence , evidence_types ):
73
+ pass
74
+ # Check main page and about for Sanic content
75
+ if self ._check_content_patterns (evidence , evidence_types ):
76
+ pass
77
+ # Check headers for Uvicorn and JSON
78
+ if self ._check_headers (evidence , evidence_types ):
79
+ pass
80
+ # Check common paths for 200 OK (low confidence)
81
+ self ._check_common_paths (evidence , evidence_types )
82
+ # Assign confidence based on evidence
83
+ self ._score_evidence (evidence , evidence_types )
82
84
self .detect_version ()
83
-
84
- def _add_score (self ,
85
- points : int ,
86
- evidence_type : str ,
87
- detail : str ,
88
- raw_data : Optional [Dict [str , Any ]] = None ) -> None :
89
- """Add score for Sanic"""
85
+
86
+ def _add_score (self , points : int , evidence_type : str , detail : str , raw_data : Optional [Dict [str , Any ]] = None ) -> None :
90
87
self .result_manager .add_score (self .FRAMEWORK , points , evidence_type , detail , raw_data )
91
-
92
- def _add_version_hint (self ,
93
- version : str ,
94
- confidence : int ,
95
- evidence : str ) -> None :
96
- """Add version hint for Sanic"""
88
+
89
+ def _add_version_hint (self , version : str , confidence : int , evidence : str ) -> None :
97
90
self .result_manager .add_version_hint (self .FRAMEWORK , version , confidence , evidence )
98
-
99
- def _add_component (self ,
100
- component : str ,
101
- evidence : str ) -> None :
102
- """Add component for Sanic"""
91
+
92
+ def _add_component (self , component : str , evidence : str ) -> None :
103
93
self .result_manager .add_component (self .FRAMEWORK , component , evidence )
104
-
105
- def _check_headers (self ) -> None :
106
- """Check for Sanic-specific headers"""
94
+
95
+ def _check_headers (self , evidence , evidence_types ) -> bool :
107
96
response = self .request_manager .make_request ()
108
97
if not response :
109
- return
110
-
98
+ return False
111
99
headers = response .headers
112
-
113
- # Check header patterns
100
+ found = False
114
101
for header_name , pattern , description , confidence in self .HEADER_PATTERNS :
115
102
if header_name in headers :
116
103
header_value = headers [header_name ]
117
104
if re .search (pattern , header_value , re .IGNORECASE ):
118
- self ._add_score (
119
- confidence ,
120
- 'Header' ,
121
- f"{ description } : { header_name } : { header_value } "
122
- )
123
-
124
- # Check for 405 Method Not Allowed (common in Sanic for HEAD requests)
125
- if response .status_code == 405 :
126
- self ._add_score (
127
- 10 ,
128
- 'Header' ,
129
- f"405 Method Not Allowed response (common in Sanic)"
130
- )
131
-
132
- def _check_content_patterns (self ) -> None :
133
- """Check for Sanic content patterns"""
105
+ evidence .add (f"Header: { description } " )
106
+ evidence_types .add ('header' )
107
+ found = True
108
+ return found
109
+
110
+ def _check_content_patterns (self , evidence , evidence_types ) -> bool :
134
111
response = self .request_manager .make_request ()
135
112
if not response :
136
- return
137
-
138
- # Check content patterns
113
+ return False
114
+ found = False
139
115
for pattern , description , confidence in self .CONTENT_PATTERNS :
140
116
if re .search (pattern , response .text , re .IGNORECASE ):
141
- self ._add_score (
142
- confidence ,
143
- 'Content' ,
144
- f"{ description } : { pattern } "
145
- )
146
-
147
- def _check_error_patterns (self ) -> None :
148
- """Check for Sanic error patterns"""
117
+ evidence .add (f"Content: { description } " )
118
+ evidence_types .add ('content' )
119
+ found = True
120
+ return found
121
+
122
+ def _check_error_patterns (self , evidence , evidence_types ) -> bool :
149
123
base_url = self .request_manager .target_url .rstrip ('/' )
150
124
error_url = urljoin (base_url , '/error' )
151
-
152
125
response = self .request_manager .make_request (error_url )
153
126
if not response :
154
- return
155
-
156
- # Check error patterns in response text
127
+ return False
128
+ found = False
157
129
for pattern , description , confidence in self .ERROR_PATTERNS :
158
130
if re .search (pattern , response .text , re .IGNORECASE ):
159
- self ._add_score (
160
- confidence ,
161
- 'Error' ,
162
- f"{ description } : { pattern } "
163
- )
164
-
165
- def _check_api_endpoints (self ) -> None :
166
- """Check for Sanic API endpoints"""
131
+ evidence .add (f"Error: { description } " )
132
+ evidence_types .add ('error' )
133
+ found = True
134
+ return found
135
+
136
+ def _check_api_status (self , evidence , evidence_types ) -> bool :
167
137
base_url = self .request_manager .target_url .rstrip ('/' )
168
138
api_url = urljoin (base_url , '/api/status' )
169
-
170
139
response = self .request_manager .make_request (api_url )
171
140
if not response :
172
- return
173
-
174
- # Check API response patterns
141
+ return False
142
+ found = False
175
143
for pattern , description , confidence in self .API_PATTERNS :
176
144
if re .search (pattern , response .text , re .IGNORECASE ):
177
- self ._add_score (
178
- confidence ,
179
- 'API' ,
180
- f"{ description } : { pattern } "
181
- )
182
-
183
- # Check for JSON response
145
+ evidence .add (f"API: { description } " )
146
+ evidence_types .add ('api' )
147
+ found = True
184
148
if response .headers .get ('content-type' , '' ).startswith ('application/json' ):
185
- self ._add_score (
186
- 15 ,
187
- 'API' ,
188
- "JSON API response detected"
189
- )
190
-
191
- def _check_common_paths (self ) -> None :
192
- """Check for Sanic-specific paths"""
149
+ evidence .add ("API: JSON API response detected" )
150
+ evidence_types .add ('api' )
151
+ found = True
152
+ return found
153
+
154
+ def _check_common_paths (self , evidence , evidence_types ):
193
155
base_url = self .request_manager .target_url .rstrip ('/' )
194
-
195
156
for path in self .COMMON_PATHS :
196
157
url = urljoin (base_url , path )
197
158
response = self .request_manager .make_request (url )
198
-
199
- if response :
200
- # Check for successful responses
201
- if response .status_code == 200 :
202
- self ._add_score (
203
- 10 ,
204
- 'Endpoint' ,
205
- f"{ path } returns 200 OK"
206
- )
207
-
208
- # Check for 405 responses (common in Sanic)
209
- elif response .status_code == 405 :
210
- self ._add_score (
211
- 8 ,
212
- 'Endpoint' ,
213
- f"{ path } returns 405 Method Not Allowed (Sanic behavior)"
214
- )
215
-
159
+ if response and response .status_code == 200 :
160
+ # Only add as low confidence evidence
161
+ evidence .add (f"Endpoint: { path } returns 200 OK" )
162
+ evidence_types .add ('endpoint' )
163
+
164
+ def _score_evidence (self , evidence , evidence_types ):
165
+ # High confidence: at least 2 unique types of strong evidence
166
+ strong_types = {'error' , 'api' , 'content' }
167
+ strong_evidence = strong_types .intersection (evidence_types )
168
+ if len (strong_evidence ) >= 2 :
169
+ self ._add_score (90 , 'Composite' , f"Sanic detected by: { sorted (strong_evidence )} | { sorted (evidence )} " )
170
+ elif 'error' in evidence_types and 'api' in evidence_types :
171
+ self ._add_score (70 , 'Composite' , f"Sanic error and API evidence: { sorted (evidence )} " )
172
+ elif 'error' in evidence_types :
173
+ self ._add_score (30 , 'Error' , f"Sanic error evidence: { sorted (evidence )} " )
174
+ elif 'api' in evidence_types :
175
+ self ._add_score (30 , 'API' , f"Sanic API evidence: { sorted (evidence )} " )
176
+ elif 'content' in evidence_types :
177
+ self ._add_score (20 , 'Content' , f"Sanic content evidence: { sorted (evidence )} " )
178
+ elif 'header' in evidence_types :
179
+ self ._add_score (10 , 'Header' , f"Sanic header evidence: { sorted (evidence )} " )
180
+ elif 'endpoint' in evidence_types :
181
+ self ._add_score (5 , 'Endpoint' , f"Sanic endpoint evidence: { sorted (evidence )} " )
182
+
216
183
def detect_version (self ) -> None :
217
- """Attempt to detect Sanic version"""
218
- # Check error page for version hints
219
184
base_url = self .request_manager .target_url .rstrip ('/' )
220
- error_url = urljoin (base_url , '/error' )
221
-
222
- response = self .request_manager .make_request (error_url )
223
- if not response :
224
- return
225
-
226
- # Look for version in error traceback
227
- version_match = re .search (r'sanic/app\.py' , response .text )
228
- if version_match :
229
- self ._add_version_hint (
230
- "Unknown" ,
231
- 50 ,
232
- "Sanic app.py referenced in error traceback"
233
- )
234
-
235
- # Check API endpoint for version
236
185
api_url = urljoin (base_url , '/api/status' )
237
- api_response = self .request_manager .make_request (api_url )
238
- if api_response :
239
- version_match = re .search (r'"version": "(\d+\.\d+\.\d+ )"' , api_response .text )
186
+ response = self .request_manager .make_request (api_url )
187
+ if response :
188
+ version_match = re .search (r'"version": ?"([0-9.]+ )"' , response .text )
240
189
if version_match :
241
190
version = version_match .group (1 )
242
- self ._add_version_hint (
243
- version ,
244
- 80 ,
245
- f"Sanic version detected in API response: { version } "
246
- )
191
+ self ._add_version_hint (version , 80 , f"Sanic version detected in API response: { version } " )
0 commit comments