Unified OSINT reconnaissance orchestrator — 28 tools behind a single CLI + MCP server (Kali VM → Claude Desktop/Code)
OhSINT is a unified OSINT reconnaissance orchestrator that wraps 73 tools behind a single CLI and MCP server, enabling automated intelligence gatherin...
674@mcp.tool()
675async def osint_report(
676 results_dir: str,
677 output_format: str = "all",
678) -> str:
679 """Generate a consolidated report from scan results in a directory."""
680 from src.report import load_report
681
682 path = Path(results_dir)
683 json_file = path / "report.json"
684 if not json_file.exists():
685 return f"No report.json found in {results_dir}"
686
687 report = load_report(json_file)
688 save_report(report, path, output_format)
689 return report.to_markdown()// Network-exposed MCP server; no authorization required for this tool.
294@mcp.tool()
295async def osint_exiftool(
296 directory: str,
297 filter_fields: str = "Author|Creator|Email|Producer|Template|Software",
298 authorization_confirmed: bool = False,
299) -> str:
300 """Extract metadata from downloaded files in a directory."""
301 return await _run_tool_audited(
302 "exiftool", directory, authorization_confirmed,
303 filter_fields=filter_fields,
304 )// Network-exposed MCP server; tool is passive and does not require authorization.
307@mcp.tool()
308async def osint_github_dorks(
309 target: str,
310 dork_file: str = "",
311 authorization_confirmed: bool = False,
312) -> str:
313 """Scan GitHub repos/orgs for sensitive information leaks."""
314 kwargs: dict[str, Any] = {}
315 if dork_file:
316 kwargs["dork_file"] = dork_file
317 return await _run_tool_audited(
318 "github_dorks", target, authorization_confirmed, **kwargs,
319 )// Network-exposed MCP server; no authorization required for this tool.
1057@mcp.tool()
1058async def osint_js_analysis(
1059 domains: str,
1060 js_urls_file: str = "",
1061 authorization_confirmed: bool = False,
1062) -> str:
1063 """Analyze JavaScript files for endpoints, secrets, and source maps."""
1064 ...
1065 report = await asyncio.to_thread(
1066 pipeline.run, domain_list, js_urls_file or None, output_dir,
1067 )// Network-exposed MCP server; no authorization required for this tool.
863@mcp.tool()
864async def osint_subdomain_takeover(
865 domains: str,
866 re_scan_path: str = "",
867 authorization_confirmed: bool = False,
868 per_domain: bool = False,
869 max_subdomains: int = 5000,
870) -> str:
871 ...
872 re_path = Path(re_scan_path) if re_scan_path else None
873 ...
874 report = await asyncio.to_thread(
875 pipeline.run, domain_list, re_path, max_subdomains,
876 )// Network-exposed MCP server; requires authorization_confirmed but still allows arbitrary file read.
548@click.option("--scope-file", type=click.Path(exists=True), default=None,
549 help="HackerOne/Bugcrowd scope file — parses assets, prompts for selection.")
550...
551def url_harvest(ctx, target, targets_file, scope_file, max_urls):
552 ...
553 domains = _resolve_takeover_targets(ctx, target, targets_file, scope_file)
554 ...
555 report = pipeline.run(targets=domains, max_urls=max_urls, output_dir=output_dir)// Local-only MCP, requires compromised LLM to exploit; but still a finding.
356@click.option("--re-scan", type=click.Path(exists=True), default=None,
357 help="Previous report.json — skip enumeration, re-run fingerprinting only.")
358...
359re_scan_path = Path(re_scan) if re_scan else None
360...
361report = pipeline.run(
362 targets=domains,
363 re_scan_path=re_scan_path,
364 ...
365)// Local-only MCP, requires compromised LLM to exploit; but still a finding.
548@click.option("--targets-file", type=click.Path(exists=True), default=None,
549 help="File with one domain per line.")
550...
551def url_harvest(ctx, target, targets_file, scope_file, max_urls):
552 ...
553 domains = _resolve_takeover_targets(ctx, target, targets_file, scope_file)
554 ...
555 report = pipeline.run(targets=domains, max_urls=max_urls, output_dir=output_dir)// Local-only MCP, requires compromised LLM to exploit; but still a finding.
627@click.option("--js-urls-file", type=click.Path(exists=True), default=None,
628 help="JS URL list from url-harvest pipeline (js_urls.txt).")
629...
630def js_analysis(ctx, target, targets_file, scope_file, js_urls_file):
631 ...
632 report = pipeline.run(targets=domains, js_urls_file=js_urls_file, output_dir=output_dir)// Local-only MCP, requires compromised LLM to exploit; but still a finding.
36class Config:
37 def __init__(
38 self,
39 api_keys_path: Optional[Path] = None,
40 profiles_path: Optional[Path] = None,
41 ):
42 self.api_keys_path = api_keys_path or DEFAULT_API_KEYS_FILE
43 ...
44 def _load(self) -> None:
45 if self.api_keys_path.exists():
46 with open(self.api_keys_path) as f:
47 self._api_keys = yaml.safe_load(f) or {}// Network-exposed MCP server; combined with file read vulnerabilities, this becomes critical.
1261def main():
1262 parser.add_argument("--host", default="127.0.0.1",
1263 help="Bind address (default: 127.0.0.1, use 0.0.0.0 for Hyper-V)")
1264 ...
1265 if token:
1266 logger.info("Bearer token authentication ENABLED")
1267 else:
1268 logger.warning(
1269 "No bearer token configured — MCP server is UNAUTHENTICATED. "
1270 "Set --token, OHSINT_MCP_TOKEN, or mcp_server.bearer_token in api_keys.yaml"
1271 )// Network-exposed MCP server; default configuration may be unauthenticated.
163def _require_auth(authorization_confirmed: bool, tool_name: str = "") -> None:
164 if tool_name:
165 tool = _get_orchestrator().get_tool(tool_name)
166 if tool and tool.is_passive:
167 return # passive tools don't need authorization
168 if not authorization_confirmed:
169 raise ValueError(
170 "Authorization not confirmed. Active reconnaissance tools require "
171 "WRITTEN AUTHORIZATION from the target owner before execution."
172 )// Network-exposed MCP server; passive tools are accessible without authorization.
183@mcp.tool()
184async def osint_full_recon(
185 target: str,
186 profile: str = "passive",
187 authorization_confirmed: bool = False,
188 output_format: str = "all",
189 timeout: int = DEFAULT_TIMEOUT,
190) -> str:
191 """Run full OSINT reconnaissance against a target using a scan profile."""
192 _require_auth(authorization_confirmed)
193 start = _time.time()
194 timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
195 safe_target = target.replace("/", "_").replace(":", "_")
196 output_dir = DEFAULT_RESULTS_DIR / safe_target / timestamp// Network-exposed MCP server; input validation is missing across multiple tools.