# -*- coding: utf-8 -*-
# path: C:\Users\mdavi\PycharmProjects\SpyHunter\modules\deep_scan.py

import os
import subprocess
import time
import requests
import hashlib
from datetime import datetime

# ---------------------------------------------------------------------------
# Optional Androguard Imports
# ---------------------------------------------------------------------------
try:
    from androguard.core.bytecodes.apk import APK
    from androguard.core.bytecodes.dvm import DalvikVMFormat
    from androguard.core.analysis.analysis import Analysis
    ANDROGUARD_AVAILABLE = True
except (ImportError, ModuleNotFoundError):
    print("[WARN] Androguard not available; heuristic scans will be skipped.")
    ANDROGUARD_AVAILABLE = False

# ---------------------------------------------------------------------------
# Optional YARA Imports
# ---------------------------------------------------------------------------
try:
    import yara
    YARA_AVAILABLE = True
except ImportError:
    print("[WARN] YARA not available; signature scanning will be skipped.")
    YARA_AVAILABLE = False

# ---------------------------------------------------------------------------
# Constants: Directories for pulled APKs and signature rules
# ---------------------------------------------------------------------------
APK_PULL_DIR    = os.path.join(os.getcwd(), "pulled_apks")
SIGNATURES_DIR  = os.path.join(os.getcwd(), "signatures")
LOG_DIR         = os.path.join(os.getcwd(), "logs")
LOG_FILE        = os.path.join(LOG_DIR, "action_log.txt")

# ---------------------------------------------------------------------------
# Dynamic Discovery via ADB
# ---------------------------------------------------------------------------
def get_installed_apks():
    """
    Discover installed APKs via ADB and pull them locally for analysis.
    Returns a list of filepaths to the pulled APKs.
    """
    os.makedirs(APK_PULL_DIR, exist_ok=True)
    try:
        proc = subprocess.run(
            ["adb", "shell", "pm", "list", "packages", "-f"],
            capture_output=True, text=True, check=True
        )
    except subprocess.CalledProcessError as e:
        print(f"[ERROR] Failed to list packages: {e}")
        return []

    apk_paths = []
    for line in proc.stdout.splitlines():
        if not line.startswith("package:"):
            continue
        # format: package:/data/app/.../base.apk=com.example.app
        try:
            left, pkg = line[len("package:"):].split("=", 1)
            remote_path = left.strip()
            package_name = pkg.strip()
            local_name = package_name.replace(".", "_") + ".apk"
            local_path = os.path.join(APK_PULL_DIR, local_name)

            print(f"[INFO] Pulling {package_name} from {remote_path}...")
            subprocess.run(["adb", "pull", remote_path, local_path], check=True)
            apk_paths.append(local_path)
        except Exception as e:
            print(f"[WARN] Could not pull '{line}': {e}")

    return apk_paths

# ---------------------------------------------------------------------------
# Signature Scanning (YARA/CSV)
# ---------------------------------------------------------------------------
def signature_scan(apk_paths):
    """
    Scan each APK with optional YARA rules.
    Returns list of dicts:
      {
        app_name: str,
        package: str or None,
        sha256: str or None,
        apk_path: str,
        yara_matches: List[str]
      }
    """
    # compile YARA rules if available
    rules = None
    if YARA_AVAILABLE and os.path.isdir(SIGNATURES_DIR):
        files = [
            os.path.join(SIGNATURES_DIR, f)
            for f in os.listdir(SIGNATURES_DIR)
            if f.endswith((".yar", ".yara"))
        ]
        if files:
            try:
                rules = yara.compile(filepaths={os.path.basename(f): f for f in files})
            except Exception as e:
                print(f"[ERROR] Failed to compile YARA rules: {e}")

    results = []
    for path in apk_paths:
        app_name = os.path.basename(path)
        # compute SHA256
        try:
            with open(path, "rb") as f:
                sha256 = hashlib.sha256(f.read()).hexdigest()
        except Exception as e:
            print(f"[WARN] Could not hash {path}: {e}")
            sha256 = None

        # extract package via Androguard if available
        package = None
        if ANDROGUARD_AVAILABLE:
            try:
                a = APK(path)
                package = a.get_package()
            except Exception:
                pass

        # run YARA
        yara_matches = []
        if rules:
            try:
                matches = rules.match(path)
                yara_matches = [m.rule for m in matches] if matches else []
            except Exception as e:
                print(f"[WARN] YARA scan error for {path}: {e}")

        results.append({
            "app_name":    app_name,
            "package":     package,
            "sha256":      sha256,
            "apk_path":    path,
            "yara_matches": yara_matches,
        })

    return results

