feat: tool normalizers, Exploit-DB grounding, orchestrator, report, triage, CLI
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
"""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"),
|
||||
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()}"
|
||||
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,
|
||||
)
|
||||
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()
|
||||
Reference in New Issue
Block a user