SecurityIntelligenceServicesFeed

A PassiveTotal with Security Intelligence Services Feed provides you with newly observed Domain, Malware, Phishing, Content and Scam Blacklist with Hourly ingestion available.

Data Enrichment & Threat Intelligence · Security Intelligence Services Feed · Feed

Details

IDSecurityIntelligenceServicesFeed
ProviderOpen Source
CategoryData Enrichment & Threat Intelligence
From Version5.5.0
Docker Imagedemisto/boto3py3:1.0.0.10221838
Supported ModulesAgentix XSIAM

README

A PassiveTotal with Security Intelligence Services Feed provides you with newly observed Domain, Malware, Phishing, Content, and Scam Blacklist with Hourly ingestion available.
This integration was integrated and tested with version 1.0 of Security Intelligence Services Feed.

The XSOAR instance with ElasticSearch is required as this integration would ingest large amount of indicators from SIS to XSOAR.

For that same reason, in case this integration fails to fetch indicators with timeout error, the feedIntegrationScript.timeout configuration should be configured with value 45 or more.

Configure Security Intelligence Services Feed in Cortex

Parameter Description Required
accessKey S3 Access Key True
secretKey S3 Secret Key True
feedType Feed Type True
feed Fetch indicators False
feedReputation Indicator Reputation False
feedReliability Source Reliability True
tlp_color The Traffic Light Protocol (TLP) designation to apply to indicators fetched from the feed. More information about the protocol can be found at https://us-cert.cisa.gov/tlp False
feedExpirationPolicy   False
feedExpirationInterval   False
feedFetchInterval Feed Fetch Interval False
feedTags Tags False
MaxIndicators Max Indicators Per Interval True
firstFetchInterval First Fetch Time Range (<number> <time unit>, e.g., 12 hours, 7 days, 3 months, 1 year) True
feedBypassExclusionList Bypass exclusion list False
insecure Trust any certificate (not secure) False
proxy Use system proxy settings False

Commands

You can execute these commands from the CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.

sis-get-indicators


Gets indicators from Security Intelligence Services feed. Note- Indicators will fetch from the latest found object.

Base Command

sis-get-indicators

Input

Argument Name Description Required
limit The maximum number of indicators to return from S3. Note- The maximum limit supported is 1000. Optional
feed_type Indicators will be fetched based on feed_type. Optional
search Indicators that match the given search pattern will be fetched. Optional

Context Output

There is no context output for this command.

Command Example

!sis-get-indicators limit=2 type=Domain

Human Readable Output

Total indicators fetched: 2

Indicators from Security Intelligence Services feed

Value Type
0363059571.online Domain
0363059571.xyz Domain

Configuration parameters

  • accessKey — S3 Access Key (required)
  • secretKey — S3 Secret Key (required)
  • feedType — Feed Type (required)
  • feed — Fetch indicators
  • feedReputation — Indicator Reputation
  • feedReliability — Source Reliability (required)
  • tlp_color — Traffic Light Protocol Color
  • feedExpirationPolicy
  • feedExpirationInterval
  • feedFetchInterval — Feed Fetch Interval
  • feedTags — Tags
  • firstFetchInterval — First Fetch Time Range (<number> <time unit>, e.g., 12 hours, 7 days, 3 months, 1 year) (required)
  • feedBypassExclusionList — Bypass exclusion list
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings

Commands (1)

  • sis-get-indicators

    Gets indicators from Security Intelligence Services feed. Note- Indicators will fetch from the latest found object.

from CommonServerPython import *

"""IMPORTS"""

from typing import Any
from collections.abc import Generator
from datetime import UTC
import csv
import gzip
import boto3 as s3
import botocore
from botocore import config
import urllib3
import dateparser

# Disable insecure warnings
urllib3.disable_warnings()

"""CONSTANTS"""

BATCH_SIZE = 2000

BUCKETS: dict[str, str] = {
    "domain": "sis-new-observations",
    "phish": "riq-sis-blacklist-phish",
    "malware": "riq-sis-blacklist-malware",
    "content": "riq-sis-blacklist-content",
    "scam": "riq-sis-blacklist-scam",
}

FIELD_NAMES: dict[str, list[str]] = {
    "domain": ["value", "Timestamp"],
    "phish": ["value", "Category", "MatchType", "Expiration"],
    "malware": ["value", "MalwareType", "MatchType", "MaliciousExpiration"],
    "content": ["value", "Category", "MatchType", "Expiration"],
    "scam": ["value", "Category", "MatchType", "Expiration"],
}

