1+ #!/usr/bin/env python3
2+ # __ _
3+ # \/imana 2016
4+ # [|-ramewørk
5+ #
6+ # Author: s4dhu
7+ # Email: <s4dhul4bs[at]prontonmail[dot]ch
8+ # Git: @s4dhulabs
9+ # Mastodon: @s4dhu
10+ #
11+ # This file is part of Vimana Framework Project.
12+
13+ import re
14+ from typing import Dict , List , Any , Optional
15+ from urllib .parse import urljoin
16+
17+ from .base import BaseDetector
18+
19+ class TornadoDetector (BaseDetector ):
20+ """Tornado-specific detection methods"""
21+
22+ FRAMEWORK = "Tornado"
23+
24+ # Common Tornado paths to check
25+ COMMON_PATHS = [
26+ '/api/info' ,
27+ '/about' ,
28+ '/status' ,
29+ '/error' ,
30+ '/static/' ,
31+ ]
32+
33+ # Tornado error patterns
34+ ERROR_PATTERNS = [
35+ (r'tornado/web\.py' , 'Tornado error traceback' , 30 ),
36+ (r'Exception: Test error for Tornado framework detection' , 'Tornado test error' , 25 ),
37+ (r'Traceback \(most recent call last\):' , 'Python traceback in error' , 10 ),
38+ ]
39+
40+ # Tornado content patterns
41+ CONTENT_PATTERNS = [
42+ (r'Hello from Tornado!' , 'Tornado greeting in content' , 20 ),
43+ (r'<title>Tornado Test App</title>' , 'Tornado test app title' , 25 ),
44+ (r'<h1>Hello from Tornado!</h1>' , 'Tornado heading in content' , 20 ),
45+ (r'This is a minimal Tornado application' , 'Tornado app description' , 15 ),
46+ (r'<a href="/api/info">API Info</a>' , 'Tornado API info link' , 10 ),
47+ (r'<a href="/about">About</a>' , 'Tornado about link' , 10 ),
48+ (r'<a href="/status">Status</a>' , 'Tornado status link' , 10 ),
49+ ]
50+
51+ # Tornado header patterns
52+ HEADER_PATTERNS = [
53+ ('Server' , r'TornadoServer/\d+\.\d+\.\d+' , 'Tornado server header with version' , 40 ),
54+ ('Server' , r'TornadoServer' , 'Tornado server header' , 30 ),
55+ ('Content-Type' , r'text/plain' , 'Tornado plain text response' , 5 ),
56+ ]
57+
58+ def detect (self ) -> None :
59+ """Run Tornado detection methods"""
60+ self ._check_headers ()
61+ self ._check_content_patterns ()
62+ self ._check_error_patterns ()
63+ self ._check_common_paths ()
64+ self .detect_version ()
65+
66+ def _add_score (self ,
67+ points : int ,
68+ evidence_type : str ,
69+ detail : str ,
70+ raw_data : Optional [Dict [str , Any ]] = None ) -> None :
71+ self .result_manager .add_score (self .FRAMEWORK , points , evidence_type , detail , raw_data )
72+
73+ def _add_version_hint (self ,
74+ version : str ,
75+ confidence : int ,
76+ evidence : str ) -> None :
77+ self .result_manager .add_version_hint (self .FRAMEWORK , version , confidence , evidence )
78+
79+ def _add_component (self ,
80+ component : str ,
81+ evidence : str ) -> None :
82+ self .result_manager .add_component (self .FRAMEWORK , component , evidence )
83+
84+ def _check_headers (self ) -> None :
85+ response = self .request_manager .make_request ()
86+ if not response :
87+ return
88+ headers = response .headers
89+ for header_name , pattern , description , confidence in self .HEADER_PATTERNS :
90+ if header_name in headers :
91+ header_value = headers [header_name ]
92+ if re .search (pattern , header_value , re .IGNORECASE ):
93+ self ._add_score (
94+ confidence ,
95+ 'Header' ,
96+ f"{ description } : { header_name } : { header_value } "
97+ )
98+ if response .status_code == 405 :
99+ self ._add_score (
100+ 10 ,
101+ 'Header' ,
102+ "405 Method Not Allowed response (common in Tornado)"
103+ )
104+
105+ def _check_content_patterns (self ) -> None :
106+ response = self .request_manager .make_request ()
107+ if not response :
108+ return
109+ for pattern , description , confidence in self .CONTENT_PATTERNS :
110+ if re .search (pattern , response .text , re .IGNORECASE ):
111+ self ._add_score (
112+ confidence ,
113+ 'Content' ,
114+ f"{ description } : { pattern } "
115+ )
116+
117+ def _check_error_patterns (self ) -> None :
118+ base_url = self .request_manager .target_url .rstrip ('/' )
119+ error_url = urljoin (base_url , '/error' )
120+ response = self .request_manager .make_request (error_url )
121+ if not response :
122+ return
123+ for pattern , description , confidence in self .ERROR_PATTERNS :
124+ if re .search (pattern , response .text , re .IGNORECASE ):
125+ self ._add_score (
126+ confidence ,
127+ 'Error' ,
128+ f"{ description } : { pattern } "
129+ )
130+
131+ def _check_common_paths (self ) -> None :
132+ base_url = self .request_manager .target_url .rstrip ('/' )
133+ for path in self .COMMON_PATHS :
134+ url = urljoin (base_url , path )
135+ response = self .request_manager .make_request (url )
136+ if response :
137+ if response .status_code == 200 :
138+ self ._add_score (
139+ 10 ,
140+ 'Endpoint' ,
141+ f"{ path } returns 200 OK"
142+ )
143+ elif response .status_code == 405 :
144+ self ._add_score (
145+ 8 ,
146+ 'Endpoint' ,
147+ f"{ path } returns 405 Method Not Allowed (Tornado behavior)"
148+ )
149+
150+ def detect_version (self ) -> None :
151+ response = self .request_manager .make_request ()
152+ if not response :
153+ return
154+ server_header = response .headers .get ('Server' , '' )
155+ version_match = re .search (r'TornadoServer/(\d+\.\d+\.\d+)' , server_header )
156+ if version_match :
157+ version = version_match .group (1 )
158+ self ._add_version_hint (
159+ version ,
160+ 80 ,
161+ f"Tornado version detected in Server header: { version } "
162+ )
0 commit comments