Upload files to "redflare"
This commit is contained in:
+181
@@ -0,0 +1,181 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from . import __version__
|
||||
from .core.scope import ScopeError, ScopePolicy, normalize_target
|
||||
from .core.standards import registry_document
|
||||
from .core.runner import Runner
|
||||
from .core.storage import RunStore, target_run_id
|
||||
from .modules.repository import run_repository_intelligence
|
||||
from .modules.base import ModuleContext
|
||||
from .profiles import PROFILES, build_modules
|
||||
from .interactive import interactive_arguments, yes_no
|
||||
from .ui import LiveConsole
|
||||
from .visualize import VisualServer
|
||||
|
||||
|
||||
def build_parser() -> argparse.ArgumentParser:
|
||||
parser = argparse.ArgumentParser(prog="redflare", description="Authorized web assessment orchestrator")
|
||||
parser.add_argument("--version", action="version", version=f"REDflare {__version__}")
|
||||
sub = parser.add_subparsers(dest="command", required=True)
|
||||
|
||||
sub.add_parser("modules", help="List profiles and modules")
|
||||
sub.add_parser("doctor", help="Check native REDflare capabilities")
|
||||
sub.add_parser("tests", help="List stable test IDs and standards mappings")
|
||||
visualize = sub.add_parser("visualize", help="Open the local visual investigation console for a run")
|
||||
visualize.add_argument("run_directory", help="REDflare run directory or local file:/// URL")
|
||||
visualize.add_argument("--port", type=int, default=8765, help="Loopback port (0 chooses an available port)")
|
||||
visualize.add_argument("--no-browser", action="store_true", help="Do not open the default browser")
|
||||
|
||||
intel = sub.add_parser("intel", help="Run native REDflare repository secret intelligence")
|
||||
intel.add_argument("--repo", action="append", required=True, help="Authorized GitHub repository URL; repeatable")
|
||||
intel.add_argument("--authorized", action="store_true", help="Acknowledge authorization for every repository")
|
||||
default_runs = str(Path(os.environ.get("REDFLARE_HOME", Path.cwd())) / "runs")
|
||||
intel.add_argument("--output", default=default_runs, help="Base run output directory")
|
||||
|
||||
scan = sub.add_parser("scan", help="Run an authorized assessment")
|
||||
scan.add_argument("targets", nargs="*", help="HTTP(S) URLs or hostnames")
|
||||
scan.add_argument("--targets-file", help="Targets file, one per line")
|
||||
scan.add_argument("--scope", help="JSON scope policy with allowed_hosts")
|
||||
scan.add_argument("--authorized", action="store_true", help="Acknowledge explicit authorization for every target")
|
||||
scan.add_argument("--allow-public", action="store_true", help="Permit authorized public targets")
|
||||
scan.add_argument("--profile", choices=sorted(PROFILES), default="quick")
|
||||
scan.add_argument("--output", default=default_runs, help="Base run output directory")
|
||||
scan.add_argument("--wordlist", help="Path wordlist for the web profile")
|
||||
scan.add_argument("--max-paths", type=int, default=100)
|
||||
scan.add_argument("--max-crawl-pages", type=int, default=30)
|
||||
scan.add_argument("--max-crawl-depth", type=int, default=2)
|
||||
scan.add_argument("--max-scripts", type=int, default=20)
|
||||
scan.add_argument("--max-schema-documents", type=int, default=8)
|
||||
scan.add_argument("--max-exposure-endpoints", type=int, default=75)
|
||||
scan.add_argument("--max-exposure-findings", type=int, default=100)
|
||||
scan.add_argument("--max-exposure-body-bytes", type=int, default=2_000_000)
|
||||
scan.add_argument("--max-cve-products", type=int, default=12, help="Maximum exact product/version fingerprints to correlate with NVD")
|
||||
scan.add_argument("--max-cves-per-product", type=int, default=100, help="Maximum NVD CVEs retained per fingerprint")
|
||||
scan.add_argument(
|
||||
"--graphql-introspection",
|
||||
action="store_true",
|
||||
help="Explicitly permit bounded GraphQL schema introspection on in-scope endpoints",
|
||||
)
|
||||
scan.add_argument("--rate", type=float, default=2.0, help="Path requests per second")
|
||||
scan.add_argument("--timeout", type=float, default=8.0)
|
||||
scan.add_argument("--workers", type=int, default=2)
|
||||
scan.add_argument("--github-repo", action="append", default=[], help="Associated authorized GitHub repository URL; repeatable")
|
||||
return parser
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> int:
|
||||
guided = argv is None and len(sys.argv) == 1
|
||||
if guided:
|
||||
argv = interactive_arguments()
|
||||
if argv is None:
|
||||
return 0
|
||||
args = build_parser().parse_args(argv)
|
||||
if args.command == "modules":
|
||||
for name, classes in PROFILES.items():
|
||||
print(f"{name:5} " + ", ".join(module.name for module in classes))
|
||||
return 0
|
||||
if args.command == "doctor":
|
||||
try:
|
||||
import playwright # noqa: F401
|
||||
browser = "native-playwright"
|
||||
except ImportError:
|
||||
browser = "native-http-fallback"
|
||||
print(json.dumps({"standalone": True, "external_tools_required": False,
|
||||
"capabilities": {"browser_runtime": browser, "unauthenticated_surface": "native",
|
||||
"repository_intelligence": "native"}}, indent=2))
|
||||
return 0
|
||||
if args.command == "tests":
|
||||
print(json.dumps(registry_document(), indent=2))
|
||||
return 0
|
||||
if args.command == "visualize":
|
||||
try:
|
||||
server = VisualServer(args.run_directory, max(0, args.port))
|
||||
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
||||
print(f"Visualization error: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
server.serve(open_browser=not args.no_browser)
|
||||
return 0
|
||||
if args.command == "intel":
|
||||
if not args.authorized:
|
||||
print("Refusing to run intelligence collection without --authorized.", file=sys.stderr)
|
||||
return 2
|
||||
store = RunStore(args.output)
|
||||
result = run_repository_intelligence(args.repo, store.artifacts / "repository_intelligence")
|
||||
store.write_manifest(
|
||||
{"run_id": store.run_id, "kind": "repository-intelligence", "repositories": args.repo}
|
||||
)
|
||||
(store.root / "summary.json").write_text(json.dumps(result, indent=2), encoding="utf-8")
|
||||
print(json.dumps(result, indent=2))
|
||||
return 0 if result["status"] == "completed" else 1
|
||||
if not args.authorized:
|
||||
print("Refusing to scan without --authorized acknowledgement.", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
values = list(args.targets)
|
||||
if args.targets_file:
|
||||
values.extend(
|
||||
line.strip()
|
||||
for line in Path(args.targets_file).read_text(encoding="utf-8").splitlines()
|
||||
if line.strip() and not line.lstrip().startswith("#")
|
||||
)
|
||||
if not values:
|
||||
print("Provide at least one target or --targets-file.", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
try:
|
||||
policy = ScopePolicy.from_file(args.scope, allow_public=args.allow_public)
|
||||
targets = []
|
||||
seen = set()
|
||||
for value in values:
|
||||
target = normalize_target(value)
|
||||
policy.validate(target)
|
||||
if target.url not in seen:
|
||||
targets.append(target)
|
||||
seen.add(target.url)
|
||||
except (ScopeError, OSError, json.JSONDecodeError) as exc:
|
||||
print(f"Scope error: {exc}", file=sys.stderr)
|
||||
return 2
|
||||
|
||||
store = RunStore(args.output, target_run_id(targets))
|
||||
console = LiveConsole()
|
||||
context = ModuleContext(
|
||||
run_id=store.run_id,
|
||||
artifact_dir=store.artifacts,
|
||||
timeout=args.timeout,
|
||||
rate=args.rate,
|
||||
wordlist=args.wordlist,
|
||||
max_paths=max(1, args.max_paths),
|
||||
max_crawl_pages=max(1, args.max_crawl_pages),
|
||||
max_crawl_depth=max(0, args.max_crawl_depth),
|
||||
max_scripts=max(0, args.max_scripts),
|
||||
max_schema_documents=max(0, args.max_schema_documents),
|
||||
max_exposure_endpoints=max(1, args.max_exposure_endpoints),
|
||||
max_exposure_findings=max(1, args.max_exposure_findings),
|
||||
max_exposure_body_bytes=max(1_024, args.max_exposure_body_bytes),
|
||||
max_cve_products=max(1, args.max_cve_products),
|
||||
max_cves_per_product=max(1, args.max_cves_per_product),
|
||||
graphql_introspection=args.graphql_introspection,
|
||||
allow_public=args.allow_public,
|
||||
reporter=console.emit,
|
||||
)
|
||||
print(f"REDflare {__version__} | run={store.run_id} | profile={args.profile}")
|
||||
print(f"Evidence: {store.root}")
|
||||
results, summary = Runner(store, build_modules(args.profile), context, args.workers).run(targets)
|
||||
if args.github_repo:
|
||||
intel = run_repository_intelligence(args.github_repo, store.artifacts / "repository_intelligence")
|
||||
summary["repository_intelligence"] = intel
|
||||
(store.root / "summary.json").write_text(json.dumps(summary, indent=2), encoding="utf-8")
|
||||
console.final_report(summary, results, store.root)
|
||||
intel_ok = not args.github_repo or summary["repository_intelligence"]["status"] == "completed"
|
||||
if guided and yes_no(f"Open this run in the visual console now ({store.root.name})", True):
|
||||
try:
|
||||
VisualServer(store.root, 8765).serve(open_browser=True)
|
||||
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
||||
print(f"Visualization error: {exc}", file=sys.stderr)
|
||||
return 0 if summary["errors"] == 0 and intel_ok else 1
|
||||
Reference in New Issue
Block a user