Add Android forensics, IOC threat-intel DB, Compose companion; scrub secrets from configs

This commit is contained in:
SsSnake
2026-07-13 15:45:47 -07:00
parent 925216290f
commit e48d577bd5
387 changed files with 211976 additions and 921 deletions

View File

@@ -0,0 +1,864 @@
{% extends "base.html" %}
{% block title %}Android Forensics — AUTARCH{% endblock %}
{% block content %}
<div class="page-header">
<h1>Autarch Android Forensics</h1>
<p class="subtitle">Forensic acquisition and IOC scanning for Android devices</p>
</div>
<!-- Mode bar -->
<div class="card" style="margin:0.5rem 0 0.5rem 0;padding:0.6rem 1rem;display:flex;align-items:center;gap:0.6rem;flex-wrap:wrap">
<span style="font-weight:600;font-size:0.85rem;color:var(--text-secondary)">ADB Mode:</span>
<button id="af-mode-server" class="btn btn-sm active" onclick="afSetMode('server')">Server (Local ADB)</button>
<button id="af-mode-direct" class="btn btn-sm" onclick="afSetMode('direct')">Direct (WebUSB)</button>
<!-- Direct mode: connect/disconnect -->
<span id="af-direct-bar" style="display:none;align-items:center;gap:0.5rem">
<span id="af-device-label" style="font-size:0.85rem;color:var(--text-secondary)">Not connected</span>
<button class="btn btn-sm" onclick="afDirectConnect()">Connect</button>
<button class="btn btn-sm btn-danger" id="af-disconnect-btn" style="display:none" onclick="afDirectDisconnect()">Disconnect</button>
</span>
<span id="af-webusb-warning" style="display:none;color:#f97316;font-size:0.8rem;margin-left:0.5rem"></span>
</div>
<!-- Server mode: device selector -->
<div id="af-server-selector" class="card" style="margin:0.5rem 0 1rem 0;padding:0.8rem 1rem;display:flex;align-items:center;gap:0.8rem;flex-wrap:wrap">
<label style="font-weight:600">Device:</label>
<select id="af-device" style="flex:1;min-width:200px;padding:0.4rem;background:var(--bg-secondary);color:var(--text-primary);border:1px solid var(--border-color);border-radius:4px" onchange="afDeviceChanged()">
<option value="">-- Select --</option>
</select>
<button class="btn btn-sm" onclick="afRefreshDevices()">Refresh</button>
<!-- IOC DB badges -->
<span class="badge" id="af-badge-pkgs" style="background:var(--bg-secondary);color:var(--text-secondary);margin-left:auto">pkgs: --</span>
<span class="badge" id="af-badge-domains" style="background:var(--bg-secondary);color:var(--text-secondary)">domains: --</span>
<span class="badge" id="af-badge-certs" style="background:var(--bg-secondary);color:var(--text-secondary)">certs: --</span>
</div>
<!-- Status message -->
<div id="af-status" style="display:none;padding:0.5rem 1rem;margin-bottom:0.75rem;border-radius:4px;font-size:0.9rem;"></div>
<!-- Tabs -->
<div class="tab-bar">
<button class="tab active" data-tab-group="af" data-tab="acquire" onclick="showTab('af','acquire')">Acquire</button>
<button class="tab" data-tab-group="af" data-tab="scan" onclick="showTab('af','scan')">IOC Scan</button>
<button class="tab" data-tab-group="af" data-tab="reports" onclick="showTab('af','reports')">Reports</button>
<button class="tab" data-tab-group="af" data-tab="database" onclick="showTab('af','database')">Database</button>
<button class="tab" data-tab-group="af" data-tab="ai" onclick="showTab('af','ai')">AI &amp; CVE</button>
<button class="tab" data-tab-group="af" data-tab="alerts" onclick="showTab('af','alerts')">Runtime Alerts</button>
</div>
<!-- ── Acquire ──────────────────────────────────────────────────── -->
<div class="tab-content active" data-tab-group="af" data-tab="acquire">
<div class="section">
<h2>Acquisition Modules</h2>
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(170px,1fr));gap:0.6rem;margin-bottom:1rem">
<button class="btn" onclick="afAcquire('packages')">Package List</button>
<button class="btn" onclick="afAcquire('processes')">Running Processes</button>
<button class="btn" onclick="afAcquire('getprop')">System Properties</button>
<button class="btn" onclick="afAcquire('settings')">Settings</button>
<button class="btn" onclick="afAcquire('services')">Services</button>
<button class="btn" onclick="afAcquire('dumpsys')">Dumpsys <small>(server only)</small></button>
<button class="btn" onclick="afAcquire('logcat')">Logcat</button>
<button class="btn" onclick="afAcquire('tmp_files')">Temp Files</button>
<button class="btn" onclick="afAcquire('files')">File Listing</button>
<button class="btn" onclick="afAcquire('logs')">Full Logs</button>
<button class="btn" onclick="afAcquire('bugreport')">Bugreport Info</button>
</div>
<button class="btn btn-primary" onclick="afFullAcquisition()">&#x25B6; Full Acquisition</button>
<span id="af-acq-spinner" style="display:none;margin-left:0.75rem;color:var(--text-secondary)">Running&#x2026;</span>
</div>
<div class="section" id="af-acq-results-section" style="display:none">
<h2>Results</h2>
<div id="af-acq-results"></div>
</div>
</div>
<!-- ── IOC Scan ─────────────────────────────────────────────────── -->
<div class="tab-content" data-tab-group="af" data-tab="scan">
<div class="section">
<h2>IOC Scanning</h2>
<!-- Heuristic controls -->
<div style="display:flex;align-items:center;gap:0.75rem;flex-wrap:wrap;margin-bottom:0.75rem;padding:0.5rem 0.75rem;background:rgba(241,196,15,0.06);border:1px solid rgba(241,196,15,0.2);border-radius:4px">
<label style="display:flex;align-items:center;gap:0.4rem;font-size:0.88rem;cursor:pointer">
<input type="checkbox" id="af-heuristic-toggle" checked>
<b>SLM Heuristics</b> — AI pattern scan after IOC match
</label>
<span style="font-size:0.78rem;color:var(--text-muted)">Threshold:</span>
<input type="range" id="af-heuristic-threshold" min="40" max="90" value="65" style="width:80px" oninput="document.getElementById('af-heuristic-threshold-val').textContent=Math.round(this.value)+'%'">
<span id="af-heuristic-threshold-val" style="font-size:0.82rem;min-width:30px">65%</span>
</div>
<div style="display:flex;gap:0.6rem;flex-wrap:wrap;margin-bottom:1rem">
<button class="btn btn-primary" onclick="afScan('apps')">Scan Apps</button>
<button class="btn btn-primary" onclick="afScan('certs')">Scan Certificates <small>(server only)</small></button>
<button class="btn btn-primary" onclick="afScan('network')">Scan Network</button>
<button class="btn btn-primary" onclick="afScan('files')">Scan File Artifacts <small>(server only)</small></button>
<button class="btn btn-primary" onclick="afScan('processes')">Scan Processes</button>
<button class="btn btn-danger" onclick="afScan('full')">Full Scan <small>(server only)</small></button>
</div>
<span id="af-scan-spinner" style="display:none;color:var(--text-secondary)">Scanning&#x2026;</span>
</div>
<div class="section" id="af-scan-results-section" style="display:none">
<h2 id="af-scan-title">Findings</h2>
<div id="af-scan-clean" style="display:none;padding:0.75rem;background:rgba(39,174,96,0.1);border-radius:4px;color:#27ae60">
&#x2705; No IOC matches found. Device appears clean.
</div>
<table class="table" id="af-scan-table" style="display:none">
<thead><tr>
<th>Severity</th><th>Type</th><th>Threat</th><th>Detail</th><th style="font-size:0.75rem">Source</th>
</tr></thead>
<tbody id="af-scan-body"></tbody>
</table>
</div>
</div>
<!-- ── Reports ──────────────────────────────────────────────────── -->
<div class="tab-content" data-tab-group="af" data-tab="reports">
<div class="section">
<h2>Past Acquisitions</h2>
<button class="btn" onclick="afLoadReports()">Load Acquisitions</button>
</div>
<div id="af-reports-section" style="display:none">
<table class="table">
<thead><tr><th>Timestamp</th><th>Modules</th><th>Findings</th><th></th></tr></thead>
<tbody id="af-reports-body"></tbody>
</table>
</div>
<div id="af-report-viewer" style="display:none;margin-top:1rem">
<h3>Report: <span id="af-report-id"></span></h3>
<pre id="af-report-json" style="background:var(--bg-secondary);padding:1rem;border-radius:4px;max-height:500px;overflow:auto;font-size:0.78rem;white-space:pre-wrap;word-break:break-all"></pre>
</div>
</div>
<!-- ── Database ─────────────────────────────────────────────────── -->
<div class="tab-content" data-tab-group="af" data-tab="database">
<div class="section">
<h2>IOC Database</h2>
<div style="display:flex;gap:0.6rem;flex-wrap:wrap">
<button class="btn" onclick="afDbStats()">Load Stats</button>
<button class="btn btn-primary" onclick="afDbReload()">Reload Database</button>
</div>
</div>
<div id="af-db-section" style="display:none">
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:0.75rem;margin:1rem 0">
<div class="stat-card"><div class="stat-val" id="af-st-packages">--</div><div class="stat-lbl">Packages</div></div>
<div class="stat-card"><div class="stat-val" id="af-st-domains">--</div><div class="stat-lbl">Domains</div></div>
<div class="stat-card"><div class="stat-val" id="af-st-certs">--</div><div class="stat-lbl">Certs</div></div>
<div class="stat-card"><div class="stat-val" id="af-st-hashes">--</div><div class="stat-lbl">Hashes</div></div>
<div class="stat-card"><div class="stat-val" id="af-st-ips">--</div><div class="stat-lbl">IPs</div></div>
<div class="stat-card"><div class="stat-val" id="af-st-processes">--</div><div class="stat-lbl">Processes</div></div>
<div class="stat-card"><div class="stat-val" id="af-st-filepaths">--</div><div class="stat-lbl">File Paths</div></div>
<div class="stat-card"><div class="stat-val" id="af-st-yara">--</div><div class="stat-lbl">YARA Rules</div></div>
</div>
<table class="table" style="max-width:500px">
<thead><tr><th>Source</th><th>Count / Status</th></tr></thead>
<tbody id="af-db-sources"></tbody>
</table>
</div>
</div>
<!-- ── AI & CVE ───────────────────────────────────────────────── -->
<div class="tab-content" data-tab-group="af" data-tab="ai">
<div class="section">
<h2>AI Threat Analysis</h2>
<div style="display:flex;align-items:center;gap:0.75rem;flex-wrap:wrap;margin-bottom:0.75rem">
<span style="font-weight:600;font-size:0.9rem">Backend:</span>
<label style="font-size:0.88rem"><input type="radio" name="af-ai-backend" value="autarch" checked> AUTARCH LLM</label>
<label style="font-size:0.88rem"><input type="radio" name="af-ai-backend" value="ondevice"> On-device prompt</label>
<label style="font-size:0.88rem"><input type="radio" name="af-ai-backend" value="raw"> Raw export</label>
</div>
<div style="display:flex;align-items:center;gap:0.75rem;flex-wrap:wrap;margin-bottom:0.75rem">
<span style="font-weight:600;font-size:0.9rem">Focus:</span>
<label style="font-size:0.88rem"><input type="checkbox" class="af-focus" value="packages" checked> Packages</label>
<label style="font-size:0.88rem"><input type="checkbox" class="af-focus" value="network" checked> Network</label>
<label style="font-size:0.88rem"><input type="checkbox" class="af-focus" value="processes" checked> Processes</label>
<label style="font-size:0.88rem"><input type="checkbox" class="af-focus" value="files"> Files</label>
</div>
<div style="display:flex;gap:0.6rem;flex-wrap:wrap;margin-bottom:0.75rem">
<input type="text" id="af-ai-acq-id" placeholder="Acquisition ID (leave empty for live scan)" style="flex:1;min-width:220px;padding:0.4rem;background:var(--bg-secondary);color:var(--text-primary);border:1px solid var(--border-color);border-radius:4px;font-size:0.88rem">
</div>
<button class="btn btn-primary" onclick="afAiAnalyze()">&#x1F4A1; Run AI Analysis</button>
<span id="af-ai-spinner" style="display:none;margin-left:0.75rem;color:var(--text-secondary)">Analyzing&#x2026;</span>
</div>
<div id="af-ai-results-section" class="section" style="display:none">
<div style="display:flex;align-items:center;gap:1rem;margin-bottom:0.5rem;flex-wrap:wrap">
<span style="font-weight:600">Risk Level:</span>
<span id="af-ai-risk" style="font-size:1.2rem;font-weight:700"></span>
</div>
<p id="af-ai-summary" style="color:var(--text-secondary);font-size:0.9rem;margin-bottom:1rem"></p>
<table class="table" id="af-ai-findings-table" style="display:none">
<thead><tr><th>Severity</th><th>Type</th><th>Detail</th><th>Recommendation</th></tr></thead>
<tbody id="af-ai-findings-body"></tbody>
</table>
<pre id="af-ai-raw" style="display:none;background:var(--bg-secondary);padding:1rem;border-radius:4px;max-height:400px;overflow:auto;font-size:0.78rem;white-space:pre-wrap;word-break:break-all"></pre>
</div>
<hr style="border-color:var(--border-color);margin:0.5rem 0">
<div class="section">
<h2>CVE Lookup</h2>
<div style="display:flex;gap:0.6rem;flex-wrap:wrap;margin-bottom:0.75rem">
<input type="text" id="af-cve-pkg" placeholder="Package name (e.g. com.whatsapp)" style="flex:1;min-width:200px;padding:0.4rem;background:var(--bg-secondary);color:var(--text-primary);border:1px solid var(--border-color);border-radius:4px;font-size:0.88rem">
<input type="text" id="af-cve-ver" placeholder="Version (optional)" style="width:130px;padding:0.4rem;background:var(--bg-secondary);color:var(--text-primary);border:1px solid var(--border-color);border-radius:4px;font-size:0.88rem">
<button class="btn" onclick="afCveLookup()">Look Up CVEs</button>
</div>
<button class="btn" onclick="afCveCheckDevice()">&#x1F50D; Scan All Device Packages</button>
<span id="af-cve-spinner" style="display:none;margin-left:0.75rem;color:var(--text-secondary)">Checking CVEs&#x2026;</span>
</div>
<div id="af-cve-results-section" style="display:none;margin:0 1rem 1rem">
<div style="margin-bottom:0.5rem;font-size:0.88rem;color:var(--text-secondary)" id="af-cve-summary"></div>
<table class="table">
<thead><tr><th>Package</th><th>CVE ID</th><th>Severity</th><th>Description</th><th>Patched In</th></tr></thead>
<tbody id="af-cve-body"></tbody>
</table>
</div>
<hr style="border-color:var(--border-color);margin:0.5rem 0">
<div class="section">
<h2>CVE Database Stats</h2>
<button class="btn" onclick="afCveStats()">Load Stats</button>
<div id="af-cve-stats-section" style="display:none;margin-top:1rem">
<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:0.75rem">
<div class="stat-card"><div class="stat-val" id="af-cvs-cached">--</div><div class="stat-lbl">Cached Pkgs</div></div>
<div class="stat-card"><div class="stat-val" id="af-cvs-query" style="font-size:0.9rem">--</div><div class="stat-lbl">Last Query</div></div>
</div>
</div>
</div>
</div>
<!-- ── Runtime Alerts ────────────────────────────────────────── -->
<div class="tab-content" data-tab-group="af" data-tab="alerts">
<div class="section">
<h2>Runtime Alerts</h2>
<div style="display:flex;align-items:center;gap:0.75rem;flex-wrap:wrap;margin-bottom:0.75rem">
<button class="btn" onclick="afLoadAlerts()">&#x21BB; Refresh</button>
<label style="display:flex;align-items:center;gap:0.4rem;font-size:0.88rem;cursor:pointer">
<input type="checkbox" id="af-alerts-autorefresh" checked onchange="afToggleAlertAutoRefresh()">
Auto-refresh every 30s
</label>
<span id="af-alerts-count" style="font-size:0.85rem;color:var(--text-secondary)"></span>
</div>
</div>
<div id="af-alerts-empty" style="display:none;padding:1rem 1.5rem;color:var(--text-muted)">No runtime alerts received yet. Alerts are sent from the Archon companion app.</div>
<table class="table" id="af-alerts-table" style="display:none">
<thead><tr><th>Time</th><th>Device</th><th>Severity</th><th>Event</th><th>Detail</th></tr></thead>
<tbody id="af-alerts-body"></tbody>
</table>
</div>
<style>
.stat-card { background:var(--bg-secondary,#1a1a2e); border:1px solid var(--border-color,#333); border-radius:6px; padding:1rem; text-align:center; }
.stat-val { font-size:1.8rem; font-weight:700; color:var(--primary,#7c4dff); }
.stat-lbl { font-size:0.8rem; color:var(--text-muted,#888); margin-top:0.25rem; }
.sev-critical { color:#e74c3c; font-weight:700; }
.sev-high { color:#e67e22; font-weight:600; }
.sev-medium { color:#f1c40f; }
.sev-low { color:#888; }
.acq-panel { background:var(--bg-secondary,#1a1a2e); border:1px solid var(--border-color,#333); border-radius:6px; margin-bottom:0.6rem; }
.acq-ph { padding:0.5rem 0.9rem; cursor:pointer; display:flex; justify-content:space-between; font-weight:600; font-size:0.9rem; }
.acq-pb { padding:0.75rem; display:none; max-height:280px; overflow:auto; }
.acq-pb pre { font-size:0.76rem; margin:0; white-space:pre-wrap; word-break:break-all; }
.heuristic-tag { display:inline-block; background:rgba(241,196,15,0.2); color:#f1c40f; font-size:0.72rem; padding:0.1rem 0.3rem; border-radius:3px; margin-right:0.3rem; font-weight:700; }
.sev-clean { color:#27ae60; font-weight:600; }
</style>
<script>
/* ── State ─────────────────────────────────────────────────────── */
var afMode = 'server';
var afSerial = '';
/* ── Init ──────────────────────────────────────────────────────── */
document.addEventListener('DOMContentLoaded', function() {
var saved = localStorage.getItem('af_mode') || 'server';
afSetMode(saved);
if (saved === 'server') afRefreshDevices();
afLoadBadges();
if (document.getElementById('af-alerts-autorefresh').checked) {
afAlertTimer = setInterval(afLoadAlerts, 30000);
}
});
/* ── Mode Switching ─────────────────────────────────────────────── */
function afSetMode(mode) {
afMode = mode;
localStorage.setItem('af_mode', mode);
document.getElementById('af-mode-server').classList.toggle('active', mode === 'server');
document.getElementById('af-mode-direct').classList.toggle('active', mode === 'direct');
document.getElementById('af-direct-bar').style.display = mode === 'direct' ? 'flex' : 'none';
document.getElementById('af-server-selector').style.display = mode === 'server' ? '' : 'none';
if (mode === 'direct') {
var warn = document.getElementById('af-webusb-warning');
if (!navigator.usb) {
warn.textContent = location.protocol !== 'https:'
? 'WebUSB requires HTTPS'
: 'WebUSB not supported — use Chrome, Edge, or Brave';
warn.style.display = '';
} else {
warn.style.display = 'none';
}
afUpdateDirectStatus();
} else {
afRefreshDevices();
}
}
/* ── Direct Mode Connection ─────────────────────────────────────── */
async function afDirectConnect() {
var lbl = document.getElementById('af-device-label');
lbl.textContent = 'Connecting...';
try {
var deviceObj = await HWDirect.adbRequestDevice();
if (!deviceObj) { lbl.textContent = 'Cancelled'; return; }
await HWDirect.adbConnect(deviceObj);
afUpdateDirectStatus();
HWDirect.adbGetInfo().then(function() { afUpdateDirectStatus(); }).catch(function() {});
} catch(e) {
lbl.textContent = 'Error: ' + e.message;
}
}
function afDirectDisconnect() {
HWDirect.adbDisconnect();
afSerial = '';
afUpdateDirectStatus();
}
function afUpdateDirectStatus() {
var connected = HWDirect.adbIsConnected();
document.getElementById('af-device-label').textContent = connected ? HWDirect.adbGetDeviceLabel() : 'Not connected';
document.getElementById('af-disconnect-btn').style.display = connected ? '' : 'none';
afSerial = connected ? '__webusb__' : '';
}
/* ── Server Mode Device Selection ──────────────────────────────── */
function afRefreshDevices() {
if (afMode === 'direct') { afUpdateDirectStatus(); return; }
fetch('/hardware/adb/devices')
.then(function(r) { return r.json(); })
.then(function(data) {
var sel = document.getElementById('af-device');
var prev = sel.value;
sel.innerHTML = '<option value="">-- Select --</option>';
(data.devices || []).forEach(function(d) {
var opt = document.createElement('option');
opt.value = d.serial;
opt.textContent = d.serial + (d.model ? ' (' + d.model + ')' : '');
sel.appendChild(opt);
});
if (prev) sel.value = prev;
afDeviceChanged();
}).catch(function() {});
}
function afDeviceChanged() {
if (afMode === 'direct') return;
afSerial = document.getElementById('af-device').value;
}
/* ── Core Request Helper ────────────────────────────────────────── */
function afPost(url, body) {
if (afMode === 'direct') return afDirect(url, body);
return fetch(url, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(body)
}).then(function(r) { return r.json(); });
}
async function afDirect(url, body) {
if (!HWDirect.adbIsConnected()) {
return {error: 'No WebUSB device connected — click Connect first.'};
}
// Convert URL to op key: /android-forensics/acquire/packages → acquire_packages
var op = url.replace('/android-forensics/', '').replace(/\//g, '_');
try {
var cmdRes = await fetch('/android-forensics/cmd', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({op: op})
});
var cmdData = await cmdRes.json();
if (!cmdData.supported) {
return {error: cmdData.reason || 'This operation requires Server mode.'};
}
var raw = {};
for (var key in cmdData.commands) {
if (!cmdData.commands.hasOwnProperty(key)) continue;
var result = await HWDirect.adbShell(cmdData.commands[key]);
raw[key] = (typeof result === 'object' && result !== null)
? (result.stdout || result.output || '')
: (result || '');
}
var parseRes = await fetch('/android-forensics/parse', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({op: op, body: body, raw: raw})
});
return parseRes.json();
} catch(e) {
return {error: 'Direct mode error: ' + e.message};
}
}
/* ── Status ─────────────────────────────────────────────────────── */
function afStatus(msg, type) {
var el = document.getElementById('af-status');
el.style.background = {success:'rgba(39,174,96,0.15)', danger:'rgba(231,76,60,0.15)', warning:'rgba(241,196,15,0.15)', info:'rgba(52,152,219,0.15)'}[type] || 'rgba(52,152,219,0.15)';
el.style.color = {success:'#27ae60', danger:'#e74c3c', warning:'#f39c12', info:'#3498db'}[type] || '#3498db';
el.textContent = msg;
el.style.display = 'block';
setTimeout(function() { el.style.display = 'none'; }, 7000);
}
/* ── IOC Badges ─────────────────────────────────────────────────── */
function afLoadBadges() {
fetch('/android-forensics/db/stats')
.then(function(r) { return r.json(); })
.then(function(data) {
var c = data.counts || {};
document.getElementById('af-badge-pkgs').textContent = 'pkgs: ' + (c.packages || 0);
document.getElementById('af-badge-domains').textContent = 'domains: ' + (c.domains || 0);
document.getElementById('af-badge-certs').textContent = 'certs: ' + (c.certs || 0);
}).catch(function() {});
}
/* ── Acquisition ────────────────────────────────────────────────── */
function afAcquire(module) {
if (!afSerial) { afStatus('Select a device first.', 'warning'); return; }
afStatus('Acquiring ' + module + '...', 'info');
afPost('/android-forensics/acquire/' + module, {serial: afSerial})
.then(function(data) {
afShowAcqPanel(module, data);
document.getElementById('af-acq-results-section').style.display = '';
afStatus(module + ' acquired.', data.ok ? 'success' : 'danger');
})
.catch(function(e) { afStatus('Error: ' + e, 'danger'); });
}
function afFullAcquisition() {
if (!afSerial) { afStatus('Select a device first.', 'warning'); return; }
if (afMode === 'direct') { afStatus('Full acquisition requires Server mode.', 'warning'); return; }
var spinner = document.getElementById('af-acq-spinner');
spinner.style.display = 'inline';
afStatus('Running full acquisition...', 'info');
fetch('/android-forensics/acquire/full', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({serial: afSerial})
}).then(function(r) { return r.json(); }).then(function(data) {
spinner.style.display = 'none';
var summary = data.scan_summary || {};
var n = summary.total_findings || 0;
afStatus('Full acquisition complete. Findings: ' + n, n > 0 ? 'danger' : 'success');
var mods = data.modules || {};
Object.keys(mods).forEach(function(mod) { afShowAcqPanel(mod, mods[mod]); });
document.getElementById('af-acq-results-section').style.display = '';
if ((data.ioc_findings || []).length > 0) {
showTab('af', 'scan');
afRenderFindings(data.ioc_findings, 'Full Acquisition');
}
}).catch(function(e) { spinner.style.display = 'none'; afStatus('Error: ' + e, 'danger'); });
}
function afShowAcqPanel(module, data) {
var container = document.getElementById('af-acq-results');
var existing = document.getElementById('af-panel-' + module);
if (existing) existing.remove();
var summary = data.total ? data.total + ' packages'
: data.count ? data.count + ' items'
: data.lines ? data.lines + ' lines'
: data.size ? data.size + ' bytes'
: !data.ok ? 'Error: ' + (data.error || 'unknown')
: 'OK';
var panel = document.createElement('div');
panel.className = 'acq-panel';
panel.id = 'af-panel-' + module;
panel.innerHTML = '<div class="acq-ph" onclick="this.nextElementSibling.style.display=this.nextElementSibling.style.display===\'block\'?\'none\':\'block\'">'
+ '<span>' + afEsc(module) + ' &mdash; ' + afEsc(summary) + '</span><span>&#x25BC;</span></div>'
+ '<div class="acq-pb"><pre>' + afEsc(JSON.stringify(data, null, 2)) + '</pre></div>';
container.prepend(panel);
}
/* ── IOC Scanning ───────────────────────────────────────────────── */
async function afScan(type) {
if (!afSerial) { afStatus('Select a device first.', 'warning'); return; }
if (afMode === 'direct' && (type === 'certs' || type === 'files' || type === 'full')) {
afStatus(type + ' scan requires Server mode.', 'warning'); return;
}
var spinner = document.getElementById('af-scan-spinner');
spinner.style.display = 'inline';
afStatus('Scanning ' + type + '...', 'info');
try {
var data = await afPost('/android-forensics/scan/' + type, {serial: afSerial});
if (data.error) { spinner.style.display = 'none'; afStatus('Error: ' + data.error, 'danger'); return; }
var findings = (data.ioc_findings || data.findings || []).slice();
// SLM heuristic pass (server mode + toggle on)
var doHeuristic = document.getElementById('af-heuristic-toggle').checked && afMode === 'server';
if (doHeuristic && type !== 'full') {
afStatus('Running SLM heuristics...', 'info');
var hFindings = await afHeuristicPass(type, data);
findings = findings.concat(hFindings);
}
spinner.style.display = 'none';
afRenderFindings(findings, type + ' scan');
afStatus('Scan complete. Findings: ' + findings.length, findings.length > 0 ? 'danger' : 'success');
} catch(e) {
spinner.style.display = 'none';
afStatus('Error: ' + e, 'danger');
}
}
async function afHeuristicPass(scanType, scanData) {
var threshold = parseInt(document.getElementById('af-heuristic-threshold').value, 10) / 100;
var items = [], dataType = 'packages';
try {
if (scanType === 'apps') {
var r = await afPost('/android-forensics/acquire/packages', {serial: afSerial});
items = (r.packages || []).map(function(p) { return p.package; });
dataType = 'packages';
} else if (scanType === 'processes') {
var r = await afPost('/android-forensics/acquire/processes', {serial: afSerial});
items = (r.output || '').split('\n').slice(1, 120)
.map(function(l) { var p = l.trim().split(/\s+/); return p[p.length-1] || ''; })
.filter(Boolean);
dataType = 'processes';
} else if (scanType === 'network') {
var r = await afPost('/android-forensics/acquire/getprop', {serial: afSerial});
var props = r.props || {};
items = Object.values(props).filter(function(v) { return v && (v.indexOf('.') > -1 || v.indexOf(':') > -1); }).slice(0, 80);
dataType = 'network';
} else if (scanType === 'files') {
var r = await afPost('/android-forensics/acquire/tmp_files', {serial: afSerial});
items = (r.output || '').split('\n').map(function(l) { var p = l.trim().split(/\s+/); return p[p.length-1] || ''; }).filter(Boolean);
dataType = 'files';
}
} catch(e) { return []; }
if (!items.length) return [];
try {
var res = await fetch('/android-forensics/ai/heuristic', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({data_type: dataType, items: items.slice(0, 200), threshold: threshold})
}).then(function(r) { return r.json(); });
return (res.findings || []).map(function(f) {
return {
severity: f.score >= 0.8 ? 'high' : 'medium',
type: 'heuristic',
threat_name: f.category + ' [SLM]',
detail: f.item,
source: f.reason,
heuristic: true,
score: f.score
};
});
} catch(e) { return []; }
}
function afRenderFindings(findings, title) {
var section = document.getElementById('af-scan-results-section');
var clean = document.getElementById('af-scan-clean');
var table = document.getElementById('af-scan-table');
var tbody = document.getElementById('af-scan-body');
var ioc = (findings || []).filter(function(f) { return !f.heuristic; });
var heur = (findings || []).filter(function(f) { return f.heuristic; });
document.getElementById('af-scan-title').textContent =
(title || 'Findings') + ' — ' + ioc.length + ' IOC match(es)' + (heur.length ? ', ' + heur.length + ' heuristic(s)' : '');
section.style.display = '';
if (!findings || findings.length === 0) {
clean.style.display = 'block';
table.style.display = 'none';
return;
}
clean.style.display = 'none';
table.style.display = '';
tbody.innerHTML = '';
findings.forEach(function(f) {
var sev = f.severity || 'high';
var detail = f.package || f.process || f.path || f.remote_ip || f.detail || f.item || '';
var hTag = f.heuristic
? '<span class="heuristic-tag">' + Math.round((f.score || 0) * 100) + '%</span>'
: '';
var rowBg = f.heuristic ? ' style="background:rgba(241,196,15,0.04)"' : '';
tbody.insertAdjacentHTML('beforeend',
'<tr' + rowBg + '>'
+ '<td><span class="sev-' + afEsc(sev) + '">' + afEsc(sev.toUpperCase()) + '</span></td>'
+ '<td>' + hTag + afEsc(f.type || '') + '</td>'
+ '<td>' + afEsc(f.threat_name || '') + '</td>'
+ '<td><code style="font-size:0.82rem">' + afEsc(detail) + '</code></td>'
+ '<td style="font-size:0.72rem;color:var(--text-muted,#888)" title="' + afEsc(f.source || '') + '">'
+ afEsc((f.source || '').substring(0, 60)) + '</td>'
+ '</tr>');
});
}
/* ── AI Analysis ────────────────────────────────────────────────── */
function afAiAnalyze() {
if (!afSerial && afMode === 'server') { afStatus('Select a device first.', 'warning'); return; }
var backend = document.querySelector('input[name="af-ai-backend"]:checked').value;
var focus = Array.from(document.querySelectorAll('.af-focus:checked')).map(function(el) { return el.value; });
var acqId = document.getElementById('af-ai-acq-id').value.trim();
var spinner = document.getElementById('af-ai-spinner');
spinner.style.display = 'inline';
document.getElementById('af-ai-results-section').style.display = 'none';
fetch('/android-forensics/ai/analyze', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({serial: afSerial, acq_id: acqId, backend: backend, focus: focus})
}).then(function(r) { return r.json(); }).then(function(data) {
spinner.style.display = 'none';
if (!data.ok) { afStatus('AI error: ' + (data.error || 'unknown'), 'danger'); return; }
var section = document.getElementById('af-ai-results-section');
section.style.display = '';
if (data.mode === 'raw') {
document.getElementById('af-ai-risk').textContent = 'RAW';
document.getElementById('af-ai-summary').textContent = 'Raw acquisition dump — use for external analysis.';
var raw = document.getElementById('af-ai-raw');
raw.textContent = JSON.stringify(data.dump, null, 2);
raw.style.display = '';
document.getElementById('af-ai-findings-table').style.display = 'none';
return;
}
if (data.mode === 'ondevice') {
document.getElementById('af-ai-risk').textContent = 'ON-DEVICE';
document.getElementById('af-ai-summary').textContent = 'Send this prompt to Gemini Nano on the device.';
var raw = document.getElementById('af-ai-raw');
raw.textContent = data.prompt || '';
raw.style.display = '';
document.getElementById('af-ai-findings-table').style.display = 'none';
return;
}
// autarch mode — parse analysis
var analysis = data.analysis || {};
if (typeof analysis === 'string') {
try { analysis = JSON.parse(analysis); } catch(e) {}
}
var risk = typeof analysis === 'object' ? (analysis.risk_level || 'unknown') : 'unknown';
var riskColors = {critical:'#e74c3c', high:'#e67e22', medium:'#f1c40f', low:'#27ae60', clean:'#27ae60'};
var riskEl = document.getElementById('af-ai-risk');
riskEl.textContent = risk.toUpperCase();
riskEl.style.color = riskColors[risk.toLowerCase()] || 'var(--text-primary)';
document.getElementById('af-ai-summary').textContent = (typeof analysis === 'object' ? analysis.summary : String(analysis)) || '';
var aFindings = typeof analysis === 'object' ? (analysis.findings || []) : [];
var ftable = document.getElementById('af-ai-findings-table');
var ftbody = document.getElementById('af-ai-findings-body');
document.getElementById('af-ai-raw').style.display = 'none';
ftbody.innerHTML = '';
if (aFindings.length) {
aFindings.forEach(function(f) {
var sev = f.severity || 'medium';
ftbody.insertAdjacentHTML('beforeend',
'<tr><td><span class="sev-' + afEsc(sev) + '">' + afEsc(sev.toUpperCase()) + '</span></td>'
+ '<td>' + afEsc(f.type || '') + '</td>'
+ '<td>' + afEsc(f.detail || '') + '</td>'
+ '<td style="font-size:0.82rem">' + afEsc(f.recommendation || '') + '</td></tr>');
});
ftable.style.display = '';
} else {
ftable.style.display = 'none';
if (!document.getElementById('af-ai-summary').textContent)
document.getElementById('af-ai-summary').textContent = 'No findings reported.';
}
}).catch(function(e) { spinner.style.display = 'none'; afStatus('AI error: ' + e, 'danger'); });
}
/* ── CVE Lookup ─────────────────────────────────────────────────── */
function afCveLookup() {
var pkg = document.getElementById('af-cve-pkg').value.trim();
if (!pkg) { afStatus('Enter a package name.', 'warning'); return; }
var ver = document.getElementById('af-cve-ver').value.trim() || null;
var spinner = document.getElementById('af-cve-spinner');
spinner.style.display = 'inline';
fetch('/android-forensics/ai/cve-check', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({packages: [{name: pkg, version: ver}]})
}).then(function(r) { return r.json(); }).then(function(data) {
spinner.style.display = 'none';
if (!data.ok) { afStatus('CVE error: ' + data.error, 'danger'); return; }
afRenderCveFindings(data.findings || [], '1 package');
}).catch(function(e) { spinner.style.display = 'none'; afStatus('CVE error: ' + e, 'danger'); });
}
function afCveCheckDevice() {
if (!afSerial) { afStatus('Select a device first.', 'warning'); return; }
var spinner = document.getElementById('af-cve-spinner');
spinner.style.display = 'inline';
afStatus('Fetching package list...', 'info');
afPost('/android-forensics/acquire/packages', {serial: afSerial}).then(function(r) {
var pkgs = (r.packages || []).map(function(p) { return {name: p.package}; });
if (!pkgs.length) { spinner.style.display = 'none'; afStatus('No packages found.', 'warning'); return; }
afStatus('Checking ' + pkgs.length + ' packages against CVE DB...', 'info');
return fetch('/android-forensics/ai/cve-check', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({packages: pkgs})
}).then(function(res) { return res.json(); });
}).then(function(data) {
if (!data) return;
spinner.style.display = 'none';
afRenderCveFindings(data.findings || [], data.packages_checked + ' packages');
afStatus('CVE check complete. ' + (data.total || 0) + ' CVE(s) found.', (data.total || 0) > 0 ? 'danger' : 'success');
}).catch(function(e) { spinner.style.display = 'none'; afStatus('CVE error: ' + e, 'danger'); });
}
function afRenderCveFindings(findings, label) {
var section = document.getElementById('af-cve-results-section');
document.getElementById('af-cve-summary').textContent = label + ' — ' + findings.length + ' CVE(s) found.';
var tbody = document.getElementById('af-cve-body');
tbody.innerHTML = '';
if (!findings.length) {
tbody.innerHTML = '<tr><td colspan="5" style="color:var(--text-muted)">No CVEs found.</td></tr>';
} else {
findings.forEach(function(f) {
var sev = f.severity || 'medium';
tbody.insertAdjacentHTML('beforeend',
'<tr><td style="font-size:0.82rem"><code>' + afEsc(f.package || '') + '</code></td>'
+ '<td><b>' + afEsc(f.cve_id || '') + '</b></td>'
+ '<td><span class="sev-' + afEsc(sev) + '">' + afEsc(sev.toUpperCase()) + '</span></td>'
+ '<td style="font-size:0.82rem">' + afEsc((f.description || '').substring(0, 120)) + '</td>'
+ '<td style="font-size:0.82rem">' + afEsc(f.patched_version || 'unknown') + '</td></tr>');
});
}
section.style.display = '';
}
function afCveStats() {
fetch('/android-forensics/ai/cve-stats').then(function(r) { return r.json(); }).then(function(data) {
var s = data.stats || {};
document.getElementById('af-cvs-cached').textContent = s.total_cached || 0;
document.getElementById('af-cvs-query').textContent = s.last_query ? s.last_query.split('T')[0] : 'never';
document.getElementById('af-cve-stats-section').style.display = '';
}).catch(function() {});
}
/* ── Runtime Alerts ─────────────────────────────────────────────── */
var afAlertTimer = null;
function afLoadAlerts() {
fetch('/android-forensics/ai/alerts/list').then(function(r) { return r.json(); }).then(function(data) {
var alerts = (data.alerts || []).slice().reverse();
document.getElementById('af-alerts-count').textContent = 'Total: ' + (data.total || 0);
var empty = document.getElementById('af-alerts-empty');
var table = document.getElementById('af-alerts-table');
var tbody = document.getElementById('af-alerts-body');
if (!alerts.length) {
empty.style.display = '';
table.style.display = 'none';
return;
}
empty.style.display = 'none';
table.style.display = '';
tbody.innerHTML = '';
alerts.forEach(function(a) {
var sev = a.severity || 'medium';
var ts = a.timestamp ? a.timestamp.replace('T', ' ').split('.')[0] : '';
tbody.insertAdjacentHTML('beforeend',
'<tr><td style="font-size:0.82rem">' + afEsc(ts) + '</td>'
+ '<td style="font-size:0.82rem"><code>' + afEsc(a.serial || '') + '</code></td>'
+ '<td><span class="sev-' + afEsc(sev) + '">' + afEsc(sev.toUpperCase()) + '</span></td>'
+ '<td>' + afEsc(a.event_type || '') + '</td>'
+ '<td style="font-size:0.82rem">' + afEsc(a.detail || '') + '</td></tr>');
});
}).catch(function() {});
}
function afToggleAlertAutoRefresh() {
var on = document.getElementById('af-alerts-autorefresh').checked;
if (afAlertTimer) { clearInterval(afAlertTimer); afAlertTimer = null; }
if (on) { afAlertTimer = setInterval(afLoadAlerts, 30000); }
}
/* ── Reports ────────────────────────────────────────────────────── */
function afLoadReports() {
if (!afSerial) { afStatus('Select a device first.', 'warning'); return; }
afPost('/android-forensics/report/list', {serial: afSerial})
.then(function(data) {
var acqs = data.acquisitions || [];
var tbody = document.getElementById('af-reports-body');
tbody.innerHTML = '';
if (!acqs.length) {
tbody.innerHTML = '<tr><td colspan="4" style="color:var(--text-muted)">No acquisitions found.</td></tr>';
} else {
acqs.forEach(function(a) {
var btnSerial = afEsc(afSerial);
var btnId = afEsc(a.acq_id);
tbody.insertAdjacentHTML('beforeend',
'<tr>'
+ '<td>' + afEsc(a.timestamp || a.acq_id) + '</td>'
+ '<td style="font-size:0.82rem">' + afEsc((a.modules || []).join(', ')) + '</td>'
+ '<td>' + (a.findings || 0) + '</td>'
+ '<td><button class="btn btn-sm" onclick="afViewReport(\'' + btnSerial + '\',\'' + btnId + '\')">View</button></td>'
+ '</tr>');
});
}
document.getElementById('af-reports-section').style.display = '';
})
.catch(function(e) { afStatus('Error: ' + e, 'danger'); });
}
function afViewReport(serial, acqId) {
fetch('/android-forensics/report/view', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({serial: serial, acq_id: acqId})
}).then(function(r) { return r.json(); }).then(function(data) {
if (data.error) { afStatus('Error: ' + data.error, 'danger'); return; }
document.getElementById('af-report-id').textContent = acqId;
document.getElementById('af-report-json').textContent = JSON.stringify(data.report || data, null, 2);
document.getElementById('af-report-viewer').style.display = '';
}).catch(function(e) { afStatus('Error: ' + e, 'danger'); });
}
/* ── Database ───────────────────────────────────────────────────── */
function afDbStats() {
fetch('/android-forensics/db/stats')
.then(function(r) { return r.json(); })
.then(function(data) {
var c = data.counts || {};
var s = data.sources || {};
document.getElementById('af-st-packages').textContent = c.packages || 0;
document.getElementById('af-st-domains').textContent = c.domains || 0;
document.getElementById('af-st-certs').textContent = c.certs || 0;
document.getElementById('af-st-hashes').textContent = c.hashes || 0;
document.getElementById('af-st-ips').textContent = c.ips || 0;
document.getElementById('af-st-processes').textContent = c.processes || 0;
document.getElementById('af-st-filepaths').textContent = c.file_paths || 0;
document.getElementById('af-st-yara').textContent = s.yara_rules || 0;
var tbody = document.getElementById('af-db-sources');
tbody.innerHTML = '';
[
['Stalkerware YAML', s.stalkerware_ioc_yaml ? 'Yes' : 'No'],
['Amnesty campaigns', s.amnesty_investigations || 0],
['MVT campaigns', s.mvt_campaigns || 0],
['Citizen Lab', s.citizen_lab_campaigns || 0],
['YARA rules', s.yara_rules || 0],
].forEach(function(row) {
tbody.insertAdjacentHTML('beforeend',
'<tr><td>' + afEsc(row[0]) + '</td><td><b>' + afEsc(String(row[1])) + '</b></td></tr>');
});
document.getElementById('af-db-section').style.display = '';
}).catch(function(e) { afStatus('Error: ' + e, 'danger'); });
}
function afDbReload() {
afStatus('Reloading IOC database...', 'info');
fetch('/android-forensics/db/reload', {method: 'POST'})
.then(function(r) { return r.json(); })
.then(function() { afStatus('IOC database reloaded.', 'success'); afLoadBadges(); afDbStats(); })
.catch(function(e) { afStatus('Error: ' + e, 'danger'); });
}
/* ── Utility ────────────────────────────────────────────────────── */
function afEsc(s) {
return String(s || '').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
}
</script>
{% endblock %}