"""
APK Deep Diff Analyzer - Gradio Web Interface
Hugging Face Space Application
"""
import gradio as gr
import os
import json
import tempfile
import shutil
from typing import Optional, Tuple, List
# Add src to path
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "src"))
from apk_analyzer import APKDeepAnalyzer
from report_generator import ReportGenerator
from utils import get_apk_info, get_file_size_formatted
# Global state for analysis results
_analysis_results = {}
def validate_apk_file(file_path: str) -> Tuple[bool, str]:
"""Validate uploaded APK file."""
if file_path is None:
return False, "لم يتم اختيار ملف"
if not os.path.exists(file_path):
return False, "الملف غير موجود"
if not file_path.endswith('.apk'):
return False, "الملف ليس بصيغة APK"
import zipfile
if not zipfile.is_zipfile(file_path):
return False, "الملف ليس ملف ZIP/APK صالح"
try:
with zipfile.ZipFile(file_path, 'r') as zf:
if "AndroidManifest.xml" not in zf.namelist():
return False, "AndroidManifest.xml غير موجود"
has_dex = any(name.endswith('.dex') for name in zf.namelist())
if not has_dex:
return False, "لا يوجد ملف DEX"
except Exception as e:
return False, f"خطأ في التحقق: {str(e)}"
return True, "صالح"
def get_file_info_display(file_path: str) -> str:
"""Get formatted file info for display."""
if file_path is None or not os.path.exists(file_path):
return "لم يتم اختيار ملف"
info = get_apk_info(file_path)
return f"""
📄 **الاسم:** `{info['filename']}`
📦 **الحجم:** {info['size_formatted']} ({info['size']:,} bytes)
🔐 **SHA-256:** `{info['sha256']}`
🔑 **MD5:** `{info['md5']}`
"""
def update_file_info(file_path, is_original=True):
"""Update file info display."""
if file_path is None:
return ""
info = get_file_info_display(file_path)
return info
def run_analysis(orig_file, mod_file, progress=gr.Progress()):
"""Run deep analysis and return all results."""
global _analysis_results
if orig_file is None or mod_file is None:
return [
gr.update(visible=True, value="❌ الرجاء رفع الملفين أولاً"),
gr.update(visible=False),
gr.update(visible=False),
gr.update(visible=False),
gr.update(visible=False),
gr.update(visible=False),
gr.update(visible=False),
gr.update(visible=False),
gr.update(visible=False),
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
"",
]
# Validate files
valid_orig, msg_orig = validate_apk_file(orig_file)
valid_mod, msg_mod = validate_apk_file(mod_file)
if not valid_orig:
return [
gr.update(visible=True, value=f"❌ APK الأصلي: {msg_orig}"),
gr.update(visible=False), gr.update(visible=False),
gr.update(visible=False), gr.update(visible=False),
gr.update(visible=False), gr.update(visible=False),
gr.update(visible=False), gr.update(visible=False),
"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "",
]
if not valid_mod:
return [
gr.update(visible=True, value=f"❌ APK المعدل: {msg_mod}"),
gr.update(visible=False), gr.update(visible=False),
gr.update(visible=False), gr.update(visible=False),
gr.update(visible=False), gr.update(visible=False),
gr.update(visible=False), gr.update(visible=False),
"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "",
]
progress(0, desc="بدء التحليل...")
def progress_callback(stage, message):
progress(stage / 12, desc=message)
try:
analyzer = APKDeepAnalyzer(orig_file, mod_file, progress_callback)
results = analyzer.analyze()
_analysis_results = results
if "error" in results:
return [
gr.update(visible=True, value=f"❌ خطأ: {results['error']}"),
gr.update(visible=False), gr.update(visible=False),
gr.update(visible=False), gr.update(visible=False),
gr.update(visible=False), gr.update(visible=False),
gr.update(visible=False), gr.update(visible=False),
"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "",
]
# Generate displays
dashboard = create_dashboard(results)
manifest_html = create_manifest_display(results)
dex_html = create_dex_display(results)
native_html = create_native_display(results)
resources_html = create_resources_display(results)
signature_html = create_signature_display(results)
suspicious_html = create_suspicious_display(results)
file_table = create_file_table(results)
report_gen = ReportGenerator(results)
txt_report = report_gen.generate_txt_report()
json_report = report_gen.generate_json_report()
html_report = report_gen.generate_html_report()
progress(1.0, desc="اكتمل التحليل!")
return [
gr.update(visible=False),
gr.update(visible=True),
gr.update(visible=True),
gr.update(visible=True),
gr.update(visible=True),
gr.update(visible=True),
gr.update(visible=True),
gr.update(visible=True),
gr.update(visible=True),
dashboard,
manifest_html,
dex_html,
native_html,
resources_html,
signature_html,
suspicious_html,
file_table,
txt_report,
json_report,
html_report,
gr.update(visible=True),
gr.update(visible=True),
gr.update(visible=True),
gr.update(visible=True),
gr.update(visible=True),
gr.update(visible=True),
gr.update(visible=True),
gr.update(visible=True),
gr.update(visible=True),
gr.update(visible=True),
]
except Exception as e:
return [
gr.update(visible=True, value=f"❌ خطأ غير متوقع: {str(e)}"),
gr.update(visible=False), gr.update(visible=False),
gr.update(visible=False), gr.update(visible=False),
gr.update(visible=False), gr.update(visible=False),
gr.update(visible=False), gr.update(visible=False),
"", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "", "",
]
def create_dashboard(data: dict) -> str:
"""Create dashboard HTML."""
orig = data.get("original_info", {})
mod = data.get("modified_info", {})
summary = data.get("file_comparison", {}).get("summary", {})
manifest = data.get("manifest_comparison", {}).get("changes", {})
perms = manifest.get("permissions", {})
dex = data.get("dex_analysis", {})
native = data.get("native_libraries", {})
sig = data.get("signature_comparison", {})
html = f"""
📦 APK الأصلي
الحجم: {orig.get('size_formatted', 'N/A')}
SHA-256: {orig.get('sha256', 'N/A')[:32]}...
📦 APK المعدل
الحجم: {mod.get('size_formatted', 'N/A')}
SHA-256: {mod.get('sha256', 'N/A')[:32]}...
📊 إجمالي الملفات
{summary.get('total', 0)}
✅ متطابقة
{summary.get('unchanged', 0)}
⚠️ معدلة
{summary.get('modified', 0)}
➕ مضافة
{summary.get('added', 0)}
➖ محذوفة
{summary.get('removed', 0)}
🔐 التوقيع
{'تغير!' if sig.get('signature_changed') else 'متطابق'}
📋 ملفات DEX
العدد: {dex.get('dex_count', 0)}
🔧 Native Libs
مضافة: {len(native.get('added', []))}
محذوفة: {len(native.get('removed', []))}
🔑 صلاحيات جديدة
{len(perms.get('added', []))}
"""
return html
def create_manifest_display(data: dict) -> str:
"""Create manifest changes display."""
manifest = data.get("manifest_comparison", {})
changes = manifest.get("changes", {})
html = ''
html += '
📋 تغييرات AndroidManifest.xml
'
# Package info
pkg = changes.get("package_info", {})
if pkg:
html += '
معلومات الحزمة:
'
for key, vals in pkg.items():
html += f'{key}: {vals.get("original", "N/A")} → {vals.get("modified", "N/A")} '
html += '
'
# Permissions
perms = changes.get("permissions", {})
if perms.get("added"):
html += '
➕ صلاحيات مضافة:
'
for p in perms["added"]:
html += f'- + {p}
'
html += '
'
if perms.get("removed"):
html += '
➖ صلاحيات محذوفة:
'
for p in perms["removed"]:
html += f'- - {p}
'
html += '
'
# Activities
acts = changes.get("activities", {})
if acts.get("added"):
html += '
➕ Activities جديدة:
'
for a in acts["added"]:
html += f'- + {a.get("name", "Unknown")}
'
html += '
'
if acts.get("removed"):
html += '
➖ Activities محذوفة:
'
for a in acts["removed"]:
html += f'- - {a.get("name", "Unknown")}
'
html += '
'
# Services
svcs = changes.get("services", {})
if svcs.get("added"):
html += '
➕ Services جديدة:
'
for s in svcs["added"]:
html += f'- + {s.get("name", "Unknown")}
'
html += '
'
if svcs.get("removed"):
html += '
➖ Services محذوفة:
'
for s in svcs["removed"]:
html += f'- - {s.get("name", "Unknown")}
'
html += '
'
# Receivers
recs = changes.get("receivers", {})
if recs.get("added"):
html += '
➕ Receivers جديدة:
'
for r in recs["added"]:
html += f'- + {r.get("name", "Unknown")}
'
html += '
'
if recs.get("removed"):
html += '
➖ Receivers محذوفة:
'
for r in recs["removed"]:
html += f'- - {r.get("name", "Unknown")}
'
html += '
'
html += '
'
return html
def create_dex_display(data: dict) -> str:
"""Create DEX analysis display."""
dex = data.get("dex_analysis", {})
html = ''
html += '
📊 تحليل ملفات DEX
'
comp = dex.get("comparison", {})
html += f'
عدد ملفات DEX في الأصلي: {comp.get("original_count", 0)}
'
html += f'
عدد ملفات DEX في المعدل: {comp.get("modified_count", 0)}
'
if comp.get("added"):
html += '
➕ DEX files مضافة:
'
for f in comp["added"]:
html += f'- + {f}
'
html += '
'
if comp.get("removed"):
html += '
➖ DEX files محذوفة:
'
for f in comp["removed"]:
html += f'- - {f}
'
html += '
'
# DEX file details
html += '
تفاصيل ملفات DEX:
'
for fname, fdata in dex.get("dex_files", {}).items():
header = fdata.get("header", {})
if "error" not in header:
html += f'
'
html += f'{fname}
'
html += f'Classes: {header.get("class_defs_count", "N/A")} | '
html += f'Methods: {header.get("method_ids_count", "N/A")} | '
html += f'Fields: {header.get("field_ids_count", "N/A")} | '
html += f'Strings: {header.get("string_ids_count", "N/A")}'
html += '
'
# URLs
urls = dex.get("all_urls", [])
if urls:
html += '
🔗 URLs المستخرجة:
'
for url in urls[:30]:
html += f'{url} '
if len(urls) > 30:
html += f'- ... و {len(urls) - 30} أخرى
'
html += '
'
# IPs
ips = dex.get("all_ips", [])
if ips:
html += '
🌐 عناوين IP:
'
for ip in ips[:20]:
html += f'{ip} '
html += '
'
html += '
'
return html
def create_native_display(data: dict) -> str:
"""Create native libraries display."""
native = data.get("native_libraries", {})
html = ''
html += '
🔧 مكتبات Native
'
if native.get("added"):
html += '
➕ مكتبات مضافة:
'
for lib in native["added"]:
html += f'- + {lib}
'
html += '
'
if native.get("removed"):
html += '
➖ مكتبات محذوفة:
'
for lib in native["removed"]:
html += f'- - {lib}
'
html += '
'
if native.get("modified"):
html += '
⚠️ مكتبات معدلة:
'
for lib in native["modified"]:
html += f'- ~ {lib}
'
html += '
'
if native.get("details"):
html += '
تفاصيل جميع المكتبات:
'
html += '
'
html += '| المكتبة | الحالة | الحجم الأصلي | الحجم المعدل |
'
for detail in native["details"]:
status_color = {"Added": "#4ecdc4", "Removed": "#ff6b6b", "Modified": "#feca57", "Unchanged": "#48dbfb"}.get(detail["status"], "#eee")
html += f'| {detail["name"]} | '
html += f'{detail["status"]} | '
html += f'{detail.get("original_size", "N/A")} | '
html += f'{detail.get("modified_size", "N/A")} |
'
html += '
'
html += '
'
return html
def create_resources_display(data: dict) -> str:
"""Create resources display."""
res = data.get("resources", {})
html = ''
html += '
🎨 الموارد (Resources)
'
res_data = res.get("res", {})
orig_res = res_data.get("original", {})
mod_res = res_data.get("modified", {})
html += f'
ملفات res في الأصلي: {orig_res.get("count", 0)}
'
html += f'
ملفات res في المعدل: {mod_res.get("count", 0)}
'
assets_data = res.get("assets", {})
orig_assets = assets_data.get("original", {})
mod_assets = assets_data.get("modified", {})
html += f'
ملفات assets في الأصلي: {orig_assets.get("count", 0)}
'
html += f'
ملفات assets في المعدل: {mod_assets.get("count", 0)}
'
arsc = res.get("resources_arsc", {})
if arsc.get("original_exists") or arsc.get("modified_exists"):
html += '
resources.arsc:
'
html += f'
الأصلي: {"موجود" if arsc.get("original_exists") else "غير موجود"} ({arsc.get("original_size", 0):,} bytes)
'
html += f'
المعدل: {"موجود" if arsc.get("modified_exists") else "غير موجود"} ({arsc.get("modified_size", 0):,} bytes)
'
html += '
'
return html
def create_signature_display(data: dict) -> str:
"""Create signature display."""
sig = data.get("signature_comparison", {})
html = ''
html += '
🔐 تحليل التوقيع
'
if sig.get("signature_changed"):
html += '
'
html += '
⚠️ التوقيع تغير!
'
html += '
تم إعادة توقيع APK. هذا أمر طبيعي إذا تم تعديل التطبيق.
'
html += '
'
else:
html += '
'
html += '
✅ التوقيع متطابق
'
html += '
التوقيع الرقمي لم يتغير بين النسختين.
'
html += '
'
orig_sig = sig.get("original", {})
mod_sig = sig.get("modified", {})
html += '
معلومات التوقيع الأصلي:
'
html += f'
V1 Signing: {"نعم" if orig_sig.get("has_v1_signing") else "لا"}
'
html += f'
V2 Signing: {"نعم" if orig_sig.get("has_v2_signing") else "لا"}
'
html += f'
V3 Signing: {"نعم" if orig_sig.get("has_v3_signing") else "لا"}
'
html += '
معلومات التوقيع المعدل:
'
html += f'
V1 Signing: {"نعم" if mod_sig.get("has_v1_signing") else "لا"}
'
html += f'
V2 Signing: {"نعم" if mod_sig.get("has_v2_signing") else "لا"}
'
html += f'
V3 Signing: {"نعم" if mod_sig.get("has_v3_signing") else "لا"}
'
# Certificates
orig_certs = orig_sig.get("certificates", [])
if orig_certs:
html += '
الشهادات الأصلية:
'
for cert in orig_certs:
html += f'{cert["filename"]} - SHA-256: {cert["sha256"][:32]}... '
html += '
'
html += '
'
return html
def create_suspicious_display(data: dict) -> str:
"""Create suspicious changes display."""
suspicious = data.get("suspicious_changes", [])
html = ''
html += '
⚠️ تغييرات مشبوهة محتملة
'
if not suspicious:
html += '
'
html += '
✅ لم يتم اكتشاف تغييرات مشبوهة واضحة.
'
html += '
ملاحظة: هذا لا يعني أن التطبيق آمن 100%. يُنصح بالمراجعة اليدوية.
'
html += '
'
else:
html += '
'
html += '
⚠️ تم اكتشاف المؤشرات التالية (تتطلب مراجعة يدوية):
'
html += '
'
for item in suspicious:
html += f'- ⚠️ {item}
'
html += '
'
html += '
ملاحظة: هذه المؤشرات لا تعني بالضرورة وجود تهديد. تتطلب مراجعة يدوية.
'
html += '
'
html += '
'
return html
def create_file_table(data: dict) -> str:
"""Create detailed file comparison table."""
details = data.get("file_comparison", {}).get("details", [])
html = ''
html += '
📑 مقارنة تفصيلية للملفات
'
html += '
'
html += '
'
html += ''
html += '| المسار | '
html += 'الحالة | '
html += 'حجم أصلي | '
html += 'حجم معدل | '
html += 'SHA-256 أصلي | '
html += 'SHA-256 معدل | '
html += '
'
for detail in details:
status = detail["status"]
status_color = {
"Added": "#4ecdc4",
"Removed": "#ff6b6b",
"Modified": "#feca57",
"Unchanged": "#48dbfb"
}.get(status, "#eee")
orig_size = detail.get("original_size", 0)
mod_size = detail.get("modified_size", 0)
html += f''
html += f'{detail["path"]} | '
html += f'{status} | '
html += f'{orig_size:,} | '
html += f'{mod_size:,} | '
html += f'{detail.get("original_sha256", "")[:16]}... | '
html += f'{detail.get("modified_sha256", "")[:16]}... | '
html += '
'
html += '
'
return html
def copy_report(report_text):
"""Return report for copying."""
return report_text
def download_txt(report_text):
"""Prepare TXT download."""
return report_text
def download_json(report_text):
"""Prepare JSON download."""
return report_text
def download_html(report_text):
"""Prepare HTML download."""
return report_text
# ==================== GRADIO UI ====================
css = """
:root {
--primary: #00d4ff;
--secondary: #ff6b6b;
--accent: #feca57;
--success: #4ecdc4;
--bg: #1a1a2e;
--card: #16213e;
--border: #0f3460;
}
body {
background: var(--bg) !important;
color: #eee !important;
}
.gradio-container {
background: var(--bg) !important;
}
.tabs {
background: var(--card) !important;
border-radius: 12px !important;
border: 1px solid var(--border) !important;
}
.tab-nav {
background: var(--card) !important;
border-bottom: 2px solid var(--border) !important;
}
.tab-nav button {
color: #eee !important;
font-weight: bold !important;
}
.tab-nav button.selected {
color: var(--primary) !important;
border-bottom: 3px solid var(--primary) !important;
}
.upload-box {
border: 2px dashed var(--border) !important;
border-radius: 12px !important;
background: var(--card) !important;
}
.upload-box:hover {
border-color: var(--primary) !important;
}
.primary-btn {
background: linear-gradient(135deg, var(--primary), #0099cc) !important;
color: #000 !important;
font-weight: bold !important;
border: none !important;
border-radius: 8px !important;
}
.primary-btn:hover {
background: linear-gradient(135deg, #00e5ff, var(--primary)) !important;
}
.error-box {
background: #ff6b6b22 !important;
border: 2px solid var(--secondary) !important;
border-radius: 10px !important;
color: var(--secondary) !important;
}
.info-box {
background: var(--card) !important;
border: 1px solid var(--border) !important;
border-radius: 10px !important;
padding: 15px !important;
}
"""
with gr.Blocks(title="APK Deep Diff Analyzer") as demo:
gr.Markdown("""
🔍 APK Deep Diff Analyzer
أداة تحليل عميق ومقارنة احترافية لملفات Android APK
Professional APK Forensic Analysis & Deep Diff Tool
""")
# Error message
error_msg = gr.Markdown(visible=False, elem_classes=["error-box"])
with gr.Row():
with gr.Column():
gr.Markdown("### 📦 APK الأصلي (Original)")
orig_upload = gr.File(
label="",
file_types=[".apk"],
elem_classes=["upload-box"]
)
orig_info = gr.Markdown(elem_classes=["info-box"])
with gr.Column():
gr.Markdown("### 📦 APK المعدل (Modified)")
mod_upload = gr.File(
label="",
file_types=[".apk"],
elem_classes=["upload-box"]
)
mod_info = gr.Markdown(elem_classes=["info-box"])
# Update file info on upload
orig_upload.change(
fn=update_file_info,
inputs=[orig_upload],
outputs=[orig_info]
)
mod_upload.change(
fn=update_file_info,
inputs=[mod_upload],
outputs=[mod_info]
)
# Start analysis button
analyze_btn = gr.Button(
"🚀 بدء التحليل العميق | Start Deep Analysis",
elem_classes=["primary-btn"],
size="lg"
)
# Results tabs (initially hidden)
with gr.Tabs(visible=False) as results_tabs:
with gr.TabItem("📊 Dashboard"):
dashboard_output = gr.HTML()
with gr.TabItem("📋 Manifest"):
manifest_output = gr.HTML()
with gr.TabItem("📊 DEX Analysis"):
dex_output = gr.HTML()
with gr.TabItem("🔧 Native Libraries"):
native_output = gr.HTML()
with gr.TabItem("🎨 Resources"):
resources_output = gr.HTML()
with gr.TabItem("🔐 Signature"):
signature_output = gr.HTML()
with gr.TabItem("⚠️ Suspicious"):
suspicious_output = gr.HTML()
with gr.TabItem("📑 File Comparison"):
file_table_output = gr.HTML()
with gr.TabItem("📄 Reports"):
with gr.Row():
with gr.Column():
gr.Markdown("### 📝 تقرير نصي (TXT)")
txt_report = gr.Textbox(
label="",
lines=20,
interactive=False
)
copy_txt_btn = gr.Button("📋 نسخ التقرير", visible=False)
with gr.Column():
gr.Markdown("### 📊 تقرير JSON")
json_report = gr.Textbox(
label="",
lines=20,
interactive=False
)
copy_json_btn = gr.Button("📋 نسخ JSON", visible=False)
with gr.Row():
with gr.Column():
gr.Markdown("### 🌐 تقرير HTML")
html_report = gr.Textbox(
label="",
lines=20,
interactive=False
)
copy_html_btn = gr.Button("📋 نسخ HTML", visible=False)
# Privacy notice
gr.Markdown("""
🔒 Privacy Notice: الملفات المرفوعة تُستخدم للتحليل فقط ولا تُحفظ على الخادم.
Uploaded files are used for analysis only and are not stored on the server.
""")
# Analysis button click
analyze_btn.click(
fn=run_analysis,
inputs=[orig_upload, mod_upload],
outputs=[
error_msg,
results_tabs,
dashboard_output,
manifest_output,
dex_output,
native_output,
resources_output,
signature_output,
suspicious_output,
file_table_output,
txt_report,
json_report,
html_report,
copy_txt_btn,
copy_json_btn,
copy_html_btn,
]
)
if __name__ == "__main__":
demo.launch(css=css, theme=gr.themes.Soft())