"""The main runtime of the MythX CLI."""
import logging
import sys
from pathlib import Path

import click
import yaml
from pythx import Client, MythXAPIError
from pythx.middleware.toolname import ClientToolNameMiddleware

from mythx_cli import __version__
from mythx_cli.analysis.list import analysis_list
from mythx_cli.analysis.report import analysis_report
from mythx_cli.analysis.status import analysis_status
from mythx_cli.analyze.command import analyze
from mythx_cli.formatter import FORMAT_RESOLVER
from mythx_cli.fuzz.arm import fuzz_arm
from mythx_cli.fuzz.disarm import fuzz_disarm
from mythx_cli.fuzz.run import fuzz_run
from mythx_cli.group.close import group_close
from mythx_cli.group.list import group_list
from mythx_cli.group.open import group_open
from mythx_cli.group.status import group_status
from mythx_cli.project import project_list
from mythx_cli.render.command import render
from mythx_cli.util import update_context
from mythx_cli.version.command import version

LOGGER = logging.getLogger("mythx-cli")
logging.basicConfig(level=logging.WARNING)


class APIErrorCatcherGroup(click.Group):
    """A custom click group to catch API-related errors.

    This custom Group implementation catches :code:`MythXAPIError`
    exceptions, which get raised when the API returns a non-200
    status code. It is used to notify the user about the error that
    happened instead of triggering an uncaught exception traceback.

    It is given to the main CLI entrypoint and propagated to all
    subcommands.
    """

    def __call__(self, *args, **kwargs):
        try:
            return self.main(*args, **kwargs)
        except MythXAPIError as exc:
            LOGGER.debug("Caught API error")
            click.echo("The API returned an error:\n{}".format(exc))
            sys.exit(1)


# noinspection PyIncorrectDocstring
@click.group(cls=APIErrorCatcherGroup)
@click.option(
    "--debug",
    is_flag=True,
    default=False,
    envvar="MYTHX_DEBUG",
    help="Provide additional debug output",
)
@click.option(
    "--api-key", envvar="MYTHX_API_KEY", help="Your MythX API key from the dashboard"
)
@click.option(
    "--username", envvar="MYTHX_USERNAME", help="Your MythX account's username"
)
@click.option(
    "--password", envvar="MYTHX_PASSWORD", help="Your MythX account's password"
)
@click.option(
    "--format",
    "fmt",
    default=None,
    type=click.Choice(FORMAT_RESOLVER.keys()),
    help="The format to display the results in",
)
@click.option(
    "--ci",
    is_flag=True,
    default=None,
    help="Return exit code 1 if high-severity issue is found",
)
@click.option(
    "-y",
    "--yes",
    is_flag=True,
    default=None,
    help="Do not prompt for any confirmations",
)
@click.option(
    "-o", "--output", default=None, help="Output file to write the results into"
)
@click.option(
    "-c",
    "--config",
    type=click.Path(exists=True),
    help="YAML config file for default parameters",
)
@click.option("--stdout", is_flag=True, default=False, help="Force printing to stdout")
@click.option(
    "--table-sort-key",
    type=click.Choice(["line", "title", "severity", "description"]),
    default="line",
    help="The column to sort the default table output by",
)
@click.pass_context
def cli(
    ctx,
    debug: bool,
    api_key: str,
    username: str,
    password: str,
    fmt: str,
    ci: bool,
    output: str,
    yes: bool,
    config: str,
    stdout: bool,
    table_sort_key: str,
) -> None:
    """Your CLI for interacting with https://mythx.io/

    \f

    :param ctx: Click context holding group-level parameters
    :param debug: Boolean to enable the `logging` debug mode
    :param api_key: User JWT api token from the MythX dashboard
    :param username: The MythX account ETH address/username
    :param password: The account password from the MythX dashboard
    :param fmt: The formatter to use for the subcommand output
    :param ci: Boolean to return exit code 1 on medium/high-sev issues
    :param output: Output file to write the results into
    :param config: YAML config file to read default parameters from
    :param stdout: Force printing to stdout and ignore output files
    :param table_sort_key: The column to sort the default table output by
    """

    # set loggers to debug mode
    if debug:
        for name in logging.root.manager.loggerDict:
            logging.getLogger(name).setLevel(logging.DEBUG)

    ctx.obj = {
        "debug": debug,
        "api_key": api_key,
        "username": username,
        "password": password,
        "fmt": fmt,
        "ci": ci,
        "output": output,
        "yes": yes,
        "config": config,
        "table_sort_key": table_sort_key,
    }

    LOGGER.debug("Initializing configuration context")
    config_file = config or ".mythx.yml"
    if Path(config_file).is_file():
        LOGGER.debug(f"Parsing config at {config_file}")
        with open(config_file) as config_f:
            parsed_config = yaml.safe_load(config_f.read())
    else:
        parsed_config = {"analyze": {}}

    # The analyze/fuzz context is updated separately in the command
    # implementation
    ctx.obj["analyze"] = parsed_config.get("analyze", {})
    ctx.obj["fuzz"] = parsed_config.get("fuzz", {})

    # overwrite context with top-level YAML config keys if necessary
    update_context(ctx.obj, "ci", parsed_config, "ci", False)
    if stdout:
        # if forced stdout, don't set output file
        ctx.obj["output"] = None
    else:
        update_context(ctx.obj, "output", parsed_config, "output", None)
    update_context(ctx.obj, "fmt", parsed_config, "format", "table")
    update_context(ctx.obj, "yes", parsed_config, "confirm", False)
    update_context(ctx.obj, "table_sort_key", parsed_config, "table-sort-key", "line")

    # set return value - used for CI failures
    ctx.obj["retval"] = 0

    LOGGER.debug(f"Initializing tool name middleware with {__version__}")
    toolname_mw = ClientToolNameMiddleware(name="mythx-cli-{}".format(__version__))

    if api_key is not None:
        LOGGER.debug("Initializing client with API key")
        ctx.obj["client"] = Client(api_key=api_key, middlewares=[toolname_mw])
    elif username and password:
        LOGGER.debug("Initializing client with username and password")
        ctx.obj["client"] = Client(
            username=username, password=password, middlewares=[toolname_mw]
        )
    elif "fuzz" not in sys.argv:
        # fuzz subcommand is exempt from API auth
        raise click.UsageError(
            (
                "The trial user has been deprecated. You can still use the MythX CLI for free "
                "by signing up for a free account at https://mythx.io/ and entering your access "
                "credentials."
            )
        )


