1+ #!/usr/bin/env python3
2+ # __ _
3+ # \/imana 2016
4+ # [|-ramewørk
5+ #
6+ #
7+ # Author: s4dhu
8+ # Email: <s4dhul4bs[at]prontonmail[dot]ch
9+ # Git: @s4dhulabs
10+ # Mastodon: @s4dhu
11+ #
12+ # This file is part of Vimana Framework Project.
13+
14+ import re
15+ from typing import Dict , List , Any , Optional
16+ from urllib .parse import urljoin
17+
18+ from .base import BaseDetector
19+
20+
21+ class SanicDetector (BaseDetector ):
22+ """Sanic-specific detection methods"""
23+
24+ FRAMEWORK = "Sanic"
25+
26+ # Common Sanic paths to check
27+ COMMON_PATHS = [
28+ '/api/status' ,
29+ '/about' ,
30+ '/error' ,
31+ '/health' ,
32+ '/metrics' ,
33+ '/docs' ,
34+ '/openapi.json' ,
35+ '/api/docs' ,
36+ ]
37+
38+ # Sanic error patterns
39+ 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 ),
46+ (r'Exception: Test error for framework detection' , 'Sanic test error' , 20 ),
47+ ]
48+
49+ # Sanic content patterns
50+ CONTENT_PATTERNS = [
51+ # Pattern, Description, Confidence
52+ (r'Hello from Sanic!' , 'Sanic greeting in content' , 20 ),
53+ (r'<title>Sanic Test App</title>' , 'Sanic test app title' , 25 ),
54+ (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 ),
58+ ]
59+
60+ # Sanic header patterns (though Sanic doesn't have many unique headers)
61+ 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 ),
65+ ]
66+
67+ # Sanic API response patterns
68+ 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 ),
73+ ]
74+
75+ 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 ()
82+ 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"""
90+ 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"""
97+ 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"""
103+ self .result_manager .add_component (self .FRAMEWORK , component , evidence )
104+
105+ def _check_headers (self ) -> None :
106+ """Check for Sanic-specific headers"""
107+ response = self .request_manager .make_request ()
108+ if not response :
109+ return
110+
111+ headers = response .headers
112+
113+ # Check header patterns
114+ for header_name , pattern , description , confidence in self .HEADER_PATTERNS :
115+ if header_name in headers :
116+ header_value = headers [header_name ]
117+ 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"""
134+ response = self .request_manager .make_request ()
135+ if not response :
136+ return
137+
138+ # Check content patterns
139+ for pattern , description , confidence in self .CONTENT_PATTERNS :
140+ 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"""
149+ base_url = self .request_manager .target_url .rstrip ('/' )
150+ error_url = urljoin (base_url , '/error' )
151+
152+ response = self .request_manager .make_request (error_url )
153+ if not response :
154+ return
155+
156+ # Check error patterns in response text
157+ for pattern , description , confidence in self .ERROR_PATTERNS :
158+ 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"""
167+ base_url = self .request_manager .target_url .rstrip ('/' )
168+ api_url = urljoin (base_url , '/api/status' )
169+
170+ response = self .request_manager .make_request (api_url )
171+ if not response :
172+ return
173+
174+ # Check API response patterns
175+ for pattern , description , confidence in self .API_PATTERNS :
176+ 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
184+ 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"""
193+ base_url = self .request_manager .target_url .rstrip ('/' )
194+
195+ for path in self .COMMON_PATHS :
196+ url = urljoin (base_url , path )
197+ 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+
216+ def detect_version (self ) -> None :
217+ """Attempt to detect Sanic version"""
218+ # Check error page for version hints
219+ 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+ 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 )
240+ if version_match :
241+ version = version_match .group (1 )
242+ self ._add_version_hint (
243+ version ,
244+ 80 ,
245+ f"Sanic version detected in API response: { version } "
246+ )
0 commit comments