From 3d37a3b58e62f2bd142478576213ce1e948098a9 Mon Sep 17 00:00:00 2001 From: krviolet Date: Fri, 20 Sep 2019 09:17:35 +0300 Subject: [PATCH 1/2] Added option --report-name --- qark.py | 149 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ report.py | 73 ++++++++++++++++++++++++++ 2 files changed, 222 insertions(+) create mode 100644 qark.py create mode 100644 report.py diff --git a/qark.py b/qark.py new file mode 100644 index 00000000..ac000a65 --- /dev/null +++ b/qark.py @@ -0,0 +1,149 @@ +#!/usr/bin/env python + +from __future__ import absolute_import + +import logging +import logging.config +import os + +import click + +from qark.apk_builder import APKBuilder +from qark.decompiler.decompiler import Decompiler +from qark.report import Report +from qark.scanner.scanner import Scanner +from qark.utils import environ_path_variable_exists + +DEBUG_LOG_PATH = os.path.join(os.getcwd(), + "qark_debug.log") + +# Environment variable names for the SDK +ANDROID_SDK_HOME = "ANDROID_SDK_HOME" +ANDROID_HOME = "ANDROID_HOME" +ANDROID_SDK_ROOT = "ANDROID_SDK_ROOT" + +logger = logging.getLogger(__name__) + + +@click.command() +@click.option("--sdk-path", type=click.Path(exists=True, file_okay=False, resolve_path=True), + help="Path to the downloaded SDK directory if already downloaded. " + "Only necessary if --exploit-apk is passed. If --exploit-apk is passed and this flag is not passed," + "QARK will attempt to use the ANDROID_SDK_HOME, ANDROID_HOME, ANDROID_SDK_ROOT " + "environment variables (in that order) for a path.") +@click.option("--build-path", type=click.Path(resolve_path=True, file_okay=False), + help="Path to place decompiled files and exploit APK.", default="build", show_default=True) +@click.option("--debug/--no-debug", default=False, help="Show debugging statements (helpful for issues).", + show_default=True) +@click.option("--apk", "source", help="APK to decompile and run static analysis. If passed, " + "the --java option is not used.", + type=click.Path(exists=True, resolve_path=True, file_okay=True, dir_okay=True)) +@click.option("--java", "source", type=click.Path(exists=True, resolve_path=True, file_okay=True, dir_okay=True), + help="A directory containing Java code, or a Java file, to run static analysis. If passed," + "the --apk option is not used.") +@click.option("--report-name", help="Name of report file. For instance, Report_Instagram", default = 'Report', show_default=True) +@click.option("--report-type", help="Type of report to generate along with terminal output.", default="html", show_default=True) +@click.option("--exploit-apk/--no-exploit-apk", default=False, + help="Create an exploit APK targetting a few vulnerabilities.", show_default=True) +@click.option("--report-path", type=click.Path(resolve_path=True, file_okay=False), default=None, + help="report output path.", show_default=True) +@click.option("--keep-report/--no-keep-report", default=False, + help="Append to final report file.", show_default=True) +@click.version_option() +@click.pass_context +def cli(ctx, sdk_path, build_path, debug, source, report_type, exploit_apk, report_path, keep_report, report_name): + if not source: + click.secho("Please pass a source for scanning through either --java or --apk") + click.secho(ctx.get_help()) + return + + if exploit_apk: + + if not sdk_path: + # Try to set the SDK from environment variables if they exist + # Follows the guidelines from https://developer.android.com/studio/command-line/variables + if environ_path_variable_exists(ANDROID_SDK_HOME): + sdk_path = os.environ[ANDROID_SDK_HOME] + + elif environ_path_variable_exists(ANDROID_HOME): + sdk_path = os.environ[ANDROID_HOME] + + elif environ_path_variable_exists(ANDROID_SDK_ROOT): + sdk_path = os.environ[ANDROID_SDK_ROOT] + + else: + click.secho("Please provide path to android SDK if building exploit APK.") + return + + # Debug controls the output to stderr, debug logs are ALWAYS stored in `qark_debug.log` + if debug: + level = "DEBUG" + else: + level = "INFO" + + initialize_logging(level) + + click.secho("Decompiling...") + decompiler = Decompiler(path_to_source=source, build_directory=build_path) + decompiler.run() + + click.secho("Running scans...") + path_to_source = decompiler.path_to_source if decompiler.source_code else decompiler.build_directory + + scanner = Scanner(manifest_path=decompiler.manifest_path, path_to_source=path_to_source) + scanner.run() + click.secho("Finish scans...") + + click.secho("Writing report...") + report = Report(issues=set(scanner.issues), report_path=report_path, keep_report=keep_report, report_name = report_name) + report_path = report.generate(file_type=report_type, report_name = report_name) + click.secho("Finish writing report to {report_path} ...".format(report_path=report_path)) + + if exploit_apk: + click.secho("Building exploit APK...") + exploit_builder = APKBuilder(exploit_apk_path=build_path, issues=scanner.issues, apk_name=decompiler.apk_name, + manifest_path=decompiler.manifest_path, sdk_path=sdk_path) + exploit_builder.build() + click.secho("Finish building exploit APK...") + + +def initialize_logging(level): + """Creates two root handlers, one to file called `qark_debug.log` and one to stderr""" + handlers = { + "stderr_handler": { + "level": level, + "class": "logging.StreamHandler" + } + } + loggers = ["stderr_handler"] + + if level == "DEBUG": + handlers["debug_handler"] = { + "level": "DEBUG", + "class": "logging.FileHandler", + "filename": DEBUG_LOG_PATH, + "mode": "w", + "formatter": "standard" + } + loggers.append("debug_handler") + + logging.config.dictConfig({ + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "standard": { + 'format': '%(asctime)s [%(levelname)s] %(name)s: %(message)s' + } + }, + "handlers": handlers, + "loggers": { + "": { + "handlers": handlers, + "level": "DEBUG", + "propagate": True + } + } + }) + + if level == "DEBUG": + logger.debug("Debug logging enabled") diff --git a/report.py b/report.py new file mode 100644 index 00000000..8d9f89e7 --- /dev/null +++ b/report.py @@ -0,0 +1,73 @@ +from __future__ import absolute_import + +from os import path + +from jinja2 import Environment, PackageLoader, select_autoescape, Template + +from qark.issue import (Issue, Severity, issue_json) # noqa:F401 These are expected to be used later. +from qark.utils import create_directories_to_path + +DEFAULT_REPORT_PATH = path.join(path.dirname(path.realpath(__file__)), 'report', '') + + +jinja_env = Environment( + loader=PackageLoader('qark', 'templates'), + autoescape=select_autoescape(['html', 'xml']) +) + +jinja_env.filters['issue_json'] = issue_json + + +class Report(object): + """An object to store issues against and to generate reports in different formats. + + There is one instance created per QARK run and it uses a classic Singleton pattern + to make it easy to get a reference to that instance anywhere in QARK. + """ + + # The one instance to rule them all + # http://python-3-patterns-idioms-test.readthedocs.io/en/latest/Singleton.html#the-singleton + __instance = None + + def __new__(cls, issues=None, report_path=None, keep_report=False, report_name = None): + if Report.__instance is None: + Report.__instance = object.__new__(cls) + + return Report.__instance + + def __init__(self, issues=None, report_path=None, keep_report=False, report_name = None): + """This will give you an instance of a report, with a default report path which is local + to where QARK is on the file system. + + :param report_path: The path to the report directory where all generated report files will be written. + :type report_path: str or None + + """ + self.issues = issues if issues else [] + self.report_path = report_path or DEFAULT_REPORT_PATH + self.keep_report = keep_report + + def generate(self, file_type='html', template_file=None, report_name = 'report'): + """This method uses Jinja2 to generate a standalone HTML version of the report. + + :param str file_type: The type of file for the report. Defaults to 'html'. + :param str template_file: The path to an optional template file to override the default. + :return: Path to the written report + :rtype: str + """ + create_directories_to_path(self.report_path) + + full_report_path = path.join(self.report_path, '{report_name}_QARK.{file_type}'.format(report_name = report_name, file_type=file_type)) + + open_flag = 'w' + if self.keep_report: + open_flag = 'a' + with open(full_report_path, mode=open_flag) as report_file: + if not template_file: + template = jinja_env.get_template('{file_type}_report.jinja'.format(file_type=file_type)) + else: + template = Template(template_file) + report_file.write(template.render(issues=list(self.issues))) + report_file.write('\n') + + return full_report_path From a0e726f524e48a36ace1e39f1070091c0fe256e9 Mon Sep 17 00:00:00 2001 From: krviolet Date: Fri, 20 Sep 2019 10:36:54 +0300 Subject: [PATCH 2/2] Add files via upload --- requirements-test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements-test.txt b/requirements-test.txt index 449eea6f..7ae59b7f 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -77,7 +77,7 @@ mccabe==0.6.1 \ --hash=sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f \ # via flake8 pluggy==0.6.0 \ - --hash=sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff \ + --hash=sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff # via pytest pluginbase==0.5 \ --hash=sha256:b4f830242a078a4f44c978a84f3365bba4d008fdd71a591c71447f4df35354dd