LOGGER.debug("Registering main commands")
cli.add_command(analyze)
cli.add_command(render)
cli.add_command(version)


@cli.group()
def project() -> None:
    """Create, modify, and view analysis projects.

    \f

    This subcommand holds all project-related actions, such as creating,
    listing, and managing projects, as well as fetching the status of one
    or more groups inside a project.
    """
    pass


LOGGER.debug("Registering project commands")
project.add_command(project_list)


@cli.group()
def group() -> None:
    """Create, modify, and view analysis groups.

    \f

    This subcommand holds all group-related actions, such as creating,
    listing, closing groups, as well as fetching the status of one
    or more group IDs.
    """
    pass


LOGGER.debug("Registering group commands")
group.add_command(group_list)
group.add_command(group_status)
group.add_command(group_open)
group.add_command(group_close)


@cli.group()
def analysis() -> None:
    """Get information on running and finished analyses.

    \f

    This subcommand holds all analysis-related actions, such as submitting new
    analyses, listing existing ones, fetching their status, as well as fetching
    the reports of one or more finished analysis jobs.
    """
    pass


LOGGER.debug("Registering analysis commands")
analysis.add_command(analysis_status)
analysis.add_command(analysis_list)
analysis.add_command(analysis_report)


@cli.group()
def fuzz() -> None:
    """Interact with the MythX FaaS solution.

    \f

    This subcommand holds all fuzz-related actions, such as initializing
    new fuzzing campaigns, preparing projects for FaaS submission, and
    launching new campaigns.
    """
    pass


LOGGER.debug("Registering fuzz commands")
fuzz.add_command(fuzz_run)
fuzz.add_command(fuzz_arm)
fuzz.add_command(fuzz_disarm)


if __name__ == "__main__":
    sys.exit(cli())  # pragma: no cover

"""The main runtime of the MythX CLI."""
import logging
import sys
from pathlib import Path

import click
import yaml
from pythx import Client, MythXAPIError
from pythx.middleware.toolname import ClientToolNameMiddleware

from mythx_cli import __version__
from mythx_cli.analysis.list import analysis_list
from mythx_cli.analysis.report import analysis_report
from mythx_cli.analysis.status import analysis_status
from mythx_cli.analyze.command import analyze
from mythx_cli.formatter import FORMAT_RESOLVER
from mythx_cli.fuzz.arm import fuzz_arm
from mythx_cli.fuzz.disarm import fuzz_disarm
from mythx_cli.fuzz.run import fuzz_run
from mythx_cli.group.close import group_close
from mythx_cli.group.list import group_list
from mythx_cli.group.open import group_open
from mythx_cli.group.status import group_status
from mythx_cli.project import project_list
from mythx_cli.render.command import render
from mythx_cli.util import update_context
from mythx_cli.version.command import version

