1 Commits

Author SHA1 Message Date
0xWheatyz 94c8bbede9 feat: widen port enumeration across recon stages 2026-06-29 19:43:56 -04:00
6 changed files with 106 additions and 5 deletions
+6
View File
@@ -61,6 +61,9 @@ def scan(
out: Path = typer.Option(Path("/data/out"), "--out", help="Output directory"),
enable_nuclei: bool = typer.Option(False, "--enable-nuclei", help="Run nuclei (needs network/time)"),
passive_only: bool = typer.Option(False, "--passive-only", help="Passive enumeration only"),
ports: str | None = typer.Option(None, "--ports", help="Explicit naabu port spec, e.g. '22,80,5000' or '1-10000'. Overrides --top-ports."),
top_ports: int = typer.Option(1000, "--top-ports", help="Scan naabu's top-N ports when --ports is unset (default 1000)."),
full_ports: bool = typer.Option(False, "--full-ports", help="Full 1-65535 sweep (slow). Shorthand for --ports '-'."),
rate_limit: int | None = typer.Option(None, "--rate-limit", help="Per-tool rate limit"),
timeout: float = typer.Option(600.0, "--timeout", help="Per-tool timeout (seconds)"),
templates_dir: str | None = typer.Option(None, "--templates-dir", help="nuclei templates cache dir"),
@@ -71,6 +74,7 @@ def scan(
typer.echo(AUTH_NOTICE)
sc = Scope.load(scope)
run_id = f"scan-{_utcnow()}"
port_spec = "-" if full_ports else ports
report = run_scan(
run_id=run_id,
target=target,
@@ -80,6 +84,8 @@ def scan(
rate_limit=rate_limit,
timeout=timeout,
templates_dir=templates_dir,
ports=port_spec,
top_ports=top_ports,
)
report.started_at = report.started_at or run_id.replace("scan-", "")
report.finished_at = _utcnow()
+13 -4
View File
@@ -261,6 +261,8 @@ def run_scan(
timeout: float = 600.0,
templates_dir: str | None = None,
search_fn: SearchFn | None = None,
ports: str | None = None,
top_ports: int | None = 1000,
) -> ReconReport:
"""Run the full live pipeline. Scope is enforced before any active probing.
@@ -292,9 +294,12 @@ def run_scan(
stages.append(StageResult(name=name, status=StageStatus.SKIPPED, detail="passive-only"))
return _finalize(run_id, target, scope, table, stages, [], search_fn)
# Stage 3: naabu connect-scan (unprivileged)
# Stage 3: naabu connect-scan (unprivileged). Port breadth widened beyond
# naabu's top-100 default so high/uncommon-port services aren't missed.
scan_targets = _scope_targets(table, scope)
nb, nb_run = naabu.run(scan_targets, timeout=timeout, rate_limit=rate_limit)
nb, nb_run = naabu.run(
scan_targets, timeout=timeout, rate_limit=rate_limit, ports=ports, top_ports=top_ports
)
nb = [h for h in nb if _hit_in_scope(h, scope)]
table.add_naabu(nb)
stages.append(_stage("naabu", nb_run, count=len(nb)))
@@ -308,8 +313,12 @@ def run_scan(
table.add_nmap(nm)
stages.append(_stage("nmap", nm_run, count=len(nm)))
# Stage 5: httpx probing
hx, hx_run = httpx.run(_scope_targets(table, scope), timeout=timeout, rate_limit=rate_limit)
# Stage 5: httpx probing. Feed it every discovered open port — otherwise
# httpx only checks 80/443 and misses web apps on ports like 5000/8080.
open_for_http = sorted({p.number for h in table.hosts() for p in h.ports})
hx, hx_run = httpx.run(
_scope_targets(table, scope), timeout=timeout, rate_limit=rate_limit, ports=open_for_http
)
table.add_httpx(hx)
stages.append(_stage("httpx", hx_run, count=len(hx)))
+11 -1
View File
@@ -82,8 +82,18 @@ def _looks_like_ip(v) -> bool:
return False
def run(targets: list[str], *, timeout: float = 180.0, rate_limit: int | None = None) -> tuple[list[HttpProbe], object]:
def run(
targets: list[str],
*,
timeout: float = 180.0,
rate_limit: int | None = None,
ports: list[int] | None = None,
) -> tuple[list[HttpProbe], object]:
cmd = [BINARY, "-json", "-td", "-silent", "-title", "-tech-detect", "-web-server", "-status-code"]
# Without -p, httpx only probes 80/443. Feed it the ports discovered by
# naabu/nmap so web apps on non-standard ports (e.g. 5000, 8080) are probed.
if ports:
cmd += ["-p", ",".join(str(p) for p in sorted(set(ports)))]
if rate_limit:
cmd += ["-rate-limit", str(rate_limit)]
res = run_tool(cmd, timeout=timeout, input_text="\n".join(targets) + "\n")
@@ -57,11 +57,18 @@ def run(
timeout: float = 300.0,
rate_limit: int | None = None,
ports: str | None = None,
top_ports: int | None = 1000,
) -> tuple[list[PortHit], object]:
# connect scan keeps the container unprivileged (no NET_RAW needed).
cmd = [BINARY, "-json", "-scan-type", "connect", "-silent"]
# naabu's own default is only the top-100 ports, which silently misses
# services on higher/uncommon ports (a frequent THM/HTB/VulnHub case).
# An explicit `ports` spec wins; otherwise widen to top-N (default 1000).
# Pass ports="-" for a full 1-65535 sweep.
if ports:
cmd += ["-p", ports]
elif top_ports:
cmd += ["-top-ports", str(top_ports)]
if rate_limit:
cmd += ["-rate", str(rate_limit)]
res = run_tool(cmd, timeout=timeout, input_text="\n".join(targets) + "\n")
@@ -102,8 +102,14 @@ def run(
*,
timeout: float = 600.0,
unprivileged: bool = True,
version_all: bool = True,
) -> tuple[list[NmapHost], object]:
cmd = [BINARY, "-sV", "-oX", "-", "-Pn"]
if version_all:
# Max version-detection intensity. Default (7) often yields only a
# low-confidence service name with null product/version on services
# that don't volunteer a banner; intensity 9 probes harder.
cmd.append("--version-all")
if unprivileged:
cmd.append("-sT") # TCP connect scan; no raw sockets required
if ports:
+63
View File
@@ -0,0 +1,63 @@
"""The recon stages must scan wider than the tools' narrow built-in defaults.
These guard the regressions behind the 'single open port / no banner' report:
- naabu must not silently fall back to its top-100 default.
- httpx must be told the discovered open ports (else it only probes 80/443).
- nmap must probe version detection at full intensity.
"""
from __future__ import annotations
from recon_triage.tools import httpx, naabu, nmap
from recon_triage.tools.base import ToolRun
def _capture(monkeypatch, module):
"""Patch a tool module's run_tool to record the command and return empty output."""
seen: dict = {}
def fake_run_tool(cmd, *, timeout=120.0, input_text=None):
seen["cmd"] = cmd
return ToolRun(cmd=cmd, returncode=0, stdout="", stderr="")
monkeypatch.setattr(module, "run_tool", fake_run_tool)
return seen
def test_naabu_widens_beyond_top_100_by_default(monkeypatch):
seen = _capture(monkeypatch, naabu)
naabu.run(["10.0.0.1"])
cmd = seen["cmd"]
# Default must explicitly request top-1000, not rely on naabu's top-100 default.
assert "-top-ports" in cmd
assert cmd[cmd.index("-top-ports") + 1] == "1000"
def test_naabu_explicit_ports_override_top_ports(monkeypatch):
seen = _capture(monkeypatch, naabu)
naabu.run(["10.0.0.1"], ports="-") # full sweep
cmd = seen["cmd"]
assert "-p" in cmd and cmd[cmd.index("-p") + 1] == "-"
assert "-top-ports" not in cmd # explicit ports win
def test_httpx_probes_discovered_ports(monkeypatch):
seen = _capture(monkeypatch, httpx)
httpx.run(["10.0.0.1"], ports=[5000, 8080])
cmd = seen["cmd"]
assert "-p" in cmd
assert cmd[cmd.index("-p") + 1] == "5000,8080"
def test_httpx_without_ports_omits_flag(monkeypatch):
seen = _capture(monkeypatch, httpx)
httpx.run(["10.0.0.1"])
assert "-p" not in seen["cmd"]
def test_nmap_uses_full_version_intensity(monkeypatch):
seen = _capture(monkeypatch, nmap)
nmap.run(["10.0.0.1"], ports=[5000])
cmd = seen["cmd"]
assert "-sV" in cmd
assert "--version-all" in cmd