Add Android forensics, IOC threat-intel DB, Compose companion; scrub secrets from configs
This commit is contained in:
367
web/routes/android_forensics.py
Normal file
367
web/routes/android_forensics.py
Normal file
@@ -0,0 +1,367 @@
|
||||
"""Autarch Android Forensics routes — forensic acquisition and IOC scanning."""
|
||||
|
||||
from datetime import datetime
|
||||
from flask import Blueprint, render_template, request, jsonify
|
||||
from web.auth import login_required
|
||||
|
||||
android_forensics_bp = Blueprint('android_forensics', __name__, url_prefix='/android-forensics')
|
||||
|
||||
|
||||
def _mgr():
|
||||
from core.android_forensics import get_android_forensics_manager
|
||||
return get_android_forensics_manager()
|
||||
|
||||
|
||||
def _body():
|
||||
return request.get_json(silent=True) or {}
|
||||
|
||||
|
||||
def _serial():
|
||||
data = _body()
|
||||
serial = data.get('serial') or request.form.get('serial') or request.args.get('serial', '')
|
||||
return str(serial).strip() if serial else ''
|
||||
|
||||
|
||||
# ── Main Page ────────────────────────────────────────────────────────
|
||||
|
||||
@android_forensics_bp.route('/')
|
||||
@login_required
|
||||
def index():
|
||||
from core.hardware import get_hardware_manager
|
||||
hw = get_hardware_manager()
|
||||
status = hw.get_status()
|
||||
ioc_stats = _mgr().get_ioc_stats()
|
||||
return render_template('android_forensics.html', status=status, ioc_stats=ioc_stats)
|
||||
|
||||
|
||||
# ── Acquisition Routes ───────────────────────────────────────────────
|
||||
|
||||
@android_forensics_bp.route('/acquire/<module>', methods=['POST'])
|
||||
@login_required
|
||||
def acquire(module):
|
||||
serial = _serial()
|
||||
if not serial:
|
||||
return jsonify({'error': 'No serial provided'})
|
||||
func_map = {
|
||||
'packages': _mgr().acquire_packages,
|
||||
'processes': _mgr().acquire_processes,
|
||||
'getprop': _mgr().acquire_getprop,
|
||||
'settings': _mgr().acquire_settings,
|
||||
'services': _mgr().acquire_services,
|
||||
'dumpsys': _mgr().acquire_dumpsys,
|
||||
'logcat': _mgr().acquire_logcat,
|
||||
'tmp_files': _mgr().acquire_tmp_files,
|
||||
}
|
||||
func = func_map.get(module)
|
||||
if not func:
|
||||
return jsonify({'error': f'Unknown module: {module}'})
|
||||
return jsonify(func(serial))
|
||||
|
||||
|
||||
@android_forensics_bp.route('/acquire/full', methods=['POST'])
|
||||
@login_required
|
||||
def acquire_full():
|
||||
serial = _serial()
|
||||
if not serial:
|
||||
return jsonify({'error': 'No serial provided'})
|
||||
return jsonify(_mgr().full_acquisition(serial))
|
||||
|
||||
|
||||
# ── IOC Scan Routes ──────────────────────────────────────────────────
|
||||
|
||||
@android_forensics_bp.route('/scan/apps', methods=['POST'])
|
||||
@login_required
|
||||
def scan_apps():
|
||||
serial = _serial()
|
||||
if not serial:
|
||||
return jsonify({'error': 'No serial provided'})
|
||||
return jsonify(_mgr().scan_packages(serial))
|
||||
|
||||
|
||||
@android_forensics_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_forensics_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_indicators(serial))
|
||||
|
||||
|
||||
@android_forensics_bp.route('/scan/files', methods=['POST'])
|
||||
@login_required
|
||||
def scan_files():
|
||||
serial = _serial()
|
||||
if not serial:
|
||||
return jsonify({'error': 'No serial provided'})
|
||||
return jsonify(_mgr().scan_file_indicators(serial))
|
||||
|
||||
|
||||
@android_forensics_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_process_indicators(serial))
|
||||
|
||||
|
||||
@android_forensics_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_acquisition(serial))
|
||||
|
||||
|
||||
# ── Report Routes ────────────────────────────────────────────────────
|
||||
|
||||
@android_forensics_bp.route('/report/list', methods=['POST'])
|
||||
@login_required
|
||||
def report_list():
|
||||
serial = _serial()
|
||||
if not serial:
|
||||
return jsonify({'error': 'No serial provided'})
|
||||
return jsonify({'ok': True, 'acquisitions': _mgr().get_acquisition_list(serial)})
|
||||
|
||||
|
||||
@android_forensics_bp.route('/report/view', methods=['POST'])
|
||||
@login_required
|
||||
def report_view():
|
||||
data = _body()
|
||||
serial = data.get('serial', '').strip()
|
||||
acq_id = data.get('acq_id', '').strip()
|
||||
if not serial or not acq_id:
|
||||
return jsonify({'error': 'serial and acq_id required'})
|
||||
return jsonify(_mgr().export_report(serial, acq_id))
|
||||
|
||||
|
||||
# ── Database Routes ──────────────────────────────────────────────────
|
||||
|
||||
@android_forensics_bp.route('/db/stats', methods=['GET'])
|
||||
@login_required
|
||||
def db_stats():
|
||||
return jsonify(_mgr().get_ioc_stats())
|
||||
|
||||
|
||||
@android_forensics_bp.route('/db/reload', methods=['POST'])
|
||||
@login_required
|
||||
def db_reload():
|
||||
db = _mgr().reload_ioc_database()
|
||||
return jsonify({
|
||||
'ok': True,
|
||||
'counts': {k: len(v) for k, v in db.items() if isinstance(v, (dict, list))},
|
||||
})
|
||||
|
||||
|
||||
@android_forensics_bp.route('/ioc/export', methods=['GET'])
|
||||
@login_required
|
||||
def ioc_export():
|
||||
"""Compact IOC export for companion app caching."""
|
||||
db = _mgr().load_ioc_database()
|
||||
return jsonify({
|
||||
'packages': list(db.get('packages', {}).keys()),
|
||||
'domains': list(db.get('domains', {}).keys()),
|
||||
'hashes': list(db.get('hashes', {}).keys()),
|
||||
'ips': list(db.get('ips', [])),
|
||||
'generated': datetime.now().isoformat(),
|
||||
'counts': {
|
||||
'packages': len(db.get('packages', {})),
|
||||
'domains': len(db.get('domains', {})),
|
||||
'hashes': len(db.get('hashes', {})),
|
||||
'ips': len(db.get('ips', [])),
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
# ── Direct (WebUSB) Mode ─────────────────────────────────────────────
|
||||
# Op keys match the JS URL→op transformation:
|
||||
# url.replace('/android-forensics/', '').replace(/\//g, '_')
|
||||
# So /acquire/packages → acquire_packages, /scan/apps → scan_apps, etc.
|
||||
|
||||
_DIRECT_COMMANDS = {
|
||||
'acquire_packages': {
|
||||
'packages': 'pm list packages -f',
|
||||
'packages_sys': 'pm list packages -s',
|
||||
},
|
||||
'acquire_processes': {
|
||||
'processes': 'ps -A',
|
||||
},
|
||||
'acquire_getprop': {
|
||||
'getprop': 'getprop',
|
||||
},
|
||||
'acquire_settings': {
|
||||
'settings_global': 'settings list global',
|
||||
'settings_secure': 'settings list secure',
|
||||
'settings_system': 'settings list system',
|
||||
},
|
||||
'acquire_services': {
|
||||
'services': 'service list',
|
||||
},
|
||||
'acquire_logcat': {
|
||||
'logcat': 'logcat -d -t 1000',
|
||||
},
|
||||
'acquire_tmp_files': {
|
||||
'tmp_files': 'ls -la /data/local/tmp/ 2>/dev/null',
|
||||
},
|
||||
'scan_apps': {
|
||||
'packages': 'pm list packages -f',
|
||||
'packages_sys': 'pm list packages -s',
|
||||
},
|
||||
'scan_network': {
|
||||
'tcp_connections': 'cat /proc/net/tcp 2>/dev/null',
|
||||
'dns1': 'getprop net.dns1',
|
||||
'dns2': 'getprop net.dns2',
|
||||
},
|
||||
'scan_processes': {
|
||||
'processes': 'ps -A',
|
||||
},
|
||||
'scan_files': {
|
||||
# file artifact scanning requires server-side IOC DB; not supported in direct mode
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@android_forensics_bp.route('/cmd', methods=['POST'])
|
||||
@login_required
|
||||
def direct_cmd():
|
||||
"""Return ADB shell commands for Direct (WebUSB) mode."""
|
||||
data = _body()
|
||||
op = data.get('op', '').replace('/', '_').replace('-', '_')
|
||||
cmds = _DIRECT_COMMANDS.get(op)
|
||||
if cmds:
|
||||
return jsonify({'commands': cmds, 'supported': True})
|
||||
return jsonify({'commands': {}, 'supported': False,
|
||||
'reason': 'This operation requires Server mode.'})
|
||||
|
||||
|
||||
@android_forensics_bp.route('/parse', methods=['POST'])
|
||||
@login_required
|
||||
def direct_parse():
|
||||
"""Parse raw ADB output from Direct (WebUSB) mode."""
|
||||
import re
|
||||
data = _body()
|
||||
op = data.get('op', '').replace('/', '_').replace('-', '_')
|
||||
raw = data.get('raw', {})
|
||||
mgr = _mgr()
|
||||
|
||||
def _parse_packages(pkg_output, sys_output=''):
|
||||
sys_pkgs = set()
|
||||
for line in (sys_output or '').splitlines():
|
||||
line = line.strip()
|
||||
if line.startswith('package:'):
|
||||
sys_pkgs.add(line[8:].strip())
|
||||
pkgs = []
|
||||
for line in (pkg_output or '').splitlines():
|
||||
line = line.strip()
|
||||
if not line.startswith('package:'):
|
||||
continue
|
||||
line = line[8:]
|
||||
eq_pos = line.rfind('=')
|
||||
if eq_pos == -1:
|
||||
pkg_name, apk_path = line, ''
|
||||
else:
|
||||
apk_path, pkg_name = line[:eq_pos], line[eq_pos + 1:]
|
||||
pkg_name = pkg_name.strip()
|
||||
if pkg_name:
|
||||
pkgs.append({'package': pkg_name, 'path': apk_path.strip(),
|
||||
'system': pkg_name in sys_pkgs})
|
||||
return pkgs
|
||||
|
||||
try:
|
||||
if op in ('acquire_packages',):
|
||||
pkgs = _parse_packages(raw.get('packages', ''), raw.get('packages_sys', ''))
|
||||
return jsonify({'ok': True, 'packages': pkgs, 'total': len(pkgs)})
|
||||
|
||||
elif op == 'scan_apps':
|
||||
pkgs = _parse_packages(raw.get('packages', ''), raw.get('packages_sys', ''))
|
||||
return jsonify(mgr.scan_packages(None, packages_data=pkgs))
|
||||
|
||||
elif op == 'acquire_processes':
|
||||
lines = [l for l in (raw.get('processes', '') or '').splitlines() if l.strip()]
|
||||
return jsonify({'ok': True, 'output': raw.get('processes', ''),
|
||||
'count': max(0, len(lines) - 1)})
|
||||
|
||||
elif op == 'acquire_getprop':
|
||||
props = {}
|
||||
for line in (raw.get('getprop', '') or '').splitlines():
|
||||
m = re.match(r'\[(.+)\]:\s*\[(.*)\]\s*$', line)
|
||||
if m:
|
||||
props[m.group(1)] = m.group(2)
|
||||
return jsonify({'ok': True, 'props': props})
|
||||
|
||||
elif op == 'acquire_settings':
|
||||
result = {}
|
||||
for ns in ('global', 'secure', 'system'):
|
||||
settings = {}
|
||||
for line in (raw.get(f'settings_{ns}', '') or '').splitlines():
|
||||
if '=' in line:
|
||||
k, _, v = line.partition('=')
|
||||
settings[k.strip()] = v.strip()
|
||||
result[ns] = settings
|
||||
return jsonify({'ok': True, 'settings': result})
|
||||
|
||||
elif op in ('acquire_services', 'acquire_logcat', 'acquire_tmp_files'):
|
||||
key = op.replace('acquire_', '')
|
||||
out = raw.get(key, '')
|
||||
return jsonify({'ok': True, 'output': out, 'lines': len((out or '').splitlines())})
|
||||
|
||||
elif op == 'scan_network':
|
||||
db = mgr.load_ioc_database()
|
||||
findings = []
|
||||
ioc_ips = set(db.get('ips', []))
|
||||
# Check DNS properties
|
||||
for dns_key in ('dns1', 'dns2'):
|
||||
val = (raw.get(dns_key, '') or '').strip().lower()
|
||||
if val and val in db.get('domains', {}):
|
||||
match = db['domains'][val]
|
||||
findings.append({'type': 'dns_property', 'key': dns_key, 'value': val,
|
||||
'threat_name': match['name'], 'source': match['source'],
|
||||
'severity': 'high'})
|
||||
# Check TCP connections
|
||||
if ioc_ips:
|
||||
for line in (raw.get('tcp_connections', '') or '').splitlines()[1:]:
|
||||
parts = line.strip().split()
|
||||
if len(parts) < 3:
|
||||
continue
|
||||
try:
|
||||
remote_hex = parts[2]
|
||||
ip_hex, port_hex = remote_hex.split(':')
|
||||
ip = '.'.join(str(int(ip_hex[i:i+2], 16)) for i in (6, 4, 2, 0))
|
||||
port = int(port_hex, 16)
|
||||
if ip not in ('0.0.0.0', '127.0.0.1') and ip in ioc_ips:
|
||||
findings.append({'type': 'tcp_connection', 'remote_ip': ip,
|
||||
'remote_port': port, 'severity': 'critical',
|
||||
'threat_name': 'Known malicious IP',
|
||||
'source': 'network_scan'})
|
||||
except Exception:
|
||||
pass
|
||||
return jsonify({'ok': True, 'findings': findings, 'clean': len(findings) == 0})
|
||||
|
||||
elif op == 'scan_processes':
|
||||
db = mgr.load_ioc_database()
|
||||
proc_output = (raw.get('processes', '') or '').lower()
|
||||
findings = []
|
||||
for proc_name, match in db.get('processes', {}).items():
|
||||
if proc_name and proc_name in proc_output:
|
||||
findings.append({'type': 'process_match', 'process': proc_name,
|
||||
'threat_name': match['name'], 'source': match['source'],
|
||||
'severity': 'critical'})
|
||||
return jsonify({'ok': True, 'findings': findings, 'clean': len(findings) == 0})
|
||||
|
||||
else:
|
||||
key = op.replace('acquire_', '')
|
||||
out = raw.get(key, '') if isinstance(raw, dict) else ''
|
||||
return jsonify({'ok': True, 'output': out})
|
||||
|
||||
except Exception as exc:
|
||||
return jsonify({'error': str(exc)})
|
||||
Reference in New Issue
Block a user