Example #1
0
 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")
Example #2
0
    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")
Example #3
0
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
Example #4
0
    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)
Example #5
0
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
Example #6
0
    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)
Example #7
0
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'))
Example #8
0
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
Example #9
0
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)
        )
Example #10
0
    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")
Example #11
0
 def setUp(self):
     self.framework = Bundle.make(
         "/System/Library/Frameworks/DVDPlayback.framework")
Example #12
0
 def test_get_sandbox_rules(self):
     bundle = Bundle.make("/Applications/Calculator.app")
     self.assertIsNotNone(app_utils.get_sandbox_rules(bundle))
Example #13
0
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")
Example #14
0
 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")