# ---------------------------------------------------------------------------
# Logging Setup
# ---------------------------------------------------------------------------
def ensure_log_dir():
    os.makedirs(LOG_DIR, exist_ok=True)

def log_action(action, package, status, message=""):
    """
    Append a timestamped action entry to the log file.
    """
    ensure_log_dir()
    ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    with open(LOG_FILE, "a", encoding="utf-8") as f:
        f.write(f"[{ts}] {action.upper()} | {package} | {status} | {message}\n")

# ---------------------------------------------------------------------------
# abuse.ch Integration
# ---------------------------------------------------------------------------
def query_abuse_ch(sha256: str) -> bool:
    """
    Return True if abuse.ch confirms this hash, False otherwise.
    """
    try:
        resp = requests.post(
            "https://mb-api.abuse.ch/api/v1/",
            data={"query": "get_info", "hash": sha256},
            timeout=10
        )
        return resp.ok and bool(resp.json().get("data"))
    except Exception as e:
        print(f"[ERROR] abuse.ch query failed: {e}")
        return False

# ---------------------------------------------------------------------------
# Heuristics / Behavioral Detection
# ---------------------------------------------------------------------------
SUSPICIOUS_PERMS = {
    "android.permission.READ_SMS",
    "android.permission.READ_CONTACTS",
    "android.permission.RECORD_AUDIO",
    "android.permission.CAMERA",
    "android.permission.ACCESS_FINE_LOCATION",
}
SUSPICIOUS_COMPONENT_KEYWORDS = ("tracker", "spy", "monitor", "surveil")

def heuristics_scan(apk_paths):
    """
    Perform heuristic analysis on each APK.
    Returns list of dicts: { apk_path, reason }.
    Skipped entirely if Androguard is unavailable.
    """
    if not ANDROGUARD_AVAILABLE:
        return []

    hits = []
    for path in apk_paths:
        try:
            a = APK(path)
            perms = set(a.get_permissions())
            bad = perms & SUSPICIOUS_PERMS
            if bad:
                hits.append({
                    "apk_path": path,
                    "reason": f"Suspicious perms: {', '.join(bad)}"
                })
                continue

            for comp in a.get_services() + a.get_receivers():
                if any(k in comp.lower() for k in SUSPICIOUS_COMPONENT_KEYWORDS):
                    hits.append({
                        "apk_path": path,
                        "reason": f"Suspicious component: {comp}"
                    })
                    raise StopIteration

            dex = DalvikVMFormat(a.get_dex())
            for method in dex.get_methods():
                src = method.get_source()
                if "forName(" in src or "loadLibrary(" in src:
                    hits.append({
                        "apk_path": path,
                        "reason": "Uses reflection/dynamic load"
                    })
                    raise StopIteration

        except StopIteration:
            continue
        except Exception as e:
            print(f"[WARN] Heuristic error on {path}: {e}")

    return hits

# ---------------------------------------------------------------------------
# Freeze / Uninstall with Retry + Logging
# ---------------------------------------------------------------------------
def prompt_reboot_and_retry(pkg, action):
    print(f"\n[NOTE] {action.title()} failed for {pkg}.")
    if input("Reboot device and retry? [y/N]: ").strip().lower() != "y":
        return

    try:
        subprocess.run(["adb", "reboot"], check=True)
        print("[INFO] Rebooting... press Enter once device is back online.")
        input()
        cmd = (
            ["adb","shell","pm","disable-user","--user","0",pkg]
            if action == "freeze"
            else ["adb","uninstall",pkg]
        )
        subprocess.run(cmd, check=True)
        print(f"[SUCCESS] Retry {action} succeeded for {pkg}.")
        log_action(action, pkg, "success_after_reboot")
    except subprocess.CalledProcessError:
        print(f"[FAIL] Still unable to {action} {pkg}.")
        log_action(action, pkg, "final_fail_after_reboot")

def freeze_single_apk(info):
    pkg = info.get("package")
    if not pkg:
        print("[ERROR] No package name.")
        return

    print(f"[INFO] Freezing {pkg}...")
    for attempt in range(3):
        try:
            subprocess.run(
                ["adb","shell","pm","disable-user","--user","0",pkg],
                check=True
            )
            print(f"[SUCCESS] {pkg} frozen.")
            log_action("freeze", pkg, "success")
            return
        except subprocess.CalledProcessError:
            print(f"[WARN] Attempt {attempt+1} failed.")
            time.sleep(1)

    print(f"[FAIL] Could not freeze {pkg}.")
    log_action("freeze", pkg, "fail", "3 attempts")
    prompt_reboot_and_retry(pkg, "freeze")

