From ddc03e7a55c8f4a66330c2a5a1c30b988f7bef5f Mon Sep 17 00:00:00 2001 From: DigiJ Date: Sun, 8 Mar 2026 18:09:49 -0700 Subject: [PATCH] Add Port Scanner, fix Hack Hijack SSE, fix debug console, fix tab layout bugs - Add advanced Port Scanner with live SSE output, nmap integration, result export - Add Port Scanner to sidebar nav and register blueprint - Fix Hack Hijack scan: replace polling with SSE streaming, add live output box and real-time port discovery table; add port_found_cb/status_cb to module - Fix debug console: capture print()/stdout/stderr via _PrintCapture wrapper, install handler at startup (not just on toggle), fix SSE stream history replay - Add missing CSS: .card, .tabs, .btn-sm, .form-control, --primary, --surface - Fix tab switching bug: style.display='' falls back to CSS display:none; use explicit 'block' in hack_hijack, c2_framework, net_mapper, password_toolkit, report_engine, social_eng, webapp_scanner - Fix defense/linux layout: rewrite with card-based layout, remove slow load_modules() call on every page request - Fix sms_forge missing run() function warning on startup - Fix port scanner JS: was used instead of closing tag - Port scanner advanced options: remove collapsible toggle, show as always-visible bar Co-Authored-By: Claude Sonnet 4.6 --- modules/hack_hijack.py | 29 +- modules/sms_forge.py | 6 + web/app.py | 2 + web/routes/defense.py | 6 +- web/routes/hack_hijack.py | 70 +++- web/routes/port_scanner.py | 473 ++++++++++++++++++++++++++++ web/routes/settings.py | 95 ++++-- web/static/css/style.css | 31 +- web/static/js/app.js | 6 +- web/templates/base.html | 1 + web/templates/c2_framework.html | 2 +- web/templates/defense_linux.html | 127 +++----- web/templates/hack_hijack.html | 261 +++++++++++---- web/templates/net_mapper.html | 2 +- web/templates/password_toolkit.html | 2 +- web/templates/port_scanner.html | 402 +++++++++++++++++++++++ web/templates/report_engine.html | 2 +- web/templates/social_eng.html | 2 +- web/templates/webapp_scanner.html | 2 +- 19 files changed, 1325 insertions(+), 196 deletions(-) create mode 100644 web/routes/port_scanner.py create mode 100644 web/templates/port_scanner.html diff --git a/modules/hack_hijack.py b/modules/hack_hijack.py index 9e9b097..e22b6d6 100644 --- a/modules/hack_hijack.py +++ b/modules/hack_hijack.py @@ -370,7 +370,9 @@ class HackHijackService: def scan_target(self, target: str, scan_type: str = 'quick', custom_ports: List[int] = None, timeout: float = 3.0, - progress_cb=None) -> ScanResult: + progress_cb=None, + port_found_cb=None, + status_cb=None) -> ScanResult: """Scan a target for open ports and backdoor indicators. scan_type: 'quick' (signature ports only), 'full' (signature + extra), @@ -396,11 +398,16 @@ class HackHijackService: # Try nmap first if requested and available if scan_type == 'nmap': + if status_cb: + status_cb('Running nmap scan…') nmap_result = self._nmap_scan(target, ports, timeout) if nmap_result: result.open_ports = nmap_result.get('ports', []) result.os_guess = nmap_result.get('os', '') result.nmap_raw = nmap_result.get('raw', '') + if port_found_cb: + for pr in result.open_ports: + port_found_cb(pr) # Fallback: socket-based scan if not result.open_ports: @@ -408,28 +415,40 @@ class HackHijackService: total = len(sorted_ports) results_lock = threading.Lock() open_ports = [] + scanned = [0] - def scan_port(port): + if status_cb: + status_cb(f'Socket scanning {total} ports on {target}…') + + def scan_port(port, idx): pr = self._check_port(target, port, timeout) + with results_lock: + scanned[0] += 1 + done = scanned[0] if pr and pr.state == 'open': with results_lock: open_ports.append(pr) + if port_found_cb: + port_found_cb(pr) + if progress_cb and done % 10 == 0: + progress_cb(done, total, f'Scanning port {port}…') # Threaded scan — 50 concurrent threads threads = [] for i, port in enumerate(sorted_ports): - t = threading.Thread(target=scan_port, args=(port,), daemon=True) + t = threading.Thread(target=scan_port, args=(port, i), daemon=True) threads.append(t) t.start() if len(threads) >= 50: for t in threads: t.join(timeout=timeout + 2) threads.clear() - if progress_cb and i % 10 == 0: - progress_cb(i, total) for t in threads: t.join(timeout=timeout + 2) + if progress_cb: + progress_cb(total, total, 'Scan complete') + result.open_ports = sorted(open_ports, key=lambda p: p.port) # Match open ports against backdoor signatures diff --git a/modules/sms_forge.py b/modules/sms_forge.py index f3c7e03..d3b49a5 100644 --- a/modules/sms_forge.py +++ b/modules/sms_forge.py @@ -11,6 +11,12 @@ AUTHOR = "AUTARCH" VERSION = "1.0" CATEGORY = "offense" + +def run(): + """CLI entry point — this module is used via the web UI.""" + print("SMS Forge is managed through the AUTARCH web interface.") + print("Navigate to Offense → SMS Forge in the dashboard.") + import os import csv import json diff --git a/web/app.py b/web/app.py index 2b143ff..fd47e5f 100644 --- a/web/app.py +++ b/web/app.py @@ -102,6 +102,7 @@ def create_app(): from web.routes.sms_forge import sms_forge_bp from web.routes.starlink_hack import starlink_hack_bp from web.routes.rcs_tools import rcs_tools_bp + from web.routes.port_scanner import port_scanner_bp app.register_blueprint(auth_bp) app.register_blueprint(dashboard_bp) @@ -163,6 +164,7 @@ def create_app(): app.register_blueprint(sms_forge_bp) app.register_blueprint(starlink_hack_bp) app.register_blueprint(rcs_tools_bp) + app.register_blueprint(port_scanner_bp) # Start network discovery advertising (mDNS + Bluetooth) try: diff --git a/web/routes/defense.py b/web/routes/defense.py index e6111a8..c9d926c 100644 --- a/web/routes/defense.py +++ b/web/routes/defense.py @@ -48,11 +48,7 @@ def index(): @defense_bp.route('/linux') @login_required def linux_index(): - from core.menu import MainMenu - menu = MainMenu() - menu.load_modules() - modules = {k: v for k, v in menu.modules.items() if v.category == 'defense'} - return render_template('defense_linux.html', modules=modules) + return render_template('defense_linux.html') @defense_bp.route('/linux/audit', methods=['POST']) diff --git a/web/routes/hack_hijack.py b/web/routes/hack_hijack.py index 1f73663..7c085fc 100644 --- a/web/routes/hack_hijack.py +++ b/web/routes/hack_hijack.py @@ -1,13 +1,16 @@ """Hack Hijack — web routes for scanning and taking over compromised systems.""" +import json +import queue import threading +import time import uuid from flask import Blueprint, render_template, request, jsonify, Response from web.auth import login_required hack_hijack_bp = Blueprint('hack_hijack', __name__) -# Running scans keyed by job_id +# job_id -> {'q': Queue, 'result': dict|None, 'error': str|None, 'done': bool, 'cancel': bool} _running_scans: dict = {} @@ -37,34 +40,86 @@ def start_scan(): if not target: return jsonify({'ok': False, 'error': 'Target IP required'}) - # Validate scan type if scan_type not in ('quick', 'full', 'nmap', 'custom'): scan_type = 'quick' job_id = str(uuid.uuid4())[:8] - result_holder = {'result': None, 'error': None, 'done': False} - _running_scans[job_id] = result_holder + q = queue.Queue() + job = {'q': q, 'result': None, 'error': None, 'done': False, 'cancel': False} + _running_scans[job_id] = job + + def _push(evt_type, **kw): + kw['type'] = evt_type + kw['ts'] = time.time() + q.put(kw) def do_scan(): try: svc = _svc() + # Build a progress callback that feeds the queue + def progress_cb(current, total, message=''): + _push('progress', current=current, total=total, + pct=round(current * 100 / total) if total else 0, + msg=message) + + def port_found_cb(port_info): + _push('port_found', + port=port_info.get('port') or (port_info.port if hasattr(port_info, 'port') else 0), + service=getattr(port_info, 'service', port_info.get('service', '')), + banner=getattr(port_info, 'banner', port_info.get('banner', ''))[:80]) + + def status_cb(msg): + _push('status', msg=msg) + r = svc.scan_target( target, scan_type=scan_type, custom_ports=custom_ports, timeout=3.0, + progress_cb=progress_cb, + port_found_cb=port_found_cb, + status_cb=status_cb, ) - result_holder['result'] = r.to_dict() + job['result'] = r.to_dict() except Exception as e: - result_holder['error'] = str(e) + job['error'] = str(e) + _push('error', msg=str(e)) finally: - result_holder['done'] = True + job['done'] = True + _push('done', ok=job['error'] is None) threading.Thread(target=do_scan, daemon=True).start() return jsonify({'ok': True, 'job_id': job_id, 'message': f'Scan started on {target} ({scan_type})'}) +@hack_hijack_bp.route('/hack-hijack/scan//stream') +@login_required +def scan_stream(job_id): + """SSE stream for live scan progress.""" + job = _running_scans.get(job_id) + if not job: + def _err(): + yield f"data: {json.dumps({'type': 'error', 'msg': 'Job not found'})}\n\n" + return Response(_err(), mimetype='text/event-stream') + + def generate(): + q = job['q'] + while True: + try: + item = q.get(timeout=0.5) + yield f"data: {json.dumps(item)}\n\n" + if item.get('type') == 'done': + break + except queue.Empty: + if job['done']: + break + yield ': keepalive\n\n' + + return Response(generate(), mimetype='text/event-stream', + headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'}) + + @hack_hijack_bp.route('/hack-hijack/scan/', methods=['GET']) @login_required def scan_status(job_id): @@ -75,7 +130,6 @@ def scan_status(job_id): return jsonify({'ok': True, 'done': False, 'message': 'Scan in progress...'}) if holder['error']: return jsonify({'ok': False, 'error': holder['error'], 'done': True}) - # Clean up _running_scans.pop(job_id, None) return jsonify({'ok': True, 'done': True, 'result': holder['result']}) diff --git a/web/routes/port_scanner.py b/web/routes/port_scanner.py new file mode 100644 index 0000000..95075cf --- /dev/null +++ b/web/routes/port_scanner.py @@ -0,0 +1,473 @@ +"""Advanced Port Scanner — streaming SSE-based port scanner with nmap integration.""" + +import json +import queue +import socket +import subprocess +import threading +import time +import uuid +from datetime import datetime, timezone +from typing import Optional + +from flask import Blueprint, render_template, request, jsonify, Response +from web.auth import login_required + +port_scanner_bp = Blueprint('port_scanner', __name__, url_prefix='/port-scanner') + +# job_id -> {'q': Queue, 'result': dict|None, 'done': bool, 'cancel': bool} +_jobs: dict = {} + +# ── Common port lists ────────────────────────────────────────────────────────── + +QUICK_PORTS = [ + 21, 22, 23, 25, 53, 80, 110, 111, 135, 139, 143, 443, 445, 993, 995, + 1723, 3306, 3389, 5900, 8080, 8443, 8888, +] + +COMMON_PORTS = [ + 20, 21, 22, 23, 25, 53, 67, 68, 69, 80, 88, 110, 111, 119, 123, 135, + 137, 138, 139, 143, 161, 162, 179, 194, 389, 443, 445, 465, 500, 514, + 515, 587, 593, 631, 636, 873, 902, 993, 995, 1080, 1194, 1433, 1521, + 1723, 1883, 2049, 2121, 2181, 2222, 2375, 2376, 2483, 2484, 3000, 3306, + 3389, 3690, 4000, 4040, 4333, 4444, 4567, 4899, 5000, 5432, 5601, 5672, + 5900, 5984, 6000, 6379, 6443, 6881, 7000, 7001, 7080, 7443, 7474, 8000, + 8001, 8008, 8080, 8081, 8082, 8083, 8088, 8089, 8161, 8333, 8443, 8444, + 8500, 8888, 8983, 9000, 9001, 9042, 9090, 9092, 9200, 9300, 9418, 9999, + 10000, 11211, 15432, 15672, 27017, 27018, 27019, 28017, 50000, 54321, +] + +SERVICE_MAP = { + 20: 'FTP-data', 21: 'FTP', 22: 'SSH', 23: 'Telnet', 25: 'SMTP', + 53: 'DNS', 67: 'DHCP', 68: 'DHCP', 69: 'TFTP', 80: 'HTTP', + 88: 'Kerberos', 110: 'POP3', 111: 'RPC', 119: 'NNTP', 123: 'NTP', + 135: 'MS-RPC', 137: 'NetBIOS-NS', 138: 'NetBIOS-DGM', 139: 'NetBIOS-SSN', + 143: 'IMAP', 161: 'SNMP', 162: 'SNMP-Trap', 179: 'BGP', 194: 'IRC', + 389: 'LDAP', 443: 'HTTPS', 445: 'SMB', 465: 'SMTPS', 500: 'IKE/ISAKMP', + 514: 'Syslog/RSH', 515: 'LPD', 587: 'SMTP-Submission', 631: 'IPP', + 636: 'LDAPS', 873: 'rsync', 993: 'IMAPS', 995: 'POP3S', + 1080: 'SOCKS', 1194: 'OpenVPN', 1433: 'MSSQL', 1521: 'Oracle', + 1723: 'PPTP', 1883: 'MQTT', 2049: 'NFS', 2181: 'Zookeeper', + 2222: 'SSH-alt', 2375: 'Docker', 2376: 'Docker-TLS', 3000: 'Grafana', + 3306: 'MySQL', 3389: 'RDP', 3690: 'SVN', 4444: 'Meterpreter', + 5000: 'Flask/Dev', 5432: 'PostgreSQL', 5601: 'Kibana', 5672: 'AMQP/RabbitMQ', + 5900: 'VNC', 5984: 'CouchDB', 6379: 'Redis', 6443: 'Kubernetes-API', + 7474: 'Neo4j', 8080: 'HTTP-Alt', 8443: 'HTTPS-Alt', 8500: 'Consul', + 8888: 'Jupyter/HTTP-Alt', 9000: 'SonarQube/PHP-FPM', 9001: 'Tor/Supervisor', + 9042: 'Cassandra', 9090: 'Prometheus/Cockpit', 9092: 'Kafka', + 9200: 'Elasticsearch', 9300: 'Elasticsearch-node', 9418: 'Git', + 10000: 'Webmin', 11211: 'Memcached', 15672: 'RabbitMQ-Mgmt', + 27017: 'MongoDB', 27018: 'MongoDB', 27019: 'MongoDB', 50000: 'DB2', +} + +PROBE_MAP = { + 21: b'', + 22: b'', + 23: b'', + 25: b'', + 80: b'HEAD / HTTP/1.0\r\nHost: localhost\r\n\r\n', + 110: b'', + 143: b'', + 443: b'', + 3306: b'', + 5432: b'', + 6379: b'INFO\r\n', + 8080: b'HEAD / HTTP/1.0\r\nHost: localhost\r\n\r\n', + 8443: b'', + 8888: b'HEAD / HTTP/1.0\r\nHost: localhost\r\n\r\n', + 9200: b'GET / HTTP/1.0\r\nHost: localhost\r\n\r\n', + 27017: b'', +} + + +def _push(q: queue.Queue, event_type: str, data: dict) -> None: + data['type'] = event_type + data['ts'] = time.time() + q.put(data) + + +def _grab_banner(sock: socket.socket, port: int, timeout: float = 2.0) -> str: + try: + sock.settimeout(timeout) + probe = PROBE_MAP.get(port, b'') + if probe: + sock.sendall(probe) + raw = sock.recv(2048) + return raw.decode('utf-8', errors='replace').strip()[:512] + except Exception: + return '' + + +def _identify_service(port: int, banner: str) -> str: + bl = banner.lower() + if 'ssh-' in bl: + return 'SSH' + if 'ftp' in bl or '220 ' in bl[:10]: + return 'FTP' + if 'smtp' in bl or ('220 ' in bl and 'mail' in bl): + return 'SMTP' + if 'http/' in bl or ' Optional[dict]: + """TCP connect scan a single port. Returns port info dict or None if closed.""" + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.settimeout(timeout) + err = sock.connect_ex((host, port)) + if err == 0: + banner = _grab_banner(sock, port) + sock.close() + service = _identify_service(port, banner) + return { + 'port': port, + 'protocol': 'tcp', + 'state': 'open', + 'service': service, + 'banner': banner, + } + sock.close() + except Exception: + pass + return None + + +def _run_nmap_scan(host: str, ports: list, options: dict, q: queue.Queue, + job: dict) -> Optional[list]: + """Run nmap and parse output. Returns list of port dicts or None if nmap unavailable.""" + import shutil + nmap = shutil.which('nmap') + if not nmap: + _push(q, 'warning', {'msg': 'nmap not found — falling back to TCP connect scan'}) + return None + + port_str = ','.join(str(p) for p in sorted(ports)) + cmd = [nmap, '-Pn', '--open', '-p', port_str] + + if options.get('service_detection'): + cmd += ['-sV', '--version-intensity', '5'] + if options.get('os_detection'): + cmd += ['-O', '--osscan-guess'] + if options.get('aggressive'): + cmd += ['-A'] + if options.get('timing'): + cmd += [f'-T{options["timing"]}'] + else: + cmd += ['-T4'] + + cmd += ['-oN', '-', '--host-timeout', '120s', host] + + _push(q, 'nmap_start', {'cmd': ' '.join(cmd[:-1] + [''])}) + + open_ports = [] + os_guess = '' + nmap_raw_lines = [] + + try: + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, + text=True, bufsize=1) + for line in proc.stdout: + line = line.rstrip() + if job.get('cancel'): + proc.terminate() + break + nmap_raw_lines.append(line) + _push(q, 'nmap_line', {'line': line}) + + # Parse open port lines: "80/tcp open http Apache httpd 2.4.54" + stripped = line.strip() + if '/tcp' in stripped or '/udp' in stripped: + parts = stripped.split() + if len(parts) >= 2 and parts[1] == 'open': + port_proto = parts[0].split('/') + port_num = int(port_proto[0]) + proto = port_proto[1] if len(port_proto) > 1 else 'tcp' + service = parts[2] if len(parts) > 2 else SERVICE_MAP.get(port_num, 'unknown') + version = ' '.join(parts[3:]) if len(parts) > 3 else '' + open_ports.append({ + 'port': port_num, + 'protocol': proto, + 'state': 'open', + 'service': service, + 'banner': version, + }) + _push(q, 'port_open', { + 'port': port_num, 'protocol': proto, + 'service': service, 'banner': version, + }) + # OS detection line + if 'OS details:' in line or 'Running:' in line: + os_guess = line.split(':', 1)[-1].strip() + + proc.wait(timeout=10) + except Exception as exc: + _push(q, 'warning', {'msg': f'nmap error: {exc} — falling back to TCP connect scan'}) + return None + + return open_ports, os_guess, '\n'.join(nmap_raw_lines) + + +def _socket_scan(host: str, ports: list, timeout: float, concurrency: int, + q: queue.Queue, job: dict) -> list: + """Concurrent TCP connect scan. Returns list of open port dicts.""" + open_ports = [] + lock = threading.Lock() + scanned = [0] + total = len(ports) + start = time.time() + + def worker(port: int): + if job.get('cancel'): + return + result = _scan_port(host, port, timeout) + with lock: + scanned[0] += 1 + done = scanned[0] + if result: + with lock: + open_ports.append(result) + _push(q, 'port_open', { + 'port': result['port'], 'protocol': result['protocol'], + 'service': result['service'], 'banner': result['banner'], + }) + # Progress every 25 ports or on first/last + if done == 1 or done % 25 == 0 or done == total: + elapsed = time.time() - start + rate = done / elapsed if elapsed > 0 else 0 + eta = int((total - done) / rate) if rate > 0 else 0 + _push(q, 'progress', { + 'current': done, 'total': total, + 'pct': round(done * 100 / total), + 'eta': f'{eta}s' if eta < 3600 else f'{eta//60}m', + 'port': port, + 'open_count': len(open_ports), + }) + + sem = threading.Semaphore(concurrency) + threads = [] + + for port in sorted(ports): + if job.get('cancel'): + break + sem.acquire() + def _run(p=port): + try: + worker(p) + finally: + sem.release() + t = threading.Thread(target=_run, daemon=True) + threads.append(t) + t.start() + + for t in threads: + t.join(timeout=timeout + 1) + + return sorted(open_ports, key=lambda x: x['port']) + + +def _do_scan(job_id: str, host: str, ports: list, options: dict) -> None: + """Main scan worker — runs in a background thread.""" + job = _jobs[job_id] + q = job['q'] + start = time.time() + + _push(q, 'start', { + 'target': host, 'total_ports': len(ports), + 'mode': options.get('mode', 'custom'), + }) + + # Resolve hostname + ip = host + try: + ip = socket.gethostbyname(host) + if ip != host: + _push(q, 'info', {'msg': f'Resolved {host} → {ip}'}) + except Exception: + _push(q, 'warning', {'msg': f'Could not resolve {host} — using as-is'}) + + open_ports = [] + os_guess = '' + nmap_raw = '' + + try: + use_nmap = options.get('use_nmap', False) + timeout = float(options.get('timeout', 1.0)) + concurrency = int(options.get('concurrency', 100)) + + if use_nmap: + nmap_result = _run_nmap_scan(ip, ports, options, q, job) + if nmap_result is not None: + open_ports, os_guess, nmap_raw = nmap_result + else: + # fallback + _push(q, 'info', {'msg': 'Running TCP connect scan fallback...'}) + open_ports = _socket_scan(ip, ports, timeout, concurrency, q, job) + else: + _push(q, 'info', {'msg': f'Scanning {len(ports)} ports on {ip} ' + f'(concurrency={concurrency}, timeout={timeout}s)'}) + open_ports = _socket_scan(ip, ports, timeout, concurrency, q, job) + + duration = round(time.time() - start, 2) + + result = { + 'target': host, + 'ip': ip, + 'scan_time': datetime.now(timezone.utc).isoformat(), + 'duration': duration, + 'ports_scanned': len(ports), + 'open_ports': open_ports, + 'os_guess': os_guess, + 'nmap_raw': nmap_raw, + 'options': options, + } + job['result'] = result + + _push(q, 'done', { + 'open_count': len(open_ports), + 'ports_scanned': len(ports), + 'duration': duration, + 'os_guess': os_guess, + }) + + except Exception as exc: + _push(q, 'error', {'msg': str(exc)}) + finally: + job['done'] = True + + +# ── Routes ──────────────────────────────────────────────────────────────────── + +@port_scanner_bp.route('/') +@login_required +def index(): + return render_template('port_scanner.html') + + +@port_scanner_bp.route('/start', methods=['POST']) +@login_required +def start_scan(): + data = request.get_json(silent=True) or {} + host = data.get('target', '').strip() + if not host: + return jsonify({'ok': False, 'error': 'Target required'}) + + mode = data.get('mode', 'common') + + if mode == 'quick': + ports = list(QUICK_PORTS) + elif mode == 'common': + ports = list(COMMON_PORTS) + elif mode == 'full': + ports = list(range(1, 65536)) + elif mode == 'custom': + raw = data.get('ports', '').strip() + ports = _parse_port_spec(raw) + if not ports: + return jsonify({'ok': False, 'error': 'No valid ports in custom range'}) + else: + ports = list(COMMON_PORTS) + + options = { + 'mode': mode, + 'use_nmap': bool(data.get('use_nmap', False)), + 'service_detection': bool(data.get('service_detection', False)), + 'os_detection': bool(data.get('os_detection', False)), + 'aggressive': bool(data.get('aggressive', False)), + 'timing': data.get('timing', '4'), + 'timeout': float(data.get('timeout', 1.0)), + 'concurrency': min(int(data.get('concurrency', 200)), 500), + } + + job_id = str(uuid.uuid4())[:8] + job = {'q': queue.Queue(), 'result': None, 'done': False, 'cancel': False} + _jobs[job_id] = job + + t = threading.Thread(target=_do_scan, args=(job_id, host, ports, options), daemon=True) + t.start() + + return jsonify({'ok': True, 'job_id': job_id, 'port_count': len(ports)}) + + +@port_scanner_bp.route('/stream/') +@login_required +def stream(job_id): + job = _jobs.get(job_id) + if not job: + def err_gen(): + yield f"data: {json.dumps({'type': 'error', 'msg': 'Job not found'})}\n\n" + return Response(err_gen(), mimetype='text/event-stream') + + def generate(): + q = job['q'] + while True: + try: + item = q.get(timeout=0.5) + yield f"data: {json.dumps(item)}\n\n" + if item.get('type') in ('done', 'error'): + break + except queue.Empty: + if job['done']: + break + yield ': keepalive\n\n' + + return Response(generate(), mimetype='text/event-stream', + headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'}) + + +@port_scanner_bp.route('/result/') +@login_required +def get_result(job_id): + job = _jobs.get(job_id) + if not job: + return jsonify({'ok': False, 'error': 'Job not found'}) + if not job['done']: + return jsonify({'ok': True, 'done': False}) + return jsonify({'ok': True, 'done': True, 'result': job['result']}) + + +@port_scanner_bp.route('/cancel/', methods=['POST']) +@login_required +def cancel_scan(job_id): + job = _jobs.get(job_id) + if job: + job['cancel'] = True + return jsonify({'ok': True}) + + +# ── Helpers ─────────────────────────────────────────────────────────────────── + +def _parse_port_spec(spec: str) -> list: + """Parse port specification: '22,80,443', '1-1024', '22,80-100,443'.""" + ports = set() + for part in spec.split(','): + part = part.strip() + if '-' in part: + try: + lo, hi = part.split('-', 1) + lo, hi = int(lo.strip()), int(hi.strip()) + if 1 <= lo <= hi <= 65535: + ports.update(range(lo, hi + 1)) + except ValueError: + pass + else: + try: + p = int(part) + if 1 <= p <= 65535: + ports.add(p) + except ValueError: + pass + return sorted(ports) diff --git a/web/routes/settings.py b/web/routes/settings.py index 7fd6c29..4f26752 100644 --- a/web/routes/settings.py +++ b/web/routes/settings.py @@ -21,42 +21,83 @@ _debug_enabled: bool = False _debug_handler_installed: bool = False +def _buf_append(level: str, name: str, raw: str, msg: str, exc: str = '') -> None: + """Thread-safe append to the debug buffer.""" + entry: dict = {'ts': time.time(), 'level': level, 'name': name, 'raw': raw, 'msg': msg} + if exc: + entry['exc'] = exc + _debug_buffer.append(entry) + + class _DebugBufferHandler(logging.Handler): - """Captures log records into the in-memory debug buffer.""" + """Captures ALL log records into the in-memory debug buffer (always active).""" def emit(self, record: logging.LogRecord) -> None: - if not _debug_enabled: - return try: - entry: dict = { - 'ts': record.created, - 'level': record.levelname, - 'name': record.name, - 'raw': record.getMessage(), - 'msg': self.format(record), - } + exc_text = '' if record.exc_info: import traceback as _tb - entry['exc'] = ''.join(_tb.format_exception(*record.exc_info)) - _debug_buffer.append(entry) + exc_text = ''.join(_tb.format_exception(*record.exc_info)) + _buf_append( + level=record.levelname, + name=record.name, + raw=record.getMessage(), + msg=self.format(record), + exc=exc_text, + ) except Exception: pass +class _PrintCapture: + """Wraps sys.stdout or sys.stderr — passes through AND feeds lines to the debug buffer.""" + + def __init__(self, original, level: str = 'STDOUT'): + self._orig = original + self._level = level + self._line_buf = '' + + def write(self, text: str) -> int: + self._orig.write(text) + self._line_buf += text + while '\n' in self._line_buf: + line, self._line_buf = self._line_buf.split('\n', 1) + if line.strip(): + _buf_append(self._level, 'print', line, line) + return len(text) + + def flush(self) -> None: + self._orig.flush() + + def __getattr__(self, name): + return getattr(self._orig, name) + + def _ensure_debug_handler() -> None: + """Install logging handler + stdout/stderr capture once, at startup.""" global _debug_handler_installed if _debug_handler_installed: return + # Logging handler handler = _DebugBufferHandler() handler.setLevel(logging.DEBUG) handler.setFormatter(logging.Formatter('%(name)s — %(message)s')) root = logging.getLogger() root.addHandler(handler) - # Lower root level to DEBUG so records reach the handler if root.level == logging.NOTSET or root.level > logging.DEBUG: root.setLevel(logging.DEBUG) + # stdout / stderr capture + import sys as _sys + if not isinstance(_sys.stdout, _PrintCapture): + _sys.stdout = _PrintCapture(_sys.stdout, 'STDOUT') + if not isinstance(_sys.stderr, _PrintCapture): + _sys.stderr = _PrintCapture(_sys.stderr, 'STDERR') _debug_handler_installed = True + +# Install immediately so we capture from process start, not just after toggle +_ensure_debug_handler() + settings_bp = Blueprint('settings', __name__, url_prefix='/settings') @@ -429,28 +470,42 @@ def discovery_stop(): @settings_bp.route('/debug/toggle', methods=['POST']) @login_required def debug_toggle(): - """Enable or disable the debug log capture.""" + """Enable or disable the debug console UI (capture always runs).""" global _debug_enabled data = request.get_json(silent=True) or {} _debug_enabled = bool(data.get('enabled', False)) if _debug_enabled: - _ensure_debug_handler() - logging.getLogger('autarch.debug').info('Debug console enabled') + logging.getLogger('autarch.debug').info('Debug console opened') return jsonify({'ok': True, 'enabled': _debug_enabled}) @settings_bp.route('/debug/stream') @login_required def debug_stream(): - """SSE stream — pushes new log records to the browser as they arrive.""" + """SSE stream — pushes log records to the browser as they arrive. + + On connect: sends the last 200 buffered entries as history, then streams + new entries live. Handles deque wrap-around correctly. + """ def generate(): - sent = 0 + buf = list(_debug_buffer) + # Send last 200 entries as catch-up history + history_start = max(0, len(buf) - 200) + for entry in buf[history_start:]: + yield f"data: {json.dumps(entry)}\n\n" + sent = len(buf) + while True: + time.sleep(0.2) buf = list(_debug_buffer) - while sent < len(buf): + n = len(buf) + if sent > n: + # deque wrapped; re-orient to current tail + sent = n + while sent < n: yield f"data: {json.dumps(buf[sent])}\n\n" sent += 1 - time.sleep(0.25) + yield ': keepalive\n\n' return Response(generate(), mimetype='text/event-stream', headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'}) diff --git a/web/static/css/style.css b/web/static/css/style.css index 5f4572d..6c403f2 100644 --- a/web/static/css/style.css +++ b/web/static/css/style.css @@ -9,6 +9,8 @@ --text-muted: #5c6078; --accent: #6366f1; --accent-hover: #818cf8; + --primary: #6366f1; + --surface: #222536; --success: #22c55e; --warning: #f59e0b; --danger: #ef4444; @@ -101,6 +103,19 @@ pre { background: var(--bg-primary); border: 1px solid var(--border); border-rad .flash-info { background: rgba(99,102,241,0.15); color: var(--accent); border: 1px solid rgba(99,102,241,0.3); } /* Forms */ +/* Standalone form control (used outside .form-group) */ +.form-control { + display: block; width: 100%; padding: 8px 12px; + background: var(--bg-input); border: 1px solid var(--border); + border-radius: var(--radius); color: var(--text-primary); + font-size: 0.9rem; font-family: inherit; +} +.form-control:focus { outline: none; border-color: var(--accent); } +.form-control[type="text"], .form-control[type="number"], +.form-control[type="password"], .form-control[type="email"] { height: 38px; } +select.form-control { height: 38px; cursor: pointer; } +textarea.form-control { font-family: monospace; resize: vertical; } + .form-group { margin-bottom: 16px; text-align: left; } .form-group label { display: block; margin-bottom: 6px; font-size: 0.85rem; color: var(--text-secondary); } .form-group input, .form-group select, .form-group textarea { @@ -133,8 +148,10 @@ pre { background: var(--bg-primary); border: 1px solid var(--border); border-rad .btn-warning:hover { background: rgba(245,158,11,0.3); } .btn-danger { background: rgba(239,68,68,0.2); border-color: var(--danger); color: var(--danger); } .btn-danger:hover { background: rgba(239,68,68,0.3); } -.btn-small { padding: 6px 12px; font-size: 0.8rem; } +.btn-small, .btn-sm { padding: 6px 12px; font-size: 0.8rem; } .btn-full { width: 100%; } +.btn-outline { background: transparent; border-color: var(--accent); color: var(--accent); } +.btn-outline:hover { background: rgba(99,102,241,0.15); } .btn-group { display: flex; gap: 8px; flex-wrap: wrap; } /* Page header */ @@ -176,6 +193,18 @@ pre { background: var(--bg-primary); border: 1px solid var(--border); border-rad .section h2 { font-size: 1rem; margin-bottom: 16px; } .section h3 { font-size: 0.9rem; margin-bottom: 12px; color: var(--text-secondary); } +/* Generic card — used throughout module pages */ +.card { + background: var(--bg-card); border: 1px solid var(--border); + border-radius: var(--radius); padding: 20px; margin-bottom: 16px; +} +.card h3 { font-size: 0.95rem; margin-bottom: 12px; } +.card h4 { font-size: 0.85rem; margin-bottom: 8px; color: var(--text-secondary); } + +/* Tab container — alias for .tab-bar */ +.tabs { display: flex; gap: 0; border-bottom: 1px solid var(--border); margin-bottom: 20px; } +.tabs .tab { padding: 10px 18px; } + /* Tables */ .data-table { width: 100%; border-collapse: collapse; } .data-table th { diff --git a/web/static/js/app.js b/web/static/js/app.js index 25aec18..3199524 100644 --- a/web/static/js/app.js +++ b/web/static/js/app.js @@ -2256,10 +2256,12 @@ var _DBG_LEVELS = { WARNING: { cls: 'dbg-warn', sym: '⚠' }, ERROR: { cls: 'dbg-err', sym: '✕' }, CRITICAL: { cls: 'dbg-crit', sym: '☠' }, + STDOUT: { cls: 'dbg-info', sym: '»' }, + STDERR: { cls: 'dbg-warn', sym: '»' }, }; // Output-tagged logger names (treated as operational output in "Output Only" mode) -var _OUTPUT_LOGGERS = ['msf', 'agent', 'autarch', 'output', 'scanner', 'tools']; +var _OUTPUT_LOGGERS = ['msf', 'agent', 'autarch', 'output', 'scanner', 'tools', 'print']; function debugToggle(enabled) { fetch('/settings/debug/toggle', { @@ -2325,6 +2327,8 @@ function _dbgShouldShow(entry) { case 'verbose': return lvl !== 'DEBUG' && lvl !== 'NOTSET'; case 'warn': return lvl === 'WARNING' || lvl === 'ERROR' || lvl === 'CRITICAL'; case 'output': + var lvlO = (entry.level || '').toUpperCase(); + if (lvlO === 'STDOUT' || lvlO === 'STDERR') return true; var name = (entry.name || '').toLowerCase(); return _OUTPUT_LOGGERS.some(function(pfx) { return name.indexOf(pfx) >= 0; }); } diff --git a/web/templates/base.html b/web/templates/base.html index f4f8146..ac6cc65 100644 --- a/web/templates/base.html +++ b/web/templates/base.html @@ -29,6 +29,7 @@ diff --git a/web/templates/c2_framework.html b/web/templates/c2_framework.html index 1bac5f7..d513f75 100644 --- a/web/templates/c2_framework.html +++ b/web/templates/c2_framework.html @@ -97,7 +97,7 @@ let refreshTimer=null; function switchTab(name){ document.querySelectorAll('.tab').forEach((t,i)=>t.classList.toggle('active',['dashboard','agents','generate'][i]===name)); document.querySelectorAll('.tab-content').forEach(c=>c.style.display='none'); - document.getElementById('tab-'+name).style.display=''; + document.getElementById('tab-'+name).style.display='block'; if(name==='dashboard'||name==='agents') refreshDashboard(); } diff --git a/web/templates/defense_linux.html b/web/templates/defense_linux.html index 14a38d7..b2375fd 100644 --- a/web/templates/defense_linux.html +++ b/web/templates/defense_linux.html @@ -1,29 +1,29 @@ {% extends "base.html" %} -{% block title %}Linux Defense - AUTARCH{% endblock %} +{% block title %}Linux Defense — AUTARCH{% endblock %} {% block content %} -
-

Security Audit

-
- +
+
+

Security Audit

+
-
-
--
-
Security Score
+
+
--
+
Security Score
-
+
@@ -34,94 +34,53 @@ - -
-

Quick Checks

-
-
-

Firewall

-

Check iptables/ufw/firewalld status

- -

-        
-
-

SSH Config

-

Check SSH hardening settings

- -

-        
-
-

Open Ports

-

Scan for high-risk listening ports

- -

-        
-
-

Users

-

Check UID 0 users and empty passwords

- -

-        
-
-

Permissions

-

Check critical file permissions

- -

-        
-
-

Services

-

Check for dangerous services

- -

+
+
+

Quick Checks

+
+ {% for check_id, check_name, check_desc in [ + ('firewall', 'Firewall', 'Check iptables/ufw/firewalld status'), + ('ssh', 'SSH Config', 'Check SSH hardening settings'), + ('ports', 'Open Ports', 'Scan for high-risk listening ports'), + ('users', 'Users', 'Check UID 0 users and empty passwords'), + ('permissions', 'Permissions', 'Check critical file permissions'), + ('services', 'Services', 'Check for dangerous services') + ] %} +
+
{{ check_name }}
+
{{ check_desc }}
+ +
+ {% endfor %}
-
-

Firewall Manager (iptables)

-
- +
+
+

Firewall Manager (iptables)

+
-
Click "Refresh Rules" to load current iptables rules.
-
-
- - - -
-

+    
Click "Refresh Rules" to load current iptables rules.
+
+ + +
+

 
-
-

Log Analysis

-
- +
+
+

Log Analysis

+
-
Click "Analyze Logs" to parse auth and web server logs.
+
Click "Analyze Logs" to parse auth and web server logs.
-{% if modules %} -
-

Defense Modules

-
    - {% for name, info in modules.items() %} -
  • -
    -
    {{ name }}
    -
    {{ info.description }}
    -
    -
    v{{ info.version }}
    -
  • - {% endfor %} -
-
-{% endif %} - + + + +{% endblock %} diff --git a/web/templates/report_engine.html b/web/templates/report_engine.html index d95115a..0a7469b 100644 --- a/web/templates/report_engine.html +++ b/web/templates/report_engine.html @@ -104,7 +104,7 @@ let currentReportId=null; function switchTab(name){ document.querySelectorAll('.tab').forEach((t,i)=>t.classList.toggle('active',['reports','editor','templates'][i]===name)); document.querySelectorAll('.tab-content').forEach(c=>c.style.display='none'); - document.getElementById('tab-'+name).style.display=''; + document.getElementById('tab-'+name).style.display='block'; if(name==='reports') loadReports(); if(name==='templates') loadTemplates(); } diff --git a/web/templates/social_eng.html b/web/templates/social_eng.html index ee83798..a3c6850 100644 --- a/web/templates/social_eng.html +++ b/web/templates/social_eng.html @@ -323,7 +323,7 @@ function switchTab(name){ document.querySelectorAll('.tab').forEach((t,i)=>t.classList.toggle('active', ['harvest','pretexts','qr','campaigns'][i]===name)); document.querySelectorAll('.tab-content').forEach(c=>c.style.display='none'); - document.getElementById('tab-'+name).style.display=''; + document.getElementById('tab-'+name).style.display='block'; if(name==='harvest'){loadPages();loadCaptures();} if(name==='pretexts') loadPretexts(); if(name==='campaigns') loadCampaigns(); diff --git a/web/templates/webapp_scanner.html b/web/templates/webapp_scanner.html index d3a6df4..ca06109 100644 --- a/web/templates/webapp_scanner.html +++ b/web/templates/webapp_scanner.html @@ -110,7 +110,7 @@ function switchTab(name){ document.querySelectorAll('.tab').forEach((t,i)=>t.classList.toggle('active', ['quick','dirbust','subdomain','vuln','crawl'][i]===name)); document.querySelectorAll('.tab-content').forEach(c=>c.style.display='none'); - document.getElementById('tab-'+name).style.display=''; + document.getElementById('tab-'+name).style.display='block'; } function quickScan(){
CheckStatusDetails