"""recon-triage CLI. Single entrypoint with `scan` and `replay` subcommands. AUTHORIZED USE ONLY. This tool performs recon and triage only. It never runs, generates, downloads, or executes exploits. """ from __future__ import annotations import logging from datetime import UTC from pathlib import Path import typer from . import __version__ from .orchestrator import build_report_from_fixtures, run_scan from .report import markdown from .schema import ReconReport, export_json_schema from .scope import Scope from .triage.llm import run_triage AUTH_NOTICE = ( "AUTHORIZED USE ONLY — recon & triage scope. This tool enumerates assets, " "normalizes output, and references Exploit-DB candidates. It never runs or " "generates exploits. You are responsible for ensuring all targets are in scope." ) app = typer.Typer( add_completion=False, help=f"recon-triage v{__version__}\n\n{AUTH_NOTICE}", no_args_is_help=True, ) def _setup_logging(verbose: bool) -> None: logging.basicConfig( level=logging.DEBUG if verbose else logging.INFO, format="%(asctime)s %(levelname)s %(name)s: %(message)s", ) def _utcnow() -> str: from datetime import datetime return datetime.now(UTC).isoformat() def _write_outputs(report: ReconReport, out_dir: Path) -> None: out_dir.mkdir(parents=True, exist_ok=True) (out_dir / "report.json").write_text(report.model_dump_json(indent=2) + "\n") (out_dir / "report.md").write_text(markdown.render(report)) # Always export the schema alongside for inspection. export_json_schema(out_dir / "schemas") typer.echo(f"Wrote {out_dir/'report.json'} and {out_dir/'report.md'}") @app.command() def scan( scope: Path = typer.Option(..., "--scope", help="Path to scope.yaml"), target: str = typer.Option(..., "--target", help="In-scope root domain to enumerate"), 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"), verbose: bool = typer.Option(False, "--verbose", "-v"), ) -> None: """Run the live recon pipeline against an in-scope target.""" _setup_logging(verbose) 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, scope=sc, enable_nuclei=enable_nuclei, passive_only=passive_only, 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() report.triage = run_triage(report) _write_outputs(report, out) @app.command() def replay( fixtures: Path = typer.Option(Path("tests/fixtures"), "--fixtures", help="Fixtures directory"), out: Path = typer.Option(Path("/data/out"), "--out", help="Output directory"), scope: Path | None = typer.Option(None, "--scope", help="Optional scope.yaml to gate fixtures"), enable_nuclei: bool = typer.Option(True, "--enable-nuclei/--no-nuclei", help="Include nuclei fixture"), verbose: bool = typer.Option(False, "--verbose", "-v"), ) -> None: """Run the full normalize->ground->report path on canned fixtures (no network).""" _setup_logging(verbose) typer.echo(AUTH_NOTICE) sc = Scope.load(scope) if scope else None report = build_report_from_fixtures( fixtures, run_id="replay", scope=sc, enable_nuclei=enable_nuclei ) report.started_at = "replay" report.finished_at = "replay" report.triage = run_triage(report) _write_outputs(report, out) @app.command() def schema(out: Path = typer.Option(Path("schemas"), "--out")) -> None: """Export the JSON Schema for the report contract.""" p = export_json_schema(out) typer.echo(f"Wrote schema to {p.parent}") def main() -> None: # entrypoint for the console script app() if __name__ == "__main__": # pragma: no cover main()