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 CherryPyDetector (BaseDetector ):
20+ """CherryPy-specific detection methods"""
21+
22+ FRAMEWORK = "CherryPy"
23+
24+ COMMON_PATHS = [
25+ '/admin' ,
26+ '/api/status' ,
27+ '/about' ,
28+ '/error' ,
29+ '/tools' ,
30+ '/config' ,
31+ '/sessions' ,
32+ ]
33+
34+ ERROR_PATTERNS = [
35+ (r'cherrypy\.HTTPError' , 'CherryPy HTTPError in error page' , 40 ),
36+ (r'cherrypy\.lib\.cptools' , 'CherryPy tools in error traceback' , 35 ),
37+ (r'cherrypy\.wsgiserver' , 'CherryPy WSGI server in error' , 30 ),
38+ (r'cherrypy\._cperror' , 'CherryPy error module in traceback' , 30 ),
39+ (r'cherrypy\.expose' , 'CherryPy expose decorator in error' , 25 ),
40+ (r'cherrypy\.quickstart' , 'CherryPy quickstart in error' , 25 ),
41+ (r'cherrypy\.config' , 'CherryPy config in error' , 20 ),
42+ (r'(?i)cherrypy' , 'CherryPy reference in error' , 15 ),
43+ ]
44+
45+ CONTENT_PATTERNS = [
46+ (r'Hello from CherryPy!' , 'CherryPy greeting in content' , 30 ),
47+ (r'<title>CherryPy Test App</title>' , 'CherryPy test app title' , 25 ),
48+ (r'<h1>Hello from CherryPy!</h1>' , 'CherryPy heading in content' , 25 ),
49+ (r'CherryPy Framework Detection Test' , 'CherryPy detection test string' , 25 ),
50+ (r'CherryPy is a pythonic, object-oriented HTTP framework' , 'CherryPy description' , 20 ),
51+ (r'CherryPy Admin Interface' , 'CherryPy admin interface title' , 30 ),
52+ (r'cherrypy\.quickstart' , 'CherryPy quickstart in content' , 20 ),
53+ (r'(?i)cherrypy' , 'CherryPy reference in HTML' , 15 ),
54+ ]
55+
56+ HEADER_PATTERNS = [
57+ ('server' , r'cherrypy' , 'CherryPy server header' , 40 ),
58+ ('x-powered-by' , r'cherrypy' , 'CherryPy X-Powered-By header' , 35 ),
59+ ('content-type' , r'application/json' , 'JSON API response' , 5 ),
60+ ]
61+
62+ API_PATTERNS = [
63+ (r'"framework": ?"cherrypy"' , 'CherryPy framework in API response' , 40 ),
64+ (r'"status": ?"running"' , 'Status running in API response' , 10 ),
65+ (r'"version": ?"[0-9.]+"' , 'Version in API response' , 10 ),
66+ ]
67+
68+ SESSION_PATTERNS = [
69+ (r'session_id' , 'CherryPy session cookie pattern' , 25 ),
70+ (r'cherrypy_session' , 'CherryPy session cookie' , 30 ),
71+ ]
72+
73+ def detect (self ) -> None :
74+ evidence = set ()
75+ evidence_types = set ()
76+
77+ # Check error page for CherryPy-specific patterns
78+ if self ._check_error_patterns (evidence , evidence_types ):
79+ pass
80+
81+ # Check API status endpoint for CherryPy-specific JSON
82+ if self ._check_api_status (evidence , evidence_types ):
83+ pass
84+
85+ # Check main page and about for CherryPy content
86+ if self ._check_content_patterns (evidence , evidence_types ):
87+ pass
88+
89+ # Check headers for CherryPy server and X-Powered-By
90+ if self ._check_headers (evidence , evidence_types ):
91+ pass
92+
93+ # Check for CherryPy session cookies
94+ if self ._check_session_patterns (evidence , evidence_types ):
95+ pass
96+
97+ # Check common paths for 200 OK (low confidence)
98+ self ._check_common_paths (evidence , evidence_types )
99+
100+ # Assign confidence based on evidence
101+ self ._score_evidence (evidence , evidence_types )
102+ self .detect_version ()
103+
104+ def _add_score (self , points : int , evidence_type : str , detail : str , raw_data : Optional [Dict [str , Any ]] = None ) -> None :
105+ self .result_manager .add_score (self .FRAMEWORK , points , evidence_type , detail , raw_data )
106+
107+ def _add_version_hint (self , version : str , confidence : int , evidence : str ) -> None :
108+ self .result_manager .add_version_hint (self .FRAMEWORK , version , confidence , evidence )
109+
110+ def _add_component (self , component : str , evidence : str ) -> None :
111+ self .result_manager .add_component (self .FRAMEWORK , component , evidence )
112+
113+ def _check_headers (self , evidence , evidence_types ) -> bool :
114+ response = self .request_manager .make_request ()
115+ if not response :
116+ return False
117+ headers = response .headers
118+ found = False
119+ for header_name , pattern , description , confidence in self .HEADER_PATTERNS :
120+ if header_name in headers :
121+ header_value = headers [header_name ]
122+ if re .search (pattern , header_value , re .IGNORECASE ):
123+ evidence .add (f"Header: { description } " )
124+ evidence_types .add ('header' )
125+ found = True
126+ return found
127+
128+ def _check_content_patterns (self , evidence , evidence_types ) -> bool :
129+ response = self .request_manager .make_request ()
130+ if not response :
131+ return False
132+ found = False
133+ for pattern , description , confidence in self .CONTENT_PATTERNS :
134+ if re .search (pattern , response .text , re .IGNORECASE ):
135+ evidence .add (f"Content: { description } " )
136+ evidence_types .add ('content' )
137+ found = True
138+ return found
139+
140+ def _check_error_patterns (self , evidence , evidence_types ) -> bool :
141+ base_url = self .request_manager .target_url .rstrip ('/' )
142+ error_url = urljoin (base_url , '/error' )
143+ response = self .request_manager .make_request (error_url )
144+ if not response :
145+ return False
146+ found = False
147+ for pattern , description , confidence in self .ERROR_PATTERNS :
148+ if re .search (pattern , response .text , re .IGNORECASE ):
149+ evidence .add (f"Error: { description } " )
150+ evidence_types .add ('error' )
151+ found = True
152+ return found
153+
154+ def _check_api_status (self , evidence , evidence_types ) -> bool :
155+ base_url = self .request_manager .target_url .rstrip ('/' )
156+ api_url = urljoin (base_url , '/api/status' )
157+ response = self .request_manager .make_request (api_url )
158+ if not response :
159+ return False
160+ found = False
161+ for pattern , description , confidence in self .API_PATTERNS :
162+ if re .search (pattern , response .text , re .IGNORECASE ):
163+ evidence .add (f"API: { description } " )
164+ evidence_types .add ('api' )
165+ found = True
166+ if response .headers .get ('content-type' , '' ).startswith ('application/json' ):
167+ evidence .add ("API: JSON API response detected" )
168+ evidence_types .add ('api' )
169+ found = True
170+ return found
171+
172+ def _check_session_patterns (self , evidence , evidence_types ) -> bool :
173+ response = self .request_manager .make_request ()
174+ if not response :
175+ return False
176+ found = False
177+ cookies = response .cookies
178+ for name , value in cookies .items ():
179+ for pattern , description , confidence in self .SESSION_PATTERNS :
180+ if re .search (pattern , name , re .IGNORECASE ):
181+ evidence .add (f"Session: { description } " )
182+ evidence_types .add ('session' )
183+ found = True
184+ return found
185+
186+ def _check_common_paths (self , evidence , evidence_types ):
187+ base_url = self .request_manager .target_url .rstrip ('/' )
188+ for path in self .COMMON_PATHS :
189+ url = urljoin (base_url , path )
190+ response = self .request_manager .make_request (url )
191+ if response and response .status_code == 200 :
192+ # Only add as low confidence evidence
193+ evidence .add (f"Endpoint: { path } returns 200 OK" )
194+ evidence_types .add ('endpoint' )
195+
196+ def _score_evidence (self , evidence , evidence_types ):
197+ # High confidence: at least 2 unique types of strong evidence
198+ strong_types = {'error' , 'api' , 'content' , 'header' , 'session' }
199+ strong_evidence = strong_types .intersection (evidence_types )
200+
201+ if len (strong_evidence ) >= 3 :
202+ self ._add_score (95 , 'Composite' , f"CherryPy detected by: { sorted (strong_evidence )} | { sorted (evidence )} " )
203+ elif len (strong_evidence ) >= 2 :
204+ self ._add_score (85 , 'Composite' , f"CherryPy detected by: { sorted (strong_evidence )} | { sorted (evidence )} " )
205+ elif 'header' in evidence_types and 'content' in evidence_types :
206+ self ._add_score (75 , 'Composite' , f"CherryPy header and content evidence: { sorted (evidence )} " )
207+ elif 'error' in evidence_types :
208+ self ._add_score (40 , 'Error' , f"CherryPy error evidence: { sorted (evidence )} " )
209+ elif 'header' in evidence_types :
210+ self ._add_score (35 , 'Header' , f"CherryPy header evidence: { sorted (evidence )} " )
211+ elif 'session' in evidence_types :
212+ self ._add_score (30 , 'Session' , f"CherryPy session evidence: { sorted (evidence )} " )
213+ elif 'api' in evidence_types :
214+ self ._add_score (25 , 'API' , f"CherryPy API evidence: { sorted (evidence )} " )
215+ elif 'content' in evidence_types :
216+ self ._add_score (20 , 'Content' , f"CherryPy content evidence: { sorted (evidence )} " )
217+ elif 'endpoint' in evidence_types :
218+ self ._add_score (10 , 'Endpoint' , f"CherryPy endpoint evidence: { sorted (evidence )} " )
219+
220+ def detect_version (self ) -> None :
221+ # Try to detect version from server header
222+ response = self .request_manager .make_request ()
223+ if response and 'server' in response .headers :
224+ server_header = response .headers ['server' ]
225+ version_match = re .search (r'cherrypy[/\s]+(\d+\.\d+\.\d+)' , server_header , re .IGNORECASE )
226+ if version_match :
227+ version = version_match .group (1 )
228+ self ._add_version_hint (version , 80 , f"CherryPy version detected in server header: { version } " )
229+
230+ # Try to detect version from API response
231+ base_url = self .request_manager .target_url .rstrip ('/' )
232+ api_url = urljoin (base_url , '/api/status' )
233+ response = self .request_manager .make_request (api_url )
234+ if response :
235+ version_match = re .search (r'"version": ?"([0-9.]+)"' , response .text )
236+ if version_match :
237+ version = version_match .group (1 )
238+ self ._add_version_hint (version , 85 , f"CherryPy version detected in API response: { version } " )
0 commit comments