From 22741bfdf1b9de081209c743b64a227438987aac Mon Sep 17 00:00:00 2001 From: Daniel Madden Date: Sat, 25 Oct 2025 12:47:26 -0700 Subject: [PATCH] Remove consent manifest requirements from UI --- reconscript/static/js/consent.js | 52 ------ reconscript/templates/consent_modal.html | 7 - reconscript/templates/index.html | 18 +-- reconscript/templates/layout.html | 9 +- reconscript/ui.py | 195 ++++------------------- 5 files changed, 34 insertions(+), 247 deletions(-) delete mode 100644 reconscript/static/js/consent.js delete mode 100644 reconscript/templates/consent_modal.html diff --git a/reconscript/static/js/consent.js b/reconscript/static/js/consent.js deleted file mode 100644 index 773e3cc..0000000 --- a/reconscript/static/js/consent.js +++ /dev/null @@ -1,52 +0,0 @@ -document.addEventListener('DOMContentLoaded', () => { - const targetInput = document.getElementById('target'); - const evidenceSelect = document.getElementById('evidence_level'); - const consentFile = document.getElementById('consent_file'); - const consentCheckbox = document.getElementById('consent_confirm'); - const submitBtn = document.getElementById('submit-btn'); - const modal = document.getElementById('consent-modal'); - const modalClose = document.getElementById('consent-close'); - let modalAcknowledged = false; - - const isLocal = (value) => { - const normalized = (value || '').trim().toLowerCase(); - return normalized === '' || normalized === '127.0.0.1' || normalized === 'localhost' || normalized === '::1'; - }; - - const requiresConsent = () => { - if (!targetInput) return false; - if (isLocal(targetInput.value)) return false; - return true; - }; - - const updateState = () => { - if (!submitBtn) return; - if (!requiresConsent()) { - submitBtn.disabled = false; - if (modal) modal.style.display = 'none'; - return; - } - const hasFile = consentFile && consentFile.files && consentFile.files.length > 0; - const confirmed = consentCheckbox && consentCheckbox.checked; - submitBtn.disabled = !(hasFile && confirmed); - if (!modalAcknowledged && modal) { - modal.style.display = 'flex'; - } - }; - - if (modalClose) { - modalClose.addEventListener('click', () => { - modalAcknowledged = true; - if (modal) modal.style.display = 'none'; - updateState(); - }); - } - - [targetInput, evidenceSelect, consentFile, consentCheckbox].forEach((element) => { - if (!element) return; - const events = element === consentFile ? ['change'] : ['input', 'change']; - events.forEach((event) => element.addEventListener(event, updateState)); - }); - - updateState(); -}); diff --git a/reconscript/templates/consent_modal.html b/reconscript/templates/consent_modal.html deleted file mode 100644 index 4d4e68c..0000000 --- a/reconscript/templates/consent_modal.html +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/reconscript/templates/index.html b/reconscript/templates/index.html index b4ca634..b6c80de 100644 --- a/reconscript/templates/index.html +++ b/reconscript/templates/index.html @@ -2,8 +2,8 @@ {% block content %}

Launch Scoped Scan

-

All scans are read-only and respect signed consent manifests. Targets outside localhost require a validated manifest before scanning.

-
+

All scans are read-only. Provide a target host and optional parameters, then launch the scan.

+ @@ -20,20 +20,10 @@

Launch Scoped Scan

