"""
##################################### TERMS OF USE ###########################################
# The following code is provided for demonstration purpose only, and should not              #
# be used without independent verification. Recorded Future makes no representations         #
# or warranties, express, implied, statutory, or otherwise, regarding any aspect of          #
# this code or of the information it may retrieve, and provides it both strictly “as-is”     #
# and without assuming responsibility for any information it may retrieve. Recorded Future   #
# shall not be liable for, and you assume all risk of using, the foregoing. By using this    #
# code, Customer represents that it is solely responsible for having all necessary licenses, #
# permissions, rights, and/or consents to connect to third party APIs, and that it is solely #
# responsible for having all necessary licenses, permissions, rights, and/or consents to     #
# any data accessed from any third party API.                                                #
##############################################################################################
"""

import os
import sys
import csv
import json
import argparse
import logging
from logging.handlers import RotatingFileHandler
import requests
from requests import ConnectTimeout, ConnectionError, HTTPError, ReadTimeout

APP_ID = "CB-Collective_Insights"
APP_VERSION = "1.0.0"
COLLECTIVE_INSIGHTS = "https://api.recordedfuture.com/collective-insights/detections"

"""Set the global variables for the Carbon Black Incident fields
    These fields may need to be updated based on the fields available or configured
    in your Carbon Black instance.
"""
#######################################################
MAX_RESULTS = 1000
LOOKBACK = "-24w"

CARBON_BLACK_INCIDENT_TIMESTAMP = "detection_timestamp"
CARBON_BLACK_INCIDENT_ID = "id"
CARBON_BLACK_INCIDENT_HASH = "process_sha256"
#######################################################


class RecordedFutureLogger:
    logging.basicConfig(filename="recordedfuture-cb-collectiveInsights.log", filemode="a")
    logger = logging.getLogger(__name__)
    file_handler = RotatingFileHandler(
        "recordedfuture-cb-collectiveInsights.log", maxBytes=10000000, backupCount=5
    )
    formatter_file = logging.Formatter(
        fmt="%(asctime)s,%(msecs)03d [%(threadName)s] %(levelname)s [%(module)s] "
        + "%(funcName)s:%(lineno)d - %(message)s",
        datefmt="%Y-%m-%d %H:%M:%S",
    )
    console_handler = logging.StreamHandler()
    formatter_console = logging.Formatter(
        fmt="%(asctime)s,%(msecs)03d %(levelname)s [%(module)s] - %(message)s",
        datefmt="%Y-%m-%d %H:%M:%S",
    )
    file_handler.setFormatter(formatter_file)
    console_handler.setFormatter(formatter_console)
    logger.addHandler(file_handler)
    logger.addHandler(console_handler)
    logger.propagate = False


LOG = RecordedFutureLogger.logger


class CarbonBlackError(Exception):
    """Error raised when there is an issue with the Carbon Black API."""

    pass


class RecordedFutureAPIError(Exception):
    """Error raised when there is an issue with the Recorded Future API."""

    pass


class RFScriptError(Exception):
    """Base class for other exceptions raised by this script."""

    pass