def uninstall_single_apk(info):
    pkg = info.get("package")
    if not pkg:
        print("[ERROR] No package name.")
        return

    print(f"[INFO] Uninstalling {pkg}...")
    for attempt in range(3):
        try:
            subprocess.run(["adb","uninstall",pkg], check=True)
            print(f"[SUCCESS] {pkg} uninstalled.")
            log_action("uninstall", pkg, "success")
            return
        except subprocess.CalledProcessError:
            print(f"[WARN] Attempt {attempt+1} failed.")
            time.sleep(1)

    print(f"[FAIL] Could not uninstall {pkg}.")
    log_action("uninstall", pkg, "fail", "3 attempts")
    prompt_reboot_and_retry(pkg, "uninstall")

def freeze_apks(results):
    for app in results:
        freeze_single_apk(app)

def uninstall_apks(results):
    for app in results:
        uninstall_single_apk(app)

# ---------------------------------------------------------------------------
# Results Display
# ---------------------------------------------------------------------------
def show_result_detail(r):
    print("\n--- App Details ---")
    for k, v in r.items():
        print(f"{k}: {v}")
    sha = r.get("sha256")
    if sha:
        print("\nQuerying abuse.ch…")
        print("[ALERT] Confirmed" if query_abuse_ch(sha)
              else "[WARNING] Not found")
    while True:
        cmd = input("[a] Freeze | [u] Uninstall | [b] Back: ").strip().lower()
        if cmd == "a":
            freeze_single_apk(r)
        elif cmd == "u":
            uninstall_single_apk(r)
        elif cmd == "b":
            break

def show_results_info(results):
    per_page, page = 10, 0
    total = len(results)
    while True:
        start, end = page * per_page, (page + 1) * per_page
        print(f"\n--- Page {page+1}/{(total-1)//per_page+1} ---")
        print("{:<3}{:<30}{:<20}{:<8}".format("#", "App", "Package", "Heur"))
        for idx, item in enumerate(results[start:end], start+1):
            h = "Y" if item.get("heuristic") else ""
            print(
                f"{idx:<3}"
                f"{item.get('app_name','-')[:28]:<30}"
                f"{item.get('package','-'):<20}"
                f"{h:<8}"
            )
        cmd = input("[n]Next [p]Prev [#]Detail [b]Back: ").strip().lower()
        if cmd == "n" and end < total:
            page += 1
        elif cmd == "p" and page > 0:
            page -= 1
        elif cmd.isdigit() and 1 <= int(cmd) <= total:
            show_result_detail(results[int(cmd) - 1])
        elif cmd == "b":
            break

# ---------------------------------------------------------------------------
# End-of-Scan Menu
# ---------------------------------------------------------------------------
def show_post_scan_menu(static_results, apk_paths):
    """
    Merge static (YARA/CSV) hits with heuristics and launch the menu.
    static_results: list of dicts from signature_scan()
    apk_paths:      list of pulled APK file paths
    """
    heur_hits = heuristics_scan(apk_paths)
    combined = []
    for r in static_results:
        r["heuristic"] = False
        combined.append(r)
    for h in heur_hits:
        combined.append({
            "app_name": os.path.basename(h["apk_path"]),
            "package": None,
            "sha256": None,
            "heuristic": True,
            "reason": h["reason"],
            "apk_path": h["apk_path"],
        })

    while True:
        print("\n--- End of Scan Menu ---")
        print("1) Show Results and Information")
        print("2) Freeze Possible Infected APK")
        print("3) Uninstall Possible Infected APKs")
        print("4) Exit")
        choice = input("Select: ").strip()
        if choice == "1":
            show_results_info(combined)
        elif choice == "2":
            freeze_apks(combined)
        elif choice == "3":
            uninstall_apks(combined)
        elif choice == "4":
            break
        else:
            print("Invalid selection.")

# ---------------------------------------------------------------------------
# Entry Point
# ---------------------------------------------------------------------------
def main():
    """
    Invoked by spyhunter.py as modules.scan.main()
    """
    apk_paths      = get_installed_apks()
    static_results = signature_scan(apk_paths)
    show_post_scan_menu(static_results, apk_paths)

