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: </style> was used instead of </script> closing tag - Port scanner advanced options: remove collapsible toggle, show as always-visible bar Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
6c8e9235c9
commit
ddc03e7a55
@ -370,7 +370,9 @@ class HackHijackService:
|
|||||||
def scan_target(self, target: str, scan_type: str = 'quick',
|
def scan_target(self, target: str, scan_type: str = 'quick',
|
||||||
custom_ports: List[int] = None,
|
custom_ports: List[int] = None,
|
||||||
timeout: float = 3.0,
|
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 a target for open ports and backdoor indicators.
|
||||||
|
|
||||||
scan_type: 'quick' (signature ports only), 'full' (signature + extra),
|
scan_type: 'quick' (signature ports only), 'full' (signature + extra),
|
||||||
@ -396,11 +398,16 @@ class HackHijackService:
|
|||||||
|
|
||||||
# Try nmap first if requested and available
|
# Try nmap first if requested and available
|
||||||
if scan_type == 'nmap':
|
if scan_type == 'nmap':
|
||||||
|
if status_cb:
|
||||||
|
status_cb('Running nmap scan…')
|
||||||
nmap_result = self._nmap_scan(target, ports, timeout)
|
nmap_result = self._nmap_scan(target, ports, timeout)
|
||||||
if nmap_result:
|
if nmap_result:
|
||||||
result.open_ports = nmap_result.get('ports', [])
|
result.open_ports = nmap_result.get('ports', [])
|
||||||
result.os_guess = nmap_result.get('os', '')
|
result.os_guess = nmap_result.get('os', '')
|
||||||
result.nmap_raw = nmap_result.get('raw', '')
|
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
|
# Fallback: socket-based scan
|
||||||
if not result.open_ports:
|
if not result.open_ports:
|
||||||
@ -408,28 +415,40 @@ class HackHijackService:
|
|||||||
total = len(sorted_ports)
|
total = len(sorted_ports)
|
||||||
results_lock = threading.Lock()
|
results_lock = threading.Lock()
|
||||||
open_ports = []
|
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)
|
pr = self._check_port(target, port, timeout)
|
||||||
|
with results_lock:
|
||||||
|
scanned[0] += 1
|
||||||
|
done = scanned[0]
|
||||||
if pr and pr.state == 'open':
|
if pr and pr.state == 'open':
|
||||||
with results_lock:
|
with results_lock:
|
||||||
open_ports.append(pr)
|
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
|
# Threaded scan — 50 concurrent threads
|
||||||
threads = []
|
threads = []
|
||||||
for i, port in enumerate(sorted_ports):
|
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)
|
threads.append(t)
|
||||||
t.start()
|
t.start()
|
||||||
if len(threads) >= 50:
|
if len(threads) >= 50:
|
||||||
for t in threads:
|
for t in threads:
|
||||||
t.join(timeout=timeout + 2)
|
t.join(timeout=timeout + 2)
|
||||||
threads.clear()
|
threads.clear()
|
||||||
if progress_cb and i % 10 == 0:
|
|
||||||
progress_cb(i, total)
|
|
||||||
for t in threads:
|
for t in threads:
|
||||||
t.join(timeout=timeout + 2)
|
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)
|
result.open_ports = sorted(open_ports, key=lambda p: p.port)
|
||||||
|
|
||||||
# Match open ports against backdoor signatures
|
# Match open ports against backdoor signatures
|
||||||
|
|||||||
@ -11,6 +11,12 @@ AUTHOR = "AUTARCH"
|
|||||||
VERSION = "1.0"
|
VERSION = "1.0"
|
||||||
CATEGORY = "offense"
|
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 os
|
||||||
import csv
|
import csv
|
||||||
import json
|
import json
|
||||||
|
|||||||
@ -102,6 +102,7 @@ def create_app():
|
|||||||
from web.routes.sms_forge import sms_forge_bp
|
from web.routes.sms_forge import sms_forge_bp
|
||||||
from web.routes.starlink_hack import starlink_hack_bp
|
from web.routes.starlink_hack import starlink_hack_bp
|
||||||
from web.routes.rcs_tools import rcs_tools_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(auth_bp)
|
||||||
app.register_blueprint(dashboard_bp)
|
app.register_blueprint(dashboard_bp)
|
||||||
@ -163,6 +164,7 @@ def create_app():
|
|||||||
app.register_blueprint(sms_forge_bp)
|
app.register_blueprint(sms_forge_bp)
|
||||||
app.register_blueprint(starlink_hack_bp)
|
app.register_blueprint(starlink_hack_bp)
|
||||||
app.register_blueprint(rcs_tools_bp)
|
app.register_blueprint(rcs_tools_bp)
|
||||||
|
app.register_blueprint(port_scanner_bp)
|
||||||
|
|
||||||
# Start network discovery advertising (mDNS + Bluetooth)
|
# Start network discovery advertising (mDNS + Bluetooth)
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -48,11 +48,7 @@ def index():
|
|||||||
@defense_bp.route('/linux')
|
@defense_bp.route('/linux')
|
||||||
@login_required
|
@login_required
|
||||||
def linux_index():
|
def linux_index():
|
||||||
from core.menu import MainMenu
|
return render_template('defense_linux.html')
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
@defense_bp.route('/linux/audit', methods=['POST'])
|
@defense_bp.route('/linux/audit', methods=['POST'])
|
||||||
|
|||||||
@ -1,13 +1,16 @@
|
|||||||
"""Hack Hijack — web routes for scanning and taking over compromised systems."""
|
"""Hack Hijack — web routes for scanning and taking over compromised systems."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import queue
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
import uuid
|
import uuid
|
||||||
from flask import Blueprint, render_template, request, jsonify, Response
|
from flask import Blueprint, render_template, request, jsonify, Response
|
||||||
from web.auth import login_required
|
from web.auth import login_required
|
||||||
|
|
||||||
hack_hijack_bp = Blueprint('hack_hijack', __name__)
|
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 = {}
|
_running_scans: dict = {}
|
||||||
|
|
||||||
|
|
||||||
@ -37,34 +40,86 @@ def start_scan():
|
|||||||
if not target:
|
if not target:
|
||||||
return jsonify({'ok': False, 'error': 'Target IP required'})
|
return jsonify({'ok': False, 'error': 'Target IP required'})
|
||||||
|
|
||||||
# Validate scan type
|
|
||||||
if scan_type not in ('quick', 'full', 'nmap', 'custom'):
|
if scan_type not in ('quick', 'full', 'nmap', 'custom'):
|
||||||
scan_type = 'quick'
|
scan_type = 'quick'
|
||||||
|
|
||||||
job_id = str(uuid.uuid4())[:8]
|
job_id = str(uuid.uuid4())[:8]
|
||||||
result_holder = {'result': None, 'error': None, 'done': False}
|
q = queue.Queue()
|
||||||
_running_scans[job_id] = result_holder
|
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():
|
def do_scan():
|
||||||
try:
|
try:
|
||||||
svc = _svc()
|
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(
|
r = svc.scan_target(
|
||||||
target,
|
target,
|
||||||
scan_type=scan_type,
|
scan_type=scan_type,
|
||||||
custom_ports=custom_ports,
|
custom_ports=custom_ports,
|
||||||
timeout=3.0,
|
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:
|
except Exception as e:
|
||||||
result_holder['error'] = str(e)
|
job['error'] = str(e)
|
||||||
|
_push('error', msg=str(e))
|
||||||
finally:
|
finally:
|
||||||
result_holder['done'] = True
|
job['done'] = True
|
||||||
|
_push('done', ok=job['error'] is None)
|
||||||
|
|
||||||
threading.Thread(target=do_scan, daemon=True).start()
|
threading.Thread(target=do_scan, daemon=True).start()
|
||||||
return jsonify({'ok': True, 'job_id': job_id,
|
return jsonify({'ok': True, 'job_id': job_id,
|
||||||
'message': f'Scan started on {target} ({scan_type})'})
|
'message': f'Scan started on {target} ({scan_type})'})
|
||||||
|
|
||||||
|
|
||||||
|
@hack_hijack_bp.route('/hack-hijack/scan/<job_id>/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/<job_id>', methods=['GET'])
|
@hack_hijack_bp.route('/hack-hijack/scan/<job_id>', methods=['GET'])
|
||||||
@login_required
|
@login_required
|
||||||
def scan_status(job_id):
|
def scan_status(job_id):
|
||||||
@ -75,7 +130,6 @@ def scan_status(job_id):
|
|||||||
return jsonify({'ok': True, 'done': False, 'message': 'Scan in progress...'})
|
return jsonify({'ok': True, 'done': False, 'message': 'Scan in progress...'})
|
||||||
if holder['error']:
|
if holder['error']:
|
||||||
return jsonify({'ok': False, 'error': holder['error'], 'done': True})
|
return jsonify({'ok': False, 'error': holder['error'], 'done': True})
|
||||||
# Clean up
|
|
||||||
_running_scans.pop(job_id, None)
|
_running_scans.pop(job_id, None)
|
||||||
return jsonify({'ok': True, 'done': True, 'result': holder['result']})
|
return jsonify({'ok': True, 'done': True, 'result': holder['result']})
|
||||||
|
|
||||||
|
|||||||
473
web/routes/port_scanner.py
Normal file
473
web/routes/port_scanner.py
Normal file
@ -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 '<html' in bl or '<!doctype' in bl:
|
||||||
|
return 'HTTP'
|
||||||
|
if 'mysql' in bl:
|
||||||
|
return 'MySQL'
|
||||||
|
if 'redis' in bl:
|
||||||
|
return 'Redis'
|
||||||
|
if 'mongo' in bl:
|
||||||
|
return 'MongoDB'
|
||||||
|
if 'postgresql' in bl:
|
||||||
|
return 'PostgreSQL'
|
||||||
|
if 'rabbitmq' in bl:
|
||||||
|
return 'RabbitMQ'
|
||||||
|
if 'elastic' in bl:
|
||||||
|
return 'Elasticsearch'
|
||||||
|
return SERVICE_MAP.get(port, 'unknown')
|
||||||
|
|
||||||
|
|
||||||
|
def _scan_port(host: str, port: int, timeout: float) -> 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] + ['<target>'])})
|
||||||
|
|
||||||
|
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/<job_id>')
|
||||||
|
@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/<job_id>')
|
||||||
|
@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/<job_id>', 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)
|
||||||
@ -21,42 +21,83 @@ _debug_enabled: bool = False
|
|||||||
_debug_handler_installed: 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):
|
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:
|
def emit(self, record: logging.LogRecord) -> None:
|
||||||
if not _debug_enabled:
|
|
||||||
return
|
|
||||||
try:
|
try:
|
||||||
entry: dict = {
|
exc_text = ''
|
||||||
'ts': record.created,
|
|
||||||
'level': record.levelname,
|
|
||||||
'name': record.name,
|
|
||||||
'raw': record.getMessage(),
|
|
||||||
'msg': self.format(record),
|
|
||||||
}
|
|
||||||
if record.exc_info:
|
if record.exc_info:
|
||||||
import traceback as _tb
|
import traceback as _tb
|
||||||
entry['exc'] = ''.join(_tb.format_exception(*record.exc_info))
|
exc_text = ''.join(_tb.format_exception(*record.exc_info))
|
||||||
_debug_buffer.append(entry)
|
_buf_append(
|
||||||
|
level=record.levelname,
|
||||||
|
name=record.name,
|
||||||
|
raw=record.getMessage(),
|
||||||
|
msg=self.format(record),
|
||||||
|
exc=exc_text,
|
||||||
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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:
|
def _ensure_debug_handler() -> None:
|
||||||
|
"""Install logging handler + stdout/stderr capture once, at startup."""
|
||||||
global _debug_handler_installed
|
global _debug_handler_installed
|
||||||
if _debug_handler_installed:
|
if _debug_handler_installed:
|
||||||
return
|
return
|
||||||
|
# Logging handler
|
||||||
handler = _DebugBufferHandler()
|
handler = _DebugBufferHandler()
|
||||||
handler.setLevel(logging.DEBUG)
|
handler.setLevel(logging.DEBUG)
|
||||||
handler.setFormatter(logging.Formatter('%(name)s — %(message)s'))
|
handler.setFormatter(logging.Formatter('%(name)s — %(message)s'))
|
||||||
root = logging.getLogger()
|
root = logging.getLogger()
|
||||||
root.addHandler(handler)
|
root.addHandler(handler)
|
||||||
# Lower root level to DEBUG so records reach the handler
|
|
||||||
if root.level == logging.NOTSET or root.level > logging.DEBUG:
|
if root.level == logging.NOTSET or root.level > logging.DEBUG:
|
||||||
root.setLevel(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
|
_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')
|
settings_bp = Blueprint('settings', __name__, url_prefix='/settings')
|
||||||
|
|
||||||
|
|
||||||
@ -429,28 +470,42 @@ def discovery_stop():
|
|||||||
@settings_bp.route('/debug/toggle', methods=['POST'])
|
@settings_bp.route('/debug/toggle', methods=['POST'])
|
||||||
@login_required
|
@login_required
|
||||||
def debug_toggle():
|
def debug_toggle():
|
||||||
"""Enable or disable the debug log capture."""
|
"""Enable or disable the debug console UI (capture always runs)."""
|
||||||
global _debug_enabled
|
global _debug_enabled
|
||||||
data = request.get_json(silent=True) or {}
|
data = request.get_json(silent=True) or {}
|
||||||
_debug_enabled = bool(data.get('enabled', False))
|
_debug_enabled = bool(data.get('enabled', False))
|
||||||
if _debug_enabled:
|
if _debug_enabled:
|
||||||
_ensure_debug_handler()
|
logging.getLogger('autarch.debug').info('Debug console opened')
|
||||||
logging.getLogger('autarch.debug').info('Debug console enabled')
|
|
||||||
return jsonify({'ok': True, 'enabled': _debug_enabled})
|
return jsonify({'ok': True, 'enabled': _debug_enabled})
|
||||||
|
|
||||||
|
|
||||||
@settings_bp.route('/debug/stream')
|
@settings_bp.route('/debug/stream')
|
||||||
@login_required
|
@login_required
|
||||||
def debug_stream():
|
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():
|
def generate():
|
||||||
sent = 0
|
|
||||||
while True:
|
|
||||||
buf = list(_debug_buffer)
|
buf = list(_debug_buffer)
|
||||||
while sent < len(buf):
|
# 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)
|
||||||
|
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"
|
yield f"data: {json.dumps(buf[sent])}\n\n"
|
||||||
sent += 1
|
sent += 1
|
||||||
time.sleep(0.25)
|
yield ': keepalive\n\n'
|
||||||
|
|
||||||
return Response(generate(), mimetype='text/event-stream',
|
return Response(generate(), mimetype='text/event-stream',
|
||||||
headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})
|
headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})
|
||||||
|
|||||||
@ -9,6 +9,8 @@
|
|||||||
--text-muted: #5c6078;
|
--text-muted: #5c6078;
|
||||||
--accent: #6366f1;
|
--accent: #6366f1;
|
||||||
--accent-hover: #818cf8;
|
--accent-hover: #818cf8;
|
||||||
|
--primary: #6366f1;
|
||||||
|
--surface: #222536;
|
||||||
--success: #22c55e;
|
--success: #22c55e;
|
||||||
--warning: #f59e0b;
|
--warning: #f59e0b;
|
||||||
--danger: #ef4444;
|
--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); }
|
.flash-info { background: rgba(99,102,241,0.15); color: var(--accent); border: 1px solid rgba(99,102,241,0.3); }
|
||||||
|
|
||||||
/* Forms */
|
/* 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 { margin-bottom: 16px; text-align: left; }
|
||||||
.form-group label { display: block; margin-bottom: 6px; font-size: 0.85rem; color: var(--text-secondary); }
|
.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 {
|
.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-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 { 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-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-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; }
|
.btn-group { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||||
|
|
||||||
/* Page header */
|
/* 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 h2 { font-size: 1rem; margin-bottom: 16px; }
|
||||||
.section h3 { font-size: 0.9rem; margin-bottom: 12px; color: var(--text-secondary); }
|
.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 */
|
/* Tables */
|
||||||
.data-table { width: 100%; border-collapse: collapse; }
|
.data-table { width: 100%; border-collapse: collapse; }
|
||||||
.data-table th {
|
.data-table th {
|
||||||
|
|||||||
@ -2256,10 +2256,12 @@ var _DBG_LEVELS = {
|
|||||||
WARNING: { cls: 'dbg-warn', sym: '⚠' },
|
WARNING: { cls: 'dbg-warn', sym: '⚠' },
|
||||||
ERROR: { cls: 'dbg-err', sym: '✕' },
|
ERROR: { cls: 'dbg-err', sym: '✕' },
|
||||||
CRITICAL: { cls: 'dbg-crit', 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)
|
// 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) {
|
function debugToggle(enabled) {
|
||||||
fetch('/settings/debug/toggle', {
|
fetch('/settings/debug/toggle', {
|
||||||
@ -2325,6 +2327,8 @@ function _dbgShouldShow(entry) {
|
|||||||
case 'verbose': return lvl !== 'DEBUG' && lvl !== 'NOTSET';
|
case 'verbose': return lvl !== 'DEBUG' && lvl !== 'NOTSET';
|
||||||
case 'warn': return lvl === 'WARNING' || lvl === 'ERROR' || lvl === 'CRITICAL';
|
case 'warn': return lvl === 'WARNING' || lvl === 'ERROR' || lvl === 'CRITICAL';
|
||||||
case 'output':
|
case 'output':
|
||||||
|
var lvlO = (entry.level || '').toUpperCase();
|
||||||
|
if (lvlO === 'STDOUT' || lvlO === 'STDERR') return true;
|
||||||
var name = (entry.name || '').toLowerCase();
|
var name = (entry.name || '').toLowerCase();
|
||||||
return _OUTPUT_LOGGERS.some(function(pfx) { return name.indexOf(pfx) >= 0; });
|
return _OUTPUT_LOGGERS.some(function(pfx) { return name.indexOf(pfx) >= 0; });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,6 +29,7 @@
|
|||||||
<div class="nav-section">
|
<div class="nav-section">
|
||||||
<ul class="nav-links">
|
<ul class="nav-links">
|
||||||
<li><a href="{{ url_for('dashboard.index') }}" class="{% if request.endpoint == 'dashboard.index' %}active{% endif %}">Dashboard</a></li>
|
<li><a href="{{ url_for('dashboard.index') }}" class="{% if request.endpoint == 'dashboard.index' %}active{% endif %}">Dashboard</a></li>
|
||||||
|
<li><a href="{{ url_for('port_scanner.index') }}" class="{% if request.blueprint == 'port_scanner' %}active{% endif %}">🔍 Port Scanner</a></li>
|
||||||
<li><a href="{{ url_for('targets.index') }}" class="{% if request.blueprint == 'targets' %}active{% endif %}">Targets</a></li>
|
<li><a href="{{ url_for('targets.index') }}" class="{% if request.blueprint == 'targets' %}active{% endif %}">Targets</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -97,7 +97,7 @@ let refreshTimer=null;
|
|||||||
function switchTab(name){
|
function switchTab(name){
|
||||||
document.querySelectorAll('.tab').forEach((t,i)=>t.classList.toggle('active',['dashboard','agents','generate'][i]===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.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();
|
if(name==='dashboard'||name==='agents') refreshDashboard();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,29 +1,29 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% block title %}Linux Defense - AUTARCH{% endblock %}
|
{% block title %}Linux Defense — AUTARCH{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="page-header" style="display:flex;align-items:center;gap:1rem;flex-wrap:wrap">
|
<div class="page-header" style="display:flex;align-items:center;gap:1rem;flex-wrap:wrap">
|
||||||
<div>
|
<div>
|
||||||
<h1>Linux Defense</h1>
|
<h1>Linux Defense</h1>
|
||||||
<p style="margin:0;font-size:0.85rem;color:var(--text-secondary)">
|
<p style="margin:0;font-size:0.85rem;color:var(--text-secondary)">
|
||||||
Linux system hardening, iptables firewall management, and log analysis.
|
System hardening, iptables firewall management, and log analysis
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<a href="{{ url_for('defense.index') }}" class="btn btn-sm" style="margin-left:auto">← Defense</a>
|
<a href="{{ url_for('defense.index') }}" class="btn btn-sm" style="margin-left:auto">← Defense</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Security Audit -->
|
<!-- Security Audit -->
|
||||||
<div class="section">
|
<div class="card">
|
||||||
<h2>Security Audit</h2>
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem;flex-wrap:wrap;gap:0.5rem">
|
||||||
<div class="tool-actions">
|
<h3 style="margin:0">Security Audit</h3>
|
||||||
<button id="btn-audit" class="btn btn-primary" onclick="linuxRunAudit()">Run Full Audit</button>
|
<button id="btn-audit" class="btn btn-primary btn-sm" onclick="linuxRunAudit()">Run Full Audit</button>
|
||||||
</div>
|
</div>
|
||||||
<div style="display:flex;gap:24px;align-items:flex-start;flex-wrap:wrap">
|
<div style="display:flex;gap:24px;align-items:flex-start;flex-wrap:wrap">
|
||||||
<div class="score-display">
|
<div style="text-align:center;padding:12px 20px;background:var(--bg-primary);border:1px solid var(--border);border-radius:var(--radius);min-width:120px">
|
||||||
<div class="score-value" id="audit-score">--</div>
|
<div id="audit-score" style="font-size:2.5rem;font-weight:700;line-height:1">--</div>
|
||||||
<div class="score-label">Security Score</div>
|
<div style="font-size:0.78rem;color:var(--text-secondary);margin-top:4px">Security Score</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="flex:1;min-width:300px">
|
<div style="flex:1;min-width:280px">
|
||||||
<table class="data-table">
|
<table class="data-table">
|
||||||
<thead><tr><th>Check</th><th>Status</th><th>Details</th></tr></thead>
|
<thead><tr><th>Check</th><th>Status</th><th>Details</th></tr></thead>
|
||||||
<tbody id="audit-results">
|
<tbody id="audit-results">
|
||||||
@ -34,94 +34,53 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Individual Checks -->
|
<!-- Quick Checks -->
|
||||||
<div class="section">
|
<div class="card">
|
||||||
<h2>Quick Checks</h2>
|
<h3>Quick Checks</h3>
|
||||||
<div class="tool-grid">
|
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(200px,1fr));gap:10px">
|
||||||
<div class="tool-card">
|
{% for check_id, check_name, check_desc in [
|
||||||
<h4>Firewall</h4>
|
('firewall', 'Firewall', 'Check iptables/ufw/firewalld status'),
|
||||||
<p>Check iptables/ufw/firewalld status</p>
|
('ssh', 'SSH Config', 'Check SSH hardening settings'),
|
||||||
<button class="btn btn-small" onclick="linuxRunCheck('firewall')">Run</button>
|
('ports', 'Open Ports', 'Scan for high-risk listening ports'),
|
||||||
<pre class="output-panel tool-result" id="check-result-firewall"></pre>
|
('users', 'Users', 'Check UID 0 users and empty passwords'),
|
||||||
</div>
|
('permissions', 'Permissions', 'Check critical file permissions'),
|
||||||
<div class="tool-card">
|
('services', 'Services', 'Check for dangerous services')
|
||||||
<h4>SSH Config</h4>
|
] %}
|
||||||
<p>Check SSH hardening settings</p>
|
<div style="background:var(--bg-primary);border:1px solid var(--border);border-radius:var(--radius);padding:14px">
|
||||||
<button class="btn btn-small" onclick="linuxRunCheck('ssh')">Run</button>
|
<div style="font-weight:600;font-size:0.88rem;margin-bottom:4px">{{ check_name }}</div>
|
||||||
<pre class="output-panel tool-result" id="check-result-ssh"></pre>
|
<div style="color:var(--text-secondary);font-size:0.78rem;margin-bottom:10px">{{ check_desc }}</div>
|
||||||
</div>
|
<button class="btn btn-sm" style="width:100%" onclick="linuxRunCheck('{{ check_id }}')">Run</button>
|
||||||
<div class="tool-card">
|
<pre class="output-panel tool-result" id="check-result-{{ check_id }}" style="display:none;margin-top:10px;font-size:0.75rem;min-height:0"></pre>
|
||||||
<h4>Open Ports</h4>
|
|
||||||
<p>Scan for high-risk listening ports</p>
|
|
||||||
<button class="btn btn-small" onclick="linuxRunCheck('ports')">Run</button>
|
|
||||||
<pre class="output-panel tool-result" id="check-result-ports"></pre>
|
|
||||||
</div>
|
|
||||||
<div class="tool-card">
|
|
||||||
<h4>Users</h4>
|
|
||||||
<p>Check UID 0 users and empty passwords</p>
|
|
||||||
<button class="btn btn-small" onclick="linuxRunCheck('users')">Run</button>
|
|
||||||
<pre class="output-panel tool-result" id="check-result-users"></pre>
|
|
||||||
</div>
|
|
||||||
<div class="tool-card">
|
|
||||||
<h4>Permissions</h4>
|
|
||||||
<p>Check critical file permissions</p>
|
|
||||||
<button class="btn btn-small" onclick="linuxRunCheck('permissions')">Run</button>
|
|
||||||
<pre class="output-panel tool-result" id="check-result-permissions"></pre>
|
|
||||||
</div>
|
|
||||||
<div class="tool-card">
|
|
||||||
<h4>Services</h4>
|
|
||||||
<p>Check for dangerous services</p>
|
|
||||||
<button class="btn btn-small" onclick="linuxRunCheck('services')">Run</button>
|
|
||||||
<pre class="output-panel tool-result" id="check-result-services"></pre>
|
|
||||||
</div>
|
</div>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Firewall Manager -->
|
<!-- Firewall Manager -->
|
||||||
<div class="section">
|
<div class="card">
|
||||||
<h2>Firewall Manager (iptables)</h2>
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem;flex-wrap:wrap;gap:0.5rem">
|
||||||
<div class="tool-actions">
|
<h3 style="margin:0">Firewall Manager (iptables)</h3>
|
||||||
<button class="btn btn-small" onclick="linuxLoadFwRules()">Refresh Rules</button>
|
<button class="btn btn-sm" onclick="linuxLoadFwRules()">Refresh Rules</button>
|
||||||
</div>
|
</div>
|
||||||
<pre class="output-panel scrollable" id="fw-rules">Click "Refresh Rules" to load current iptables rules.</pre>
|
<pre class="output-panel" id="fw-rules" style="max-height:300px;overflow-y:auto">Click "Refresh Rules" to load current iptables rules.</pre>
|
||||||
<div style="margin-top:12px">
|
<div style="display:flex;gap:8px;margin-top:12px;flex-wrap:wrap">
|
||||||
<div class="input-row">
|
<input type="text" id="block-ip" class="form-control" placeholder="IP address to block" style="flex:1;min-width:180px">
|
||||||
<input type="text" id="block-ip" placeholder="IP address to block">
|
<button class="btn btn-sm btn-danger" onclick="linuxBlockIP()">Block IP</button>
|
||||||
<button class="btn btn-danger btn-small" onclick="linuxBlockIP()">Block IP</button>
|
<button class="btn btn-sm" onclick="linuxUnblockIP()">Unblock IP</button>
|
||||||
<button class="btn btn-small" onclick="linuxUnblockIP()">Unblock IP</button>
|
|
||||||
</div>
|
|
||||||
<pre class="output-panel" id="fw-result" style="min-height:0"></pre>
|
|
||||||
</div>
|
</div>
|
||||||
|
<pre class="output-panel" id="fw-result" style="min-height:0;margin-top:8px"></pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Log Analysis -->
|
<!-- Log Analysis -->
|
||||||
<div class="section">
|
<div class="card">
|
||||||
<h2>Log Analysis</h2>
|
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1rem;flex-wrap:wrap;gap:0.5rem">
|
||||||
<div class="tool-actions">
|
<h3 style="margin:0">Log Analysis</h3>
|
||||||
<button id="btn-logs" class="btn btn-primary" onclick="linuxAnalyzeLogs()">Analyze Logs</button>
|
<button id="btn-logs" class="btn btn-primary btn-sm" onclick="linuxAnalyzeLogs()">Analyze Logs</button>
|
||||||
</div>
|
</div>
|
||||||
<pre class="output-panel scrollable" id="log-output">Click "Analyze Logs" to parse auth and web server logs.</pre>
|
<pre class="output-panel" id="log-output" style="max-height:350px;overflow-y:auto">Click "Analyze Logs" to parse auth and web server logs.</pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if modules %}
|
|
||||||
<div class="section">
|
|
||||||
<h2>Defense Modules</h2>
|
|
||||||
<ul class="module-list">
|
|
||||||
{% for name, info in modules.items() %}
|
|
||||||
<li class="module-item">
|
|
||||||
<div>
|
|
||||||
<div class="module-name">{{ name }}</div>
|
|
||||||
<div class="module-desc">{{ info.description }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="module-meta">v{{ info.version }}</div>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
/* ── Linux Defense (routes prefixed with /defense/linux/) ── */
|
|
||||||
function linuxRunAudit() {
|
function linuxRunAudit() {
|
||||||
var btn = document.getElementById('btn-audit');
|
var btn = document.getElementById('btn-audit');
|
||||||
setLoading(btn, true);
|
setLoading(btn, true);
|
||||||
|
|||||||
@ -15,7 +15,9 @@
|
|||||||
|
|
||||||
<!-- Scan Tab -->
|
<!-- Scan Tab -->
|
||||||
<div id="tab-scan" class="tab-content active">
|
<div id="tab-scan" class="tab-content active">
|
||||||
<div class="card" style="max-width:700px">
|
<div style="display:grid;grid-template-columns:360px 1fr;gap:1.5rem;align-items:start">
|
||||||
|
<!-- Config panel -->
|
||||||
|
<div class="card">
|
||||||
<h3>Target Scan</h3>
|
<h3>Target Scan</h3>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>Target IP Address</label>
|
<label>Target IP Address</label>
|
||||||
@ -34,10 +36,41 @@
|
|||||||
<label>Custom Ports (comma-separated)</label>
|
<label>Custom Ports (comma-separated)</label>
|
||||||
<input type="text" id="hh-custom-ports" class="form-control" placeholder="22,80,443,445,4444,8080">
|
<input type="text" id="hh-custom-ports" class="form-control" placeholder="22,80,443,445,4444,8080">
|
||||||
</div>
|
</div>
|
||||||
<button id="hh-scan-btn" class="btn btn-primary" onclick="startScan()">Scan for Compromises</button>
|
<div style="display:flex;gap:0.5rem">
|
||||||
<div id="hh-scan-status" style="margin-top:1rem;display:none">
|
<button id="hh-scan-btn" class="btn btn-primary" onclick="startScan()" style="flex:1">Scan for Compromises</button>
|
||||||
<div class="spinner-inline"></div>
|
<button id="hh-cancel-btn" class="btn" style="display:none;background:var(--danger);color:#fff" onclick="cancelScan()">Cancel</button>
|
||||||
<span id="hh-scan-msg">Scanning...</span>
|
</div>
|
||||||
|
|
||||||
|
<!-- Progress bar -->
|
||||||
|
<div id="hh-progress-wrap" style="display:none;margin-top:1rem">
|
||||||
|
<div style="display:flex;justify-content:space-between;font-size:0.8rem;margin-bottom:4px">
|
||||||
|
<span id="hh-prog-label">Scanning…</span>
|
||||||
|
<span id="hh-prog-pct">0%</span>
|
||||||
|
</div>
|
||||||
|
<div style="background:var(--border);border-radius:4px;height:6px;overflow:hidden">
|
||||||
|
<div id="hh-prog-bar" style="height:100%;background:var(--accent);transition:width 0.3s;width:0"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Live output -->
|
||||||
|
<div class="card" style="display:flex;flex-direction:column">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:0.5rem">
|
||||||
|
<h3 style="margin:0">Live Output</h3>
|
||||||
|
<button class="btn btn-sm" onclick="clearOutput()">Clear</button>
|
||||||
|
</div>
|
||||||
|
<pre id="hh-output" style="background:#0d0d14;color:#c9d1d9;font-family:monospace;font-size:0.78rem;
|
||||||
|
padding:12px;border-radius:var(--radius);height:340px;overflow-y:auto;
|
||||||
|
white-space:pre-wrap;word-break:break-all;margin:0;border:1px solid var(--border)">Ready. Enter target and click Scan.</pre>
|
||||||
|
|
||||||
|
<!-- Live found ports mini-table -->
|
||||||
|
<div id="hh-live-ports" style="margin-top:0.75rem;display:none">
|
||||||
|
<h4 style="font-size:0.8rem;color:var(--text-secondary);margin-bottom:6px">Live Discovered Ports</h4>
|
||||||
|
<table class="data-table" style="font-size:0.78rem">
|
||||||
|
<thead><tr><th>Port</th><th>Service</th><th>Banner</th></tr></thead>
|
||||||
|
<tbody id="hh-live-ports-body"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -141,8 +174,6 @@
|
|||||||
.conf-high{color:var(--danger);font-weight:700}
|
.conf-high{color:var(--danger);font-weight:700}
|
||||||
.conf-medium{color:#f59e0b;font-weight:600}
|
.conf-medium{color:#f59e0b;font-weight:600}
|
||||||
.conf-low{color:var(--text-muted)}
|
.conf-low{color:var(--text-muted)}
|
||||||
.spinner-inline{display:inline-block;width:14px;height:14px;border:2px solid var(--border);border-top-color:var(--accent);border-radius:50%;animation:spin 0.8s linear infinite;vertical-align:middle;margin-right:6px}
|
|
||||||
@keyframes spin{to{transform:rotate(360deg)}}
|
|
||||||
.cat-eternalblue{color:var(--danger)}
|
.cat-eternalblue{color:var(--danger)}
|
||||||
.cat-rat{color:#f59e0b}
|
.cat-rat{color:#f59e0b}
|
||||||
.cat-shell{color:#6366f1}
|
.cat-shell{color:#6366f1}
|
||||||
@ -150,18 +181,26 @@
|
|||||||
.cat-proxy{color:#8b5cf6}
|
.cat-proxy{color:#8b5cf6}
|
||||||
.cat-miner{color:#06b6d4}
|
.cat-miner{color:#06b6d4}
|
||||||
.cat-generic{color:var(--text-secondary)}
|
.cat-generic{color:var(--text-secondary)}
|
||||||
|
/* output line colors */
|
||||||
|
#hh-output .ln-status{color:#7dd3fc}
|
||||||
|
#hh-output .ln-open{color:#4ade80;font-weight:700}
|
||||||
|
#hh-output .ln-warn{color:#fbbf24}
|
||||||
|
#hh-output .ln-error{color:#f87171}
|
||||||
|
#hh-output .ln-done{color:#a78bfa;font-weight:700}
|
||||||
|
#hh-output .ln-prog{color:#6b7280}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
let currentScanResult = null;
|
let currentScanResult = null;
|
||||||
let currentSessionId = null;
|
let currentSessionId = null;
|
||||||
let pollTimer = null;
|
let activeStream = null;
|
||||||
|
let currentJobId = null;
|
||||||
|
|
||||||
function switchTab(name){
|
function switchTab(name){
|
||||||
document.querySelectorAll('.tab').forEach((t,i)=>t.classList.toggle('active',
|
document.querySelectorAll('.tab').forEach((t,i)=>t.classList.toggle('active',
|
||||||
['scan','results','sessions','history'][i]===name));
|
['scan','results','sessions','history'][i]===name));
|
||||||
document.querySelectorAll('.tab-content').forEach(c=>c.style.display='none');
|
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==='sessions') loadSessions();
|
if(name==='sessions') loadSessions();
|
||||||
if(name==='history') loadHistory();
|
if(name==='history') loadHistory();
|
||||||
}
|
}
|
||||||
@ -171,57 +210,148 @@ function toggleCustomPorts(){
|
|||||||
document.getElementById('hh-scan-type').value==='custom'?'':'none';
|
document.getElementById('hh-scan-type').value==='custom'?'':'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function appendOutput(text, cls){
|
||||||
|
const out = document.getElementById('hh-output');
|
||||||
|
const line = document.createElement('span');
|
||||||
|
if(cls) line.className = cls;
|
||||||
|
line.textContent = text + '\n';
|
||||||
|
out.appendChild(line);
|
||||||
|
out.scrollTop = out.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearOutput(){
|
||||||
|
document.getElementById('hh-output').textContent = '';
|
||||||
|
}
|
||||||
|
|
||||||
function startScan(){
|
function startScan(){
|
||||||
const target = document.getElementById('hh-target').value.trim();
|
const target = document.getElementById('hh-target').value.trim();
|
||||||
if(!target){alert('Enter a target IP');return}
|
if(!target){ alert('Enter a target IP'); return; }
|
||||||
const scanType = document.getElementById('hh-scan-type').value;
|
const scanType = document.getElementById('hh-scan-type').value;
|
||||||
let customPorts = [];
|
let customPorts = [];
|
||||||
if(scanType === 'custom'){
|
if(scanType === 'custom'){
|
||||||
customPorts = document.getElementById('hh-custom-ports').value
|
customPorts = document.getElementById('hh-custom-ports').value
|
||||||
.split(',').map(p=>parseInt(p.trim())).filter(p=>p>0&&p<65536);
|
.split(',').map(p=>parseInt(p.trim())).filter(p=>p>0&&p<65536);
|
||||||
if(!customPorts.length){alert('Enter valid ports');return}
|
if(!customPorts.length){ alert('Enter valid ports'); return; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reset UI
|
||||||
|
clearOutput();
|
||||||
|
document.getElementById('hh-live-ports').style.display = 'none';
|
||||||
|
document.getElementById('hh-live-ports-body').innerHTML = '';
|
||||||
document.getElementById('hh-scan-btn').disabled = true;
|
document.getElementById('hh-scan-btn').disabled = true;
|
||||||
document.getElementById('hh-scan-status').style.display='';
|
document.getElementById('hh-cancel-btn').style.display = '';
|
||||||
document.getElementById('hh-scan-msg').textContent='Scanning '+target+'...';
|
document.getElementById('hh-progress-wrap').style.display = '';
|
||||||
|
document.getElementById('hh-prog-bar').style.width = '0';
|
||||||
|
document.getElementById('hh-prog-pct').textContent = '0%';
|
||||||
|
document.getElementById('hh-prog-label').textContent = 'Starting…';
|
||||||
|
appendOutput(`[*] Starting ${scanType} scan on ${target}…`, 'ln-status');
|
||||||
|
|
||||||
fetch('/hack-hijack/scan', {method:'POST', headers:{'Content-Type':'application/json'},
|
fetch('/hack-hijack/scan', {method:'POST', headers:{'Content-Type':'application/json'},
|
||||||
body: JSON.stringify({target, scan_type: scanType, custom_ports: customPorts})})
|
body: JSON.stringify({target, scan_type: scanType, custom_ports: customPorts})})
|
||||||
.then(r=>r.json()).then(d=>{
|
.then(r=>r.json()).then(d=>{
|
||||||
if(!d.ok){showScanError(d.error);return}
|
if(!d.ok){ scanDone(false, d.error); return; }
|
||||||
pollScan(d.job_id);
|
currentJobId = d.job_id;
|
||||||
}).catch(e=>showScanError(e.message));
|
openStream(d.job_id);
|
||||||
|
}).catch(e=>scanDone(false, e.message));
|
||||||
}
|
}
|
||||||
|
|
||||||
function pollScan(jobId){
|
function openStream(jobId){
|
||||||
if(pollTimer) clearInterval(pollTimer);
|
if(activeStream){ activeStream.close(); activeStream = null; }
|
||||||
pollTimer=setInterval(()=>{
|
const es = new EventSource('/hack-hijack/scan/' + jobId + '/stream');
|
||||||
fetch('/hack-hijack/scan/'+jobId).then(r=>r.json()).then(d=>{
|
activeStream = es;
|
||||||
if(!d.done) return;
|
|
||||||
clearInterval(pollTimer);pollTimer=null;
|
es.onmessage = function(e){
|
||||||
document.getElementById('hh-scan-btn').disabled=false;
|
try{ handleEvent(JSON.parse(e.data)); } catch(ex){}
|
||||||
document.getElementById('hh-scan-status').style.display='none';
|
};
|
||||||
if(!d.ok){showScanError(d.error);return}
|
es.onerror = function(){
|
||||||
|
es.close(); activeStream = null;
|
||||||
|
appendOutput('[!] Stream connection lost', 'ln-error');
|
||||||
|
scanDone(false, 'Stream disconnected');
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEvent(ev){
|
||||||
|
switch(ev.type){
|
||||||
|
case 'progress': {
|
||||||
|
const pct = ev.pct || 0;
|
||||||
|
document.getElementById('hh-prog-bar').style.width = pct + '%';
|
||||||
|
document.getElementById('hh-prog-pct').textContent = pct + '%';
|
||||||
|
if(ev.msg) document.getElementById('hh-prog-label').textContent = ev.msg;
|
||||||
|
if(ev.msg) appendOutput('[~] ' + ev.msg, 'ln-prog');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'status':
|
||||||
|
appendOutput('[*] ' + ev.msg, 'ln-status');
|
||||||
|
document.getElementById('hh-prog-label').textContent = ev.msg;
|
||||||
|
break;
|
||||||
|
case 'port_found': {
|
||||||
|
const svc = ev.service ? ` (${ev.service})` : '';
|
||||||
|
const banner = ev.banner ? ` "${ev.banner.slice(0,60)}"` : '';
|
||||||
|
appendOutput(`[+] OPEN ${ev.port}/tcp${svc}${banner}`, 'ln-open');
|
||||||
|
addLivePort(ev.port, ev.service, ev.banner);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'error':
|
||||||
|
appendOutput('[!] ' + ev.msg, 'ln-error');
|
||||||
|
break;
|
||||||
|
case 'done':
|
||||||
|
if(activeStream){ activeStream.close(); activeStream = null; }
|
||||||
|
document.getElementById('hh-prog-bar').style.width = '100%';
|
||||||
|
document.getElementById('hh-prog-pct').textContent = '100%';
|
||||||
|
if(ev.ok){
|
||||||
|
appendOutput('[✓] Scan complete — fetching results…', 'ln-done');
|
||||||
|
fetchResult(currentJobId);
|
||||||
|
} else {
|
||||||
|
appendOutput('[!] Scan failed', 'ln-error');
|
||||||
|
scanDone(false, 'Scan failed');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addLivePort(port, service, banner){
|
||||||
|
const tbody = document.getElementById('hh-live-ports-body');
|
||||||
|
document.getElementById('hh-live-ports').style.display = '';
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
tr.innerHTML = `<td>${port}</td><td>${esc(service||'—')}</td>
|
||||||
|
<td style="font-family:monospace;font-size:0.72rem;max-width:300px;overflow:hidden;text-overflow:ellipsis">${esc((banner||'').slice(0,80))}</td>`;
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fetchResult(jobId){
|
||||||
|
fetch('/hack-hijack/scan/' + jobId)
|
||||||
|
.then(r=>r.json()).then(d=>{
|
||||||
|
if(!d.done){ setTimeout(()=>fetchResult(jobId), 500); return; }
|
||||||
|
if(!d.ok){ scanDone(false, d.error); return; }
|
||||||
currentScanResult = d.result;
|
currentScanResult = d.result;
|
||||||
|
appendOutput(`[✓] Done — ${d.result.open_ports.length} ports, ${d.result.backdoors.length} backdoor indicators`, 'ln-done');
|
||||||
|
scanDone(true);
|
||||||
renderResults(d.result);
|
renderResults(d.result);
|
||||||
switchTab('results');
|
switchTab('results');
|
||||||
}).catch(()=>{});
|
}).catch(e=>scanDone(false, e.message));
|
||||||
},1500);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function showScanError(msg){
|
function scanDone(ok, errMsg){
|
||||||
document.getElementById('hh-scan-btn').disabled = false;
|
document.getElementById('hh-scan-btn').disabled = false;
|
||||||
document.getElementById('hh-scan-status').style.display='none';
|
document.getElementById('hh-cancel-btn').style.display = 'none';
|
||||||
alert('Scan error: '+msg);
|
document.getElementById('hh-progress-wrap').style.display = 'none';
|
||||||
|
if(!ok && errMsg) appendOutput('[!] Error: ' + errMsg, 'ln-error');
|
||||||
|
currentJobId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelScan(){
|
||||||
|
if(activeStream){ activeStream.close(); activeStream = null; }
|
||||||
|
appendOutput('[x] Scan cancelled by user', 'ln-warn');
|
||||||
|
scanDone(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderResults(r){
|
function renderResults(r){
|
||||||
document.getElementById('hh-no-results').style.display='none';
|
document.getElementById('hh-no-results').style.display='none';
|
||||||
document.getElementById('hh-results').style.display='';
|
document.getElementById('hh-results').style.display='';
|
||||||
document.getElementById('res-target').textContent=r.target;
|
document.getElementById('res-target').textContent=r.target;
|
||||||
document.getElementById('res-time').textContent=r.scan_time.replace('T',' ').slice(0,19)+' UTC';
|
document.getElementById('res-time').textContent=(r.scan_time||'').replace('T',' ').slice(0,19)+' UTC';
|
||||||
document.getElementById('res-ports-count').textContent=r.open_ports.length;
|
document.getElementById('res-ports-count').textContent=(r.open_ports||[]).length;
|
||||||
document.getElementById('res-backdoors-count').textContent=r.backdoors.length;
|
document.getElementById('res-backdoors-count').textContent=(r.backdoors||[]).length;
|
||||||
document.getElementById('res-duration').textContent=r.duration;
|
document.getElementById('res-duration').textContent=r.duration;
|
||||||
if(r.os_guess){
|
if(r.os_guess){
|
||||||
document.getElementById('res-os').style.display='';
|
document.getElementById('res-os').style.display='';
|
||||||
@ -231,7 +361,7 @@ function renderResults(r){
|
|||||||
// Ports table
|
// Ports table
|
||||||
const pb=document.getElementById('hh-ports-body');
|
const pb=document.getElementById('hh-ports-body');
|
||||||
pb.innerHTML='';
|
pb.innerHTML='';
|
||||||
r.open_ports.forEach(p=>{
|
(r.open_ports||[]).forEach(p=>{
|
||||||
const tr=document.createElement('tr');
|
const tr=document.createElement('tr');
|
||||||
tr.innerHTML=`<td>${p.port}</td><td>${p.protocol}</td><td>${p.service||'—'}</td>
|
tr.innerHTML=`<td>${p.port}</td><td>${p.protocol}</td><td>${p.service||'—'}</td>
|
||||||
<td style="font-family:monospace;font-size:0.75rem;max-width:400px;overflow:hidden;text-overflow:ellipsis">${esc(p.banner||'')}</td>`;
|
<td style="font-family:monospace;font-size:0.75rem;max-width:400px;overflow:hidden;text-overflow:ellipsis">${esc(p.banner||'')}</td>`;
|
||||||
@ -241,7 +371,7 @@ function renderResults(r){
|
|||||||
// Backdoors
|
// Backdoors
|
||||||
const bs=document.getElementById('hh-backdoors-section');
|
const bs=document.getElementById('hh-backdoors-section');
|
||||||
const bb=document.getElementById('hh-backdoors-body');
|
const bb=document.getElementById('hh-backdoors-body');
|
||||||
if(r.backdoors.length){
|
if((r.backdoors||[]).length){
|
||||||
bs.style.display='';
|
bs.style.display='';
|
||||||
bb.innerHTML='';
|
bb.innerHTML='';
|
||||||
r.backdoors.forEach((b,i)=>{
|
r.backdoors.forEach((b,i)=>{
|
||||||
@ -285,7 +415,6 @@ function tryTakeover(idx){
|
|||||||
} else {
|
} else {
|
||||||
alert(d.message||d.error||'Takeover result received');
|
alert(d.message||d.error||'Takeover result received');
|
||||||
if(d.msf_command){
|
if(d.msf_command){
|
||||||
// Copy MSF command to clipboard
|
|
||||||
navigator.clipboard.writeText(d.msf_command).then(()=>{
|
navigator.clipboard.writeText(d.msf_command).then(()=>{
|
||||||
alert('MSF command copied to clipboard');
|
alert('MSF command copied to clipboard');
|
||||||
}).catch(()=>{});
|
}).catch(()=>{});
|
||||||
@ -311,7 +440,7 @@ function loadSessions(){
|
|||||||
onclick="openShell('${esc(s.session_id)}','${esc(s.host)}:${s.port}','')">
|
onclick="openShell('${esc(s.session_id)}','${esc(s.host)}:${s.port}','')">
|
||||||
<div style="display:flex;justify-content:space-between;align-items:center">
|
<div style="display:flex;justify-content:space-between;align-items:center">
|
||||||
<div><strong>${esc(s.type)}</strong> → ${esc(s.host)}:${s.port}</div>
|
<div><strong>${esc(s.type)}</strong> → ${esc(s.host)}:${s.port}</div>
|
||||||
<div style="font-size:0.75rem;color:var(--text-muted)">${s.connected_at.slice(0,19)}</div>
|
<div style="font-size:0.75rem;color:var(--text-muted)">${(s.connected_at||'').slice(0,19)}</div>
|
||||||
</div></div>`).join('');
|
</div></div>`).join('');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -90,7 +90,7 @@ let discPoll=null;
|
|||||||
function switchTab(name){
|
function switchTab(name){
|
||||||
document.querySelectorAll('.tab').forEach((t,i)=>t.classList.toggle('active',['discover','map','scans'][i]===name));
|
document.querySelectorAll('.tab').forEach((t,i)=>t.classList.toggle('active',['discover','map','scans'][i]===name));
|
||||||
document.querySelectorAll('.tab-content').forEach(c=>c.style.display='none');
|
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==='scans') loadScans();
|
if(name==='scans') loadScans();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -201,7 +201,7 @@ function switchTab(name){
|
|||||||
document.querySelectorAll('.tab').forEach((t,i)=>t.classList.toggle('active',
|
document.querySelectorAll('.tab').forEach((t,i)=>t.classList.toggle('active',
|
||||||
['identify','crack','generate','spray','wordlists'][i]===name));
|
['identify','crack','generate','spray','wordlists'][i]===name));
|
||||||
document.querySelectorAll('.tab-content').forEach(c=>c.style.display='none');
|
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==='crack') loadTools();
|
if(name==='crack') loadTools();
|
||||||
if(name==='wordlists') loadWordlists();
|
if(name==='wordlists') loadWordlists();
|
||||||
}
|
}
|
||||||
|
|||||||
402
web/templates/port_scanner.html
Normal file
402
web/templates/port_scanner.html
Normal file
@ -0,0 +1,402 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
{% block title %}Port Scanner — AUTARCH{% endblock %}
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
<h1>Port Scanner</h1>
|
||||||
|
<p class="text-muted" style="font-size:0.85rem;color:var(--text-secondary)">
|
||||||
|
Advanced TCP port scanner with nmap integration and real-time live output
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:grid;grid-template-columns:380px 1fr;gap:1.25rem;align-items:start">
|
||||||
|
|
||||||
|
<!-- ── Config Panel ── -->
|
||||||
|
<div>
|
||||||
|
<div class="card">
|
||||||
|
<h3>Scan Configuration</h3>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Target Host / IP</label>
|
||||||
|
<input type="text" id="ps-target" class="form-control"
|
||||||
|
placeholder="192.168.1.1 or hostname.local"
|
||||||
|
onkeypress="if(event.key==='Enter')startScan()">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Scan Mode</label>
|
||||||
|
<select id="ps-mode" class="form-control" onchange="onModeChange()">
|
||||||
|
<option value="quick">Quick — 22 common ports (~1s)</option>
|
||||||
|
<option value="common" selected>Common — 110 well-known ports (~5s)</option>
|
||||||
|
<option value="full">Full — All 65,535 ports (may take minutes)</option>
|
||||||
|
<option value="custom">Custom — Specify ports / ranges</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group" id="custom-ports-group" style="display:none">
|
||||||
|
<label>Ports / Ranges (e.g. 22,80,443,8000-9000)</label>
|
||||||
|
<input type="text" id="ps-custom-ports" class="form-control"
|
||||||
|
placeholder="22,80,443,1024-2048">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Options -->
|
||||||
|
<div style="padding:12px;background:var(--bg-primary);border:1px solid var(--border);border-radius:var(--radius);margin-bottom:14px">
|
||||||
|
<div style="font-size:0.75rem;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.05em;margin-bottom:10px">Options</div>
|
||||||
|
|
||||||
|
<div style="display:flex;flex-wrap:wrap;gap:10px;margin-bottom:10px">
|
||||||
|
<label style="display:flex;align-items:center;gap:6px;font-size:0.83rem;cursor:pointer">
|
||||||
|
<input type="checkbox" id="opt-nmap" style="width:auto"> Use nmap
|
||||||
|
</label>
|
||||||
|
<label style="display:flex;align-items:center;gap:6px;font-size:0.83rem;cursor:pointer">
|
||||||
|
<input type="checkbox" id="opt-svc" style="width:auto"> Service detection (-sV)
|
||||||
|
</label>
|
||||||
|
<label style="display:flex;align-items:center;gap:6px;font-size:0.83rem;cursor:pointer">
|
||||||
|
<input type="checkbox" id="opt-os" style="width:auto"> OS fingerprint (-O)
|
||||||
|
</label>
|
||||||
|
<label style="display:flex;align-items:center;gap:6px;font-size:0.83rem;cursor:pointer">
|
||||||
|
<input type="checkbox" id="opt-agg" style="width:auto"> Aggressive (-A)
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:8px">
|
||||||
|
<div>
|
||||||
|
<div style="font-size:0.75rem;color:var(--text-muted);margin-bottom:3px">Timeout (s)</div>
|
||||||
|
<input type="number" id="opt-timeout" class="form-control" value="1.0" min="0.1" max="10" step="0.1">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style="font-size:0.75rem;color:var(--text-muted);margin-bottom:3px">Concurrency</div>
|
||||||
|
<input type="number" id="opt-concurrency" class="form-control" value="200" min="1" max="500">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style="font-size:0.75rem;color:var(--text-muted);margin-bottom:3px">nmap Timing</div>
|
||||||
|
<select id="opt-timing" class="form-control">
|
||||||
|
<option value="2">T2 — Polite</option>
|
||||||
|
<option value="3">T3 — Normal</option>
|
||||||
|
<option value="4" selected>T4 — Aggressive</option>
|
||||||
|
<option value="5">T5 — Insane</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:flex;gap:8px">
|
||||||
|
<button id="ps-start-btn" class="btn btn-primary" style="flex:1" onclick="startScan()">
|
||||||
|
▶ Start Scan
|
||||||
|
</button>
|
||||||
|
<button id="ps-cancel-btn" class="btn btn-danger btn-small" style="display:none"
|
||||||
|
onclick="cancelScan()">Cancel</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stats Card -->
|
||||||
|
<div class="card" id="stats-card" style="display:none">
|
||||||
|
<h3>Scan Stats</h3>
|
||||||
|
<table style="width:100%;font-size:0.85rem">
|
||||||
|
<tr><td style="color:var(--text-muted);padding:3px 0">Target</td>
|
||||||
|
<td id="st-target" style="text-align:right;color:var(--accent)"></td></tr>
|
||||||
|
<tr><td style="color:var(--text-muted);padding:3px 0">Ports Scanned</td>
|
||||||
|
<td id="st-scanned" style="text-align:right"></td></tr>
|
||||||
|
<tr><td style="color:var(--text-muted);padding:3px 0">Open Ports</td>
|
||||||
|
<td id="st-open" style="text-align:right;color:var(--success);font-weight:700"></td></tr>
|
||||||
|
<tr><td style="color:var(--text-muted);padding:3px 0">Duration</td>
|
||||||
|
<td id="st-duration" style="text-align:right"></td></tr>
|
||||||
|
<tr id="st-os-row" style="display:none">
|
||||||
|
<td style="color:var(--text-muted);padding:3px 0">OS Guess</td>
|
||||||
|
<td id="st-os" style="text-align:right;font-size:0.8rem"></td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ── Right Panel — Output + Results ── -->
|
||||||
|
<div>
|
||||||
|
<!-- Progress bar -->
|
||||||
|
<div id="ps-progress-wrap" style="display:none;margin-bottom:1rem">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:6px">
|
||||||
|
<span id="ps-progress-label" style="font-size:0.82rem;color:var(--text-secondary)">Initializing...</span>
|
||||||
|
<span id="ps-progress-pct" style="font-size:0.82rem;font-weight:600;color:var(--accent)">0%</span>
|
||||||
|
</div>
|
||||||
|
<div style="background:var(--bg-input);border-radius:4px;height:6px;overflow:hidden">
|
||||||
|
<div id="ps-progress-bar" style="height:100%;background:var(--accent);
|
||||||
|
transition:width 0.3s;border-radius:4px;width:0%"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Live output box -->
|
||||||
|
<div class="card" style="padding:0;overflow:hidden">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;
|
||||||
|
padding:10px 16px;border-bottom:1px solid var(--border)">
|
||||||
|
<span style="font-size:0.8rem;font-weight:600;letter-spacing:0.05em">LIVE OUTPUT</span>
|
||||||
|
<div style="display:flex;gap:6px;align-items:center">
|
||||||
|
<span id="ps-status-dot" style="width:8px;height:8px;border-radius:50%;
|
||||||
|
background:var(--text-muted);display:inline-block"></span>
|
||||||
|
<span id="ps-status-txt" style="font-size:0.75rem;color:var(--text-muted)">Idle</span>
|
||||||
|
<button class="btn btn-small" style="font-size:0.7rem;padding:3px 8px"
|
||||||
|
onclick="clearOutput()">Clear</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<pre id="ps-output" style="background:var(--bg-primary);color:#c8d3f5;
|
||||||
|
font-family:'Cascadia Code','Fira Code','Consolas',monospace;
|
||||||
|
font-size:0.78rem;line-height:1.5;padding:14px;margin:0;
|
||||||
|
height:340px;overflow-y:auto;white-space:pre-wrap;word-break:break-all"></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Results table -->
|
||||||
|
<div id="ps-results-wrap" style="display:none;margin-top:1rem">
|
||||||
|
<div class="card" style="padding:0;overflow:hidden">
|
||||||
|
<div style="display:flex;justify-content:space-between;align-items:center;
|
||||||
|
padding:12px 16px;border-bottom:1px solid var(--border)">
|
||||||
|
<span style="font-weight:600;font-size:0.9rem">
|
||||||
|
Open Ports — <span id="res-count" style="color:var(--success)">0</span> found
|
||||||
|
</span>
|
||||||
|
<div style="display:flex;gap:6px">
|
||||||
|
<button class="btn btn-small" onclick="exportResults('txt')">Export TXT</button>
|
||||||
|
<button class="btn btn-small" onclick="exportResults('json')">Export JSON</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<table class="data-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th style="width:80px">Port</th>
|
||||||
|
<th style="width:70px">Proto</th>
|
||||||
|
<th style="width:150px">Service</th>
|
||||||
|
<th>Banner / Version</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="ps-results-body"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#ps-output .ln-info { color: #c8d3f5; }
|
||||||
|
#ps-output .ln-open { color: #4ade80; font-weight: 700; }
|
||||||
|
#ps-output .ln-warn { color: #fbbf24; }
|
||||||
|
#ps-output .ln-error { color: #f87171; }
|
||||||
|
#ps-output .ln-nmap { color: #94a3b8; font-size: 0.73rem; }
|
||||||
|
#ps-output .ln-prog { color: #818cf8; }
|
||||||
|
#ps-output .ln-done { color: #34d399; font-weight: 700; }
|
||||||
|
#ps-output .ln-muted { color: #475569; }
|
||||||
|
.advanced-toggle { font-size: 0.82rem; color: var(--text-secondary); cursor: pointer;
|
||||||
|
user-select: none; display: inline-flex; align-items: center; gap: 4px; }
|
||||||
|
.advanced-toggle:hover { color: var(--text-primary); }
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let _jobId = null;
|
||||||
|
let _es = null;
|
||||||
|
let _openPorts = [];
|
||||||
|
let _scanResult = null;
|
||||||
|
|
||||||
|
function onModeChange() {
|
||||||
|
const mode = document.getElementById('ps-mode').value;
|
||||||
|
document.getElementById('custom-ports-group').style.display =
|
||||||
|
mode === 'custom' ? '' : 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function startScan() {
|
||||||
|
const target = document.getElementById('ps-target').value.trim();
|
||||||
|
if (!target) { alert('Enter a target host or IP address'); return; }
|
||||||
|
const mode = document.getElementById('ps-mode').value;
|
||||||
|
if (mode === 'custom' && !document.getElementById('ps-custom-ports').value.trim()) {
|
||||||
|
alert('Enter ports or ranges for custom mode'); return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_openPorts = [];
|
||||||
|
_scanResult = null;
|
||||||
|
clearOutput();
|
||||||
|
document.getElementById('ps-results-wrap').style.display = 'none';
|
||||||
|
document.getElementById('stats-card').style.display = 'none';
|
||||||
|
document.getElementById('ps-progress-wrap').style.display = '';
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
target,
|
||||||
|
mode,
|
||||||
|
ports: document.getElementById('ps-custom-ports').value.trim(),
|
||||||
|
use_nmap: document.getElementById('opt-nmap').checked,
|
||||||
|
service_detection: document.getElementById('opt-svc').checked,
|
||||||
|
os_detection: document.getElementById('opt-os').checked,
|
||||||
|
aggressive: document.getElementById('opt-agg').checked,
|
||||||
|
timing: document.getElementById('opt-timing').value,
|
||||||
|
timeout: parseFloat(document.getElementById('opt-timeout').value) || 1.0,
|
||||||
|
concurrency: parseInt(document.getElementById('opt-concurrency').value) || 200,
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch('/port-scanner/start', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
}).then(r => r.json()).then(d => {
|
||||||
|
if (!d.ok) { appendLine('error', '✗ ' + d.error); return; }
|
||||||
|
_jobId = d.job_id;
|
||||||
|
setStatus('scanning', 'Scanning...');
|
||||||
|
document.getElementById('ps-start-btn').disabled = true;
|
||||||
|
document.getElementById('ps-cancel-btn').style.display = '';
|
||||||
|
appendLine('info', `► Scan started | job=${d.job_id} | ${d.port_count} ports queued`);
|
||||||
|
openStream(d.job_id);
|
||||||
|
}).catch(e => appendLine('error', '✗ ' + e.message));
|
||||||
|
}
|
||||||
|
|
||||||
|
function openStream(jobId) {
|
||||||
|
if (_es) { _es.close(); _es = null; }
|
||||||
|
_es = new EventSource('/port-scanner/stream/' + jobId);
|
||||||
|
_es.onmessage = e => handleEvent(JSON.parse(e.data));
|
||||||
|
_es.onerror = () => { setStatus('idle', 'Connection lost'); };
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEvent(ev) {
|
||||||
|
switch (ev.type) {
|
||||||
|
case 'start':
|
||||||
|
appendLine('info', `▶ Target: ${ev.target} | Ports: ${ev.total_ports} | Mode: ${ev.mode}`);
|
||||||
|
setProgress(0, ev.total_ports, '0%', 'Starting...');
|
||||||
|
document.getElementById('st-target').textContent = ev.target;
|
||||||
|
document.getElementById('st-scanned').textContent = ev.total_ports;
|
||||||
|
document.getElementById('stats-card').style.display = '';
|
||||||
|
break;
|
||||||
|
case 'info':
|
||||||
|
appendLine('info', ' ' + ev.msg);
|
||||||
|
break;
|
||||||
|
case 'warning':
|
||||||
|
appendLine('warn', '⚠ ' + ev.msg);
|
||||||
|
break;
|
||||||
|
case 'error':
|
||||||
|
appendLine('error', '✗ ' + ev.msg);
|
||||||
|
scanFinished();
|
||||||
|
break;
|
||||||
|
case 'progress':
|
||||||
|
setProgress(ev.current, ev.total, ev.pct + '%', `Port ${ev.port} | ${ev.open_count} open | ETA ${ev.eta}`);
|
||||||
|
break;
|
||||||
|
case 'port_open':
|
||||||
|
_openPorts.push(ev);
|
||||||
|
const svc = ev.service || 'unknown';
|
||||||
|
const banner = ev.banner ? ` — ${ev.banner.substring(0, 60)}` : '';
|
||||||
|
appendLine('open', ` ✔ ${String(ev.port).padEnd(6)} ${svc.padEnd(18)}${banner}`);
|
||||||
|
addResultRow(ev);
|
||||||
|
document.getElementById('res-count').textContent = _openPorts.length;
|
||||||
|
document.getElementById('st-open').textContent = _openPorts.length;
|
||||||
|
document.getElementById('ps-results-wrap').style.display = '';
|
||||||
|
break;
|
||||||
|
case 'nmap_start':
|
||||||
|
appendLine('info', ` nmap: ${ev.cmd}`);
|
||||||
|
break;
|
||||||
|
case 'nmap_line':
|
||||||
|
if (ev.line.trim()) appendLine('nmap', ' ' + ev.line);
|
||||||
|
break;
|
||||||
|
case 'done':
|
||||||
|
appendLine('done',
|
||||||
|
`\n✔ SCAN COMPLETE — ${ev.open_count} open ports found in ${ev.duration}s ` +
|
||||||
|
`(${ev.ports_scanned} ports scanned)`
|
||||||
|
);
|
||||||
|
if (ev.os_guess) {
|
||||||
|
appendLine('info', ` OS: ${ev.os_guess}`);
|
||||||
|
document.getElementById('st-os-row').style.display = '';
|
||||||
|
document.getElementById('st-os').textContent = ev.os_guess;
|
||||||
|
}
|
||||||
|
document.getElementById('st-duration').textContent = ev.duration + 's';
|
||||||
|
setProgress(100, 100, '100%', 'Complete');
|
||||||
|
scanFinished();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function addResultRow(port) {
|
||||||
|
const tbody = document.getElementById('ps-results-body');
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
const banner = port.banner
|
||||||
|
? `<span style="font-family:monospace;font-size:0.75rem;color:var(--text-secondary)">${esc(port.banner.substring(0, 120))}</span>`
|
||||||
|
: '<span style="color:var(--text-muted)">—</span>';
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td><strong style="color:var(--success)">${port.port}</strong></td>
|
||||||
|
<td style="color:var(--text-muted)">${port.protocol}</td>
|
||||||
|
<td>${esc(port.service || '—')}</td>
|
||||||
|
<td>${banner}</td>`;
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelScan() {
|
||||||
|
if (!_jobId) return;
|
||||||
|
fetch('/port-scanner/cancel/' + _jobId, { method: 'POST' });
|
||||||
|
appendLine('warn', '⚠ Scan cancelled by user');
|
||||||
|
scanFinished();
|
||||||
|
}
|
||||||
|
|
||||||
|
function scanFinished() {
|
||||||
|
if (_es) { _es.close(); _es = null; }
|
||||||
|
document.getElementById('ps-start-btn').disabled = false;
|
||||||
|
document.getElementById('ps-cancel-btn').style.display = 'none';
|
||||||
|
setStatus('idle', _openPorts.length + ' open ports');
|
||||||
|
_jobId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setStatus(state, text) {
|
||||||
|
const dot = document.getElementById('ps-status-dot');
|
||||||
|
const txt = document.getElementById('ps-status-txt');
|
||||||
|
txt.textContent = text;
|
||||||
|
dot.style.background = state === 'scanning' ? 'var(--success)'
|
||||||
|
: state === 'error' ? 'var(--danger)'
|
||||||
|
: 'var(--text-muted)';
|
||||||
|
if (state === 'scanning') {
|
||||||
|
dot.style.animation = 'ps-pulse 1.2s ease infinite';
|
||||||
|
} else {
|
||||||
|
dot.style.animation = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setProgress(current, total, pct, label) {
|
||||||
|
document.getElementById('ps-progress-bar').style.width = (typeof pct === 'string' ? pct : pct + '%');
|
||||||
|
document.getElementById('ps-progress-pct').textContent = (typeof pct === 'string' ? pct : pct + '%');
|
||||||
|
document.getElementById('ps-progress-label').textContent = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendLine(cls, text) {
|
||||||
|
const out = document.getElementById('ps-output');
|
||||||
|
const el = document.createElement('span');
|
||||||
|
el.className = 'ln-' + cls;
|
||||||
|
el.textContent = text + '\n';
|
||||||
|
out.appendChild(el);
|
||||||
|
out.scrollTop = out.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearOutput() {
|
||||||
|
document.getElementById('ps-output').innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportResults(fmt) {
|
||||||
|
if (!_openPorts.length) { alert('No results to export'); return; }
|
||||||
|
let content, mime, ext;
|
||||||
|
if (fmt === 'json') {
|
||||||
|
content = JSON.stringify({ target: document.getElementById('ps-target').value,
|
||||||
|
scan_time: new Date().toISOString(),
|
||||||
|
open_ports: _openPorts }, null, 2);
|
||||||
|
mime = 'application/json'; ext = 'json';
|
||||||
|
} else {
|
||||||
|
const target = document.getElementById('ps-target').value;
|
||||||
|
const lines = [`# AUTARCH Port Scan Results`, `# Target: ${target}`,
|
||||||
|
`# Date: ${new Date().toISOString()}`, `# Open Ports: ${_openPorts.length}`, ''];
|
||||||
|
_openPorts.forEach(p => {
|
||||||
|
lines.push(`${p.port}/tcp\topen\t${p.service || 'unknown'}\t${p.banner || ''}`);
|
||||||
|
});
|
||||||
|
content = lines.join('\n'); mime = 'text/plain'; ext = 'txt';
|
||||||
|
}
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = URL.createObjectURL(new Blob([content], { type: mime }));
|
||||||
|
a.download = `scan_${document.getElementById('ps-target').value}_${Date.now()}.${ext}`;
|
||||||
|
a.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function esc(s) {
|
||||||
|
return s ? String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>') : '';
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@keyframes ps-pulse {
|
||||||
|
0%, 100% { opacity: 1; }
|
||||||
|
50% { opacity: 0.4; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
@ -104,7 +104,7 @@ let currentReportId=null;
|
|||||||
function switchTab(name){
|
function switchTab(name){
|
||||||
document.querySelectorAll('.tab').forEach((t,i)=>t.classList.toggle('active',['reports','editor','templates'][i]===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.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==='reports') loadReports();
|
||||||
if(name==='templates') loadTemplates();
|
if(name==='templates') loadTemplates();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -323,7 +323,7 @@ function switchTab(name){
|
|||||||
document.querySelectorAll('.tab').forEach((t,i)=>t.classList.toggle('active',
|
document.querySelectorAll('.tab').forEach((t,i)=>t.classList.toggle('active',
|
||||||
['harvest','pretexts','qr','campaigns'][i]===name));
|
['harvest','pretexts','qr','campaigns'][i]===name));
|
||||||
document.querySelectorAll('.tab-content').forEach(c=>c.style.display='none');
|
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==='harvest'){loadPages();loadCaptures();}
|
||||||
if(name==='pretexts') loadPretexts();
|
if(name==='pretexts') loadPretexts();
|
||||||
if(name==='campaigns') loadCampaigns();
|
if(name==='campaigns') loadCampaigns();
|
||||||
|
|||||||
@ -110,7 +110,7 @@ function switchTab(name){
|
|||||||
document.querySelectorAll('.tab').forEach((t,i)=>t.classList.toggle('active',
|
document.querySelectorAll('.tab').forEach((t,i)=>t.classList.toggle('active',
|
||||||
['quick','dirbust','subdomain','vuln','crawl'][i]===name));
|
['quick','dirbust','subdomain','vuln','crawl'][i]===name));
|
||||||
document.querySelectorAll('.tab-content').forEach(c=>c.style.display='none');
|
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(){
|
function quickScan(){
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user