Skip to content

Commit 721ef81

Browse files
committed
Updates framework detectors for Sanic
1 parent 1848143 commit 721ef81

File tree

2 files changed

+249
-1
lines changed

2 files changed

+249
-1
lines changed

siddhis/framewalk/detectors/sanic.py

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
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+
)

siddhis/framewalk/orchestrator/fwalk_orchestrator.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ def _init_detectors(self) -> None:
243243
from ..detectors.pyramid import PyramidDetector
244244
from ..detectors.bottle import BottleDetector
245245
from ..detectors.web2py import Web2pyDetector
246+
from ..detectors.sanic import SanicDetector
246247

247248
# Clear any existing detectors
248249
self.detectors = []
@@ -262,7 +263,8 @@ def _init_detectors(self) -> None:
262263
'fastapi': FastAPIDetector,
263264
'pyramid': PyramidDetector,
264265
'bottle': BottleDetector,
265-
'web2py': Web2pyDetector
266+
'web2py': Web2pyDetector,
267+
'sanic': SanicDetector
266268
}
267269

268270
for framework_name, detector_class in detector_classes.items():

0 commit comments

Comments
 (0)