from __future__ import annotations from pathlib import Path import json import os from datetime import datetime BANNER = r""" ██████╗ ███████╗██████╗ ███████╗██╗ █████╗ ██████╗ ███████╗ ██╔══██╗██╔════╝██╔══██╗██╔════╝██║ ██╔══██╗██╔══██╗██╔════╝ ██████╔╝█████╗ ██║ ██║█████╗ ██║ ███████║██████╔╝█████╗ ██╔══██╗██╔══╝ ██║ ██║██╔══╝ ██║ ██╔══██║██╔══██╗██╔══╝ ██║ ██║███████╗██████╔╝██║ ███████╗██║ ██║██║ ██║███████╗ ╚═╝ ╚═╝╚══════╝╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ Authorized Web Assessment Framework """ def prompt(text: str, default: str = "") -> str: suffix = f" [{default}]" if default else "" value = input(f"{text}{suffix}: ").strip() return value or default def yes_no(text: str, default: bool = False) -> bool: marker = "Y/n" if default else "y/N" value = input(f"{text} [{marker}]: ").strip().lower() if not value: return default return value in {"y", "yes"} def recent_runs(limit: int = 10) -> list[Path]: roots = [Path(os.environ.get("REDFLARE_HOME", Path.cwd())) / "runs"] found: dict[Path, float] = {} for root in roots: if not root.is_dir(): continue for candidate in root.iterdir(): if candidate.is_dir() and ((candidate / "attack_surface.json").exists() or (candidate / "findings.jsonl").exists()): resolved = candidate.resolve() found[resolved] = max(found.get(resolved, 0), candidate.stat().st_mtime) return [path for path, _ in sorted(found.items(), key=lambda item: item[1], reverse=True)[:limit]] def run_label(run: Path) -> str: try: manifest = json.loads((run / "manifest.json").read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError): manifest = {} targets = manifest.get("targets") or [] names = [str(item.get("host") or item.get("url") or "") for item in targets if isinstance(item, dict)] target = names[0] if names else run.name if len(names) > 1: target += f" +{len(names) - 1}" created = str(manifest.get("created_at") or "") try: when = datetime.fromisoformat(created).astimezone().strftime("%b %d %H:%M") except ValueError: when = datetime.fromtimestamp(run.stat().st_mtime).strftime("%b %d %H:%M") try: summary = json.loads((run / "summary.json").read_text(encoding="utf-8")) findings = f" · {int(summary.get('findings', 0))} findings" except (OSError, json.JSONDecodeError, TypeError, ValueError): findings = "" return f"{target} · {when}{findings}" def visualize_arguments() -> list[str] | None: runs = recent_runs() print("\nVisual investigation console") if runs: print("Recent REDflare runs:") for index, run in enumerate(runs, start=1): print(f"{index}) {run_label(run)}") print(f" {run}") selection = prompt("Choose a run number or paste a run path / file URL", "1") run = str(runs[int(selection) - 1]) if selection.isdigit() and 0 < int(selection) <= len(runs) else selection else: run = prompt("Run directory or file:/// URL") if not run: print("No run selected.") return None arguments = ["visualize", run, "--port", prompt("Local visualizer port", "8765")] if not yes_no("Open the visualizer in your browser", True): arguments.append("--no-browser") print("\nOpening the REDflare visual investigation console...\n") return arguments def interactive_arguments() -> list[str] | None: print(BANNER) print("1) Full assessment pipeline (recommended)") print("2) Web assessment (native modules)") print("3) Quick reconnaissance") print("4) Visualize a completed run") print("5) Native capability health check") print("6) Exit") choice = prompt("Select an option", "1") if choice == "4": return visualize_arguments() if choice == "5": return ["doctor"] if choice == "6": return None profile = {"1": "full", "2": "web", "3": "quick"}.get(choice) if not profile: print("Unknown menu choice.") return None print("\nTarget input") print("1) Enter one or more targets") print("2) Load targets from a file") target_mode = prompt("Select target input", "1") arguments = ["scan"] if target_mode == "2": arguments.extend(["--targets-file", prompt("Path to target file")]) else: values = prompt("Target URL(s), comma-separated") arguments.extend(value.strip() for value in values.split(",") if value.strip()) scope = prompt("Optional JSON scope file (blank to use entered targets)") if scope: arguments.extend(["--scope", scope]) print("\nAuthorization gate") print("Only continue for systems covered by explicit written authorization.") if not yes_no("I confirm every entered target is authorized", False): print("Authorization was not confirmed. Scan cancelled.") return None arguments.append("--authorized") if yes_no("Does this scope include public internet hosts", True): arguments.append("--allow-public") if profile == "full" and not yes_no( "Full mode includes browser interaction and focused unauthenticated service probing. Is that permitted", False, ): print("Falling back to native web assessment mode.") profile = "web" arguments.extend(["--profile", profile]) output = prompt("Base output directory", str(Path(os.environ.get("REDFLARE_HOME", Path.cwd())) / "runs")) arguments.extend(["--output", output]) arguments.extend(["--workers", prompt("Targets to process concurrently", "1")]) arguments.extend(["--timeout", prompt("Request timeout in seconds", "10")]) if profile in {"web", "full"}: wordlist = prompt("Optional path wordlist") if wordlist: arguments.extend(["--wordlist", wordlist]) arguments.extend(["--rate", prompt("Path requests per second", "1")]) arguments.extend(["--max-paths", prompt("Maximum paths per target", "100")]) arguments.extend(["--max-crawl-pages", prompt("Maximum pages to map per target", "30")]) arguments.extend(["--max-crawl-depth", prompt("Maximum crawler depth", "2")]) if yes_no("Permit bounded GraphQL schema introspection on in-scope endpoints", False): arguments.append("--graphql-introspection") arguments.extend(["--max-exposure-endpoints", prompt("Maximum responses to inspect for sensitive exposure", "75")]) if profile == "full": repositories = prompt( "Optional associated GitHub repositories as owner/repository or URLs (blank if none)" ) for repository in repositories.split(","): value = repository.strip() if not value: continue bare = value.removesuffix(".git").removeprefix("https://github.com/").removeprefix("http://github.com/") if len([part for part in bare.split("/") if part]) != 2: print(f"Skipping invalid repository {value!r}; expected owner/repository or a GitHub URL.") continue arguments.extend(["--github-repo", value]) print("\nConfiguration complete. Starting REDflare pipeline...\n") return arguments