INDICATOR_TYPES: dict[str, str] = {
    "domain": FeedIndicatorType.Domain,
    "phish": FeedIndicatorType.URL,
    "malware": FeedIndicatorType.URL,
    "content": FeedIndicatorType.URL,
    "scam": FeedIndicatorType.URL,
}

REGIONS: dict[str, str] = {
    "domain": "us-west-1",
    "phish": "us-east-1",
    "malware": "us-west-1",
    "content": "us-west-1",
    "scam": "us-west-1",
}

MESSAGES: dict[str, str] = {
    "BAD_REQUEST_ERROR": "API call failed: Bad Request. Error: ",
    "AUTHORIZATION_ERROR": "Unauthorized. S3 Access Key or S3 Secret Key is invalid. Error: ",
    "NOT_FOUND_ERROR": "Not found. Error: ",
    "SERVER_ERROR": "API call failed. Server error received. Error: ",
    "PROXY_ERROR": "Proxy Error - if the 'Use system proxy' checkbox in the integration\
                    configuration is selected, try clearing the checkbox. Error: ",
    "HTTP_CLIENT_ERROR": "API call failed. Check the parameters configured. Error: ",
    "ERROR": "API call failed. Error: ",
    "INVALID_FEED_TYPE_ERROR": "Invalid feed type.",
    "REQUIRED_FEED_TYPE_ERROR": "Argument feedType is mandatory.",
    "INVALID_LIMIT_ERROR": "Argument limit must be a positive integer between 1 to 1000.",
    "BLANK_PROXY_ERROR": "https proxy value is empty. Check XSOAR server configuration ",
    "Feed_EXTRACT_ERROR": "Unable to extract feeds. Error: ",
    "INVALID_FIRST_FETCH_INTERVAL_ERROR": 'First fetch time range must be "number time_unit", '
    "examples: (10 days, 6 months, 1 year, etc.)",
    "INVALID_FIRST_FETCH_UNIT_ERROR": "First fetch time range field's unit is invalid. Must be in day(s), month(s) or year(s)",
    "INVALID_MAX_INDICATORS_ERROR": "Max Indicators Per Interval must be a positive integer.",
    "NO_INDICATORS_FOUND": "No indicators found for the given argument(s).",
}


