FireEyeApiModule

Common FireEye code that will be appended to each FireEye integration when it is deployed.

python · ApiModules

Source

from CommonServerPython import *

""" CONSTANTS """
FE_DATE_FORMAT = "%Y-%m-%dT%H:%M:%S"
OK_CODES = (200, 206)


class FireEyeClient(BaseClient):
    def __init__(self, base_url: str, username: str, password: str, verify: bool, proxy: bool, ok_codes: tuple = OK_CODES):
        super().__init__(base_url=base_url, auth=(username, password), verify=verify, proxy=proxy, ok_codes=ok_codes)
        self._headers = {
            "X-FeApi-Token": self._get_token(),
            "Accept": "application/json",
        }

    @logger
    def http_request(
        self,
        method: str,
        url_suffix: str = "",
        json_data: dict = None,
        params: dict = None,
        timeout: int = 10,
        resp_type: str = "json",
        retries: int = 1,
    ):
        try:
            address = urljoin(self._base_url, url_suffix)
            res = self._session.request(
                method, address, headers=self._headers, verify=self._verify, params=params, json=json_data, timeout=timeout
            )
            # Handle error responses gracefully
            if not self._is_status_code_valid(res):
                err_msg = f"Error in API call {res.status_code} - {res.reason}"
                try:
                    # Try to parse json error response
                    error_entry = res.json()
                    err_msg += f"\n{json.dumps(error_entry)}"
                    if "Server Error. code:AUTH004" in err_msg and retries:
                        # implement 1 retry to re create a token
                        self._headers["X-FeApi-Token"] = self._generate_token()
                        self.http_request(method, url_suffix, json_data, params, timeout, resp_type, retries - 1)
                    else:
                        raise DemistoException(err_msg, res=res)
                except ValueError:
                    err_msg += f"\n{res.text}"
                    raise DemistoException(err_msg, res=res)

            resp_type = resp_type.lower()
            try:
                if resp_type == "json":
                    return res.json()
                if resp_type == "text":
                    return res.text
                if resp_type == "content":
                    return res.content
                return res
            except ValueError:
                raise DemistoException("Failed to parse json object from response.")
        except requests.exceptions.ConnectTimeout as exception:
            err_msg = (
                "Connection Timeout Error - potential reasons might be that the Server URL parameter"
                " is incorrect or that the Server is not accessible from your host."
            )
            raise DemistoException(err_msg, exception)
        except requests.exceptions.SSLError as exception:
            # in case the "Trust any certificate" is already checked
            if not self._verify:
                raise
            err_msg = (
                "SSL Certificate Verification Failed - try selecting 'Trust any certificate' checkbox in"
                " the integration configuration."
            )
            raise DemistoException(err_msg, exception)
        except requests.exceptions.ProxyError as exception:
            err_msg = (
                "Proxy Error - if the 'Use system proxy' checkbox in the integration configuration is"
                " selected, try clearing the checkbox."
            )
            raise DemistoException(err_msg, exception)
        except requests.exceptions.ConnectionError as exception:
            # Get originating Exception in Exception chain
            error_class = str(exception.__class__)
            err_type = "<" + error_class[error_class.find("'") + 1 : error_class.rfind("'")] + ">"
            err_msg = (
                f"Verify that the server URL parameter"
                f" is correct and that you have access to the server from your host."
                f"\nError Type: {err_type}\nError Number: [{exception.errno}]\nMessage: {exception.strerror}\n"
            )
            raise DemistoException(err_msg, exception)

    @logger
    def _get_token(self) -> str:
        """
        Obtains token from integration context if available and still valid
        (15 minutes according to the API, we gave 10 minutes).
        After expiration, new token are generated and stored in the integration context.
        Returns:
            str: token that will be added to authorization header.
        """
        integration_context = get_integration_context()
        token = integration_context.get("token", "")
        valid_until = integration_context.get("valid_until")

        now = datetime.now()
        now_timestamp = datetime.timestamp(now)
        # if there is a key and valid_until, and the current time is smaller than the valid until
        # return the current token
        if token and valid_until and now_timestamp < valid_until:
            return token

        # else generate a token and update the integration context accordingly
        token = self._generate_token()

        return token

    @logger
    def _generate_token(self) -> str:
        try:
            resp = self._http_request(method="POST", url_suffix="auth/login", resp_type="response")
        except DemistoException as er:
            raise DemistoException(f"Token request failed. message: {er!s}")
        if "X-FeApi-Token" not in resp.headers:
            raise DemistoException(f"Token request failed. API token is missing. message: {resp!s}")
        token = resp.headers["X-FeApi-Token"]

        integration_context = get_integration_context()
        integration_context.update({"token": token})
        time_buffer = 600  # 600 seconds (10 minutes) by which to lengthen the validity period
        integration_context.update({"valid_until": datetime.timestamp(datetime.now() + timedelta(seconds=time_buffer))})
        set_integration_context(integration_context)

        return token

    @logger
    def get_alerts_request(self, request_params: Dict[str, Any], timeout: int = 120) -> Dict[str, str]:
        malware_type = request_params.get("malware_type")
        if not malware_type or malware_type in ["riskware-infection", "riskware-callback", "riskware-object"]:
            request_params["include_riskware"] = True
        return self.http_request(method="GET", url_suffix="alerts", params=request_params, resp_type="json", timeout=timeout)

    @logger
    def get_alert_details_request(self, alert_id: str, timeout: int) -> Dict[str, str]:
        return self.http_request(method="GET", url_suffix=f"alerts/alert/{alert_id}", resp_type="json", timeout=timeout)

    @logger
    def alert_acknowledge_request(self, uuid: str) -> Dict[str, str]:
        # json_data here is redundant as we are not sending any meaningful data,
        # but without it the API call to FireEye fails and we are getting an error. hence sending it with a dummy value.
        # the error we get when not sending json_data is: "Bad Request" with Invalid input. code:ALRTCONF001
        return self.http_request(
            method="POST",
            url_suffix=f"alerts/alert/{uuid}",
            params={"schema_compatibility": True},
            json_data={"annotation": "<test>"},
            resp_type="resp",
        )

    @logger
    def get_artifacts_by_uuid_request(self, uuid: str, timeout: int) -> Dict[str, str]:
        self._headers.pop("Accept")  # returns a file, hence this header is disruptive
        return self.http_request(method="GET", url_suffix=f"artifacts/{uuid}", resp_type="content", timeout=timeout)

    @logger
    def get_artifacts_metadata_by_uuid_request(self, uuid: str) -> Dict[str, str]:
        return self.http_request(method="GET", url_suffix=f"artifacts/{uuid}/meta", resp_type="json")

    @logger
    def get_events_request(self, duration: str, end_time: str, mvx_correlated_only: bool) -> Dict[str, str]:
        return self.http_request(
            method="GET",
            url_suffix="events",
            params={
                "event_type": "Ips Event",
                "duration": duration,
                "end_time": end_time,
                "mvx_correlated_only": mvx_correlated_only,
            },
            resp_type="json",
        )

    @logger
    def get_quarantined_emails_request(
        self, start_time: str, end_time: str, from_: str, subject: str, appliance_id: str, limit: int
    ) -> Dict[str, str]:
        params = {"start_time": start_time, "end_time": end_time, "limit": limit}
        if from_:
            params["from"] = from_
        if subject:
            params["subject"] = subject
        if appliance_id:
            params["appliance_id"] = appliance_id

        return self.http_request(method="GET", url_suffix="emailmgmt/quarantine", params=params, resp_type="json")

    @logger
    def release_quarantined_emails_request(self, queue_ids: list, sensor_name: str):
        return self.http_request(
            method="POST",
            url_suffix="emailmgmt/quarantine/release",
            params={"sensorName": sensor_name},
            json_data={"queue_ids": queue_ids},
            resp_type="resp",
        )

    @logger
    def delete_quarantined_emails_request(self, queue_ids: list, sensor_name: str = ""):
        return self.http_request(
            method="POST",
            url_suffix="emailmgmt/quarantine/delete",
            params={"sensorName": sensor_name},
            json_data={"queue_ids": queue_ids},
            resp_type="resp",
        )

    @logger
    def download_quarantined_emails_request(self, queue_id: str, timeout: str, sensor_name: str = ""):
        self._headers.pop("Accept")  # returns a file, hence this header is disruptive
        return self.http_request(
            method="GET",
            url_suffix=f"emailmgmt/quarantine/{queue_id}",
            params={"sensorName": sensor_name},
            resp_type="content",
            timeout=timeout,
        )

    @logger
    def get_reports_request(
        self,
        report_type: str,
        start_time: str,
        end_time: str,
        limit: str,
        interface: str,
        alert_id: str,
        infection_type: str,
        infection_id: str,
        timeout: int,
    ):
        params = {"report_type": report_type, "start_time": start_time, "end_time": end_time}
        if limit:
            params["limit"] = limit
        if interface:
            params["interface"] = interface
        if alert_id:
            params["id"] = alert_id
        if infection_type:
            params["infection_type"] = infection_type
        if infection_id:
            params["infection_id"] = infection_id

        return self.http_request(method="GET", url_suffix="reports/report", params=params, resp_type="content", timeout=timeout)

    @logger
    def list_allowedlist_request(self, type_: str) -> Dict[str, str]:
        return self.http_request(method="GET", url_suffix=f"devicemgmt/emlconfig/policy/allowed_lists/{type_}", resp_type="json")

    @logger
    def create_allowedlist_request(self, type_: str, entry_value: str, matches: int) -> Dict[str, str]:
        return self.http_request(
            method="POST",
            url_suffix=f"devicemgmt/emlconfig/policy/allowed_lists/{type_}",
            params={"operation": "create"},
            json_data={"name": entry_value, "matches": matches},
            resp_type="resp",
        )

    @logger
    def update_allowedlist_request(self, type_: str, entry_value: str, matches: int) -> Dict[str, str]:
        return self.http_request(
            method="POST",
            url_suffix=f"devicemgmt/emlconfig/policy/allowed_lists/{type_}/{entry_value}",
            json_data={"matches": matches},
            resp_type="resp",
        )

    @logger
    def delete_allowedlist_request(self, type_: str, entry_value: str) -> Dict[str, str]:
        return self.http_request(
            method="POST",
            url_suffix=f"devicemgmt/emlconfig/policy/allowed_lists/{type_}/{entry_value}",
            params={"operation": "delete"},
            resp_type="resp",
        )

    @logger
    def list_blockedlist_request(self, type_: str) -> Dict[str, str]:
        return self.http_request(method="GET", url_suffix=f"devicemgmt/emlconfig/policy/blocked_lists/{type_}", resp_type="json")

    @logger
    def create_blockedlist_request(self, type_: str, entry_value: str, matches: int) -> Dict[str, str]:
        return self.http_request(
            method="POST",
            url_suffix=f"devicemgmt/emlconfig/policy/blocked_lists/{type_}",
            params={"operation": "create"},
            json_data={"name": entry_value, "matches": matches},
            resp_type="resp",
        )

    @logger
    def update_blockedlist_request(self, type_: str, entry_value: str, matches: int) -> Dict[str, str]:
        return self.http_request(
            method="POST",
            url_suffix=f"devicemgmt/emlconfig/policy/blocked_lists/{type_}/{entry_value}",
            json_data={"matches": matches},
            resp_type="resp",
        )

    @logger
    def delete_blockedlist_request(self, type_: str, entry_value: str) -> Dict[str, str]:
        return self.http_request(
            method="POST",
            url_suffix=f"devicemgmt/emlconfig/policy/blocked_lists/{type_}/{entry_value}",
            params={"operation": "delete"},
            resp_type="resp",
        )


