mirror of
https://github.com/yogeshojha/rengine.git
synced 2026-10-21 07:41:28 +02:00
reNgine 2 had a Tools section; 3.0 lost it, so a hunter left the product for a whois or a dns lookup even though every capability was already here inside a scan. `toolbox/` is a top-level package mounted into api and worker, and a tool is one file under `toolbox/tools/` — auto-discovered exactly like stages, report sections and MCP tools, with a pydantic `Input` whose flattened schema IS the form the UI renders. Six ship: WHOIS, DNS records, subdomain finder, HTTP probe, IP address and CVE. Where a tool runs is a declaration the registry enforces. The api image carries no tool binaries, so `INLINE` answers from RDAP, the offline range tables or the threat feeds on the request's AsyncSession, and `QUEUED` shells out on the worker; `_validate` rejects a class whose `run` is async or sync the wrong way round, so the two cannot drift. A run is a question rather than inventory, so it lives in Redis with a seven-day TTL and no migration, keyed by user id. A queued run nobody collects fails after two minutes instead of spinning for ever. A tool answers in blocks — facts, table, tags, code, note — so one frontend renderer serves every tool and a new one costs no frontend work. An empty block with nothing to say about being empty is dropped. `require_public` refuses loopback, private and link-local before the HTTP probe sends anything, because an authenticated user typing 169.254.169.254 is an SSRF. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CtRcxFCPSYxW5sXam3SUWB
41 lines
1.2 KiB
Python
41 lines
1.2 KiB
Python
"""Tools are discovered by module. A new tool is a file in this directory."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
from functools import lru_cache
|
|
from pathlib import Path
|
|
|
|
from shared.logging import get_logger
|
|
from toolbox.base import Tool
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
_SKIP = {"__init__"}
|
|
|
|
|
|
@lru_cache(maxsize=1)
|
|
def discover() -> dict[str, type[Tool]]:
|
|
found: dict[str, type[Tool]] = {}
|
|
for module in sorted(Path(__file__).parent.glob("*.py")):
|
|
if module.stem in _SKIP or module.stem.startswith("_"):
|
|
continue
|
|
try:
|
|
namespace = importlib.import_module(f"toolbox.tools.{module.stem}")
|
|
except Exception as exc:
|
|
logger.warning("toolbox tool skipped", module=module.stem, error=str(exc))
|
|
continue
|
|
for obj in vars(namespace).values():
|
|
if (
|
|
isinstance(obj, type)
|
|
and issubclass(obj, Tool)
|
|
and obj is not Tool
|
|
and not getattr(obj, "__abstractmethods__", None)
|
|
and getattr(obj, "name", None)
|
|
):
|
|
found.setdefault(obj.name, obj)
|
|
return found
|
|
|
|
|
|
__all__ = ["discover"]
|