def get_args():
    """
    get_args function that defines the arguments the script can have on invocation.

    Returns:
        dict: dict with the arguments and their values
    """
    parser = argparse.ArgumentParser(
        description="Carbon Black Collective Insights Integration",
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
    )
    parser.add_argument(
        "-k",
        "--key",
        dest="rf_api_key",
        help="Recorded Future API Key",
        default=os.environ.get("RF_API_KEY"),
        required=False,
    )
    parser.add_argument(
        "-co",
        "--cb-org-id",
        dest="cb_org_id",
        help="Carbon Black Org ID",
        default=os.environ.get("CB_ORG_ID"),
        required=False,
    )
    parser.add_argument(
        "-ch",
        "--cb-hostname",
        dest="cb_hostname",
        help="Carbon Black Hostname",
        default=os.environ.get("CB_HOSTNAME"),
        required=False,
    )
    parser.add_argument(
        "-cs",
        "--cb-api-secret",
        dest="cb_secret",
        help="Carbon Black API Secret Key",
        default=os.environ.get("CB_SECRET"),
        required=False,
    )
    parser.add_argument(
        "-cid",
        "--cb-api-id",
        dest="cb_api_id",
        help="Carbon Black API ID",
        default=os.environ.get("CB_API_ID"),
        required=False,
    )
    parser.add_argument(
        "--debug",
        action="store_true",
        dest="debug_flag",
        help="Debug mode, insights not posted to Recorded Future",
        required=False,
    )
    parser.set_defaults(debug=False)
    parser.add_argument(
        "-l",
        "--loglevel",
        help="Log level",
        type=str,
        choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
        default="INFO",
        dest="loglevel",
        required=False,
    )

    parser.add_argument(
        "-ef",
        "--exclusion-file",
        help="File containing hashes to be excluded from the collective insights results",
        dest="exclusion_file",
        default="",
        required=False,
    )

    return parser.parse_args()


def query_cb(cb_org_id, cb_hostname, cb_secret, cb_api_id, collective_insights_exclusions):
    """
    query_cb queries carbon black api for alerts that have been generated based on the LOOKBACK
    global variable.

    Args:
        cb_org_id (str): Carbon Black Org ID
        cb_hostname (str): Carbon Black Hostname
        cb_secret (str): Carbon Black API Secret Key
        cb_id (str): Carbon Black API ID
        collective_insights_exclusions (list): list of hashes to be excluded from the results
          (OPTIONAL)

    Returns:
        int: uploudCount - number of results found
        list: write_back_data - list of json objects with incident data to be uploaded to
          Recorded Future
    """
    V7_ALERTS_SEARCH_URL = f"{cb_hostname}/api/alerts/v7/orgs/{cb_org_id}/alerts/_search"

    headers = {
        "X-Auth-Token": f"{cb_secret}/{cb_api_id}",
        "Content-Type": "application/json",
        "Accept": "application/json",
    }

    try:
        post_payload = json.dumps(
            {
                "query": "blocked_sha256:* OR blocked_md5:* OR ioc_hit:* NOT type:DEVICE_CONTROL OR ioc_field:*",
                "time_range": {"range": LOOKBACK},
                "start": 1,
                "rows": MAX_RESULTS,
                "sort": [{"field": "detection_timestamp", "order": "DESC"}],
            }
        )
    except (TypeError, ValueError) as cbe:
        LOG.error(f"Error creating post payload: {cbe}")
        raise CarbonBlackError(cbe)

    try:
        resp = requests.post(V7_ALERTS_SEARCH_URL, headers=headers, data=post_payload)
        resp.raise_for_status()
    except (HTTPError, ConnectTimeout, ConnectionError, ReadTimeout) as cbe:
        LOG.error(f"Error querying Carbon Black: {cbe}")
        raise CarbonBlackError(cbe)

    write_back_data = []

    if resp.json()["results"]:
        for result in resp.json()["results"]:
            if result[CARBON_BLACK_INCIDENT_HASH]:
                if result[CARBON_BLACK_INCIDENT_HASH] not in collective_insights_exclusions:
                    json_builder = {
                        "timestamp": result[CARBON_BLACK_INCIDENT_TIMESTAMP],
                        "ioc": {
                            "type": "hash",
                            "value": result[CARBON_BLACK_INCIDENT_HASH],
                        },
                        "incident": {
                            "id": result[CARBON_BLACK_INCIDENT_ID],
                            "name": "Carbon Black Threat Detection",
                            "type": "carbonblack-threat-detection",
                        },
                        "detection": {
                            "name": "Carbon Black Threat Detection",
                            "type": "correlation",
                            "sub_type": "",
                        },
                    }
                write_back_data.append(json_builder)
        upload_count = len(write_back_data)
    else:
        LOG.info("No results found")
        upload_count = 0

    return write_back_data, upload_count


