feat: tool normalizers, Exploit-DB grounding, orchestrator, report, triage, CLI

This commit is contained in:
2026-06-29 17:36:04 -04:00
parent 1f531e4948
commit aa4501f2fa
18 changed files with 1511 additions and 0 deletions
+123
View File
@@ -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()