#!/usr/bin/env python3
"""Agent for iOS app reverse engineering with Frida.

Uses frida-tools to attach to iOS processes, hook Objective-C
methods, bypass SSL pinning, dump keychain entries, and trace
API calls for security assessment.
"""

import subprocess
import json
import sys
from datetime import datetime
from pathlib import Path


class FridaIOSAgent:
    """Reverse engineers iOS applications using Frida."""

    def __init__(self, target_app, device_id=None, output_dir="./frida_ios"):
        self.target_app = target_app
        self.device_id = device_id
        self.output_dir = Path(output_dir)
        self.output_dir.mkdir(parents=True, exist_ok=True)
        self.findings = []

    def _frida_cmd(self, script_code, timeout=60):
        cmd = ["frida", "-U"]
        if self.device_id:
            cmd.extend(["-D", self.device_id])
        cmd.extend(["-n", self.target_app, "-q", "-e", script_code])
        try:
            result = subprocess.run(cmd, capture_output=True, text=True,
                                    timeout=timeout)
            return {"stdout": result.stdout, "stderr": result.stderr,
                    "returncode": result.returncode}
        except (FileNotFoundError, subprocess.TimeoutExpired) as exc:
            return {"error": str(exc)}

    def list_running_apps(self):
        """List running applications on the connected iOS device."""
        cmd = ["frida-ps", "-Ua"]
        if self.device_id:
            cmd.extend(["-D", self.device_id])
        try:
            result = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
            return {"apps": result.stdout}
        except (FileNotFoundError, subprocess.TimeoutExpired) as exc:
            return {"error": str(exc)}

    def bypass_ssl_pinning(self):
        """Inject Frida script to bypass SSL certificate pinning."""
        script = """
var m = ObjC.classes.NSURLSessionConfiguration;
Interceptor.attach(m['- setTLSMinimumSupportedProtocol:'].implementation, {
    onEnter: function(args) { console.log('[*] TLS config intercepted'); }
});
try {
    var SSLSetPeerDomainName = Module.findExportByName(null, 'SSLSetPeerDomainName');
    if (SSLSetPeerDomainName) {
        Interceptor.attach(SSLSetPeerDomainName, {
            onEnter: function(args) { },
            onLeave: function(retval) { retval.replace(0); }
        });
        console.log('[+] SSL pinning bypassed');
    }
} catch(e) { console.log('[-] ' + e); }
"""
        result = self._frida_cmd(script)
        if "SSL pinning bypassed" in result.get("stdout", ""):
            self.findings.append({"type": "SSL Pinning Bypass",
                                  "severity": "Medium",
                                  "details": "SSL pinning can be bypassed with Frida"})
        return result

    def dump_keychain(self):
        """Dump iOS Keychain entries accessible by the app."""
        script = """
var kSecClass = ObjC.classes.NSString.stringWithString_('kSecClass');
var query = ObjC.classes.NSMutableDictionary.dictionary();
query.setObject_forKey_(ObjC.classes.NSString.stringWithString_('kSecClassGenericPassword'), kSecClass);
query.setObject_forKey_(ObjC.classes.NSNumber.numberWithBool_(true), ObjC.classes.NSString.stringWithString_('kSecReturnAttributes'));
query.setObject_forKey_(ObjC.classes.NSString.stringWithString_('kSecMatchLimitAll'), ObjC.classes.NSString.stringWithString_('kSecMatchLimit'));
var result = new ObjC.Object(ptr(0));
var status = ObjC.classes.NSDictionary.alloc();
console.log('[*] Keychain query executed');
"""
        result = self._frida_cmd(script)
        return result

    def trace_objc_methods(self, class_name):
        """Trace all method calls on a specific Objective-C class."""
        script = f"""
var target = ObjC.classes.{class_name};
if (target) {{
    var methods = target.$ownMethods;
    console.log('[*] Tracing ' + methods.length + ' methods on {class_name}');
    methods.forEach(function(method) {{
        try {{
            Interceptor.attach(target[method].implementation, {{
                onEnter: function(args) {{
                    console.log('[CALL] {class_name} ' + method);
                }}
            }});
        }} catch(e) {{}}
    }});
}} else {{
    console.log('[-] Class {class_name} not found');
}}
"""
        return self._frida_cmd(script, timeout=30)

    def check_jailbreak_detection(self):
        """Test if app has jailbreak detection and attempt bypass."""
        script = """
var paths = ['/Applications/Cydia.app', '/usr/sbin/sshd',
             '/bin/bash', '/usr/bin/ssh', '/etc/apt'];
var NSFileManager = ObjC.classes.NSFileManager;
Interceptor.attach(NSFileManager['- fileExistsAtPath:'].implementation, {
    onEnter: function(args) {
        var path = ObjC.Object(args[2]).toString();
        for (var i = 0; i < paths.length; i++) {
            if (path.indexOf(paths[i]) !== -1) {
                console.log('[*] Jailbreak check: ' + path);
            }
        }
    },
    onLeave: function(retval) {}
});
console.log('[+] Jailbreak detection hooks installed');
"""
        result = self._frida_cmd(script, timeout=15)
        if "Jailbreak check" in result.get("stdout", ""):
            self.findings.append({"type": "Jailbreak Detection Present",
                                  "severity": "Info"})
        return result

    def generate_report(self):
        report = {
            "target_app": self.target_app,
            "report_date": datetime.utcnow().isoformat(),
            "findings": self.findings,
        }
        report_path = self.output_dir / "frida_ios_report.json"
        with open(report_path, "w") as f:
            json.dump(report, f, indent=2)
        print(json.dumps(report, indent=2))
        return report


def main():
    app = sys.argv[1] if len(sys.argv) > 1 else "TargetApp"
    agent = FridaIOSAgent(app)
    agent.list_running_apps()
    agent.bypass_ssl_pinning()
    agent.check_jailbreak_detection()
    agent.generate_report()


if __name__ == "__main__":
    main()
