├── .gitignore ├── README.md ├── deeplink_analyser.py ├── helpers ├── adb.py ├── apk_cert.py ├── app_links.py ├── console.py ├── get_schemes.py ├── poc.py └── setup.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | */__pycache__/* 2 | .vscode 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android "App Link Verification" Tester 2 | 3 | Tool that helps with checking if an Android application has successfully completed the "App Link Verification" process for Android App Links. 4 | 5 | You can see more info about this process [here](https://developer.android.com/training/app-links/verify-site-associations). 6 | 7 | ## How does it work? 8 | 9 | This tool supports 6 operation modes: 10 | 11 | * `list-all`: simple enumeration, lists all deep links registered by the application regardless of format 12 | * `list-applinks`: lists all Android App Links registered by the application 13 | * `verify-applinks`: for each App Link, displays checklist with each of the necessary steps for verification, indicates if they've been completed successfully 14 | * `adb-test`: uses `adb` to open all of the application's App Links and allows you to check if they're being automatically opened by the intended application 15 | * `build-poc`: creates an HTML page with links to all of the registered Android App Links, in order to simplify the process of testing their verification process 16 | * `launch-poc`: sends the HTML page created on the previus mode to a connected device (via `adb`), and opens it with Chrome 17 | 18 | It also supports 3 additional flags: 19 | 20 | * `clear`: removes the decompiled directory after execution 21 | * `verbose`: prints additional information about the execution 22 | * `ci-cd`: ideal for running in CI/CD pipelines, exits with `1` if any of the App Links are not correctly verified; automatically runs with `clear`and `verbose` flags 23 | 24 | ## Installation 25 | 26 | ``` 27 | python3 -m pip install -r requirements.txt 28 | ``` 29 | 30 | **Important Notes** 31 | 32 | 1. If you want to provide an `.apk` file instead of the `AndroidManifest.xml` and `strings.xml`, then you need to have [apktool](https://ibotpeaches.github.io/Apktool/) installed and accessible on the `$PATH`; 33 | 2. If you want to use the `adb-test` or `launch-poc` operation modes, you need to have [adb](https://developer.android.com/studio/command-line/adb) installed and accessible on the `$PATH`; 34 | 3. If you want to use the `verify-applinks` operation mode or if you want to be able to install the package on the device, you must use the `-apk` option instead of the manifest+strings file combination. 35 | 4. If you want to use the `verify-applinks` operation mode, you need to have [keytool](https://docs.oracle.com/javase/7/docs/technotes/tools/windows/keytool.html) installed and accessible on the `$PATH`; 36 | 5. If you want to use the `adb-test`, `launch-poc` or `verify-applinks` operation modes you must specify the `-p` option. 37 | 38 | ## Usage 39 | 40 | ``` 41 | ~ python3 Android-App-Link-Verification-Tester/deeplink_analyser.py --help 42 | usage: deeplink_analyser.py [-h] [-apk FILE] [-m FILE] [-s FILE] -op OP 43 | [-p PACKAGE] [-v] [-c] 44 | 45 | optional arguments: 46 | -h, --help show this help message and exit 47 | -apk FILE Path to the APK (required for `verify-applinks` 48 | operation mode) 49 | -m FILE, --manifest FILE 50 | Path to the AndroidManifest.xml file 51 | -s FILE, --strings FILE 52 | Path to the strings.xml file 53 | -op OP, --operation-mode OP 54 | Operation mode: "list-all", "list-applinks", "verify- 55 | applinks", "build-poc", "launch-poc", "adb-test". 56 | -p PACKAGE, --package PACKAGE 57 | Package identifier, e.g.: "com.myorg.appname" 58 | (required for some operation modes) 59 | -v, --verbose Verbose mode 60 | --clear Whether or not the script should delete the decompiled 61 | directory after running (default: False) 62 | --ci-cd Ideal for running in CI/CD pipelines (default: False) 63 | ``` 64 | 65 | ## Examples 66 | 67 | ### Use an APK to list all registered deep links 68 | 69 | ``` 70 | ~ python3 Android-App-Link-Verification-Tester/deeplink_analyser.py \ 71 | -op list-all \ 72 | -apk 73 | ``` 74 | 75 | ### Use the manifest+strings file to list all registered Android App links 76 | 77 | ``` 78 | ~ python3 Android-App-Link-Verification-Tester/deeplink_analyser.py \ 79 | -op list-applinks \ 80 | -m \ 81 | -s 82 | ``` 83 | 84 | Note that the strings.xml file is typically under `/res/values/strings.xml`. 85 | 86 | ### Use an APK to check for DALs for all App Links 87 | 88 | ``` 89 | ~ python3 Android-App-Link-Verification-Tester/deeplink_analyser.py \ 90 | -op verify-applinks \ 91 | -apk \ 92 | -p 93 | ``` 94 | 95 | Note that you can also specify the `-v` flag to print the entire DAL file. 96 | 97 | An example output for the Twitter Android app would be: 98 | 99 | ``` 100 | ~ python3 Android-App-Link-Verification-Tester/deeplink_analyser.py \ 101 | -apk com.twitter.android_2021-10-22.apk \ 102 | -p com.twitter.android \ 103 | -op verify-applinks 104 | 105 | [...] 106 | 107 | The APK's signing certificate's SHA-256 fingerprint is: 108 | 0F:D9:A0:CF:B0:7B:65:95:09:97:B4:EA:EB:DC:53:93:13:92:39:1A:A4:06:53:8A:3B:04:07:3B:C2:CE:2F:E9 109 | 110 | [...] 111 | 112 | Checking http://mobile.twitter.com/.* 113 | 114 | ✓ includes autoverify=true 115 | ✓ includes VIEW action 116 | ✓ includes BROWSABLE category 117 | ✓ includes DEFAULT category 118 | ✓ DAL verified 119 | 120 | Relations: 121 | - [Standard] delegate_permission/common.get_login_creds 122 | - [Standard] delegate_permission/common.handle_all_urls 123 | - [Custom] delegate_permission/common.use_as_origin 124 | 125 | Checking http://twitter.com/.* 126 | 127 | ✓ includes autoverify=true 128 | ✓ includes VIEW action 129 | ✓ includes BROWSABLE category 130 | ✓ includes DEFAULT category 131 | ✓ DAL verified 132 | 133 | Relations: 134 | - [Standard] delegate_permission/common.get_login_creds 135 | - [Standard] delegate_permission/common.handle_all_urls 136 | - [Custom] delegate_permission/common.use_as_origin 137 | 138 | [...] 139 | 140 | Read more about relation strings here: https://developers.google.com/digital-asset-links/v1/relation-strings 141 | ``` 142 | 143 | ### Use an APK to automatically test all of the App Links using ADB 144 | 145 | ``` 146 | ~ python3 Android-App-Link-Verification-Tester/deeplinks_analyser.py \ 147 | -op adb-test \ 148 | -apk \ 149 | -p 150 | ``` 151 | 152 | Note that the package was not installed on the phone previously, so the script installed the APK using `adb`. 153 | 154 | ### Use the manifest+strings file to create a local POC 155 | 156 | ``` 157 | ~ python3 Android-App-Link-Verification-Tester/deeplink_analyser.py \ 158 | -op build-poc \ 159 | -m \ 160 | -s 161 | ``` 162 | 163 | ### Use an APK to send the POC to the device via adb 164 | 165 | ``` 166 | ~ python3 Android-App-Link-Verification-Tester/deeplink_analyser.py \ 167 | -op launch-poc \ 168 | -apk \ 169 | -p 170 | ``` 171 | 172 | As a result, your Android device should display something like this: 173 | 174 | ![Screenshot_20210820-210127](https://user-images.githubusercontent.com/39055313/130288058-625056b5-c569-4597-b852-c911de1d4704.png) 175 | 176 | Then, you can manually click on each of the links: **if the OS prompts you to choose between Chrome and one or more apps, then the App Link Verification process is not correctly implemented**. 177 | -------------------------------------------------------------------------------- /deeplink_analyser.py: -------------------------------------------------------------------------------- 1 | import os 2 | from helpers.console import write_to_console, print_deeplinks, BColors 3 | from helpers.app_links import check_dals 4 | import helpers.setup 5 | import helpers.adb 6 | import helpers.get_schemes 7 | import helpers.poc 8 | 9 | APKTOOL_PATH = 'apktool' 10 | ADB_PATH = 'adb' 11 | DEFAULT_STRINGS_FILE = '/res/values/strings.xml' 12 | DEFAULT_MANIFEST_FILE = '/AndroidManifest.xml' 13 | POC_FILENAME = 'poc.html' 14 | POC_DEST_DIR = '/sdcard/' 15 | 16 | def main(strings_file, manifest_file, package, apk, op, verbose, cicd): 17 | deeplinks = helpers.get_schemes.get_schemes(strings_file, manifest_file) 18 | 19 | if op in [helpers.setup.OP_LIST_ALL, helpers.setup.OP_LIST_APPLINKS]: 20 | only_applinks = op == helpers.setup.OP_LIST_APPLINKS 21 | print_deeplinks(deeplinks, only_applinks) 22 | 23 | if op == helpers.setup.OP_VERIFY_APPLINKS: 24 | check_dals(deeplinks, apk, package, verbose, cicd) 25 | 26 | if op in [helpers.setup.OP_BUILD_POC, helpers.setup.OP_LAUNCH_POC]: 27 | helpers.poc.write_deeplinks_to_file(deeplinks, POC_FILENAME) 28 | write_to_console( 29 | f'Finished writing POC to local file {POC_FILENAME}', 30 | BColors.OKGREEN 31 | ) 32 | 33 | if op == helpers.setup.OP_LAUNCH_POC: 34 | helpers.adb.check_device_requirements(package, apk, ADB_PATH) 35 | helpers.adb.write_file_to_device(POC_FILENAME, POC_DEST_DIR) 36 | helpers.adb.open_file_in_device_with_chrome(POC_DEST_DIR + POC_FILENAME) 37 | 38 | if op == helpers.setup.OP_TEST_WITH_ADB: 39 | helpers.adb.check_device_requirements(package, apk, ADB_PATH) 40 | for activity, handlers in deeplinks.items(): 41 | write_to_console(f'\nActivity: {activity}\n', BColors.BOLD) 42 | for deeplink in handlers: 43 | if deeplink.startswith('http'): 44 | write_to_console(f'\nTesting deeplink: {deeplink}', BColors.OKGREEN) 45 | os.system( 46 | f'adb shell am start -a android.intent.action.VIEW -d "{deeplink}"' 47 | ) 48 | input("Press 'Enter' to test next App Link ...") 49 | 50 | if __name__ == '__main__': 51 | args = helpers.setup.get_parsed_args() 52 | if args.apk is not None: 53 | helpers.setup.decompile_apk(args.apk) 54 | apk_filename = os.path.basename(args.apk).split('.apk')[0] 55 | strings_file_path = open(apk_filename + DEFAULT_STRINGS_FILE, 56 | encoding='utf-8') 57 | manifest_file_path = open(apk_filename + DEFAULT_MANIFEST_FILE, 58 | encoding='utf-8') 59 | main(strings_file=strings_file_path, 60 | manifest_file=manifest_file_path, 61 | package=args.package, 62 | apk=args.apk, 63 | op=args.op, 64 | verbose=args.verbose or args.cicd, 65 | cicd=args.cicd) 66 | if args.clear or args.cicd: 67 | print('Clearing decompiled directory') 68 | os.system(f'rm -rf {dir}') 69 | else: 70 | strings_file_path = open(args.strings, encoding='utf-8') 71 | manifest_file_path = open(args.manifest, encoding='utf-8') 72 | main(strings_file=strings_file_path, 73 | manifest_file=manifest_file_path, 74 | package=args.package, 75 | apk=args.apk, 76 | op=args.op, 77 | verbose=args.verbose or args.cicd, 78 | cicd=args.cicd) 79 | -------------------------------------------------------------------------------- /helpers/adb.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | import os 3 | import helpers.console 4 | 5 | CHROME_PACKAGE = 'com.android.chrome/com.google.android.apps.chrome.Main' 6 | 7 | def get_adb_devices(adbpath): 8 | lines = subprocess.check_output([adbpath, 'devices']).splitlines() 9 | devices = [] 10 | for line in lines: 11 | if line.decode().endswith('\tdevice'): 12 | devices.append(line.decode().split('\t')[0]) 13 | return set(devices) 14 | 15 | def package_is_installed(package, adbpath): 16 | args = [adbpath] 17 | args.extend(['shell', 'pm list packages -f']) 18 | lines = subprocess.check_output(args).splitlines() 19 | for line in lines: 20 | package_name = line.decode().split('apk=')[1] 21 | if package_name == package: 22 | return True 23 | return False 24 | 25 | def check_device_requirements(package, apk, adbpath="adb"): 26 | devices = get_adb_devices(adbpath) 27 | if len(devices) == 0: 28 | helpers.console.write_to_console( 29 | 'No devices detected by adb', helpers.console.BColors.WARNING 30 | ) 31 | exit() 32 | if not package_is_installed(package, adbpath="adb"): 33 | if apk is None: 34 | error_msg = 'Package is not installed and APK was not specified ...' 35 | helpers.console.write_to_console(error_msg, helpers.console.BColors.WARNING) 36 | exit() 37 | else: 38 | error_msg = 'Package is not installed ...' 39 | helpers.console.write_to_console(error_msg, helpers.console.BColors.OKBLUE) 40 | os.system("adb install " + apk) 41 | 42 | def write_file_to_device(file, dest, adbpath="adb"): 43 | os.system(adbpath + ' push ./' + file + ' ' + dest) 44 | 45 | def open_file_in_device_with_chrome(filepath, adbpath="adb"): 46 | os.system(adbpath + ' shell am start -n ' + CHROME_PACKAGE + ' -a android.intent.action.VIEW -d "file://' + filepath + '"') -------------------------------------------------------------------------------- /helpers/apk_cert.py: -------------------------------------------------------------------------------- 1 | import subprocess 2 | 3 | KEYTOOL_PATH = 'keytool' 4 | 5 | def get_sha256_cert_fingerprint(apk): 6 | apk_cert = subprocess.Popen( 7 | KEYTOOL_PATH + ' -printcert -jarfile ' + apk, shell=True, stdout=subprocess.PIPE 8 | ).stdout.read().decode() 9 | if 'SHA256: ' in apk_cert: 10 | components = apk_cert.split('SHA256: ') 11 | if len(components) > 1: 12 | return components[1].split('\n')[0] 13 | return None 14 | -------------------------------------------------------------------------------- /helpers/app_links.py: -------------------------------------------------------------------------------- 1 | import json 2 | import requests 3 | from urllib.parse import urlparse 4 | from helpers.apk_cert import get_sha256_cert_fingerprint 5 | from helpers.console import write_to_console, BColors 6 | import helpers.get_schemes 7 | import helpers.console 8 | 9 | DEFAULT_ROBOTS_FILE = '/robots.txt' 10 | DEFAULT_DAL_FILE = '/.well-known/assetlinks.json' 11 | 12 | def get_dal(url): 13 | domain = urlparse(url).netloc 14 | res = requests.get(f'https://{domain}{DEFAULT_DAL_FILE}') 15 | if res.status_code != 200: 16 | raise Exception( 17 | f'DAL should be returned with status code 200, not {res.status_code}.' 18 | ) 19 | if 'Content-Type' not in res.headers or 'application/json' not in res.headers['Content-Type']: 20 | raise Exception('DAL should be served with \"application/json\" content type.') 21 | return res.text 22 | 23 | def get_relation_list_in_dal(url, sha256, package, verbose): 24 | try: 25 | dal = get_dal(url) 26 | if verbose: 27 | print(dal) 28 | dal_json = json.loads(dal) 29 | for entry in dal_json: 30 | if 'target' in entry: 31 | target = entry['target'] 32 | if 'namespace' not in target or target['namespace'] != 'android_app': 33 | continue 34 | if 'package_name' not in target or target['package_name'] != package: 35 | continue 36 | if 'sha256_cert_fingerprints' not in target: 37 | continue 38 | registered_certs = target['sha256_cert_fingerprints'] 39 | for cert in registered_certs: 40 | if cert == sha256: 41 | if 'relation' in entry: 42 | return entry['relation'] 43 | else: 44 | return [] 45 | except Exception as err: 46 | helpers.console.write_to_console('x ' + str(err), BColors.FAIL) 47 | return None 48 | return None 49 | 50 | def check_manifest_keys_for_deeplink(handlers, deeplink, cicd): 51 | if handlers[deeplink][helpers.get_schemes.AUTOVERIFY_KEY]: 52 | helpers.console.write_to_console('\n✓ includes autoverify=true', BColors.OKGREEN) 53 | else: 54 | helpers.console.write_to_console('\nx does not include autoverify=true', BColors.FAIL) 55 | if cicd: 56 | exit(1) 57 | if handlers[deeplink][helpers.get_schemes.INCLUDES_VIEW_ACTION_KEY]: 58 | helpers.console.write_to_console('✓ includes VIEW action', BColors.OKGREEN) 59 | else: 60 | helpers.console.write_to_console('x does not include VIEW action', BColors.FAIL) 61 | if cicd: 62 | exit(1) 63 | if handlers[deeplink][helpers.get_schemes.INCLUDES_BROWSABLE_CATEGORY_KEY]: 64 | helpers.console.write_to_console('✓ includes BROWSABLE category', BColors.OKGREEN) 65 | else: 66 | helpers.console.write_to_console('x does not include BROWSABLE category', BColors.FAIL) 67 | if cicd: 68 | exit(1) 69 | if handlers[deeplink][helpers.get_schemes.INCLUDES_DEFAULT_CATEGORY_KEY]: 70 | helpers.console.write_to_console('✓ includes DEFAULT category', BColors.OKGREEN) 71 | else: 72 | helpers.console.write_to_console('x does not include DEFAULT category', BColors.FAIL) 73 | if cicd: 74 | exit(1) 75 | 76 | def check_dals(deeplinks, apk, package, verbose, cicd): 77 | sha256 = get_sha256_cert_fingerprint(apk) 78 | if sha256 is None: 79 | write_to_console('The APK\'s signing certificate\'s SHA-256 fingerprint could not be found', 80 | BColors.FAIL) 81 | exit(1) 82 | write_to_console(f'\nThe APK\'s signing certificate\'s SHA-256 fingerprint is: \n{sha256}', 83 | BColors.HEADER) 84 | for activity, handlers in deeplinks.items(): 85 | write_to_console('\n' + activity + '\n', BColors.BOLD) 86 | for deeplink in sorted(handlers.keys()): 87 | if deeplink.startswith('http'): 88 | print('Checking ' + deeplink) 89 | check_manifest_keys_for_deeplink(handlers, deeplink, cicd) 90 | relation_list = get_relation_list_in_dal(deeplink, sha256, package, verbose) 91 | if relation_list is not None: 92 | helpers.console.write_to_console('✓ DAL verified\n', 93 | helpers.console.BColors.OKGREEN) 94 | print(' Relations: ') 95 | for relation in relation_list: 96 | if 'delegate_permission/common.handle_all_urls' in relation or ( 97 | 'delegate_permission/common.get_login_creds' in relation 98 | ): 99 | helpers.console.write_to_console(f' - [Standard] {relation}', 100 | helpers.console.BColors.OKCYAN) 101 | else: 102 | helpers.console.write_to_console(f' - [Custom] {relation}', 103 | helpers.console.BColors.WARNING) 104 | else: 105 | helpers.console.write_to_console('x DAL verification failed\n', 106 | helpers.console.BColors.FAIL) 107 | if cicd: 108 | exit(1) 109 | print() 110 | help_msg = 'Read more about relation strings here: ' 111 | help_msg += 'https://developers.google.com/digital-asset-links/v1/relation-strings\n' 112 | print(help_msg) 113 | -------------------------------------------------------------------------------- /helpers/console.py: -------------------------------------------------------------------------------- 1 | class BColors: 2 | HEADER = '\033[95m' 3 | OKBLUE = '\033[94m' 4 | OKCYAN = '\033[96m' 5 | OKGREEN = '\033[92m' 6 | WARNING = '\033[93m' 7 | FAIL = '\033[91m' 8 | ENDC = '\033[0m' 9 | BOLD = '\033[1m' 10 | UNDERLINE = '\033[4m' 11 | 12 | def write_to_console(message, color): 13 | print(color + message + BColors.ENDC) 14 | 15 | def print_deeplinks(deeplinks, only_applinks): 16 | for activity, handlers in deeplinks.items(): 17 | write_to_console(f'\n{activity}\n', BColors.BOLD) 18 | for deeplink in sorted(handlers.keys()): 19 | is_applink = deeplink.startswith('http') 20 | if not only_applinks or is_applink: 21 | write_to_console(deeplink, BColors.OKGREEN) 22 | -------------------------------------------------------------------------------- /helpers/get_schemes.py: -------------------------------------------------------------------------------- 1 | import itertools 2 | import re 3 | from bs4 import BeautifulSoup 4 | 5 | AUTOVERIFY_KEY = 'has-autoverify' 6 | INCLUDES_VIEW_ACTION_KEY = 'has-view-action' 7 | INCLUDES_BROWSABLE_CATEGORY_KEY = 'has-browsable-category' 8 | INCLUDES_DEFAULT_CATEGORY_KEY = 'has-default-category' 9 | 10 | 11 | is_scheme_data_tag = lambda tag: tag.name == 'data' and \ 12 | any(f'android:{x}' in tag.attrs for x in \ 13 | ('scheme', 'host', 'port', 'path', 'pathPrefix', 'pathPattern') \ 14 | ) 15 | 16 | def get_schemes(strings, manifest): 17 | strings_xml = BeautifulSoup(strings, 'xml') 18 | strings = {d['name']: d.text for d in strings_xml.find_all('string', {'name': True})} 19 | 20 | raw_manifest = manifest.read() 21 | raw_manifest = re.sub('"@string\/(?P[^"]+)"', lambda g: '"{}"'.format(strings.get(g.group('string_name'), 'UNKNOWN_STRING')), raw_manifest) 22 | manifest_xml = BeautifulSoup(raw_manifest, 'xml') 23 | 24 | activity_handlers = {} 25 | for intent_filter in manifest_xml.find_all('intent-filter'): 26 | scheme_items = intent_filter.findAll(is_scheme_data_tag) 27 | if len(scheme_items) > 0: 28 | activity_name = None 29 | 30 | # find activity name from parent 31 | parent_elem = intent_filter.find_parent( 32 | ['activity', 'activity-alias', 'service', 'receiver'] 33 | ) 34 | if parent_elem: 35 | # parent type 36 | p_type = parent_elem.name 37 | if p_type in ['activity', 'service', 'receiver']: 38 | activity_name = parent_elem['android:name'] 39 | elif p_type == 'activity-alias': 40 | target_activity_name = parent_elem['android:targetActivity'] 41 | target_activity = manifest_xml.find('activity', 42 | {'android:name': target_activity_name}) 43 | if target_activity: 44 | activity_name = target_activity['android:name'] 45 | 46 | if activity_name is not None: 47 | schemes, hosts, ports, paths = [], [], [], [] 48 | for item in scheme_items: 49 | schemes.append(item.get('android:scheme')) 50 | hosts.append(item.get('android:host')) 51 | ports.append(item.get('android:port')) 52 | for k in ('path', 'pathPrefix', 'pathPattern'): 53 | paths.append(item.get(f'android:{k}')) 54 | 55 | schemes, hosts, ports, paths = map(list, 56 | map(set, map(lambda x: filter(None, x), [schemes, hosts, ports, paths]))) 57 | no_port_path = (len(ports) == 0 and len(paths) == 0) 58 | if len(ports) == 0: ports.append('') 59 | if len(paths) == 0: paths.append('') 60 | if len(hosts) == 0: hosts.append('') # for filters with only :// 61 | for scheme, host, port, path in list(itertools.product(schemes, hosts, ports, paths)): 62 | if scheme: 63 | uri = f'{scheme}://' 64 | if host: 65 | uri += host 66 | if not no_port_path: 67 | if port: uri += f':{port}' 68 | if path: uri += f'{"/" if not path.startswith("/") else ""}{path}' 69 | 70 | if activity_name not in activity_handlers: 71 | activity_handlers[activity_name] = {} 72 | activity_handlers[activity_name][uri] = { 73 | AUTOVERIFY_KEY: 'android:autoVerify="true"' in str(intent_filter), 74 | INCLUDES_VIEW_ACTION_KEY: '' in str(intent_filter), 75 | INCLUDES_BROWSABLE_CATEGORY_KEY: 'android.intent.category.BROWSABLE' in str(intent_filter), 76 | INCLUDES_DEFAULT_CATEGORY_KEY: 'android.intent.category.DEFAULT' in str(intent_filter) 77 | } 78 | 79 | return activity_handlers 80 | -------------------------------------------------------------------------------- /helpers/poc.py: -------------------------------------------------------------------------------- 1 | def write_deeplinks_to_file(activity_handlers, poc_filename): 2 | html = "\n\n\n
\n" 3 | for activity, handlers in activity_handlers.items(): 4 | html += f'

{activity}

\n' 5 | for deeplink in sorted(handlers): 6 | if "http" in deeplink: 7 | html += f'{deeplink}
' 8 | html += "
\n\n" 9 | html_file = open(poc_filename, 'w', encoding='utf-8') 10 | html_file.write(html) 11 | html_file.close() 12 | -------------------------------------------------------------------------------- /helpers/setup.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import os 3 | from deeplink_analyser import APKTOOL_PATH 4 | import helpers.get_schemes 5 | import helpers.adb 6 | import helpers.console 7 | 8 | OP_LIST_ALL = 'list-all' 9 | OP_LIST_APPLINKS = 'list-applinks' 10 | OP_VERIFY_APPLINKS = 'verify-applinks' 11 | OP_BUILD_POC = 'build-poc' 12 | OP_LAUNCH_POC = 'launch-poc' 13 | OP_TEST_WITH_ADB = 'adb-test' 14 | OP_MODES = [ 15 | OP_LIST_ALL, OP_LIST_APPLINKS, OP_VERIFY_APPLINKS, OP_BUILD_POC, OP_LAUNCH_POC, OP_TEST_WITH_ADB 16 | ] 17 | 18 | def is_valid_file(parser, arg): 19 | if not os.path.exists(arg): 20 | parser.error("The file %s does not exist!" % arg) 21 | else: 22 | return arg 23 | 24 | def get_parsed_args(): 25 | parser = argparse.ArgumentParser() 26 | parser.add_argument('-apk', 27 | dest='apk', 28 | required=False, 29 | metavar="FILE", 30 | type=lambda x: helpers.setup.is_valid_file(parser, x), 31 | help='Path to the APK (required for `verify-applinks` operation mode)') 32 | parser.add_argument('-m', '--manifest', 33 | dest="manifest", 34 | required=False, 35 | metavar="FILE", 36 | type=lambda x: helpers.setup.is_valid_file(parser, x), 37 | help='Path to the AndroidManifest.xml file') 38 | parser.add_argument('-s', '--strings', 39 | dest="strings", 40 | required=False, 41 | metavar="FILE", 42 | type=lambda x: helpers.setup.is_valid_file(parser, x), 43 | help='Path to the strings.xml file') 44 | parser.add_argument('-op', '--operation-mode', 45 | dest='op', 46 | required=True, 47 | type=str, 48 | help='Operation mode: "' + '", "'.join(OP_MODES) + '".') 49 | parser.add_argument('-p', '--package', 50 | dest="package", 51 | required=False, 52 | type=str, 53 | help='Package identifier, e.g.: "com.myorg.appname" (required for some operation modes)') 54 | parser.add_argument('-v', '--verbose', 55 | dest='verbose', 56 | default=False, 57 | required=False, 58 | action='store_true', 59 | help='Verbose mode') 60 | parser.add_argument('--clear', 61 | dest='clear', 62 | default=False, 63 | required=False, 64 | action='store_true', 65 | help='Whether or not the script should delete the decompiled directory after running (default: False)') 66 | parser.add_argument('--ci-cd', 67 | dest='cicd', 68 | default=False, 69 | required=False, 70 | action='store_true', 71 | help='Ideal for running in CI/CD pipelines (default: False)') 72 | args = parser.parse_args() 73 | if args.manifest is None or args.strings is None: 74 | if args.apk is None: 75 | error_msg = 'You must specify either an APK or a manifest and strings file path' 76 | helpers.console.write_to_console(error_msg, helpers.console.BColors.FAIL) 77 | exit() 78 | elif args.op == OP_VERIFY_APPLINKS: 79 | error_msg = 'You need to use the -apk option when using the ' 80 | error_msg += '"verify-applinks" operation mode' 81 | helpers.console.write_to_console(error_msg, helpers.console.BColors.FAIL) 82 | exit() 83 | if args.op not in OP_MODES: 84 | error_msg = 'The specified operation mode is not supported.' 85 | error_msg += '\nSupported operation modes: "' + '", "'.join(OP_MODES) + '".' 86 | helpers.console.write_to_console(error_msg, helpers.console.BColors.FAIL) 87 | exit() 88 | if args.op == OP_TEST_WITH_ADB or args.op == OP_LAUNCH_POC or args.op == OP_VERIFY_APPLINKS: 89 | if args.package is None: 90 | error_msg = 'You must specify the package id in order to use this operation mode' 91 | helpers.console.write_to_console(error_msg, helpers.console.BColors.FAIL) 92 | exit() 93 | return args 94 | 95 | def decompile_apk(apk): 96 | os.system(f'{APKTOOL_PATH} d {apk}') 97 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | argparse 2 | beautifulsoup4 3 | bs4 4 | lxml 5 | requests 6 | --------------------------------------------------------------------------------