@@ -158,6 +158,9 @@ def __init__(
158158 self .modules = OrderedDict ({})
159159 self .dummy_modules = {}
160160 self .preset = None
161+ # initial status before `_prep()` runs
162+ self ._status = "NOT_STARTED"
163+ self ._status_code = self ._status_codes [self ._status ]
161164
162165 async def _prep (self ):
163166 """
@@ -216,26 +219,26 @@ async def _prep(self):
216219 self .scope_report_distance = int (self .scope_config .get ("report_distance" , 1 ))
217220
218221 # web config
219- self . web_config = self .config .get ("web" , {})
220- self .web_spider_distance = self . web_config .get ("spider_distance" , 0 )
221- self .web_spider_depth = self . web_config .get ("spider_depth" , 1 )
222- self .web_spider_links_per_page = self . web_config .get ("spider_links_per_page" , 20 )
223- max_redirects = self . web_config .get ("http_max_redirects" , 5 )
222+ web_config = self .config .get ("web" , {})
223+ self .web_spider_distance = web_config .get ("spider_distance" , 0 )
224+ self .web_spider_depth = web_config .get ("spider_depth" , 1 )
225+ self .web_spider_links_per_page = web_config .get ("spider_links_per_page" , 20 )
226+ max_redirects = web_config .get ("http_max_redirects" , 5 )
224227 self .web_max_redirects = max (max_redirects , self .web_spider_distance )
225- self .http_proxy = self . web_config .get ("http_proxy" , "" )
226- self .http_timeout = self . web_config .get ("http_timeout" , 10 )
227- self .httpx_timeout = self . web_config .get ("httpx_timeout" , 5 )
228- self .http_retries = self . web_config .get ("http_retries" , 1 )
229- self .httpx_retries = self . web_config .get ("httpx_retries" , 1 )
230- self .useragent = self . web_config .get ("user_agent" , "BBOT" )
228+ self .http_proxy = web_config .get ("http_proxy" , "" )
229+ self .http_timeout = web_config .get ("http_timeout" , 10 )
230+ self .httpx_timeout = web_config .get ("httpx_timeout" , 5 )
231+ self .http_retries = web_config .get ("http_retries" , 1 )
232+ self .httpx_retries = web_config .get ("httpx_retries" , 1 )
233+ self .useragent = web_config .get ("user_agent" , "BBOT" )
231234 # custom HTTP headers warning
232- self .custom_http_headers = self . web_config .get ("http_headers" , {})
235+ self .custom_http_headers = web_config .get ("http_headers" , {})
233236 if self .custom_http_headers :
234237 self .warning (
235238 "You have enabled custom HTTP headers. These will be attached to all in-scope requests and all requests made by httpx."
236239 )
237240 # custom HTTP cookies warning
238- self .custom_http_cookies = self . web_config .get ("http_cookies" , {})
241+ self .custom_http_cookies = web_config .get ("http_cookies" , {})
239242 if self .custom_http_cookies :
240243 self .warning (
241244 "You have enabled custom HTTP cookies. These will be attached to all in-scope requests and all requests made by httpx."
@@ -562,8 +565,18 @@ async def load_modules(self):
562565 After all modules are loaded, they are sorted by `_priority` and stored in the `modules` dictionary.
563566 """
564567 if not self ._modules_loaded :
568+ # If the preset hasn't been baked yet but modules have been
569+ # manually attached (e.g. in tests), skip the automatic loading
570+ # pipeline and operate only on the existing modules.
571+ if self .preset is None :
572+ if not self .modules :
573+ self .warning ("No modules to load" )
574+ self ._modules_loaded = True
575+ return
576+
565577 if not self .preset .modules :
566578 self .warning ("No modules to load" )
579+ self ._modules_loaded = True
567580 return
568581
569582 if not self .preset .scan_modules :
@@ -897,9 +910,15 @@ async def _cleanup(self):
897910 # clean up modules
898911 for mod in self .modules .values ():
899912 await mod ._cleanup ()
900- with contextlib .suppress (Exception ):
901- self .home .rmdir ()
902- self .helpers .rm_rf (self .temp_dir , ignore_errors = True )
913+ # In some test paths, `_prep()` is never called, so `home` and
914+ # `temp_dir` may not exist. Treat those as best-effort cleanups.
915+ home = getattr (self , "home" , None )
916+ if home is not None :
917+ with contextlib .suppress (Exception ):
918+ home .rmdir ()
919+ temp_dir = getattr (self , "temp_dir" , None )
920+ if temp_dir is not None :
921+ self .helpers .rm_rf (temp_dir , ignore_errors = True )
903922 self .helpers .clean_old_scans ()
904923
905924 def in_scope (self , * args , ** kwargs ):
@@ -913,11 +932,29 @@ def blacklisted(self, *args, **kwargs):
913932
914933 @property
915934 def core (self ):
916- return self .preset .core
935+ # Before `_prep()` runs, fall back to the unbaked preset's core so that basic configuration is still available (during module construction in tests)
936+ if self .preset is not None :
937+ return self .preset .core
938+ return self ._unbaked_preset .core
917939
918940 @property
919941 def config (self ):
920- return self .preset .core .config
942+ # Allow access to the scan config even before `_prep()` by falling back to the unbaked preset's core config.
943+ if self .preset is not None :
944+ return self .preset .core .config
945+ return self ._unbaked_preset .core .config
946+
947+ @property
948+ def web_config (self ):
949+ """
950+ Web-related configuration for the scan.
951+
952+ Exposed as a property so it is available even before `_prep()` runs,
953+ falling back to the underlying config's `web` section. During `_prep()`
954+ an instance attribute of the same name is assigned, which will then
955+ override this property for the remainder of the scan lifetime.
956+ """
957+ return self .config .get ("web" , {})
921958
922959 @property
923960 def target (self ):
@@ -937,7 +974,13 @@ def blacklist(self):
937974
938975 @property
939976 def helpers (self ):
940- return self .preset .helpers
977+ # Before `_prep()` runs, `self.preset` is None. In those cases,
978+ # fall back to the unbaked preset's helpers so that CLI utilities
979+ # (e.g. depsinstaller) and other lightweight helper functionality
980+ # remain available without requiring a full scan prep.
981+ if self .preset is not None :
982+ return self .preset .helpers
983+ return self ._unbaked_preset .helpers
941984
942985 @property
943986 def force_start (self ):
@@ -986,12 +1029,15 @@ def status(self, status):
9861029 if status != self ._status :
9871030 self ._status = status
9881031 self ._status_code = self ._status_codes [status ]
989- self .dispatcher_tasks .append (
990- asyncio .create_task (
991- self .dispatcher .catch (self .dispatcher .on_status , self ._status , self .id ),
992- name = f"{ self .name } .dispatcher.on_status({ status } )" ,
1032+ # During early initialization (or in certain tests),`dispatcher` may not be set yet. In that case we just update the status without scheduling dispatcher tasks
1033+ dispatcher = getattr (self , "dispatcher" , None )
1034+ if dispatcher is not None :
1035+ self .dispatcher_tasks .append (
1036+ asyncio .create_task (
1037+ dispatcher .catch (self .dispatcher .on_status , self ._status , self .id ),
1038+ name = f"{ self .name } .dispatcher.on_status({ status } )" ,
1039+ )
9931040 )
994- )
9951041 else :
9961042 self .debug (f'Scan status is already "{ status } "' )
9971043 else :
0 commit comments