diff --git a/sherlock_project/web.py b/sherlock_project/web.py new file mode 100644 index 0000000000..937e618ba4 --- /dev/null +++ b/sherlock_project/web.py @@ -0,0 +1,282 @@ +#! /usr/bin/env python3 + +""" +Sherlock: Optional Web UI + +A lightweight Flask wrapper around the Sherlock CLI that streams scan progress +to the browser. Three display modes surface feedback at different levels of +detail so users see continuous progress instead of waiting in silence for a +full scan to complete. + +Run with: + python -m sherlock_project.web + +Requires Flask (not a core dependency): + pip install flask +""" + +import os +import re +import subprocess +import sys +from typing import Iterator + +try: + from flask import Flask, Response, render_template_string, request +except ImportError: + print("Flask is required to run the Sherlock web UI.") + print("Install it with: pip install flask") + sys.exit(1) + + +DEFAULT_HOST = "127.0.0.1" +DEFAULT_PORT = 5000 +SCAN_TIMEOUT_SECONDS = 10 +USERNAME_PATTERN = re.compile(r"^[A-Za-z0-9_.\-]{1,64}$") + +PAGE_TEMPLATE = r""" + + + + + Sherlock + + + +
+
+

Sherlock

+

+
+
+ + + + +
+
+ +
+ + +
+ +
+
+ +
+ + + + + + +
+
+
+ 0 +  ·  + 0 +
+
+ +
+ + + +""" + + +def create_app() -> "Flask": + """Build and return the Flask application.""" + app = Flask(__name__) + + @app.route("/") + def index() -> str: + return render_template_string(PAGE_TEMPLATE) + + @app.route("/search") + def search() -> Response: + username = request.args.get("u", "").strip() + if not USERNAME_PATTERN.fullmatch(username): + return Response("Invalid username.", status=400, mimetype="text/plain") + return Response(_stream_scan(username), mimetype="text/plain") + + return app + + +def _stream_scan(username: str) -> Iterator[str]: + """Run a Sherlock scan as a subprocess and yield its output line by line.""" + proc = subprocess.Popen( + [ + sys.executable, + "-u", + "-m", + "sherlock_project", + "--print-all", + "--no-color", + "--timeout", + str(SCAN_TIMEOUT_SECONDS), + username, + ], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + bufsize=1, + text=True, + cwd=os.getcwd(), + ) + try: + assert proc.stdout is not None + for line in proc.stdout: + yield line + finally: + proc.wait() + + +def main() -> None: + """Entry point for `python -m sherlock_project.web`.""" + host = os.environ.get("SHERLOCK_WEB_HOST", DEFAULT_HOST) + port = int(os.environ.get("SHERLOCK_WEB_PORT", DEFAULT_PORT)) + print(f"Sherlock web UI running at http://{host}:{port}") + create_app().run(host=host, port=port, debug=False) + + +if __name__ == "__main__": + main()