Skip to content

Commit 4959c87

Browse files
committed
Bump version to 0.2.1 and enhance functionality with new validation checks, deduplication logic, and improved reporting features
1 parent edae672 commit 4959c87

11 files changed

Lines changed: 315 additions & 57 deletions

File tree

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ FROM python:3.11-slim
22

33
LABEL org.opencontainers.image.title="BladeRecon" \
44
org.opencontainers.image.description="Lightweight reconnaissance framework for attack-surface discovery and reporting." \
5-
org.opencontainers.image.version="0.2.0" \
5+
org.opencontainers.image.version="0.2.1" \
66
org.opencontainers.image.licenses="MIT" \
77
org.opencontainers.image.source="https://github.com/mohamedxk9tb/BladeRecon"
88

bladerecon/modules/intelligence.py

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -870,6 +870,27 @@ def _confidence_score_cap(confidence: str) -> int:
870870
return {"Low": 40, "Medium": 70, "High": 88, "Very High": 100}.get(confidence, 40)
871871

872872

873+
def _cdn_like_host(host: str, noisy_hosts: Set[str]) -> bool:
874+
lower = str(host or "").lower()
875+
return lower in noisy_hosts or any(token in lower for token in ("cdn", "static", "assets", "edge", "cache"))
876+
877+
878+
def _has_live_validation_signal(row: Dict[str, Any]) -> bool:
879+
positives = row.get("positive_validation_signals")
880+
if not isinstance(positives, list):
881+
return False
882+
text = " ".join(str(value).lower() for value in positives)
883+
return any(
884+
token in text
885+
for token in (
886+
"nuclei finding",
887+
"returned actionable response",
888+
"access confirmed",
889+
"interesting response pattern",
890+
)
891+
)
892+
893+
873894
def _evidence_summary(opportunity: HostOpportunity, strongest: List[OpportunityEvidence]) -> List[str]:
874895
summary = []
875896
if "GraphQL" in opportunity.opportunity_types:
@@ -1058,9 +1079,17 @@ def build_opportunity_priorities(scan_data: Dict[str, Any], suppressions: Option
10581079
row.update(_opportunity_validation(item, row, scan_data, noisy_hosts))
10591080
row["confidence"] = _adjust_confidence_after_validation(row)
10601081
row["score"] = min(int(row.get("score") or 0), _confidence_score_cap(str(row.get("confidence") or "Low")))
1082+
if _cdn_like_host(item.host, noisy_hosts) and not _has_live_validation_signal(row):
1083+
negatives = row.get("negative_validation_signals") if isinstance(row.get("negative_validation_signals"), list) else []
1084+
_add_unique_signal(negatives, "CDN/static infrastructure evidence should support, not lead, without live validation")
1085+
row["negative_validation_signals"] = negatives[:6]
1086+
row["confidence"] = "Medium" if row.get("confidence") in {"High", "Very High"} else str(row.get("confidence") or "Low")
1087+
row["validation_strength"] = "Weak" if row.get("validation_strength") in {"Moderate", "Strong"} else str(row.get("validation_strength") or "None")
1088+
row["validation_score"] = min(int(row.get("validation_score") or 0), 2)
1089+
row["score"] = min(int(row.get("score") or 0), 60)
10611090
row.update(_priority_label(int(row.get("score") or 0), str(row.get("confidence") or "Low"), row))
10621091
rows.append(row)
1063-
rows.sort(key=lambda item: (-int(item["score"]), str(item["host"])))
1092+
rows.sort(key=lambda item: (-int(item["score"]), _cdn_like_host(str(item["host"]), noisy_hosts), str(item["host"])))
10641093
return rows[:20]
10651094

10661095

bladerecon/modules/nuclei.py

Lines changed: 109 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from .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

2727
SEVERITY_ORDER = ("critical", "high", "medium", "low", "info", "unknown")
28+
BROAD_INFRA_TAGS = {"apache", "nginx"}
2829

2930

3031
def _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+
329401
def _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

Comments
 (0)