class Client:
    """
    Client to use in integration to fetch data from Amazon S3.
    Creates instance of botot3 client and Handles exceptions.
    """

    def __init__(self, access_key: str, secret_key: str, verify: bool, proxy: bool):
        self.access_key = access_key
        self.secret_key = secret_key
        self.verify = verify

        # Set proxy
        if proxy:
            proxies = handle_proxy()

            # Throws a ValueError if Proxy is empty in configuration.
            if not proxies.get("https", True):
                raise ValueError(MESSAGES["BLANK_PROXY_ERROR"] + str(proxies))

            self.config = config.Config(proxies=proxies)
        else:
            self.config = config.Config(proxies={})

        self.s3_client: Any = None

    def set_s3_client(self, region_name: str | None) -> None:
        """
        Create S3 Client instance specific to region.

        :param region_name: Name of the S3 supported region.
        :return: None
        """
        self.s3_client = s3.client(
            "s3",
            aws_access_key_id=self.access_key,
            region_name=region_name,
            aws_secret_access_key=self.secret_key,
            verify=self.verify,
            config=self.config,
        )

    def return_error_based_on_status_code(self, status_code, error_message: str):
        """
        Return error message based on status code.
        Throws a ValueError based on respected status code.

        :param status_code: HTTP response code.
        :param error_message: Error message.
        :return: return_error
        """
        if status_code == 400:
            raise ValueError(MESSAGES["BAD_REQUEST_ERROR"] + error_message)
        elif status_code == 401 or status_code == 403:
            raise ValueError(MESSAGES["AUTHORIZATION_ERROR"] + error_message)
        elif status_code == 404:
            raise ValueError(MESSAGES["NOT_FOUND_ERROR"] + error_message)
        elif status_code >= 500:
            raise ValueError(MESSAGES["SERVER_ERROR"] + error_message)
        else:
            raise ValueError(f"failed with status code {status_code}, error: {error_message}")

    def request_list_objects(
        self, feed_type: str, max_keys: int = 1000, start_after: str = "", prefix: str = ""
    ) -> list[dict[str, Any]]:
        """
        Makes the API call to Amazon S3 using the boto3 list_objects method to retrieve the keys of objects.

        :param feed_type: Type of the feed. That is map with bucket.
        :param max_keys: Sets the maximum number of keys returned in the response.
        :param start_after: Start_after is where you want Amazon S3 to start listing from. Amazon S3 starts listing
        after this specified key.
        :param prefix: Limits the response to keys that begin with the specified prefix.
        :return: list of keys.
        """
        response = self.s3_client.list_objects_v2(
            Bucket=BUCKETS.get(feed_type, ""), MaxKeys=max_keys, StartAfter=start_after, Prefix=prefix
        )
        return response.get("Contents", [])

    def request_select_object_content(
        self, feed_type: str, key: str, limit: str = None, search: str = None, delimiter: str = "\t"
    ) -> csv.DictReader:
        """
         Makes the API call to Amazon S3 using the boto3 select_objecy_content method to retrieve the feeds.
         Method will execute search and limit records using SQL query provided.
         Used in get-indicators command.

        :param feed_type: Type of the feed. That is map with bucket.
        :param key: Return data of specified key.
        :param limit: Number of records to fetch.
        :param search: To search specific feeds from S3.
        :param delimiter: character to split feed from.
        :return: String of records.
        """
        query = (
            "Select * from S3Object s where s._1 LIKE '%{0}%' ESCAPE '\\' LIMIT {1}"
            if search
            else "Select * from S3Object s LIMIT {}"
        )
        response = self.s3_client.select_object_content(
            Bucket=BUCKETS.get(feed_type, ""),
            Key=key,
            ExpressionType="SQL",
            Expression=query.format(search, limit) if search else query.format(limit),
            InputSerialization={
                "CompressionType": "GZIP",
                "CSV": {
                    "FileHeaderInfo": "NONE",
                    "RecordDelimiter": "\n",
                    "FieldDelimiter": "\t",
                    "QuoteCharacter": "\t",
                    "AllowQuotedRecordDelimiter": True,
                },
            },
            OutputSerialization={
                "CSV": {
                    "RecordDelimiter": "\n",
                    "FieldDelimiter": "\t",
                }
            },
        )
        records: list[bytes] = []
        for event in response.get("Payload", ""):
            if "Records" in event:
                records.append(event["Records"]["Payload"])
            elif "end" in event:
                return csv.DictReader("")

        response_string = "".join(r.decode("utf-8") for r in records)
        return csv.DictReader(
            response_string.splitlines(), fieldnames=FIELD_NAMES.get(feed_type), delimiter=delimiter, quoting=csv.QUOTE_NONE
        )

    def build_iterator(
        self, feed_type: str, key: str, limit: str = None, search: str = None, batch_size: int = 2000, **kwargs
    ) -> Any:
        """
        Retrieves all entries from the streaming response batch wise
        and prepares dictionaries of feeds.
        If any parameter required to get response from S3 is invalid then throws a ValueError.

        :param feed_type: Type of the feed. That is map with bucket.
        :param key: Return data of specified key.
        :param start_from: Integer line number to start reading file from.
        :param limit: Number of records to fetch.
        :param search: To search specific feeds from S3.
        :param delimiter: character to split feed from.
        :param batch_size: size of feeds batch to return.
        :return: list of feed dictionaries.
        """
        try:
            if kwargs.get("is_get_indicators", False):
                # Request for get-indicators
                yield self.request_select_object_content(feed_type, key, limit, search, kwargs.get("delimiter", "\t"))
            else:
                if not os.path.exists(feed_type):
                    self.s3_client.download_file(Bucket=BUCKETS.get(feed_type, ""), Key=key, Filename=feed_type)

                file_stream = gzip.open(feed_type, "rt")

                while True:
                    # Creating feeds batch
                    feed_batch = [feed for _, feed in zip(range(batch_size), file_stream) if feed]

                    if not feed_batch:
                        file_stream.close()
                        os.remove(feed_type)
                        return

                    yield csv.DictReader(
                        feed_batch,
                        fieldnames=FIELD_NAMES.get(feed_type),
                        delimiter=kwargs.get("delimiter", "\t"),
                        quoting=csv.QUOTE_NONE,
                    )

        except botocore.exceptions.ClientError as exception:
            status_code = exception.response.get("ResponseMetadata", {}).get("HTTPStatusCode", "")
            error_message = exception.response.get("Error", {}).get("Message", "")
            self.return_error_based_on_status_code(status_code, error_message)

        except botocore.exceptions.ProxyConnectionError as exception:
            raise ValueError(MESSAGES["PROXY_ERROR"] + str(exception))

        except botocore.exceptions.HTTPClientError as exception:
            raise ValueError(MESSAGES["HTTP_CLIENT_ERROR"] + str(exception))

        except Exception as exception:
            raise ValueError(MESSAGES["ERROR"] + str(exception))


