import { useState, useMemo } from 'react' import { ScanSearch, FileUp, Package, Shield, AlertTriangle, FileCode, Lock, FolderTree, Search, ChevronRight, Activity, Radar, Download, X, } from 'lucide-react' import { SelectAPKForAudit, AuditAPK, AuditInstalledApp, ListPackages, ReadAPKEntry, ExportAudit, } from '../../lib/wails' import { notify } from '../../lib/notify' import type { APKAudit, APKAuditFinding, APKEntryContent, PackageInfo } from '../../lib/types' type Tab = 'overview' | 'findings' | 'manifest' | 'components' | 'cert' | 'explorer' type Severity = 'critical' | 'high' | 'medium' | 'low' | 'info' const SEV_ORDER: Severity[] = ['critical', 'high', 'medium', 'low', 'info'] function sevText(s: string): string { switch (s) { case 'critical': return 'text-danger' case 'high': return 'text-danger' case 'medium': return 'text-warn' case 'low': return 'text-text-secondary' default: return 'text-text-muted' } } function sevBadge(s: string): string { switch (s) { case 'critical': return 'bg-danger/20 text-danger border border-danger/30' case 'high': return 'bg-danger/10 text-danger border border-danger/20' case 'medium': return 'bg-warn/15 text-warn border border-warn/25' case 'low': return 'bg-bg-raised text-text-secondary border border-bg-border' default: return 'bg-bg-raised text-text-muted border border-bg-border' } } function scoreColor(score: number): string { if (score >= 75) return 'text-accent-green' if (score >= 40) return 'text-warn' return 'text-danger' } function formatBytes(n: number): string { if (!n) return '0 B' const u = ['B', 'KB', 'MB', 'GB'] const i = Math.floor(Math.log(n) / Math.log(1024)) return `${(n / Math.pow(1024, i)).toFixed(i ? 1 : 0)} ${u[i]}` } export default function ViewApkAudit() { const [result, setResult] = useState(null) const [loading, setLoading] = useState(false) const [tab, setTab] = useState('overview') // package picker const [search, setSearch] = useState('') const [packages, setPackages] = useState([]) const [pkgsLoaded, setPkgsLoaded] = useState(false) const [showPicker, setShowPicker] = useState(false) // findings controls const [findFilter, setFindFilter] = useState('all') const [findSearch, setFindSearch] = useState('') const [openFinding, setOpenFinding] = useState(null) // explorer const [fileSearch, setFileSearch] = useState('') const [entry, setEntry] = useState(null) const [entryPath, setEntryPath] = useState('') const [entryLoading, setEntryLoading] = useState(false) // export const [showExport, setShowExport] = useState(false) const [exporting, setExporting] = useState(false) const loadPackages = async () => { if (pkgsLoaded) return try { const pkgs = await ListPackages('all') setPackages(pkgs || []) setPkgsLoaded(true) } catch { /* device may be offline; ignore */ } } const run = async (fn: () => Promise) => { setLoading(true); setResult(null); setTab('overview'); setShowPicker(false) setFindFilter('all'); setFindSearch(''); setOpenFinding(null) setEntry(null); setEntryPath(''); setFileSearch(''); setShowExport(false) try { setResult(await fn()) } catch (e: any) { notify.error(e) } finally { setLoading(false) } } const auditFile = async () => { const path = await SelectAPKForAudit() if (path) run(() => AuditAPK(path)) } const openEntry = async (path: string) => { if (!result) return setEntryPath(path); setEntry(null); setEntryLoading(true) try { setEntry(await ReadAPKEntry(result.localPath, path)) } catch (e: any) { notify.error(e); setEntryPath('') } finally { setEntryLoading(false) } } const doExport = async (format: 'json' | 'csv' | 'sarif') => { if (!result) return setShowExport(false); setExporting(true) try { const path = await ExportAudit(result, format) if (path) notify.success(`Exported to ${path}`) } catch (e: any) { notify.error(e) } finally { setExporting(false) } } const filteredPkgs = packages .filter(p => p.packageName.toLowerCase().includes(search.toLowerCase())) const findings = result?.findings ?? [] const visibleFindings = useMemo(() => findings.filter(f => { if (findFilter !== 'all' && f.severity !== findFilter) return false if (findSearch) { const q = findSearch.toLowerCase() return (f.title + f.category + f.cwe + f.masvs).toLowerCase().includes(q) } return true }), [findings, findFilter, findSearch]) const visibleFiles = useMemo(() => (result?.files ?? []).filter(f => !fileSearch || f.path.toLowerCase().includes(fileSearch.toLowerCase()) ).slice(0, 2000), [result, fileSearch]) const dangerousPerms = (result?.permissions ?? []).filter(p => p.dangerous) const tabs: { id: Tab; label: string; icon: React.ReactNode }[] = [ { id: 'overview', label: 'Overview', icon: }, { id: 'findings', label: `Findings (${findings.length})`, icon: }, { id: 'manifest', label: 'Manifest', icon: }, { id: 'components', label: `Components (${result?.components?.length || 0})`, icon: }, { id: 'cert', label: 'Cert', icon: }, { id: 'explorer', label: `Explorer (${result?.files?.length || 0})`, icon: }, ] return (
{/* Source bar */}
APK Audit
{showPicker && (
setSearch(e.target.value)} />
{!pkgsLoaded &&

Loading… (device must be connected)

} {pkgsLoaded && filteredPkgs.length === 0 && (

No matching packages

)} {filteredPkgs.map(p => ( ))}
)}
{/* Empty / loading */} {!result && !loading && (

Browse for an APK file or pick an installed app to audit

Static analysis: manifest, signing, permissions, components, secrets & trackers

)} {loading && (

Auditing… (pulling & parsing DEX, this can take a few seconds)

)} {result && ( <> {/* Header */}
{result.grade}

{result.appLabel || result.fileName} ({result.packageName})

v{result.versionName} (code {result.versionCode}) · SDK {result.minSdk}–{result.targetSdk} · {formatBytes(result.fileSize)} · score {result.score}/100

{SEV_ORDER.map(s => (result.counts?.[s] ? ( {result.counts[s]} {s} ) : null))}
{showExport && (
{(['json', 'csv', 'sarif'] as const).map(f => ( ))}
)}
{/* Tabs */}
{tabs.map(t => ( ))}
{/* OVERVIEW */} {tab === 'overview' && (
{[ { label: 'Package', value: result.packageName }, { label: 'Version', value: `${result.versionName} (${result.versionCode})` }, { label: 'SDK', value: `min ${result.minSdk} · target ${result.targetSdk} · compile ${result.compileSdk}` }, { label: 'Source', value: result.source === 'device' ? 'Installed app' : result.path }, { label: 'SHA-256', value: result.sha256 }, { label: 'Size', value: formatBytes(result.fileSize) }, ].map(({ label, value }) => (
{label} {value || 'N/A'}
))}
{/* manifest flags */}
{result.debuggable && debuggable} {result.allowBackup && allowBackup} {result.usesCleartext && cleartext traffic} {result.hasNetworkSecurityConfig && network-security-config} {result.cert.verified ? signature verified : unsigned / unverified} {result.cert.v3 && v3 sig} {result.cert.v2 && v2 sig} {result.cert.v1 && v1 sig}
{/* dangerous perms */}

Dangerous permissions ({dangerousPerms.length})

{dangerousPerms.length === 0 &&

None of the runtime-dangerous permissions are requested.

}
{dangerousPerms.map(p => ( {p.name.replace('android.permission.', '')} ))}
{/* trackers */}

Trackers / SDKs ({result.trackers?.length || 0})

{(!result.trackers || result.trackers.length === 0) &&

No known tracker SDK signatures detected.

}
{result.trackers?.map(tr => (
{tr.name} {tr.category} ×{tr.matches}
))}
)} {/* FINDINGS */} {tab === 'findings' && (
{(['all', ...SEV_ORDER] as const).map(s => ( ))}
setFindSearch(e.target.value)} />
{visibleFindings.length === 0 && (

No findings match.

)} {visibleFindings.map(f => ( setOpenFinding(openFinding === f.id ? null : f.id)} /> ))}
)} {/* MANIFEST */} {tab === 'manifest' && (

Permissions ({result.permissions?.length || 0})

{result.permissions?.map(p => (
{p.name} {p.dangerous && dangerous}
))}

Decoded AndroidManifest.xml

                    {result.manifestXml || 'Not available'}
                  
)} {/* COMPONENTS */} {tab === 'components' && (
{['activity', 'service', 'receiver', 'provider'].map(type => { const items = result.components.filter(c => c.type === type) return (

{type} ({items.length})

{items.length === 0 &&

None

} {items.map((c, i) => (
{c.name} {c.exported && exported} {!c.exported && c.exportedImplicit && implicit export} {c.permission && protected}
{c.intentFilters?.filter(Boolean).length > 0 && (

↳ {c.intentFilters.filter(Boolean).join(' · ')}

)}
))}
) })}
)} {/* CERT */} {tab === 'cert' && (
{result.cert.verified ? verified : does not verify} {result.cert.v1 && v1 scheme} {result.cert.v2 && v2 scheme} {result.cert.v3 && v3 scheme} {result.cert.isDebug && debug cert} {result.cert.expired && expired} {result.cert.weakAlgo && weak algorithm}
{result.cert.error && (

{result.cert.error}

)} {[ { label: 'Subject', value: result.cert.subject }, { label: 'Issuer', value: result.cert.issuer }, { label: 'Algorithm', value: result.cert.sigAlgo }, { label: 'Serial', value: result.cert.serial }, { label: 'Valid from', value: result.cert.validFrom }, { label: 'Valid to', value: result.cert.validTo }, { label: 'SHA-256', value: result.cert.sha256 }, { label: 'SHA-1', value: result.cert.sha1 }, ].map(({ label, value }) => (

{label}

{value || 'N/A'}

))}
)} {/* EXPLORER */} {tab === 'explorer' && (
{/* file list */}
setFileSearch(e.target.value)} />
{visibleFiles.map(f => ( ))} {(result.files?.length || 0) > visibleFiles.length && (

Showing {visibleFiles.length} of {result.files.length} — refine the filter.

)}
{/* viewer */}
{!entryPath && (
Select a file to view its contents
)} {entryPath && ( <>
{entryPath} {entry && {entry.kind} · {formatBytes(entry.size)}{entry.truncated ? ' · truncated' : ''}}
{entryLoading && (
)} {entry?.kind === 'image' && (
{entry.name}
)} {entry?.kind === 'text' && (
{entry.text}
)} {entry?.kind === 'binary' && (
{entry.hex}
)}
)}
)}
)}
) } function FindingRow({ f, open, onToggle }: { f: APKAuditFinding; open: boolean; onToggle: () => void }) { return (
{open && (

{f.description}

{f.cwe && {f.cwe}} {f.masvs && {f.masvs}} {f.category} confidence {f.confidence}%
{f.matches?.length > 0 && (
{f.matches.map((m, i) => (
{m.file && {m.file}} {m.value}
))}
)}
)}
) }