Files
autarch/web/routes/android_forensics_ai.py

315 lines
13 KiB
Python
Raw Permalink Normal View History

"""Autarch Android Forensics AI — threat analysis, CVE lookup, heuristic scanning, and alert receiver."""
from datetime import datetime
from flask import Blueprint, request, jsonify
from web.auth import login_required
android_forensics_ai_bp = Blueprint('android_forensics_ai', __name__, url_prefix='/android-forensics/ai')
_alerts = [] # in-memory alert store (last 500)
_MAX_ALERTS = 500
def _mgr():
from core.android_forensics import get_android_forensics_manager
return get_android_forensics_manager()
def _cve():
from core.cve_db import get_cve_db
return get_cve_db()
def _llm():
from core.llm import get_llm
return get_llm()
def _body():
return request.get_json(silent=True) or {}
# ── AI Analysis ──────────────────────────────────────────────────────
@android_forensics_ai_bp.route('/analyze', methods=['POST'])
@login_required
def analyze():
data = _body()
serial = data.get('serial', '').strip()
acq_id = data.get('acq_id', '').strip()
backend = data.get('backend', 'autarch')
focus = data.get('focus', ['all'])
try:
# Load acquisition report from disk, fall back to live acquisition
report = None
if serial and acq_id:
exported = _mgr().export_report(serial, acq_id)
report = exported.get('report') if exported else None
if not report:
report = _mgr().full_acquisition(serial) if serial else {}
# Build context dict from report
packages = report.get('packages', [])
processes = report.get('processes', {})
proc_output = processes.get('output', '') if isinstance(processes, dict) else str(processes)
proc_lines = proc_output.splitlines()[:50]
props = report.get('getprop', {})
model = props.get('ro.product.model', 'Unknown') if isinstance(props, dict) else 'Unknown'
android_version = props.get('ro.build.version.release', 'Unknown') if isinstance(props, dict) else 'Unknown'
network_info = report.get('network', {})
ioc_findings = report.get('ioc_findings', report.get('findings', []))
pkg_list = packages[:100] if isinstance(packages, list) else []
user_pkgs = [p for p in pkg_list if not p.get('system', False)] if pkg_list and isinstance(pkg_list[0], dict) else pkg_list
pkg_names = [p.get('package', str(p)) if isinstance(p, dict) else str(p) for p in pkg_list]
context_dict = {
'model': model,
'android_version': android_version,
'pkg_count': len(pkg_list),
'user_count': len(user_pkgs),
'pkg_sample': ', '.join(pkg_names[:20]),
'proc_sample': '\n'.join(proc_lines),
'network_info': str(network_info)[:500],
'ioc_findings': str(ioc_findings)[:500],
}
if backend == 'raw':
return jsonify({'ok': True, 'mode': 'raw', 'dump': context_dict})
if backend == 'ondevice':
prompt_str = (
f"Device: {context_dict['model']} Android {context_dict['android_version']}. "
f"Packages ({context_dict['pkg_count']}): {context_dict['pkg_sample']}. "
f"Processes: {context_dict['proc_sample'][:200]}. "
f"Network: {context_dict['network_info'][:200]}. "
f"IOC findings: {context_dict['ioc_findings'][:200]}. "
"Identify threats and return risk assessment."
)
return jsonify({
'ok': True,
'mode': 'ondevice',
'prompt': prompt_str,
'schema': {
'risk_level': 'string',
'findings': 'array',
'summary': 'string',
},
})
# autarch backend — full LLM analysis
prompt = (
"You are a mobile security analyst. Analyze this Android forensic acquisition and identify threats.\n\n"
f"Device info: {context_dict['model']} Android {context_dict['android_version']}\n"
f"Installed packages ({context_dict['pkg_count']} total, {context_dict['user_count']} user apps): {context_dict['pkg_sample']}\n"
f"Running processes (sample): {context_dict['proc_sample']}\n"
f"Network properties: {context_dict['network_info']}\n"
f"IOC pre-scan findings: {context_dict['ioc_findings']}\n\n"
'Return JSON: {"risk_level": "critical|high|medium|low|clean", '
'"findings": [{"severity": "...", "type": "...", "detail": "...", "recommendation": "..."}], '
'"summary": "one paragraph"}'
)
response = _llm().generate(prompt, max_tokens=1000)
try:
import json
parsed_result = json.loads(response)
except Exception:
parsed_result = response
return jsonify({'ok': True, 'mode': 'autarch', 'analysis': parsed_result})
except Exception as e:
return jsonify({'ok': False, 'error': str(e)})
# ── CVE Check ────────────────────────────────────────────────────────
@android_forensics_ai_bp.route('/cve-check', methods=['POST'])
@login_required
def cve_check():
data = _body()
packages = data.get('packages', [])
sdk_version = data.get('sdk_version')
try:
_severity_order = {'critical': 0, 'high': 1, 'medium': 2, 'low': 3, 'unknown': 4}
findings = []
pkg_results = _cve().check_packages_batch(packages)
for pkg_name, cves in (pkg_results or {}).items():
for cve in (cves or []):
findings.append({
'package': pkg_name,
'cve_id': cve.get('cve_id', ''),
'severity': cve.get('severity', 'unknown'),
'description': cve.get('description', ''),
'patched_version': cve.get('patched_version', ''),
'source': cve.get('source', ''),
})
if sdk_version:
sdk_cves = _cve().get_cve_for_android_sdk(sdk_version) or []
for cve in sdk_cves:
findings.append({
'package': f'android-sdk:{sdk_version}',
'cve_id': cve.get('cve_id', ''),
'severity': cve.get('severity', 'unknown'),
'description': cve.get('description', ''),
'patched_version': cve.get('patched_version', ''),
'source': cve.get('source', ''),
})
findings.sort(key=lambda f: _severity_order.get(f['severity'].lower(), 4))
return jsonify({
'ok': True,
'findings': findings,
'total': len(findings),
'packages_checked': len(packages),
})
except Exception as e:
return jsonify({'ok': False, 'error': str(e)})
# ── Classify ─────────────────────────────────────────────────────────
@android_forensics_ai_bp.route('/classify', methods=['POST'])
@login_required
def classify():
data = _body()
raw_output = data.get('raw_output', '')
data_type = data.get('type', 'packages')
try:
_type_context = {
'packages': 'a list of installed Android packages',
'logcat': 'Android logcat output',
'processes': 'a list of running Android processes',
'getprop': 'Android system properties (getprop output)',
}
context_label = _type_context.get(data_type, f'Android {data_type} data')
prompt = (
f"You are a mobile security analyst. Classify the following {context_label} "
"for security threats, stalkerware, spyware, adware, or other malicious indicators. "
"Provide a brief threat classification summary.\n\n"
f"Data:\n{raw_output[:3000]}"
)
response = _llm().generate(prompt, max_tokens=500)
return jsonify({'ok': True, 'classification': response})
except Exception as e:
return jsonify({'ok': False, 'error': str(e)})
# ── Heuristic Scan ───────────────────────────────────────────────────
@android_forensics_ai_bp.route('/heuristic', methods=['POST'])
@login_required
def heuristic():
data = _body()
data_type = data.get('data_type', 'packages')
items = data.get('items', [])
threshold = float(data.get('threshold', 0.6))
_prompts = {
'packages': (
'Classify each Android package name for stalkerware/spyware/adware risk. '
'Score 0.0-1.0. High risk: names impersonating system apps, random strings, MDM-sounding names. '
'Return JSON array: [{"item": "...", "score": 0.0, "reason": "...", "category": "stalkerware|adware|suspicious|clean"}]'
),
'processes': (
'Classify each process name for malware risk. Score 0.0-1.0. '
'Suspicious: random strings, names mimicking system processes. '
'Return JSON array: [{"item": "...", "score": 0.0, "reason": "...", "category": "malware|suspicious|clean"}]'
),
'network': (
'Classify each domain/IP for C2/malware beacon risk. Score 0.0-1.0. '
'High risk: DGA patterns, dynamic DNS abuse, unusual TLDs. '
'Return JSON array: [{"item": "...", "score": 0.0, "reason": "...", "category": "c2_beacon|dga_domain|suspicious|clean"}]'
),
'files': (
'Classify each file path for spyware/stalkerware artifact risk. Score 0.0-1.0. '
'Return JSON array: [{"item": "...", "score": 0.0, "reason": "...", "category": "spyware|suspicious|clean"}]'
),
}
try:
llm = _llm()
if llm is None:
return jsonify({'ok': False, 'error': 'LLM not loaded', 'findings': []})
base_prompt = _prompts.get(data_type, _prompts['packages'])
chunk_size = 50
all_results = []
for i in range(0, len(items), chunk_size):
chunk = items[i:i + chunk_size]
chunk_text = '\n'.join(str(it) for it in chunk)
prompt = f"{base_prompt}\n\nItems:\n{chunk_text}"
try:
response = llm.generate(prompt, max_tokens=1000)
import json
parsed = json.loads(response)
if isinstance(parsed, list):
all_results.extend(parsed)
except Exception:
# Skip chunks that fail to parse
continue
filtered = [r for r in all_results if isinstance(r, dict) and float(r.get('score', 0)) >= threshold]
return jsonify({
'ok': True,
'findings': filtered,
'total_scanned': len(items),
'flagged': len(filtered),
})
except Exception as e:
return jsonify({'ok': False, 'error': str(e), 'findings': []})
# ── Alert Receiver ───────────────────────────────────────────────────
@android_forensics_ai_bp.route('/alert', methods=['POST'])
@login_required
def alert():
data = _body()
entry = {
'serial': data.get('serial', ''),
'event_type': data.get('event_type', ''),
'severity': data.get('severity', ''),
'detail': data.get('detail', ''),
'timestamp': data.get('timestamp', datetime.utcnow().isoformat()),
}
_alerts.append(entry)
if len(_alerts) > _MAX_ALERTS:
del _alerts[:-_MAX_ALERTS]
return jsonify({'ok': True, 'alert_id': len(_alerts)})
# ── Alerts List ──────────────────────────────────────────────────────
@android_forensics_ai_bp.route('/alerts/list', methods=['GET'])
@login_required
def alerts_list():
return jsonify({'ok': True, 'alerts': _alerts[-100:], 'total': len(_alerts)})
# ── CVE Stats ────────────────────────────────────────────────────────
@android_forensics_ai_bp.route('/cve-stats', methods=['GET'])
@login_required
def cve_stats():
return jsonify({'ok': True, 'stats': _cve().get_stats()})