- - -
- - -
- - +
-
{% endblock %} diff --git a/reconscript/templates/layout.html b/reconscript/templates/layout.html index a5adbba..323582f 100644 --- a/reconscript/templates/layout.html +++ b/reconscript/templates/layout.html @@ -38,7 +38,6 @@ outline-offset: 2px; } - @@ -47,12 +46,7 @@ ReconScript
@@ -70,6 +64,5 @@ {% endwith %} {% block content %}{% endblock %}
- {% include 'consent_modal.html' %} diff --git a/reconscript/ui.py b/reconscript/ui.py index 10bc9e8..9ffda7a 100644 --- a/reconscript/ui.py +++ b/reconscript/ui.py @@ -1,20 +1,16 @@ -"""Flask UI for ReconScript with consent enforcement and RBAC.""" +"""Flask UI for ReconScript without authentication or RBAC.""" from __future__ import annotations import json import logging import os -import uuid -from functools import wraps from pathlib import Path -from typing import Optional from flask import ( Flask, abort, flash, - current_app, redirect, render_template, Response, @@ -22,16 +18,7 @@ send_from_directory, url_for, ) -from flask_login import ( - LoginManager, - UserMixin, - current_user, - login_required, - login_user, - logout_user, -) -from .consent import ConsentError, load_manifest, validate_manifest from .core import ReconError, run_recon from .logging import configure_logging from .metrics import metrics_payload @@ -42,13 +29,6 @@ DEV_KEYS_DIR = Path(__file__).resolve().parents[1] / "keys" -class StaticUser(UserMixin): - def __init__(self, username: str, role: str) -> None: - self.id = username - self.role = role - - -<<<<<<< HEAD def _allow_dev_secrets() -> bool: return os.environ.get("ALLOW_DEV_SECRETS", "false").lower() == "true" @@ -56,22 +36,14 @@ def _allow_dev_secrets() -> bool: def _enforce_secret_path(path: Path, *, env_var: str) -> Path: resolved = path.expanduser().resolve() if not resolved.exists(): - raise RuntimeError(f"{env_var} must point to an existing file (got {resolved}).") -======= -def _load_secret_key() -> bytes: - secret_path = Path( - os.environ.get("FLASK_SECRET_KEY_FILE", "keys/dev_flask_secret.key") - ) ->>>>>>> 74be2f0 (style: fix reporters.py syntax and apply Black formatting) - try: - if DEV_KEYS_DIR in resolved.parents and not _allow_dev_secrets(): - raise RuntimeError( - f"{env_var} refers to a developer sample key. Provide deployment-specific secrets or set ALLOW_DEV_SECRETS=true for local testing." - ) - except RuntimeError: - raise - except Exception: - pass + raise RuntimeError( + f"{env_var} must point to an existing file (got {resolved})." + ) + if DEV_KEYS_DIR in resolved.parents and not _allow_dev_secrets(): + raise RuntimeError( + f"{env_var} refers to a developer sample key. Provide deployment-specific " + "secrets or set ALLOW_DEV_SECRETS=true for local testing." + ) return resolved @@ -85,92 +57,25 @@ def _load_secret_key() -> bytes: try: secret = secret_path.read_bytes().strip() except OSError as exc: -<<<<<<< HEAD - raise RuntimeError(f"Unable to read Flask secret key from {secret_path}: {exc}") from exc - if not secret: - raise RuntimeError(f"Secret key file {secret_path} is empty.") - return secret -======= raise RuntimeError( f"Unable to read Flask secret key from {secret_path}: {exc}" ) from exc ->>>>>>> 74be2f0 (style: fix reporters.py syntax and apply Black formatting) - - -def _rbac_required(role: str): - def decorator(func): - @wraps(func) - def wrapper(*args, **kwargs): - if not current_user.is_authenticated: - return redirect(url_for("login")) - if ( - current_app.config.get("RBAC_ENABLED") - and getattr(current_user, "role", None) != role - ): - abort(403) - return func(*args, **kwargs) - - return wrapper - - return decorator - - -def _load_user_credentials() -> tuple[str, str]: - username = os.environ.get("ADMIN_USER", "").strip() - password = os.environ.get("ADMIN_PASSWORD", "").strip() - if not username or not password: - raise RuntimeError("ADMIN_USER and ADMIN_PASSWORD must be set for the ReconScript UI.") - if not _allow_dev_secrets() and (username == "admin" or password == "changeme"): - raise RuntimeError("Default credentials are not permitted. Set strong ADMIN_USER and ADMIN_PASSWORD values.") - if len(password) < 12 and not _allow_dev_secrets(): - raise RuntimeError("ADMIN_PASSWORD must be at least 12 characters long.") - return username, password - - -def _store_upload(file_storage) -> Path: - upload_dir = ensure_results_dir() / "uploads" - upload_dir.mkdir(parents=True, exist_ok=True) - filename = f"manifest-{uuid.uuid4().hex}.json" - path = upload_dir / filename - file_storage.save(path) - return path + if not secret: + raise RuntimeError(f"Secret key file {secret_path} is empty.") + return secret def create_app() -> Flask: configure_logging() - public_ui = os.environ.get("ENABLE_PUBLIC_UI", "false").lower() == "true" - rbac_enabled = os.environ.get("ENABLE_RBAC", "false").lower() == "true" - if public_ui and not rbac_enabled: - raise RuntimeError( - "ENABLE_PUBLIC_UI=true requires ENABLE_RBAC=true to protect access." - ) - app = Flask(__name__) - app.config["RBAC_ENABLED"] = rbac_enabled - app.config["PUBLIC_UI"] = public_ui - app.config["UPLOAD_FOLDER"] = str(ensure_results_dir() / "uploads") + app.config["PUBLIC_UI"] = os.environ.get("ENABLE_PUBLIC_UI", "false").lower() == "true" app.secret_key = _load_secret_key() - login_manager = LoginManager(app) - login_manager.login_view = "login" - - username, password = _load_user_credentials() - user = StaticUser(username, "admin") - - @login_manager.user_loader - def load_user( - user_id: str, - ) -> Optional[StaticUser]: # pragma: no cover - simple lookup - if user_id == user.id: - return user - return None - @app.context_processor def inject_globals(): return { "public_ui": app.config["PUBLIC_UI"], - "rbac_enabled": app.config["RBAC_ENABLED"], } @app.route("/healthz") @@ -184,38 +89,7 @@ def metrics() -> Response: return Response("", status=204) return Response(payload, mimetype=content_type) - @app.route("/login", methods=["GET", "POST"]) - def login(): - if request.method == "POST": - submitted_user = request.form.get("username", "") - submitted_pass = request.form.get("password", "") - LOGGER.info( - "ui.login.attempt", - extra={"event": "ui.login.attempt", "username": submitted_user, "success": submitted_user == username and submitted_pass == password}, - ) - if submitted_user == username and submitted_pass == password: - login_user(user) - return redirect(url_for("index")) - flash("Invalid credentials", "error") - return render_template("login.html") - - @app.route("/logout") - def logout(): - logout_user() - return redirect(url_for("login")) - - def _handle_manifest() -> tuple[Optional[Path], Optional[object]]: - file = request.files.get("consent_file") - if not file or not file.filename: - return None, None - temp_path = _store_upload(file) - manifest = load_manifest(temp_path) - validate_manifest(manifest) - return temp_path, manifest - @app.route("/", methods=["GET", "POST"]) - @login_required - @_rbac_required("admin") def index(): if request.method == "POST": target = request.form.get("target", "") @@ -234,17 +108,6 @@ def index(): flash(str(exc), "error") return render_template("index.html") - consent_path: Optional[Path] = None - consent_manifest = None - try: - consent_path, consent_manifest = _handle_manifest() - except ConsentError as exc: - flash(f"Consent manifest invalid: {exc}", "error") - if consent_path: - consent_path.unlink(missing_ok=True) - LOGGER.warning("ui.consent.invalid", extra={"event": "ui.consent.invalid", "target": target, "error": str(exc)}) - return render_template("index.html") - try: LOGGER.info( "ui.scan.request", @@ -263,18 +126,20 @@ def index(): ports=ports, expected_ip=expected_ip, evidence_level=evidence_level, - consent_manifest=consent_manifest, ) except ReconError as exc: flash(str(exc), "error") - if consent_path: - consent_path.unlink(missing_ok=True) - LOGGER.error("ui.scan.failed", extra={"event": "ui.scan.failed", "target": target, "error": str(exc)}) + LOGGER.error( + "ui.scan.failed", + extra={ + "event": "ui.scan.failed", + "target": target, + "error": str(exc), + }, + ) return render_template("index.html") - persisted = persist_report(report, consent_source=consent_path, sign=False) - if consent_path: - consent_path.unlink(missing_ok=True) + persisted = persist_report(report, sign=False) LOGGER.info( "ui.scan.completed", extra={ @@ -288,8 +153,6 @@ def index(): return render_template("index.html") @app.route("/reports/") - @login_required - @_rbac_required("admin") def report_detail(report_id: str): report_dir = ensure_results_dir() / report_id report_file = report_dir / "report.json" @@ -301,10 +164,10 @@ def report_detail(report_id: str): ) @app.route("/results/") - @login_required - @_rbac_required("admin") def download_result(filename: str): - return send_from_directory(ensure_results_dir(), filename, as_attachment=True) + return send_from_directory( + ensure_results_dir(), filename, as_attachment=True + ) return app @@ -313,14 +176,14 @@ def main() -> None: app = create_app() host = "0.0.0.0" if app.config["PUBLIC_UI"] else "127.0.0.1" port = int(os.environ.get("DEFAULT_PORT", "5000")) - LOGGER.warning( - "UI running with RBAC %s", - "enabled" if app.config["RBAC_ENABLED"] else "disabled", - ) if app.config["PUBLIC_UI"]: LOGGER.warning( "Public UI mode enabled — ensure reverse proxy protections are in place." ) + else: + LOGGER.warning( + "Running ReconScript UI without authentication. Restrict access appropriately." + ) app.run(host=host, port=port, threaded=True)