""" HELPER FUNCTIONS """


def validate_feeds(feed_types: list[str]) -> None:
    """
    Checks that given feeds are exist or not.
    If feed_types is empty then throws a ValueError.
    If any feed_type is not exist in BUCKET throws a ValueError.

    :param feed_types: Feed types provided.
    :return: True if feeds exist else return error.
    """
    if not feed_types:
        raise ValueError(MESSAGES["REQUIRED_FEED_TYPE_ERROR"])
    for feed in feed_types:
        if feed not in BUCKETS:
            raise ValueError(MESSAGES["INVALID_FEED_TYPE_ERROR"])


def get_last_key_from_integration_context_dict(feed_type: str, integration_context: list[Any] = []) -> str:
    """
    To get last fetched key of feed from integration context.

    :param feed_type: Type of feed to get last fetched key.
    :param integration_context: Integration context.
    :return: list of S3 object keys.
    """
    feed_context = integration_context
    for cached_feed in feed_context:
        cached_key = cached_feed.get(feed_type, "")
        if cached_key:
            return cached_key
    return ""


def set_last_key_to_integration_context_dict(feed_type: str, key: str, integration_context: list[Any]) -> None:
    """
    To set last fetched key of feed to integration context.

    :param feed_type: Type of feed to set.
    :param integration_context: context retrieved from integration context.
    :param key: Key to set.
    :return: None
    """
    for f_type_dict in integration_context:
        if f_type_dict.get(feed_type, ""):
            f_type_dict[feed_type] = key
            return
    integration_context.append({feed_type: key})


def validate_limit(limit: str) -> None:
    """
    Validates the limit argument.
    If the limit is 0 >= limit > 1000 or not an integer then throws a ValueError.

    :param limit: Number of feeds to fetch .
    :return: None
    """
    try:
        val = int(limit)
        if val <= 0 or val > 1000:
            raise ValueError
    except ValueError:
        raise ValueError(MESSAGES["INVALID_LIMIT_ERROR"])


def prepare_date_string_for_custom_fields(date_string: str) -> str:
    """
    Prepares date string in iso format and adds timezone if not exist.

    :param date_string: string represent date.
    :return: formatted date string.
    """
    parsed_dt = dateparser.parse(date_string)
    if parsed_dt:
        if parsed_dt.tzinfo is None:
            parsed_dt = parsed_dt.replace(tzinfo=UTC)
        return parsed_dt.isoformat()
    return ""


def indicator_field_mapping(feed_type: str, indicator: dict[str, Any], tags: list[str], tlp_color: str | None) -> dict[str, Any]:
    """
    Maps the indicator fields.

    :param feed_type: Type of feed.
    :param indicator: Indicator dictionary.
    :param tags: Tags specified in configuration.
    :param tlp_color: Traffic Light Protocol color.
    :return: Dict of fields.
    """
    fields: dict[str, Any] = {"service": "Passive Total", "tags": tags}
    if tlp_color:
        fields["trafficlightprotocol"] = tlp_color

    if feed_type == "domain":
        if indicator.get("Timestamp"):
            fields["firstseenbysource"] = datetime.fromtimestamp(
                int(indicator.get("Timestamp")),  # type: ignore
                UTC,
            ).isoformat()
    else:
        fields["threattypes"] = [{"threatcategory": feed_type.capitalize() if feed_type != "phish" else "Phishing"}]
        if indicator.get("MatchType"):
            fields["sismatchtype"] = indicator["MatchType"]

        if feed_type == "malware":
            if indicator.get("MalwareType"):
                fields["sismalwaretype"] = indicator["MalwareType"]

            if indicator.get("MaliciousExpiration"):
                fields["sisexpiration"] = prepare_date_string_for_custom_fields(indicator["MaliciousExpiration"])
        else:
            if indicator.get("Category"):
                fields["siscategory"] = indicator["Category"]

            if indicator.get("Expiration"):
                fields["sisexpiration"] = prepare_date_string_for_custom_fields(indicator["Expiration"])
    remove_nulls_from_dictionary(fields)
    return fields


