# Mobile Verification Toolkit (MVT)
# Copyright (c) 2021-2023 The MVT Authors.
# Use of this software is governed by the MVT License 1.1 that can be found at
#   https://license.mvt.re/1.1/

import json
import logging
import time
from typing import Optional

from rich.progress import track

from mvt.android.utils import (
    BROWSER_INSTALLERS,
    PLAY_STORE_INSTALLERS,
    ROOT_PACKAGES,
    SECURITY_PACKAGES,
    SYSTEM_UPDATE_PACKAGES,
    THIRD_PARTY_STORE_INSTALLERS,
)
from mvt.common.module_types import ModuleAtomicResult, ModuleResults
from mvt.common.virustotal import VTNoKey, VTQuotaExceeded, virustotal_lookup

from .base import AndroidQFModule


class AQFPackages(AndroidQFModule):
    """This module examines the installed packages in packages.json"""

    def __init__(
        self,
        file_path: Optional[str] = None,
        target_path: Optional[str] = None,
        results_path: Optional[str] = None,
        module_options: Optional[dict] = None,
        log: logging.Logger = logging.getLogger(__name__),
        results: Optional[ModuleResults] = None,
    ) -> None:
        super().__init__(
            file_path=file_path,
            target_path=target_path,
            results_path=results_path,
            module_options=module_options,
            log=log,
            results=results,
        )

    def check_indicators(self) -> None:
        for result in self.results:
            if result["name"] in ROOT_PACKAGES:
                self.alertstore.medium(
                    f'Found an installed package related to rooting/jailbreaking: "{result["name"]}"',
                    "",
                    result,
                )
                continue

            # Detections for apps installed via unusual methods.
            if result["installer"] in THIRD_PARTY_STORE_INSTALLERS:
                self.alertstore.info(
                    f'Found a package installed via a third party store (installer="{result["installer"]}"): "{result["name"]}"',
                    "",
                    result,
                )
            elif result["installer"] in BROWSER_INSTALLERS:
                self.alertstore.medium(
                    f'Found a package installed via a browser (installer="{result["installer"]}"): "{result["name"]}"',
                    "",
                    result,
                )
            elif result["installer"] == "null" and result["system"] is False:
                self.alertstore.medium(
                    f'Found a non-system package installed via adb or another method: "{result["name"]}"',
                    "",
                    result,
                )
            elif result["installer"] in PLAY_STORE_INSTALLERS:
                pass

            # Check for disabled security or software update packages.
            package_disabled = result.get("disabled", None)
            if result["name"] in SECURITY_PACKAGES and package_disabled:
                self.alertstore.medium(
                    f'Security package "{result["name"]}" disabled on the phone',
                    "",
                    result,
                )

            if result["name"] in SYSTEM_UPDATE_PACKAGES and package_disabled:
                self.alertstore.medium(
                    f'System OTA update package "{result["name"]}" disabled on the phone',
                    "",
                    result,
                )

            if not self.indicators:
                continue

            ioc_match = self.indicators.check_app_id(result.get("name") or "")
            if ioc_match:
                self.alertstore.critical(
                    ioc_match.message, "", result, matched_indicator=ioc_match.ioc
                )

            for package_file in result.get("files", []):
                ioc_match = self.indicators.check_file_hash(
                    package_file.get("sha256") or ""
                )
                if ioc_match:
                    self.alertstore.critical(
                        ioc_match.message, "", result, matched_indicator=ioc_match.ioc
                    )

                if "certificate" not in package_file:
                    continue

                # The keys generated by AndroidQF have a leading uppercase character.
                for hash_type in ["Md5", "Sha1", "Sha256"]:
                    certificate_hash = package_file["certificate"][hash_type]
                    ioc_match = self.indicators.check_app_certificate_hash(
                        certificate_hash
                    )
                    if ioc_match:
                        self.alertstore.critical(
                            ioc_match.message,
                            "",
                            result,
                            matched_indicator=ioc_match.ioc,
                        )
                        break

        if self.module_options.get("virustotal", False):
            self.check_virustotal(
                delay=self.module_options.get("virustotal_delay", 0)
            )

    def check_virustotal(self, delay: int = 0) -> None:
        files_by_hash: dict[
            str, list[tuple[ModuleAtomicResult, ModuleAtomicResult]]
        ] = {}
        for package in self.results:
            if package.get("system", False):
                continue

            for package_file in package.get("files", []):
                file_hash = package_file.get("sha256")
                if not file_hash:
                    continue

                files_by_hash.setdefault(file_hash, []).append((package, package_file))

        total_hashes = len(files_by_hash)
        if total_hashes == 0:
            return

        progress_desc = f"Looking up {total_hashes} package files on VirusTotal..."
        for index, file_hash in enumerate(
            track(files_by_hash, description=progress_desc)
        ):
            try:
                results = virustotal_lookup(file_hash)
            except VTNoKey as exc:
                self.log.warning("%s", exc)
                return
            except VTQuotaExceeded as exc:
                self.log.warning("Unable to continue VirusTotal lookups: %s", exc)
                break

            if index < total_hashes - 1 and delay > 0:
                time.sleep(delay)

            if not results:
                continue

            attributes = results.get("attributes", {})
            stats = attributes.get("last_analysis_stats", {})
            positives = stats.get("malicious", 0)
            total = len(attributes.get("last_analysis_results", {}))
            detection = f"{positives}/{total}"

            for package, package_file in files_by_hash[file_hash]:
                package_file["virustotal"] = detection
                if positives > 0:
                    self.alertstore.high(
                        f'VirusTotal flagged package "{package["name"]}" file '
                        f'"{package_file["path"]}" with {detection} detections',
                        "",
                        package,
                    )

    def run(self) -> None:
        packages = self._get_files_by_pattern("*/packages.json")
        if not packages:
            self.log.error(
                "packages.json file not found in this androidqf bundle. Possibly malformed?"
            )
            return

        self.results = json.loads(self._get_file_content(packages[0]))
        self.log.info("Found %d packages in packages.json", len(self.results))
