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 StarletteDetector (BaseDetector ):
20+ """Starlette-specific detection methods"""
21+
22+ FRAMEWORK = "Starlette"
23+
24+ COMMON_PATHS = [
25+ '/' ,
26+ '/about' ,
27+ '/api/status' ,
28+ '/error' ,
29+ ]
30+
31+ CONTENT_PATTERNS = [
32+ (r'Starlette' , 'Starlette reference in content' , 20 ),
33+ (r'<title>Starlette Test App</title>' , 'Starlette test app title' , 25 ),
34+ (r'<h1>Hello from Starlette!</h1>' , 'Starlette heading in content' , 20 ),
35+ (r'Starlette Framework Detection Test' , 'Starlette detection test string' , 20 ),
36+ (r'Starlette is a lightweight ASGI framework' , 'Starlette description' , 15 ),
37+ ]
38+
39+ HEADER_PATTERNS = [
40+ ('server' , r'uvicorn' , 'Uvicorn server header (common for Starlette)' , 10 ),
41+ ('content-type' , r'application/json' , 'JSON API response' , 5 ),
42+ ('content-type' , r'text/html' , 'HTML response' , 3 ),
43+ ]
44+
45+ API_PATTERNS = [
46+ (r'"framework": ?"starlette"' , 'Starlette framework in API response' , 30 ),
47+ (r'"status": ?"running"' , 'Status running in API response' , 10 ),
48+ (r'"version": ?"[0-9.]+"' , 'Version in API response' , 10 ),
49+ ]
50+
51+ def detect (self ) -> None :
52+ self ._check_headers ()
53+ self ._check_content_patterns ()
54+ self ._check_api_status ()
55+ self ._check_common_paths ()
56+ self .detect_version ()
57+
58+ def _add_score (self , points : int , evidence_type : str , detail : str , raw_data : Optional [Dict [str , Any ]] = None ) -> None :
59+ self .result_manager .add_score (self .FRAMEWORK , points , evidence_type , detail , raw_data )
60+
61+ def _add_version_hint (self , version : str , confidence : int , evidence : str ) -> None :
62+ self .result_manager .add_version_hint (self .FRAMEWORK , version , confidence , evidence )
63+
64+ def _add_component (self , component : str , evidence : str ) -> None :
65+ self .result_manager .add_component (self .FRAMEWORK , component , evidence )
66+
67+ def _check_headers (self ) -> None :
68+ response = self .request_manager .make_request ()
69+ if not response :
70+ return
71+ headers = response .headers
72+ for header_name , pattern , description , confidence in self .HEADER_PATTERNS :
73+ if header_name in headers :
74+ header_value = headers [header_name ]
75+ if re .search (pattern , header_value , re .IGNORECASE ):
76+ self ._add_score (confidence , 'Header' , f"{ description } : { header_name } : { header_value } " )
77+
78+ def _check_content_patterns (self ) -> None :
79+ response = self .request_manager .make_request ()
80+ if not response :
81+ return
82+ for pattern , description , confidence in self .CONTENT_PATTERNS :
83+ if re .search (pattern , response .text , re .IGNORECASE ):
84+ self ._add_score (confidence , 'Content' , f"{ description } : { pattern } " )
85+
86+ def _check_api_status (self ) -> None :
87+ base_url = self .request_manager .target_url .rstrip ('/' )
88+ api_url = urljoin (base_url , '/api/status' )
89+ response = self .request_manager .make_request (api_url )
90+ if not response :
91+ return
92+ for pattern , description , confidence in self .API_PATTERNS :
93+ if re .search (pattern , response .text , re .IGNORECASE ):
94+ self ._add_score (confidence , 'API' , f"{ description } : { pattern } " )
95+ if response .headers .get ('content-type' , '' ).startswith ('application/json' ):
96+ self ._add_score (10 , 'API' , 'API status endpoint returns JSON' )
97+
98+ def _check_common_paths (self ) -> None :
99+ base_url = self .request_manager .target_url .rstrip ('/' )
100+ for path in self .COMMON_PATHS :
101+ url = urljoin (base_url , path )
102+ response = self .request_manager .make_request (url )
103+ if response and response .status_code == 200 :
104+ self ._add_score (5 , 'Endpoint' , f"{ path } returns 200 OK" )
105+
106+ def detect_version (self ) -> None :
107+ base_url = self .request_manager .target_url .rstrip ('/' )
108+ api_url = urljoin (base_url , '/api/status' )
109+ response = self .request_manager .make_request (api_url )
110+ if response :
111+ version_match = re .search (r'"version": ?"([0-9.]+)"' , response .text )
112+ if version_match :
113+ version = version_match .group (1 )
114+ self ._add_version_hint (version , 80 , f"Starlette version detected in API response: { version } " )
0 commit comments