def to_fe_datetime_converter(time_given: str = "now") -> str:
    """Generates a string in the FireEye format, e.g: 2015-01-24T16:30:00.000-07:00

    Examples:
        >>> to_fe_datetime_converter('2021-05-14T01:08:04.000-02:00')
        2021-05-14T01:08:04.000-02:00
        >>> to_fe_datetime_converter('now')
        2021-05-23T06:45:16.688+00:00

    Args:
        time_given: the time given, if none given, the default is now.

    Returns:
        The time given in FireEye format.
    """
    date_obj = dateparser.parse(time_given)
    assert date_obj is not None, f"failed parsing {time_given}"
    fe_time = date_obj.strftime(FE_DATE_FORMAT)
    fe_time += f'.{date_obj.strftime("%f")[:3]}'
    if not date_obj.tzinfo:
        given_timezone = "+00:00"
    else:
        given_timezone = f'{date_obj.strftime("%z")[:3]}:{date_obj.strftime("%z")[3:]}'  # converting the timezone
    fe_time += given_timezone
    return fe_time


def alert_severity_to_dbot_score(severity_str: str):
    severity = severity_str.lower()
    if severity == "minr":
        return 1
    if severity == "majr":
        return 2
    if severity == "crit":
        return 3
    demisto.info(f"FireEye Incident severity: {severity} is not known. Setting as unknown(DBotScore of 0).")
    return 0

README

The FireEye API handles the API Key generation from the user/password authentication.

  • Manages the API Key and caches it for 10 minutes, to avoid 401 errors for consecutive API Key generation.
  • Holds the client command calls to the WSAPIs v2.0.0.
  • Includes a few generally used static function converters.

To use the common FireEye API logic, attach the from FireEyeApiModule import * # noqa: E402 line of code in the following location to import it. After you import the module, the FireEyeClient will be available for use.

def main():
    ...


from FireEyeApiModule import *  # noqa: E402

if __name__ in ('__main__', '__builtin__', 'builtins'):
    main()

For examples, see the FireEye Central Management integration.