LOGGER = logging.getLogger("mythx-cli")
logging.basicConfig(level=logging.WARNING)


class APIErrorCatcherGroup(click.Group):
    """A custom click group to catch API-related errors.

    This custom Group implementation catches :code:`MythXAPIError`
    exceptions, which get raised when the API returns a non-200
    status code. It is used to notify the user about the error that
    happened instead of triggering an uncaught exception traceback.

    It is given to the main CLI entrypoint and propagated to all
    subcommands.
    """

    def __call__(self, *args, **kwargs):
        try:
            return self.main(*args, **kwargs)
        except MythXAPIError as exc:
            LOGGER.debug("Caught API error")
            click.echo("The API returned an error:\n{}".format(exc))
            sys.exit(1)


# noinspection PyIncorrectDocstring
@click.group(cls=APIErrorCatcherGroup)
@click.option(
    "--debug",
    is_flag=True,
    default=False,
    envvar="MYTHX_DEBUG",
    help="Provide additional debug output",
)
@click.option(
    "--api-key", envvar="MYTHX_API_KEY", help="Your MythX API key from the dashboard"
)
@click.option(
    "--username", envvar="MYTHX_USERNAME", help="Your MythX account's username"
)
@click.option(
    "--password", envvar="MYTHX_PASSWORD", help="Your MythX account's password"
)
@click.option(
    "--format",
    "fmt",
    default=None,
    type=click.Choice(FORMAT_RESOLVER.keys()),
    help="The format to display the results in",
)
@click.option(
    "--ci",
    is_flag=True,
    default=None,
    help="Return exit code 1 if high-severity issue is found",
)
@click.option(
    "-y",
    "--yes",
    is_flag=True,
    default=None,
    help="Do not prompt for any confirmations",
)
@click.option(
    "-o", "--output", default=None, help="Output file to write the results into"
)
@click.option(
    "-c",
    "--config",
    type=click.Path(exists=True),
    help="YAML config file for default parameters",
)
@click.option("--stdout", is_flag=True, default=False, help="Force printing to stdout")
@click.option(
    "--table-sort-key",
    type=click.Choice(["line", "title", "severity", "description"]),
    default="line",
    help="The column to sort the default table output by",
)
@click.pass_context
def cli(
    ctx,
    debug: bool,
    api_key: str,
    username: str,
    password: str,
    fmt: str,
    ci: bool,
    output: str,
    yes: bool,
    config: str,
    stdout: bool,
    table_sort_key: str,
) -> None:
    """Your CLI for interacting with https://mythx.io/

    \f

    :param ctx: Click context holding group-level parameters
    :param debug: Boolean to enable the `logging` debug mode
    :param api_key: User JWT api token from the MythX dashboard
    :param username: The MythX account ETH address/username
    :param password: The account password from the MythX dashboard
    :param fmt: The formatter to use for the subcommand output
    :param ci: Boolean to return exit code 1 on medium/high-sev issues
    :param output: Output file to write the results into
    :param config: YAML config file to read default parameters from
    :param stdout: Force printing to stdout and ignore output files
    :param table_sort_key: The column to sort the default table output by
    """

    # set loggers to debug mode
    if debug:
        for name in logging.root.manager.loggerDict:
            logging.getLogger(name).setLevel(logging.DEBUG)

    ctx.obj = {
        "debug": debug,
        "api_key": api_key,
        "username": username,
        "password": password,
        "fmt": fmt,
        "ci": ci,
        "output": output,
        "yes": yes,
        "config": config,
        "table_sort_key": table_sort_key,
    }

    LOGGER.debug("Initializing configuration context")
    config_file = config or ".mythx.yml"
    if Path(config_file).is_file():
        LOGGER.debug(f"Parsing config at {config_file}")
        with open(config_file) as config_f:
            parsed_config = yaml.safe_load(config_f.read())
    else:
        parsed_config = {"analyze": {}}

    # The analyze/fuzz context is updated separately in the command
    # implementation
    ctx.obj["analyze"] = parsed_config.get("analyze", {})
    ctx.obj["fuzz"] = parsed_config.get("fuzz", {})

    # overwrite context with top-level YAML config keys if necessary
    update_context(ctx.obj, "ci", parsed_config, "ci", False)
    if stdout:
        # if forced stdout, don't set output file
        ctx.obj["output"] = None
    else:
        update_context(ctx.obj, "output", parsed_config, "output", None)
    update_context(ctx.obj, "fmt", parsed_config, "format", "table")
    update_context(ctx.obj, "yes", parsed_config, "confirm", False)
    update_context(ctx.obj, "table_sort_key", parsed_config, "table-sort-key", "line")

    # set return value - used for CI failures
    ctx.obj["retval"] = 0

    LOGGER.debug(f"Initializing tool name middleware with {__version__}")
    toolname_mw = ClientToolNameMiddleware(name="mythx-cli-{}".format(__version__))

    if api_key is not None:
        LOGGER.debug("Initializing client with API key")
        ctx.obj["client"] = Client(api_key=api_key, middlewares=[toolname_mw])
    elif username and password:
        LOGGER.debug("Initializing client with username and password")
        ctx.obj["client"] = Client(
            username=username, password=password, middlewares=[toolname_mw]
        )
    elif "fuzz" not in sys.argv:
        # fuzz subcommand is exempt from API auth
        raise click.UsageError(
            (
                "The trial user has been deprecated. You can still use the MythX CLI for free "
                "by signing up for a free account at https://mythx.io/ and entering your access "
                "credentials."
            )
        )


