def setUp(self): if MAS_APP: self.mas_app = Bundle.make(MAS_APP) else: self.mas_app = None # The Calculator.app app has been part of macOS for as long as I can think. # There is no risk this app is going anywhere. self.system_app = Bundle.make("/Applications/Calculator.app")
def run( self, apps_dir: str, out_dir: str, select: Selection = Selection.ALL, ) -> None: if not os.path.exists(apps_dir): print(f"Directory does not exist: {apps_dir}", file=sys.stderr) exit(1) exit_watcher = SignalIntelligence() self.logger.info(f"{self.name} starting") for app_dir in Driver.iterate_applications(apps_dir): if exit_watcher.should_exit: break app = Bundle.make(app_dir) if select == Selection.MAS and not app.is_mas_app(): continue app_out_dir = folder_for_app(out_dir, app) os.makedirs(app_out_dir, exist_ok=True) print(f"[ ] Analysing {app.filepath}") reset_cursor = "\r\033[1A[" result = self.analyse(app, app_out_dir) print(reset_cursor + result.colored) self.logger.info(f"{self.name} stopping")
def container_for_app(app): """ Returns the container directory used by the application or None if the container does not exist. :param app: The app for which to find the container directory. Note that valid arguments are both a filepath to the application and a bundle for that application :return: Filepath to the container or None, if the lookup failed. """ # Handle code that already has a bundle for an app if isinstance(app, Bundle): app_bundle = app elif isinstance(app, str): try: app_bundle = Bundle.make(app) except InvalidBundle: return None bid = app_bundle.bundle_identifier(normalized=True) # Verify the container exists. container_path = os.path.join(os.path.expanduser("~/Library/Containers/"), bid) if not os.path.exists(container_path): return None # Also verify that the metadata file is present, else the container is invalid and of # no use to other code container_metadata = os.path.join(container_path, "Container.plist") if not os.path.exists(container_metadata): return None return container_path
def test_sub_frameworks(self): app = Bundle.make("/Applications/iTunes.app") sub_frameworks = app.sub_frameworks() self.assertEqual(1, len(sub_frameworks)) self.assertEqual("/Applications/iTunes.app/Contents/Frameworks/iPodUpdater.framework", sub_frameworks[0].filepath)
def all_apps(at: str = "/Applications", mas_only: bool = False, sandboxed_only: bool = False): """ Returns all apps from a target folder :param at: The base folder where to search for applications :param mas_only: Whether to only consider applications from the Mac App Store :param sandboxed_only: Whether to only return sandboxed applications :return: Filepaths to applications fulfilling the criteria specified """ all_entries = [ os.path.join(at, x) for x in os.listdir(at) if x.endswith(".app") ] for entry in all_entries: try: app_bundle = Bundle.make(entry) if mas_only and not app_bundle.is_mas_app(): continue if sandboxed_only and not app_bundle.is_sandboxed(): continue yield entry except InvalidBundle: continue
def test_sub_bundles(self): app = Bundle.make("/Applications/iTunes.app") sub_bundle_paths = [x.filepath for x in app.sub_bundles()] required_paths = [ "/Applications/iTunes.app/Contents/XPCServices/VisualizerService.xpc", "/Applications/iTunes.app/Contents/PlugIns/iTunesStorageExtension.appex", "/Applications/iTunes.app/Contents/MacOS/iTunesHelper.app" ] for path in required_paths: self.assertIn(path, sub_bundle_paths)
def install_app(app_path: str, logger, output: str): """Install an app from the `app_path` into the `output` directory.""" app = Bundle.make(app_path) output_folder = os.path.join(folder_for_app(output, app), os.path.basename(app_path)) try: shutil.copytree(app_path, output_folder, symlinks=True) except FileExistsError: logger.error("Application already exists: {}. Skipping.".format(output_folder)) print('\r[' + colored('skip', 'yellow')) return except e: logger.error("Could not install application: {}".format(output_folder)) print('\r[' + colored('err ', 'red')) return logger.info("Installed application: {}".format(output_folder)) print('\r[' + colored(' ok ', 'green'))
def analyse_app(app_path, produce_text = False): """ Analyses an app to provide answers to the questions posed in the comment at the beginning of this file. Returns a dictionary containing the responses. """ result = dict() try: app_bundle = Bundle.make(app_path) except: print(termcolor.colored("App processing failed. Make sure the supplied application is valid.", COLOR_NEGATIVE)) return # Run all analysers analysers = AbstractAppChecker.__subclasses__() # Check if sandbox enabled success, key, local_result = run_analyser(AppSandboxStatusChecker, app_bundle, produce_text) if not success: return result result[key] = local_result # If the sandbox is not enabled, all other checks are mood. if not local_result['dynamic']: return result for analyser in analysers: if analyser == AppSandboxStatusChecker: continue success, key, local_result = run_analyser(analyser, app_bundle, produce_text) if not success: return result result[key] = local_result return result
def process_app(app_path, info_extractors, logger, output, source_hint: str=None): """Process an app using the supplied `info_extractors` Log potentially relevant information to `logger` and return results at `output`. If supplied, the `source_hint` will be written to `output/source` """ app = Bundle.make(app_path) output_folder = folder_for_app(output, app) if os.path.exists(output_folder): logger.info( "Skipping processing of {} @ {}, because the app has already been processed.".format(app, app.filepath) ) return # Make basefolder os.makedirs(output_folder) try: # The source_hint can for example be used to store an application's identifier for # the third-party platform where the app was downloaded from (i.e macupdate) if source_hint is not None: with open(os.path.join(output_folder, 'source'), 'w') as source_file: source_file.write(source_hint) extraction_status = functools.reduce( lambda status, extractor: status & run_extractor(extractor, app, output_folder), info_extractors, True ) if not extraction_status: logger.info("Processing failed for {} @ {}".format(app, app.filepath)) except: logger.error( "Exception occurred during processing of {} @ {}".format(app, app.filepath) )
def test_make_bundle(self): # Check common applications self.assertEqual(Bundle.make("/Applications/Calculator.app").bundle_type, BundleType.APPLICATION) self.assertEqual(Bundle.make("/Applications/Safari.app").bundle_type, BundleType.APPLICATION) # Check framework self.assertEqual(Bundle.make("/System/Library/Frameworks/Accelerate.framework").bundle_type, BundleType.FRAMEWORK) # KEXTs, even though actual proper support is not implemented yet. self.assertEqual(Bundle.make("/System/Library/Extensions/AppleHWSensor.kext").bundle_type, BundleType.KEXT) # Check failure cases with self.assertRaises(InvalidBundle): Bundle.make("/System/Library/Frameworks/Kernel.framework")
def setUp(self): self.framework = Bundle.make( "/System/Library/Frameworks/DVDPlayback.framework")
def test_get_sandbox_rules(self): bundle = Bundle.make("/Applications/Calculator.app") self.assertIsNotNone(app_utils.get_sandbox_rules(bundle))
def main(): logger.info("appxtractor starting") parser = argparse.ArgumentParser(description='Extract information from Mac Apps.') parser.add_argument('-i', '--input', required=True, help='The directory that contains the applications to analyse.') parser.add_argument('-t', '--type', default='app_folder', const='app_folder', nargs='?', choices=['app_folder', 'archive_folder'], help='''Process input folder as folder containing .app bundles or as folder full of archives containing .app bundles. Supported archives formats are zip, tar, gz and dmg. Default type: app_folder''') parser.add_argument('-o', '--output', required=True, help='Output directory: This directory shall also be passed to this program to update an existing output folder.') parser.add_argument('--all-apps', dest='all_apps', default=False, action='store_true', help='Analyse all apps. By default, only Mac AppStore apps are analysed.') parser.add_argument('--install-only', default=False, action='store_true', help='''Install archived applications into the output directory. This option only works with archive folders.''') args = parser.parse_args() if args.type != 'archive_folder' and args.install_only: print("Option '--install-only' is only supported for archive folders.", file=sys.stderr) exit(1) exit_watcher = SignalIntelligence() if args.install_only: print("[+] Installing apps from \"{}\" to \"{}\"".format(args.input, args.output)) print("[+] Press Ctrl+C to cancel installation\n") else: print("[+] Analysing apps at \"{}\"".format(args.input)) print("[+] Press Ctrl+C to cancel analysis (can later be resumed)\n") if args.type == 'app_folder': app_candidates = iterate_apps_folder(args.input) elif args.type == 'archive_folder': app_candidates = iterate_archived_apps_folder(args.input) else: assert False and 'Iteration type not supported.' for path, hint in app_candidates: if exit_watcher.should_exit: break if not Bundle.is_bundle(path): continue bundle = Bundle.make(path) if not bundle.is_mas_app() and not args.all_apps and not args.install_only: continue if args.install_only: print('[ ] Installing {}'.format(path), end='') install_app(app_path=path, logger=logger, output=args.output) else: print('[+] Processing {}'.format(path)) process_app(app_path=path, info_extractors=info_extractors, logger=logger, output=args.output, source_hint=hint) logger.info("appxtractor stopping")
def setUp(self): # The Calculator.app app has been part of macOS for as long as I can think. # There is no risk this app is going anywhere. self.app = Bundle.make("/Applications/Calculator.app")