2525from .utils import ModuleResult , atomic_write_text , config_get , deduplicate_alive_urls , format_duration , get_profiled_ceiling , get_profiled_concurrency , get_profiled_rate_limit , get_timeout , info , load_config , log_duration , normalize_scan_profile , normalize_target , nuclei_template_status , prepare_module_output , print_module_summary , setup_logging , skipped_result , skip , success , suppress_third_party_banner , target_output_dir , warn , write_json
2626
2727SEVERITY_ORDER = ("critical" , "high" , "medium" , "low" , "info" , "unknown" )
28+ BROAD_INFRA_TAGS = {"apache" , "nginx" }
2829
2930
3031def _nuclei_exists () -> bool :
@@ -189,6 +190,22 @@ def to_int(value: object) -> int:
189190 if explicit_templates :
190191 return {"run" : True , "reason" : "explicit templates supplied" }
191192 if selected_tags :
193+ selected = {str (tag ).strip ().lower () for tag in selected_tags if str (tag ).strip ()}
194+ if selected and selected .issubset (BROAD_INFRA_TAGS ):
195+ roi_hosts = _load_roi_target_hosts (output , target_name , max_hosts = 10 )
196+ if not roi_hosts :
197+ return {
198+ "run" : False ,
199+ "reason" : "broad infrastructure tags skipped: no validated opportunity hosts" ,
200+ "broad_tags" : sorted (selected ),
201+ "roi_hosts" : [],
202+ }
203+ return {
204+ "run" : True ,
205+ "reason" : "broad infrastructure tags constrained to ROI opportunity hosts" ,
206+ "broad_tags" : sorted (selected ),
207+ "roi_hosts" : roi_hosts ,
208+ }
192209 return {"run" : True , "reason" : "high-confidence intelligence tags selected" }
193210 if automatic_scan :
194211 return {"run" : True , "reason" : "automatic scan enabled" }
@@ -326,6 +343,61 @@ def _scope_target_list(
326343 }
327344
328345
346+ def _scope_current_targets_to_hosts (
347+ target_domain : Optional [str ],
348+ target_list : Optional [Path ],
349+ out_dir : Path ,
350+ hosts : List [str ],
351+ reason : str ,
352+ ) -> Tuple [Optional [str ], Optional [Path ], Dict [str , object ]]:
353+ host_set = {str (host ).strip ().lower () for host in hosts if str (host ).strip ()}
354+ if not host_set :
355+ return target_domain , target_list , {"enabled" : False , "reason" : "no ROI hosts" }
356+ if target_list and target_list .exists ():
357+ raw_targets = [line .strip () for line in target_list .read_text (encoding = "utf-8-sig" ).splitlines () if line .strip ()]
358+ scoped_targets : List [str ] = []
359+ for target in raw_targets :
360+ parsed = urlparse (target if "://" in target else f"https://{ target } " )
361+ host = (parsed .hostname or target ).lower ()
362+ if host in host_set :
363+ scoped_targets .append (target )
364+ if not scoped_targets :
365+ return None , target_list , {
366+ "enabled" : False ,
367+ "reason" : "ROI hosts were not present in active target list" ,
368+ "host_scope" : sorted (host_set ),
369+ "original_targets" : len (raw_targets ),
370+ "scoped_targets" : 0 ,
371+ }
372+ if len (scoped_targets ) >= len (raw_targets ):
373+ return target_domain , target_list , {
374+ "enabled" : False ,
375+ "reason" : "ROI scope did not reduce target set" ,
376+ "host_scope" : sorted (host_set ),
377+ "original_targets" : len (raw_targets ),
378+ "scoped_targets" : len (scoped_targets ),
379+ }
380+ scoped_file = out_dir / "roi_scoped_targets.txt"
381+ scoped_file .write_text ("\n " .join (scoped_targets ) + "\n " , encoding = "utf-8" )
382+ return None , scoped_file , {
383+ "enabled" : True ,
384+ "path" : str (scoped_file ),
385+ "original_targets" : len (raw_targets ),
386+ "scoped_targets" : len (scoped_targets ),
387+ "host_scope" : sorted (host_set ),
388+ "reason" : reason ,
389+ }
390+ if target_domain and _opportunity_host (target_domain ) not in host_set :
391+ return None , None , {
392+ "enabled" : False ,
393+ "reason" : "single target did not match ROI hosts" ,
394+ "host_scope" : sorted (host_set ),
395+ "original_targets" : 1 ,
396+ "scoped_targets" : 0 ,
397+ }
398+ return target_domain , target_list , {"enabled" : False , "reason" : "single target already matches ROI host" , "host_scope" : sorted (host_set )}
399+
400+
329401def _opportunity_host (value : object ) -> str :
330402 text = str (value or "" ).strip ()
331403 if not text :
@@ -926,6 +998,34 @@ def run(
926998 explicit_templates = explicit_templates ,
927999 automatic_scan = automatic_scan ,
9281000 )
1001+ selected_tag_set = {str (tag ).strip ().lower () for tag in selected_tags if str (tag ).strip ()}
1002+ broad_tag_only = bool (selected_tag_set ) and selected_tag_set .issubset (BROAD_INFRA_TAGS ) and not explicit_templates
1003+ if roi_gate_enabled and broad_tag_only and bool (roi_decision .get ("run" , True )):
1004+ roi_hosts = [str (host ) for host in roi_decision .get ("roi_hosts" , []) if str (host ).strip ()] if isinstance (roi_decision .get ("roi_hosts" ), list ) else []
1005+ target_domain , target_list , roi_scope = _scope_current_targets_to_hosts (
1006+ target_domain ,
1007+ target_list ,
1008+ out_dir ,
1009+ roi_hosts ,
1010+ "broad infrastructure tags constrained to ROI opportunity hosts" ,
1011+ )
1012+ if roi_scope .get ("enabled" ):
1013+ cmd = _remove_flag_with_value (cmd , "-l" )
1014+ cmd = _remove_flag_with_value (cmd , "-u" )
1015+ target_list = Path (str (roi_scope ["path" ]))
1016+ target_domain = None
1017+ cmd += ["-l" , str (target_list )]
1018+ target_count = _count_targets (target_domain , target_list )
1019+ target_scope = {
1020+ "enabled" : True ,
1021+ "reason" : "technology-tag scope refined by ROI opportunity hosts" ,
1022+ "technology_scope" : target_scope ,
1023+ "roi_scope" : roi_scope ,
1024+ "original_targets" : roi_scope .get ("original_targets" ),
1025+ "scoped_targets" : roi_scope .get ("scoped_targets" ),
1026+ "host_scope" : roi_scope .get ("host_scope" , []),
1027+ }
1028+ selection_reason = "broad infrastructure tags; constrained to ROI opportunity hosts"
9291029 if roi_gate_enabled and not bool (roi_decision .get ("run" , True )):
9301030 duration = time .perf_counter () - started
9311031 reason = str (roi_decision .get ("reason" ) or "baseline-only scan skipped: insufficient opportunity evidence" )
@@ -942,7 +1042,7 @@ def run(
9421042 "selected_tags_requested" : selected_tags_requested ,
9431043 "selected_tags" : selected_tags ,
9441044 "selection_reason" : selection_reason ,
945- "coverage_strategy" : "skipped_low_roi_baseline" ,
1045+ "coverage_strategy" : "skipped_low_roi_baseline" if baseline_only else "skipped_low_roi" ,
9461046 "roi_decision" : roi_decision ,
9471047 "baseline_reason" : reason ,
9481048 "baseline_skip_reason" : reason ,
@@ -1429,7 +1529,9 @@ def run(
14291529 "selected_tags" : selected_tags ,
14301530 "selection_reason" : selection_reason ,
14311531 "coverage_strategy" : "smart_tags_plus_lightweight_baseline" if baseline_needed else "baseline_only" if baseline_only else selection_reason ,
1532+ "coverage_status" : "completed" ,
14321533 "tag_fallback_reason" : tag_fallback_reason ,
1534+ "roi_decision" : roi_decision ,
14331535 "baseline_reason" : baseline_reason ,
14341536 "baseline_skip_reason" : baseline_skip_reason ,
14351537 "baseline_roi" : baseline_roi ,
@@ -1524,7 +1626,9 @@ def run(
15241626 "selected_tags" : selected_tags ,
15251627 "selection_reason" : selection_reason ,
15261628 "coverage_strategy" : "smart_tags_plus_lightweight_baseline" if "baseline_needed" in locals () and baseline_needed else "baseline_only" if "baseline_only" in locals () and baseline_only else selection_reason ,
1629+ "coverage_status" : "incomplete_timeout" ,
15271630 "tag_fallback_reason" : tag_fallback_reason ,
1631+ "roi_decision" : roi_decision if "roi_decision" in locals () else {"run" : True , "reason" : "not evaluated before timeout" },
15281632 "baseline_reason" : baseline_reason if "baseline_reason" in locals () else "not evaluated" ,
15291633 "baseline_skip_reason" : baseline_skip_reason if "baseline_skip_reason" in locals () else "" ,
15301634 "baseline_roi" : baseline_roi if "baseline_roi" in locals () else {"run" : False , "reason" : "not evaluated" , "targets" : []},
@@ -1555,6 +1659,10 @@ def run(
15551659 "duration_seconds" : round (duration , 2 ),
15561660 "timeout_seconds" : effective_timeout ,
15571661 "status" : "timed_out" ,
1662+ "findings_count" : 0 ,
1663+ "templates_executed" : None ,
1664+ "templates_skipped" : None ,
1665+ "incomplete_reason" : f"nuclei timed out after { effective_timeout } s before coverage could be trusted" ,
15581666 "command" : cmd ,
15591667 },
15601668 )
0 commit comments