LOGGER.debug("Registering main commands")
cli.add_command(analyze)
cli.add_command(render)
cli.add_command(version)


@cli.group()
def project() -> None:
    """Create, modify, and view analysis projects.

    \f

    This subcommand holds all project-related actions, such as creating,
    listing, and managing projects, as well as fetching the status of one
    or more groups inside a project.
    """
    pass


LOGGER.debug("Registering project commands")
project.add_command(project_list)


@cli.group()
def group() -> None:
    """Create, modify, and view analysis groups.

    \f

    This subcommand holds all group-related actions, such as creating,
    listing, closing groups, as well as fetching the status of one
    or more group IDs.
    """
    pass


LOGGER.debug("Registering group commands")
group.add_command(group_list)
group.add_command(group_status)
group.add_command(group_open)
group.add_command(group_close)


@cli.group()
def analysis() -> None:
    """Get information on running and finished analyses.

    \f

    This subcommand holds all analysis-related actions, such as submitting new
    analyses, listing existing ones, fetching their status, as well as fetching
    the reports of one or more finished analysis jobs.
    """
    pass


LOGGER.debug("Registering analysis commands")
analysis.add_command(analysis_status)
analysis.add_command(analysis_list)
analysis.add_command(analysis_report)


@cli.group()
def fuzz() -> None:
    """Interact with the MythX FaaS solution.

    \f

    This subcommand holds all fuzz-related actions, such as initializing
    new fuzzing campaigns, preparing projects for FaaS submission, and
    launching new campaigns.
    """
    pass


LOGGER.debug("Registering fuzz commands")
fuzz.add_command(fuzz_run)
fuzz.add_command(fuzz_arm)
fuzz.add_command(fuzz_disarm)


if __name__ == "__main__":
    sys.exit(cli())  # pragma: no cover

The main runtime of the MythX CLI. A custom click group to catch API-related errors.      This custom Group implementation catches :code:`MythXAPIError`     exceptions, which get raised when the API returns a non-200     status code. It is used to notify the user about the error that     happened instead of triggering an uncaught exception traceback.      It is given to the main CLI entrypoint and propagated to all     subcommands. # noinspection PyIncorrectDocstring Your CLI for interacting with https://mythx.io/      \f      :param ctx: Click context holding group-level parameters     :param debug: Boolean to enable the `logging` debug mode     :param api_key: User JWT api token from the MythX dashboard     :param username: The MythX account ETH address/username     :param password: The account password from the MythX dashboard     :param fmt: The formatter to use for the subcommand output     :param ci: Boolean to return exit code 1 on medium/high-sev issues     :param output: Output file to write the results into     :param config: YAML config file to read default parameters from     :param stdout: Force printing to stdout and ignore output files     :param table_sort_key: The column to sort the default table output by # set loggers to debug mode # The analyze/fuzz context is updated separately in the command # implementation # overwrite context with top-level YAML config keys if necessary # if forced stdout, don't set output file # set return value - used for CI failures # fuzz subcommand is exempt from API auth Create, modify, and view analysis projects.      \f      This subcommand holds all project-related actions, such as creating,     listing, and managing projects, as well as fetching the status of one     or more groups inside a project. Create, modify, and view analysis groups.      \f      This subcommand holds all group-related actions, such as creating,     listing, closing groups, as well as fetching the status of one     or more group IDs. Get information on running and finished analyses.      \f      This subcommand holds all analysis-related actions, such as submitting new     analyses, listing existing ones, fetching their status, as well as fetching     the reports of one or more finished analysis jobs. Interact with the MythX FaaS solution.      \f      This subcommand holds all fuzz-related actions, such as initializing     new fuzzing campaigns, preparing projects for FaaS submission, and     launching new campaigns. # pragma: no cover