def validate_first_fetch_interval(first_fetch_interval):
    """
    Validating first fetch interval. it should be in form of (<number> <time unit>, e.g., 12 hours, 7 days,
    3 months, 1 year)
    raise value error if validation fails.

    :param first_fetch_interval: first fetch interval
    :return: None
    """
    range_split = first_fetch_interval.split(" ")
    if len(range_split) != 2:
        raise ValueError(MESSAGES["INVALID_FIRST_FETCH_INTERVAL_ERROR"])
    if not range_split[0].isdigit():
        raise ValueError(MESSAGES["INVALID_FIRST_FETCH_INTERVAL_ERROR"])
    if range_split[1] not in ["minute", "minutes", "hours", "hour", "day", "days", "month", "months", "year", "years"]:
        raise ValueError(MESSAGES["INVALID_FIRST_FETCH_UNIT_ERROR"])


def get_latest_key(
    client: Client,
    feed_type: str,
    first_fetch_interval: str,
    is_get_indicators: bool = False,
    integration_context: list[Any] = [],
) -> str:
    """
    Get latest key from bucket of specified feed type.

    :param first_fetch_interval: first fetch interval.
    :param client: Client object.
    :param feed_type: Type of feed.
    :param is_get_indicators: True if get-indicators command is called.
    :param integration_context: Retrieved integration context.
    :return:
    """
    # Retrieving cached key from integration context.
    cached_key = get_last_key_from_integration_context_dict(feed_type, integration_context)
    object_key_list = client.request_list_objects(feed_type=feed_type, start_after=cached_key, prefix=feed_type.lower())
    object_key_list = [object_key for object_key in object_key_list if ".gz" in object_key.get("Key", "")]
    object_key_list.sort(key=lambda key_dict: key_dict["LastModified"])

    if is_get_indicators:
        return object_key_list[-1].get("Key", "") if object_key_list else cached_key

    # Parsing first fetch time.
    date_from, now = dateparser.parse(f"{first_fetch_interval} UTC"), datetime.now(UTC)

    # Fetching latest object keys.
    latest_key_list: list[str] = [
        key_dict.get("Key", "") for key_dict in object_key_list if key_dict.get("LastModified", now) >= date_from
    ]

    return latest_key_list[0] if latest_key_list else cached_key


""" REQUESTS FUNCTIONS """


@logger
def test_module(client: Client, feed_type: str) -> str | None:
    """
    Performs test connectivity by valid response.
    If any parameter required to get response from S3 is invalid then throws a ValueError.

    :param client: boto3 client
    :param feed_type: Type of feed to test.
    :return: 'ok' if test passed, anything else will fail the test.
    """
    try:
        client.set_s3_client(REGIONS[feed_type])
        client.request_list_objects(feed_type=feed_type, max_keys=1)
    except botocore.exceptions.ClientError as exception:
        status_code = exception.response.get("ResponseMetadata", {}).get("HTTPStatusCode", "")
        error_message = exception.response.get("Error", {}).get("Message", "")
        client.return_error_based_on_status_code(status_code, error_message)

    except botocore.exceptions.ProxyConnectionError as exception:
        raise ValueError(MESSAGES["PROXY_ERROR"] + str(exception))

    except botocore.exceptions.HTTPClientError as exception:
        raise ValueError(MESSAGES["HTTP_CLIENT_ERROR"] + str(exception))
    except Exception as exception:
        raise ValueError(MESSAGES["ERROR"] + str(exception))
    return "ok"


@logger
def fetch_indicators_command(
    client: Client,
    feed_types: list[str],
    first_fetch_interval: str = "7 day",
    limit: str = None,
    search: str = None,
    batch_size: int = 2000,
    **kwargs,
) -> Generator:
    """
    Fetches indicators from the S3 to the indicators tab.

    :param client: client object.
    :param feed_types: Types of feeds to fetch.
    :param first_fetch_interval: Interval to look back first time to fetch indicators.
    :param search: To search specific feeds from S3.
    :param limit: Number of records to fetch.
    :param batch_size: Size of the batch to create indicators.
    :param is_get_indicators: return true if get-indicators command called.
    :return: list of indicators.
    """

    for feed in feed_types:
        client.set_s3_client(region_name=REGIONS[feed])

        latest_key = get_latest_key(
            client, feed, first_fetch_interval, kwargs.get("is_get_indicators", False), kwargs.get("integration_context", [])
        )

        if latest_key:
            feed_batches = client.build_iterator(
                feed_type=feed,
                key=latest_key,
                limit=limit,
                search=search,
                batch_size=batch_size,
                is_get_indicators=kwargs.get("is_get_indicators", False),
            )
            for feed_dicts in feed_batches:
                indicators = []
                # Iterating trough each feed dictionary and creating indicators.
                for feed_dict in feed_dicts:
                    value = feed_dict.get("value", "")
                    if value:
                        indicator_type = INDICATOR_TYPES.get(feed)
                        feed_dict["type"] = indicator_type
                        remove_nulls_from_dictionary(feed_dict)
                        indicators.append(
                            {
                                "value": value,
                                "type": indicator_type,
                                "rawJSON": feed_dict,
                                "fields": indicator_field_mapping(
                                    feed, indicator=feed_dict, tags=kwargs.get("tags", []), tlp_color=kwargs.get("tlp_color")
                                ),
                            }
                        )

                yield indicators

            # Setting last key to context.
            if not kwargs.get("is_get_indicators", False):
                set_last_key_to_integration_context_dict(
                    feed_type=feed, key=latest_key, integration_context=kwargs.get("integration_context", [])
                )


