Files

177 lines
7.8 KiB
Python

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