def submit_collective_insights(collective_insights_upload, rf_api_key):
    """
    submit_collective_insights submits data to Recorded Future Collective Insights API.

    Args:
        collective_insights_upload (list): list of dictionaries containing data to be uploaded
          to Recorded Future Collective Insights API
        rf_api_key (str): Recorded Future API Key

    Returns:
        dict : response - json response from Recorded Future Collective Insights API
    """
    headers = {
        "User-Agent": f"{APP_ID}/{APP_VERSION}",
        "Content-Type": "application/json",
        "Accept": "application/json",
        "X-RFToken": f"{rf_api_key}",
    }

    # assign data variable to the json payload submitted to this function
    data = collective_insights_upload
    LOG.debug(f"Submitting data to Recorded Future: {data}")

    # post data to collective insights using arguments created above
    try:
        response = requests.post(COLLECTIVE_INSIGHTS, headers=headers, data=json.dumps(data))
        response.raise_for_status()

    except (HTTPError, ConnectTimeout, ConnectionError, ReadTimeout) as err:
        LOG.error(f"Error submitting collective insights: {err}")
        raise RecordedFutureAPIError(err)
    return response


def get_collective_insights_exclusions(filename):
    """
    get_collective_insights_exclusions reads the file inputted and returns a list of hashes to be
      excluded from the results.

    Returns:
        list: list of hashes to be excluded from the results
    """

    exclusions_list = []

    exclusion_file = os.path.join(os.path.dirname(__file__), filename)

    if os.path.isfile(exclusion_file):
        try:
            with open(exclusion_file, "r") as file:
                try:
                    csv_reader = csv.reader(file, delimiter=",")
                    try:
                        for row in csv_reader:
                            exclusions_list.append(row[0])
                    except IndexError as ie:
                        LOG.error(
                            f"""Error reading exclusions from file: {exclusion_file}continuing without
                            exclusions IndexError - {ie}"""
                        )
                except csv.Error as ce:
                    LOG.error(
                        f"""Error reading exclusions file: {exclusion_file}continuing without exclusions
                          CSVError - {ce}"""
                    )
        except (FileNotFoundError, OSError) as fe:
            LOG.error(
                f"Error reading exclusions file: {exclusion_file}, continuing without exclusions {fe}"
            )
    else:
        LOG.error(
            f"Error reading exclusions file: {exclusion_file}, continuing without exclusions..."
        )
        exclusions_list = ["empty"]

    return exclusions_list


def main():
    """main function that calls the other functions to query carbon black and submit the results
    to Recorded Future Collective Insights API."""

    try:
        args = get_args()

        if any(x is None for x in args.__dict__.values()):
            LOG.error("Args not found. Exiting program...")
            raise RFScriptError("Args not found. Exiting program...")

    except (argparse.ArgumentError, RFScriptError):
        LOG.error("Error parsing arguments", exc_info=True)
        sys.exit(1)

    json_options = {"options": {"debug": args.debug_flag, "summary": True}}
    LOG.level = logging.getLevelName(args.loglevel)

    LOG.info("Starting Carbon Black Collective Insights Integration")

    if args.exclusion_file != "":
        exclusions = get_collective_insights_exclusions(args.exclusion_file)
    else:
        exclusions = []

    try:
        write_back_data, upload_count = query_cb(
            args.cb_org_id, args.cb_hostname, args.cb_secret, args.cb_api_id, exclusions
        )

        payload = dict(json_options, data=write_back_data)
        LOG.info(f"Successfully Generated {upload_count} results")

        if upload_count > 0:
            try:
                submit_payload = submit_collective_insights(payload, args.rf_api_key)
                LOG.info(submit_payload.text)
            except (HTTPError, ConnectTimeout, ConnectionError, ReadTimeout):
                LOG.error("Error submitting collective insights", exc_info=True)
                sys.exit(1)
        else:
            LOG.info("No results to upload to Recorded Future Collective Insights.")
    except CarbonBlackError:
        LOG.error(f"Error Querying Carbon Black", exc_info=True)
        sys.exit(1)


if __name__ == "__main__":
    main()