@logger
def get_indicators_command(client: Client, args: dict[str, str]) -> CommandResults | str:
    """
    Wrapper for retrieving indicators from the feed to the war-room.

    :param client: Client object.
    :param args: demisto arguments.
    :return: CommandResult instance.
    """
    # Retrieving command arguments.
    feed_type = args.get("feed_type", "domain").lower()
    feed_type = feed_type if feed_type != "phishing" else "phish"
    limit = args.get("limit", "50")
    search = args.get("search", "")

    # Validate the provided limit
    validate_limit(limit)

    # Validate the provided feed type.
    validate_feeds([feed_type])

    # Retrieving indicators from fetch indicators command.
    indicators_list: list[dict] = []
    for indicators in fetch_indicators_command(client, [feed_type], limit=limit, search=search, is_get_indicators=True):
        indicators_list.extend(indicators)

    # Generating human-readable.
    if not indicators_list:
        return MESSAGES["NO_INDICATORS_FOUND"]

    human_readable = f"### Total indicators fetched: {len(indicators_list)}\n"
    human_readable += tableToMarkdown(
        "Indicators from Security Intelligence Services feed",
        indicators_list,
        ["value", "type"],
        removeNull=True,
        headerTransform=lambda header: header.capitalize(),
    )

    # Returning command result.
    return CommandResults(readable_output=human_readable, raw_response=indicators_list)


""" COMMANDS MANAGER / SWITCH PANEL """


def main() -> None:
    """
    PARSE AND VALIDATE INTEGRATION PARAMS
    """
    # Retrieving parameters.
    params = demisto.params()
    access_key = params.get("accessKey", "")
    secret_key = params.get("secretKey", "")
    verify_certificate = not params.get("insecure", False)
    use_proxy = params.get("proxy", False)
    feed_types = params.get("feedType", [])
    first_fetch_interval = params.get("firstFetchInterval", "1 day")

    try:
        # Prepare tags and tlp
        tags = list(set(argToList(params.get("feedTags", ""))))
        tlp_color = params.get("tlp_color")

        # validate first_fetch_time_interval parameter
        validate_first_fetch_interval(first_fetch_interval)

        # Validate the provided feed types.
        feed_types_lower = [feed_type.lower() if "phish" not in feed_type.lower() else "phish" for feed_type in feed_types]
        validate_feeds(feed_types_lower)

        client = Client(verify=verify_certificate, access_key=access_key, secret_key=secret_key, proxy=use_proxy)

        if demisto.command() == "test-module":
            demisto.results(test_module(client, feed_types_lower[0]))

        elif demisto.command() == "fetch-indicators":
            integration_context = demisto.getIntegrationContext().get("SISContext", [])

            indicators_generator = fetch_indicators_command(
                client,
                feed_types=feed_types_lower,
                first_fetch_interval=first_fetch_interval,
                batch_size=BATCH_SIZE,
                integration_context=integration_context,
                tags=tags,
                tlp_color=tlp_color,
            )
            for indicators in indicators_generator:
                demisto.createIndicators(indicators)  # type: ignore

            demisto.setIntegrationContext({"SISContext": integration_context})

        elif demisto.command() == "sis-get-indicators":
            return_results(get_indicators_command(client, demisto.args()))
    # Log exceptions
    except Exception as e:
        demisto.error(traceback.format_exc())
        return_error(f"Failed to execute {demisto.command()} command. Error: {str(e)}")


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