示例#1
0
class SearcherExample(BaseComponent):
    """
    Example of a custom threat searcher component.

    You can use this as a template when implementing custom searcher(s) in your own package.
    """

    # Register this as an async searcher for the URL /<root>/example
    channel = searcher_channel("example")

    # Handle lookups for artifacts of type 'net.uri' (see doc for full list)
    @handler("net.uri")
    def _lookup_net_uri(self, event, *args, **kwargs):
        """Return hits for URL artifacts"""
        hits = []

        # event.artifact is a ThreatServiceArtifactDTO
        artifact_type = event.artifact['type']
        artifact_value = event.artifact['value']

        # Return zero or more hits.  Here's one example.
        hits.append(
            Hit(NumberProp(name="id", value=123),
                StringProp(name="Type", value=artifact_type),
                UriProp(name="Link", value=artifact_value),
                IpProp(name="IP Address", value="127.0.0.1"),
                LatLngProp(name="Location", lat=42.366, lng=-71.081)))
        hits.append(
            Hit(
                NumberProp(name="id", value=456),
                StringProp(name="Type", value=artifact_type),
                UriProp(name="Link", value=artifact_value),
                IpProp(name="IP Address", value="0.0.0.0"),
            ))
        yield hits
示例#2
0
class GoogleSafeBrowsingThreatSearcher(BaseComponent):
    """
    Custom threat lookup for Google Safe Browsing API

    """
    channel = searcher_channel("gsb")

    def __init__(self, opts):
        super(GoogleSafeBrowsingThreatSearcher, self).__init__(opts)

        self.apikey = opts.get(CONFIG_SECTION, {}).get("google_api_key")
        if self.apikey is None:
            exc = "Configuration value `google_api_key=XXX` is missing in [{}] section".format(CONFIG_SECTION)
            raise Exception(exc)

    @handler("net.uri")
    def _lookup_net_uri(self, event, *args, **kwargs):
        LOG.info("Looking up with Google Safe Browsing API")

        value = event.artifact['value']
        LOG.info("Looking up URL: " + str(value))

        sb = SafeBrowsingAPI(self.apikey)
        resp = sb.lookup_urls(value)
        hits = []
        for match in resp.get("matches", []):
            linkurl = match["threat"]["url"]
            link = LINK_URL.format(match["threat"]["url"])
            hits.append(Hit(
                StringProp(name="Threat Type", value=match["threatType"]),
                UriProp(name="Report Link", value=link),
                StringProp(name="Platform Type", value=match["platformType"]),
                StringProp(name="URL Name", value=linkurl)
            ))
        return hits
示例#3
0
    def __init__(self,
                 request_id=None,
                 name="unknown",
                 artifact=None,
                 channel=searcher_channel()):
        super(ThreatServiceLookupEvent, self).__init__(name=name)

        self.channels = (channel, )
        self.request_id = request_id
        self.cts_channel = channel
        self.artifact = artifact
        self.name = name
示例#4
0
    def _handle_get_request(self, event, *args, **kwargs):
        """
        Responds to GET /cts/<anything>/<request-id>

        The URL below /cts/ is specific to this threat service. For example,
        /cts/one and /cts/two are considered two separate threat sources.

        Response is a ResponseDTO containing the response, or 'please retry'
        """
        LOG.info(event.args[0])
        response = event.args[1]
        request_id = None
        if not args:
            return {"id": request_id, "hits": []}

        # The ID of the lookup request
        request_id = args[-1]
        # The channel that searchers are listening for events
        cts_channel = searcher_channel(*args[:-1])

        response_object = {"id": request_id, "hits": []}

        cache_key = (cts_channel, request_id)
        request_data = self.cache.get(cache_key)
        if not request_data:
            # There's no record of this request in our cache, return empty hits
            response.status = 200
            return response_object

        response_object["hits"] = request_data["hits"]
        if not request_data["complete"]:
            # The searchers haven't finished yet, return partial hits if available
            response.status = 303
            response_object["retry_secs"] = self.later_retry_secs

            # Update the counter, so we can detect "stale" failures
            request_data["count"] = request_data.get("count", 0) + 1
            if request_data["count"] > self.max_retries:
                LOG.info("Exceeded max retries for {}".format(cache_key))
                try:
                    self.cache.pop(cache_key)
                except KeyError:
                    pass
                response.status = 200
                return response_object

            return response_object

        # Remove the result from cache
        # self.cache.pop(cache_key)

        return response_object
示例#5
0
class UrlScanIoSearcher(BaseComponent):
    """
    A custom threat service 'searcher' for urlscan.io

    Test using 'curl':
        curl -v -k --header "Content-Type: application/json" --data-binary '{"type":"net.uri","value":"http://example.org"}' 'http://127.0.0.1:9000/cts/usio'
        curl -v 'http://127.0.0.1:9000/cts/example/f9acc1b7-6184-5746-873e-e385e6214261'

    Test example of a potentially malicious url in urlscan.io search database is "http://detailsindia.in".
    Test example of a non-malicious url in urlscan.io search database is "www.bai.org"
    """

    CONFIG_SECTION = "urlscanio"
    HEADERS = {'Content-Type': 'application/json'}
    SEARCH_API_SIZE_QUERY_PARAM = "&size="
    SEARCH_API_QUERY_TERM_PAGE_URL_PARAM = "?q=page.url:"

    def __init__(self, opts):
        super(UrlScanIoSearcher, self).__init__(opts)
        self.options = opts.get(self.CONFIG_SECTION, {})

    # Register this as an async searcher for the URL /<root>/example
    channel = searcher_channel("usio")

    @handler("net.uri")
    def _lookup_net_uri(self, event, *args, **kwargs):
        """
        Handle lookups for artifacts of type 'net uri' (URL artifacts)
        """

        # Read configuration settings:

        self.urlscan_io_search_api_url = self._get_value_from_options(
            "urlscan_io_search_api_url")
        self.urlscan_io_result_api_url = self._get_value_from_options(
            "urlscan_io_result_api_url")
        self.urlscan_io_search_size = self.options.get(
            "urlscan_io_search_size", None)

        # event.artifact is a ThreatServiceArtifactDTO
        artifact_type = event.artifact['type']
        artifact_value = event.artifact['value']
        LOG.info(
            "Looking up with urlscan.io Search API started for Artifact Type {0} - Artifact Value {1}"
            .format(artifact_type, artifact_value))
        hits = self._query_urlscan_io_api(artifact_value)
        yield hits

    def _get_value_from_options(self, app_config_setting_key):
        """
        Get value from options dict or raise ValueError for the mandatory config setting.
        :param app_config_setting_name key
        """
        if app_config_setting_key in self.options:
            return self.options[app_config_setting_key]
        else:
            error_msg = "Mandatory config setting '{}' not set.".format(
                app_config_setting_key)
            LOG.error(error_msg)
            raise ValueError(error_msg)

    def _query_urlscan_io_api(self, artifact_value):
        """
        Query urlscan io Search API for the given URL.
        :param artifact_value: URL
        """
        hits = []

        try:
            optional_size_param = "{0}{1}".format(self.SEARCH_API_SIZE_QUERY_PARAM, self.urlscan_io_search_size) \
                if self.urlscan_io_search_size else ""

            url = "{0}{1}\"{2}\"{3}".format(
                self.urlscan_io_search_api_url,
                self.SEARCH_API_QUERY_TERM_PAGE_URL_PARAM, artifact_value,
                optional_size_param)

            search_response = requests.get(url, headers=self.HEADERS)

            if search_response.status_code == 200:
                content = search_response.json()

                total_hits = content.get('total', None)
                if total_hits is None or total_hits == 0:
                    LOG.info("No Results for the URL.")
                    LOG.debug(search_response.text)
                    return hits

                LOG.info("Getting the report for the URL.")

                search_results = content.get('results', None)
                if not search_results:
                    LOG.info("No Results for the URL.")
                    LOG.debug(search_response.text)
                    return hits

                for result in search_results:
                    if not result:
                        continue

                    result_hit = self._generate_hit_from_search_result(result)
                    if result_hit:  # Do not include None value
                        hits.append(result_hit)

                if not hits:
                    LOG.info("URL {0} isn't marked as malicious.".format(
                        artifact_value))
            else:
                LOG.info("No hit information found on URL: {0}".format(
                    artifact_value))
                LOG.debug(search_response.text)

        except BaseException as ex:
            LOG.exception(ex.message)
        return hits

    def _generate_hit_from_search_result(self, search_result):
        """
        Query urlscan io Result API for the given URL search result - hit.
        :param search_result: dictionary
        """
        result_id = search_result.get('_id', None)
        result_url = "{0}{1}".format(self.urlscan_io_result_api_url, result_id)
        result_response = requests.get(result_url, headers=self.HEADERS)

        if result_response.status_code == 200:
            result_content = result_response.json()

            stats = result_content.get('stats', None)
            if stats:
                malicious_flag = stats.get('malicious', None)

                if malicious_flag == 1:

                    # Some malicious scans show as failed, do not include those
                    if self._verify_for_scan_failed_flag(result_content):
                        return None

                    task = result_content.get('task', None)
                    page = result_content.get('page', None)

                    png_url = task.get('screenshotURL', None) if task else None
                    scan_time = task.get('time', None) if task else None
                    report_url = task.get('reportURL', None) if task else None
                    uniq_countries_int = stats.get('uniqCountries', None)
                    city_country_list = self._prepare_city_contry(
                        page.get('city', None), page.get(
                            'country', None)) if page else None
                    city_country = ",".join(
                        city_country_list) if city_country_list else None
                    server = page.get('server', None) if page else None
                    asn = page.get('asnname', None) if page else None

                    return Hit(
                        StringProp(name="Time Last Scanner", value=scan_time),
                        NumberProp(name="Number of Countries",
                                   value=uniq_countries_int),
                        StringProp(name="City and Country",
                                   value=city_country),
                        StringProp(name="Server", value=server),
                        StringProp(name="ASN Name", value=asn),
                        UriProp(name="Report Link", value=report_url),
                        UriProp(name="Screenshot Link", value=png_url))
        else:
            LOG.info(
                "No Result information found on URL: {0}".format(result_url))
            LOG.debug(result_response.text)

    @staticmethod
    def _verify_for_scan_failed_flag(result_content):
        """ Verify if scan failed """

        result_data = result_content.get('data', None)
        if not result_data:
            return True

        result_data_requests_list = result_data.get('requests', None)
        if not result_data_requests_list:
            return True

        # get first element from the list
        requests_first_el = result_data_requests_list[0]
        if not requests_first_el:
            return True

        response = requests_first_el.get('response', None)
        if not response or 'failed' in response:
            return True

        return False

    @staticmethod
    def _prepare_city_contry(*argv):
        """
        Prepare a list of non None value or blank "Falsy" parameters.
        :param *argv - city, country
        :return: list
        """
        city_country_list = [el for el in argv if el]
        return city_country_list
示例#6
0
class MISPThreatSearcher(BaseComponent):
    """
    Custom threat lookup for MISP

    """
    channel = searcher_channel("misp")

    def __init__(self, opts):
        super(MISPThreatSearcher, self).__init__(opts)

        # misp_url is the base URL for the MISP server (e.g. 'https://misp.example.com:883/')
        self.misp_url = opts.get(CONFIG_SECTION, {}).get("misp_url")
        if not self.misp_url:
            exc = "Configuration value `misp_url=XXX` is missing in [{}] section".format(
                CONFIG_SECTION)
            raise Exception(exc)

        # misp_link_url is the base URL for hyperlinks - default to same as 'misp_url'
        self.misp_link_url = opts.get(CONFIG_SECTION,
                                      {}).get("misp_link_url", self.misp_url)

        # Authentication key for API access, e.g.: 3PhYSFDeC8xpqjC0ZrFZDwazoSRDUQ1j4IlKbu0G
        # (get this from MISP: Event Actions -> Automation)
        self.misp_key = opts.get(CONFIG_SECTION, {}).get("misp_key")
        if not self.misp_key:
            exc = "Configuration value `misp_key=XXX` is missing in [{}] section".format(
                CONFIG_SECTION)
            raise Exception(exc)

        # verify the MISP server HTTPS certificate?
        self.misp_verifycert = opts.get(CONFIG_SECTION,
                                        {}).get("misp_verifycert", True)

        # Optionally, filter on:
        # tags=one,two
        self.misp_tag = opts.get(CONFIG_SECTION, {}).get("misp_tag")
        if self.misp_tag:
            self.misp_tag = self.misp_tag.split(",")
        # org=one,two
        self.misp_org = opts.get(CONFIG_SECTION, {}).get("misp_org")
        if self.misp_org:
            self.misp_org = self.misp_org.split(",")

    @handler()
    def _lookup_artifact(self, event, *args, **kwargs):
        """Lookup an artifact"""

        # This is a generic handler - we only care about lookup events but might be sent others
        if not isinstance(event, ThreatServiceLookupEvent):
            return

        # event.artifact is a ThreatServiceArtifactDTO
        artifact_type = event.artifact['type']
        artifact_value = event.artifact['value']

        # Check that the event matches an artifact type that we want to search in MISP
        if artifact_type not in MISP_TYPES:
            # Nothing to do
            LOG.info(u"MISP lookup not implemented for %s", artifact_type)
            return

        # Doc says: MISP search for IP addresses using CIDR, uses '|' (pipe) instead of '/' (slashes) in the value.
        # But this appears not to be the case, we just search for the unchanged value.
        # if artifact_type == "net.cidr":
        #     artifact_value = str(artifact_value).replace('/', '|')

        LOG.info("MISP Lookup: " + str(artifact_value))

        misp_api = PyMISP(self.misp_url, self.misp_key, self.misp_verifycert,
                          'json')

        # MISP search_index: produces a list of events, and their key attributes, based on search criteria.
        # But does not filter by attribute type (only by the value that matched),
        # and the value matches include substrings, so we can't filter the results effectively.
        # matches = misp_api.search_index(published=1,
        #                                 tag=self.misp_tag,
        #                                 org=self.misp_org,
        #                                 attribute=str(artifact_value))

        # MISP search: produce events or attribute values, but only for a single type of attribute (or all types).
        # Attribute value matches partial strings (LIKE %value%), but we usually want exact-match searching.
        # Our search strategy is this:
        # - search for each type, returning only attributes;
        # - filter the attribute results for case-insensitive exact matches only,
        # - collect the list of events that the attributes belong to,
        # - finally retrieve the event metadata for our results.
        #
        # (This strategy will likely change with future versions of MISP!)

        misp_types = MISP_TYPES[artifact_type]
        search_value = str(artifact_value).lower()
        event_ids = set()

        def matches(an_attribute):
            """Does the attribute value match?  MISP does substring searches but we want exact."""
            if artifact_type == "net.cidr":
                # Match any CIDR, we assume the server did the logical search
                return True
            if an_attribute["value"].lower() == search_value:
                # Match the whole attribute value, lowercase.
                return True
            return False

        for misp_type in misp_types:
            # Search for this one attribute-type
            result = misp_api.search(controller='attributes',
                                     values=[search_value],
                                     type_attribute=misp_type,
                                     tags=self.misp_tag,
                                     org=self.misp_org,
                                     withAttachments=0)
            LOG.debug("Search for %s", misp_type)
            LOG.debug(json.dumps(result))
            if result:
                response = result.get("response", {})
                if isinstance(response, dict):
                    attributes = response.get("Attribute", [])
                    for attribute in attributes:
                        if matches(attribute):
                            event_ids.add(attribute["event_id"])

        hits = []
        if not event_ids:
            # Nothing to do
            return hits

        # Finally let's get summary for each event.
        # (Don't use the search api for this, it will match partial event ids).
        for event_id in event_ids:
            result = misp_api.get_event(event_id)
            if "Event" in result:
                event = result["Event"]
                event_id = event["id"]
                link = self.misp_link_url + "/events/view/" + str(event_id)
                info = event.get("info")
                datestr = event.get("date")
                hit = Hit(
                    StringProp(name="Info", value=info),
                    StringProp(name="Date", value=datestr),
                    UriProp(name="MISP Link", value=link),
                )
                # Add all the tags as separate properties
                for tag in event.get("Tag"):
                    tag_name, tag_value = tag["name"].split(":", 1)
                    hit.append(
                        StringProp(name="{}:".format(tag_name),
                                   value=tag_value))
                hits.append(hit)

        return hits
示例#7
0
class McAfeeTieSearcher(BaseComponent):
    """
    McAfee TIE custom threat searcher component

    Test using 'curl':
        curl -v -X OPTIONS 'http://127.0.0.1:9000/cts/mcafee_tie_searcher'
        curl -v -k --header "Content-Type: application/json" --data-binary '{"type":"hash.md5",
        "value":"1CF5B6CC0E6B742F6DEF8BF96D847A25"}' 'http://127.0.0.1:9000/cts/mcafee_tie_searcher'
        curl -v -k --header "Content-Type: application/json" --data-binary '{"type":"hash.sha1",
        "value":"19A82049C4336E8A5A30426FEC3F560358FAB6ED"}' 'http://127.0.0.1:9000/cts/mcafee_tie_searcher'
        curl -v -k --header "Content-Type: application/json" --data-binary '{"type":"hash.sha256",
        "value":"A8B03AD33BC6D7A7F376B943F763EEDD1CDAF125D012F9018F2F56678AE67EA4"}'
        'http://127.0.0.1:9000/cts/mcafee_tie_searcher'

    """

    # Register this as an async searcher for the URL /<root>/mcafee_tie_searcher
    channel = searcher_channel("mcafee_tie_searcher")

    config_file = "dxlclient_config"

    def __init__(self, opts):
        super(McAfeeTieSearcher, self).__init__(opts)
        
        try:
            config = opts.get("mcafee").get(self.config_file)
            if config is None:
                LOG.error(self.config_file + " is not set. You must set this path to run this threat service")
                raise ValueError(self.config_file + " is not set. You must set this path to run this threat service")

            # Create configuration from file for DxlClient
            self.config = DxlClientConfig.create_dxl_config_from_file(config)
        except AttributeError:
            LOG.error("There is no [mcafee] section in the config file,"
                      "please set that by running resilient-circuits config -u")
            raise AttributeError("[mcafee] section is not set in the config file")

        # Create client
        self.client = DxlClient(self.config)
        self.client.connect()

    # Handle lookup for artifacts of type md5, sha1, and sha256 hashes
    @handler("hash.md5", "hash.sha1", "hash.sha256")
    def _lookup_hash(self, event, *args, **kwargs):
        artifact_type = event.artifact["type"]
        artifact_value = event.artifact["value"]
        LOG.debug("_lookup_hash started for Artifact Type {0} - Artifact Value {1}".format(
            artifact_type, artifact_value))

        tie_client = TieClient(self.client)

        if artifact_type == "hash.md5":
            resilient_hash = {HashType.MD5: artifact_value}
        elif artifact_type == "hash.sha1":
            resilient_hash = {HashType.SHA1: artifact_value}
        elif artifact_type == "hash.sha256":
            resilient_hash = {HashType.SHA256: artifact_value}
        else:
            raise ValueError("Something went wrong setting the hash value")

        reputations_dict = \
            tie_client.get_file_reputation(
                    resilient_hash
            )

        hits = self._query_mcafee_tie(reputations_dict)

        yield hits

    def _query_mcafee_tie(self, reputations_dict):
        hit = Hit()

        # Check Enterprise File Provider
        hit = self._get_enterprise_info(reputations_dict, hit)

        # Check GTI File Provider
        hit = self._get_gti_info(reputations_dict, hit)

        # Check ATD File Provider
        hit = self._get_atd_info(reputations_dict, hit)

        # Check MWG File Provider
        hit = self._get_mwg_info(reputations_dict, hit)

        # Verifies a trust level was set before returning a hit
        for prop in hit["props"]:
            if fnmatch(prop["name"], "*Trust Level"):
                return hit
        return []

    def _get_enterprise_info(self, reputations_dict, hit):
        # Information for Enterprise file provider
        if FileProvider.ENTERPRISE in reputations_dict:
            ent_rep = reputations_dict[FileProvider.ENTERPRISE]
            trust_level = self._get_trust_level(ent_rep[ReputationProp.TRUST_LEVEL])

            if trust_level:
                # Not a hit until trust level has been verified to less than or equal to MIGHT BE MALICIOUS
                hit.append(StringProp(name="Enterprise Trust Level", value=trust_level))

            # Retrieve the enterprise reputation attributes
            ent_rep_attribs = ent_rep[ReputationProp.ATTRIBUTES]

            # Get Average Local Rep
            if FileEnterpriseAttrib.AVG_LOCAL_REP in ent_rep_attribs:
                local_rep = self._get_trust_level(int(ent_rep_attribs[FileEnterpriseAttrib.AVG_LOCAL_REP]))
                if local_rep:
                    hit.append(StringProp(name="Enterprise Avg Local Trust Level", value=local_rep))

            # Get prevalence (if it exists)
            if FileEnterpriseAttrib.PREVALENCE in ent_rep_attribs:
                hit.append(StringProp(name="Enterprise Prevalence",
                                      value=ent_rep_attribs[FileEnterpriseAttrib.PREVALENCE]))

            # Get Enterprise Size (if it exists)
            if FileEnterpriseAttrib.ENTERPRISE_SIZE in ent_rep_attribs:
                hit.append(StringProp(name="Enterprise Size",
                                      value=ent_rep_attribs[FileEnterpriseAttrib.ENTERPRISE_SIZE]))

            # Get First Contact Date (if it exists)
            if FileEnterpriseAttrib.FIRST_CONTACT in ent_rep_attribs:
                hit.append(StringProp(name="Enterprise First Contact",
                                      value=FileEnterpriseAttrib.to_localtime_string(
                                          ent_rep_attribs[FileEnterpriseAttrib.FIRST_CONTACT])))

        return hit

    def _get_gti_info(self, reputations_dict, hit):
        # Information for GTI file provider
        if FileProvider.GTI in reputations_dict:
            gti_rep = reputations_dict[FileProvider.GTI]
            trust_level = self._get_trust_level(gti_rep[ReputationProp.TRUST_LEVEL])

            if trust_level:
                # Not a hit until trust level has been verified to less than or equal to MIGHT BE MALICIOUS
                hit.append(StringProp(name="GTI Trust Level", value=trust_level))

            # Retrieve the GTI reputation attributes
            gti_rep_attribs = gti_rep[ReputationProp.ATTRIBUTES]

            # Get prevalence (if it exists)
            if FileGtiAttrib.PREVALENCE in gti_rep_attribs:
                hit.append(StringProp(name="GTI Prevalence", value=gti_rep_attribs[FileGtiAttrib.PREVALENCE]))

            # Get First Contact Date (if it exists)
            if FileGtiAttrib.FIRST_CONTACT in gti_rep_attribs:
                hit.append(StringProp(name="GTI First Contact", value=EpochMixin.to_localtime_string(
                    gti_rep_attribs[FileGtiAttrib.FIRST_CONTACT])))

        return hit

    def _get_atd_info(self, reputations_dict, hit):
        # Information for Advanced Threat Defense file provider
        if FileProvider.ATD in reputations_dict:
            atd_rep = reputations_dict[FileProvider.ATD]
            trust_level = self._get_trust_level(atd_rep[ReputationProp.TRUST_LEVEL])

            if trust_level:
                # Not a hit until trust level has been verified to less than or equal to MIGHT BE MALICIOUS
                hit.append(StringProp(name="ATD Trust Level", value=trust_level))

        return hit

    def _get_mwg_info(self, reputations_dict, hit):
        # Information for  file provider
        if FileProvider.MWG in reputations_dict:
            mwg_rep = reputations_dict[FileProvider.MWG]
            trust_level = self._get_trust_level(mwg_rep[ReputationProp.TRUST_LEVEL])

            if trust_level:
                # Not a hit until trust level has been verified to less than or equal to MIGHT BE MALICIOUS
                hit.append(StringProp(name="MWG Trust Level", value=trust_level))

        return hit

    @staticmethod
    def _get_trust_level(trust_level_number):
        trust_level = ""
        if TrustLevel.MIGHT_BE_MALICIOUS is trust_level_number:
            trust_level = "Might be Malicious"
        elif TrustLevel.MOST_LIKELY_MALICIOUS is trust_level_number:
            trust_level = "Most Likely Malicious"
        elif TrustLevel.KNOWN_MALICIOUS is trust_level_number:
            trust_level = "Known Malicious"

        return trust_level
class HaveIBeenPwnedSearcher(BaseComponent):
    """
    Have I been Pwned custom threat searcher component

    Test using 'curl':
        curl -v -X OPTIONS 'http://127.0.0.1:9000/cts/have_i_been_pwned_threat_service'
        curl -v -k --header "Content-Type: application/json" --data-binary '{"type":"email.header",
        "value":"*****@*****.**"}' 'http://127.0.0.1:9000/cts/have_i_been_pwned_threat_service'
        curl -v -k --header "Content-Type: application/json" --data-binary '{"type":"email.header.to",
        "value":"*****@*****.**"}' 'http://127.0.0.1:9000/cts/have_i_been_pwned_threat_service'

    """

    HAVE_I_BEEN_PWNED_URL = "https://haveibeenpwned.com/api/v2"

    def __init__(self, opts):
        super(HaveIBeenPwnedSearcher, self).__init__(opts)
        LOG.debug(opts)

    # Register this as an async searcher for the URL /<root>/example
    channel = searcher_channel("have_i_been_pwned_threat_service")

    # Handle lookups for artifacts of type 'email sender' and 'email receiver
    @handler("email.header", "email.header.to")
    def _lookup_email_sender(self, event, *args, **kwargs):
        # event.artifact is a ThreatServiceArtifactDTO
        artifact_type = event.artifact['type']
        artifact_value = event.artifact['value']
        LOG.debug("_lookup_email_sender started for Artifact Type {0} - Artifact Value {1}".format(
            artifact_type, artifact_value))

        hits = self._query_hibp_api(artifact_value)

        yield hits

    def _query_hibp_api(self, artifact_value):
        hits = []
        retry = True
        while(retry):
            try:
                # Return zero or more hits.  Here's one example.
                breach_url = "{0}/breachedaccount/{1}".format(self.HAVE_I_BEEN_PWNED_URL, artifact_value)
                breaches_response = requests.get(breach_url, headers={'User-Agent': 'Resilient HIBP CTS'})

                paste_url = "{0}/pasteaccount/{1}".format(self.HAVE_I_BEEN_PWNED_URL, artifact_value)
                pastes_response = requests.get(paste_url, headers={'User-Agent': 'Resilient HIBP CTS'})

                if breaches_response.status_code == 200 and pastes_response.status_code == 200:
                    b_content = breaches_response.json()
                    if b_content is None:
                       b_content = []
                    p_content = pastes_response.json()
                    if p_content is None:
                        p_content = []

                    hits.append(
                        Hit(
                            NumberProp(name="Breached Sites", value=len(b_content)),
                            NumberProp(name="Pastes", value=len(p_content))
                        )
                    )
                    retry = False

                # 404 is returned when an email was not found
                elif breaches_response.status_code == 404 or pastes_response.status_code == 404:
                    LOG.info("No hit information found on email address: {0}".format(artifact_value))
                    retry = False
                elif breaches_response.status_code == 429 or pastes_response.status_code == 429:
                    # Rate limit was hit, wait 2 seconds and try again
                    time.sleep(2)
                else:
                    LOG.warn("Have I Been pwned returned expected status code")
                    retry = False
                    raise ThreatLookupIncompleteException()
            except BaseException as e:
                LOG.exception(e.message)
            return hits
示例#9
0
    def _handle_post_request(self, event, *args, **kwargs):
        """
        Responds to POST /cts/<anything>

        The URL below /cts/ is specific to this threat service. For example,
        /cts/one and /cts/two can be registered as two separate threat sources.
        The string 'one' or 'two' becomes the channel that searcher events are dispatched on.

        Request is a ThreatServiceArtifactDTO containing the artifact to be scanned
        Response is a ResponseDTO containing the response, or 'please retry' (HTTP status 303).
        """
        request = event.args[0]
        response = event.args[1]

        # The channels that searchers are listening for events
        cts_channel = searcher_channel(*args)

        value = request.body.getvalue()

        if not value:
            err = "Empty request"
            LOG.warn(err)
            return {"id": str(uuid4()), "hits": []}

        # Resilient sends artifacts in two formats: multi-part MIME, or plain JSON.
        # server may send either, even for cases where there is no file content,
        # so check content-type and decode appropriately.
        try:
            if request.headers and "form-data" in request.headers.get(
                    "Content-Type", ""):
                multipart_data = decoder.MultipartDecoder(
                    value, request.headers["Content-Type"])
                body = json.loads(multipart_data.parts[0].text)
                LOG.debug(body)
            else:
                body = json.loads(value.decode("utf-8"))
                LOG.debug(body)
        except (ValueError, NonMultipartContentTypeException) as e:
            err = "Can't handle request: {}".format(e)
            LOG.warn(err)
            LOG.debug(value)
            return {"id": str(uuid4()), "hits": []}

        if not isinstance(body, dict):
            # Valid JSON but not a valid request.
            err = "Invalid request: {}".format(json.dumps(body))
            LOG.warn(err)
            return {"id": str(uuid4()), "hits": []}

        # Generate a request ID, derived from the artifact being requested.
        request_id = str(uuid5(self.namespace, json.dumps(body)))
        artifact_type = body.get("type", "unknown")
        artifact_value = body.get("value")
        response_object = {"id": request_id, "hits": []}
        cache_key = (cts_channel, request_id)

        if artifact_type == "net.name" and artifact_value == "localhost":
            # Hard-coded response to 'net.name' of 'localhost'
            # because this is used in 'resutil threatservicetest'
            # and we want to return an immediate (not async) response
            return response_object

        # If we already have a completed query for this key, return it immmediately
        request_data = self.cache.get(cache_key)
        if request_data and request_data.get("complete"):
            response_object["hits"] = request_data.get("hits", [])
            return response_object

        response.status = 303
        response_object["retry_secs"] = self.first_retry_secs

        # Add the request to the cache, then notify searchers that there's a new request
        self.cache.setdefault(cache_key, {
            "id": request_id,
            "artifact": body,
            "hits": [],
            "complete": False
        })
        evt = ThreatServiceLookupEvent(request_id=request_id,
                                       name=artifact_type,
                                       artifact=body,
                                       channel=cts_channel)
        self.async_helper.fire(evt, HELPER_CHANNEL)

        return response_object
示例#10
0
class AbuseIPDBThreatFeedSearcher(ResilientComponent):
    """
    custom threat searcher component for abuseipdb
    """
    def __init__(self, opts):
        super(AbuseIPDBThreatFeedSearcher, self).__init__(opts)

        self.opts = opts
        self.options = opts.get("abuseipdb_cts", {})
        LOG.debug(opts)
        # check https://www.abuseipdb.com/categories for more information
        self.abuseipdb_categories = {
            3: "Fraud Orders",
            4: "DDoS Attack",
            5: "FTP Brute-Force",
            6: "Ping of Death",
            7: "Phishing",
            8: "Fraud VoIP",
            9: "Open Proxy",
            10: "Web Spam",
            11: "Email Spam",
            12: "Blog Spam",
            13: "VPN IP",
            14: "Port Scan",
            15: "Hacking",
            16: "SQL Injection",
            17: "Spoofing",
            18: "Brute-Force",
            19: "Bad Web Bot",
            20: "Exploited Host",
            21: "Web App Attack",
            22: "SSH",
            23: "IoT Targeted",
        }

    # Register this as an async searcher for the URL /<root>/example
    channel = searcher_channel("abuseipdb_threat_feed")

    @handler("net.ip")
    def _lookup_net_ip(self, event, *args, **kwargs):
        """Lookup an artifact"""

        # This is a generic handler - we only care about lookup events but might be sent others
        if not isinstance(event, ThreatServiceLookupEvent):
            return

        # event.artifact is a ThreatServiceArtifactDTO
        artifact_type = event.artifact['type']
        artifact_value = event.artifact['value']

        validate_fields(("abuseipdb_key", "abuseipdb_url"), self.options)

        LOG.info(
            "AbuseIPDB lookup started for Artifact Type {0} - Artifact Value {1}"
            .format(artifact_type, artifact_value))

        rc = RequestsCommon(self.opts, self.options)
        hits = self._query_abuseipdb(rc, artifact_value)

        yield hits

    def _query_abuseipdb(self, rc, artifact_value):
        """
        perform the query to abuseipdb
        :param rc: resilient-lib object
        :param artifact_value:
        :return: hits object
        """

        hits = []

        try:
            headers = HEADER_TEMPLATE.copy()
            headers['Key'] = self.options.get("abuseipdb_key")

            url = self.options.get("abuseipdb_url")
            params = {
                'ipAddress': artifact_value,
                'isWhitelisted': self.options.get("ignore_white_listed",
                                                  "True"),
                'verbose': True
            }

            response = rc.execute_call_v2("get",
                                          url,
                                          params=params,
                                          headers=headers)
            LOG.debug(response.json())

            resp_data = response.json()['data']
            number_of_reports = resp_data['totalReports']
            country = resp_data['countryName']
            most_recent_report = resp_data['lastReportedAt']
            confidence_score = resp_data.get("abuseConfidenceScore", 0)

            # get clean list of de-duped categories
            categories_names = ""
            if resp_data.get('reports'):
                categories_list = []
                for report in resp_data['reports']:
                    categories_list.extend(report["categories"])
                categories_set = set(categories_list)  # dedup list
                categories_names = u', '.join(
                    (self.abuseipdb_categories.get(item, 'unknown')
                     for item in categories_set))

            # only return data if there's anything useful
            if number_of_reports or confidence_score:
                # Return zero or more hits.  Here's one example.
                hits.append(
                    Hit(
                        NumberProp(name="Confidence Score",
                                   value=confidence_score),
                        NumberProp(name="Number of Reports",
                                   value=number_of_reports),
                        StringProp(name="Country", value=country),
                        StringProp(name="Most Recent Report",
                                   value=most_recent_report),
                        StringProp(name="Categories", value=categories_names)))

        except Exception as err:
            LOG.exception(str(err))
        return hits
class ShadowServerThreatFeedSearcher(ResilientComponent):
    """
    Example of a custom threat searcher component
    """
    def __init__(self, opts):
        super(ShadowServerThreatFeedSearcher, self).__init__(opts)

        self.options = opts.get("shadow_server_url", {})
        self.allowed_artifacts = {"hash.md5": "md5", "hash.sha1": "sha1"}

        self.data_to_ignore = ["md5", "sha1", "sha256", "sha512"]

    # Register this as an async searcher for the URL /<root>/example
    channel = searcher_channel("shadow_server_threat_feed")

    @handler()
    def _lookup_hash_shadow_server(self, event, *args, **kwargs):
        """Lookup an artifact"""

        # This is a generic handler - we only care about lookup events but might be sent others
        if not isinstance(event, ThreatServiceLookupEvent):
            return

        # event.artifact is a ThreatServiceArtifactDTO
        artifact_type = event.artifact['type']
        artifact_value = event.artifact['value']

        # Check that the event matches an artifact type that we want to search in Shadow Server
        if artifact_type not in self.allowed_artifacts:
            # Nothing to do
            LOG.info("Shadow Server lookup not implemented for {0}".format(
                artifact_type))
            return

        LOG.info(
            "Shadow Server lookup started for Artifact Type {0} - Artifact Value {1}"
            .format(artifact_type, artifact_value))

        hits = self._query_shadow_server(artifact_type, artifact_value)

        yield hits

    def _query_shadow_server(self, artifact_type, artifact_value):
        hits = []
        try:
            url = "{0}?{1}={2}".format(
                self.options.get("shadow_server_url",
                                 "http://bin-test.shadowserver.org/api"),
                self.allowed_artifacts.get(artifact_type), artifact_value)
            LOG.debug("Getting info from {0}".format(url))
            response = requests.get(url)
            if response.status_code == 200:
                LOG.debug(response.text)

                # return hash {...} for found result or just hash
                if not response.text.strip() == artifact_value:
                    resp_json = json.loads(
                        response.text.replace(artifact_value, "", 1))
                    hit = Hit()

                    for attribute, value in resp_json.items():
                        if attribute not in self.data_to_ignore:
                            hit.append(StringProp(name=attribute, value=value))

                    # Return zero or more hits.  Here's one example.
                    hits.append(hit)

            else:
                LOG.warn("Got response status {0} from Shadow Server".format(
                    response.status_code))

        except BaseException as e:
            LOG.exception(str(e))
        return hits
class YetiThreatFeedSearcher(BaseComponent):
    """
       YETI custom threat searcher component

       Test using 'curl':
           curl -v -X OPTIONS 'http://127.0.0.1:9000/cts/yeti_threat_service'
           curl -v -k --header "Content-Type: application/json" --data-binary '{"type":"net.ip",
           "value":"11.11.11.11"}' 'http://127.0.0.1:9000/cts/yeti_threat_service'

    """
    channel = searcher_channel("yeti_threat_service")

    def __init__(self, opts):
        super(YetiThreatFeedSearcher, self).__init__(opts)

        self.options = opts.get(CONFIG_SECTION)
        url = self.options.get("urlbase")
        username = self.options.get("username")
        password = self.options.get("password")
        api_key = self.options.get("api_key")

        self.yeti_client = pyeti.YetiApi(url, (username, password), api_key)

    @handler()
    def lookup_artifact(self, event, *args, **kwargs):
        """
        Use YETI to search for artifact value

        """

        if not isinstance(event, ThreatServiceLookupEvent):
            return

        hits = []
        LOG.info("Querying YETI")

        artifact = event.artifact
        artifact_value = artifact['value']
        LOG.info(artifact)
        LOG.info("Looking up ({art_type}): {art_value}".format(
            art_type=artifact['type'], art_value=artifact_value))

        try:
            # init new indicators object
            indicators = self.yeti_client.observable_search(
                regex=False, value=artifact_value)
            LOG.debug(indicators)
        except ValueError as e:
            LOG.error(traceback.format_exc())
            raise e

        if not indicators or len(indicators) < 1:
            return hits

        tags = ""
        for tag in indicators[0]["tags"]:
            if tags != "":
                tags += ", "
            tags += tag["name"]

        description = indicators[0]["description"] if indicators[0][
            "description"] else "None"
        try:
            hits.append(
                Hit(StringProp(name="Type", value=indicators[0]["type"]),
                    StringProp(name="Value", value=indicators[0]["value"]),
                    StringProp(name="Tags", value=tags),
                    StringProp(name="Created", value=indicators[0]["created"]),
                    UriProp(name="URL", value=indicators[0]["human_url"]),
                    StringProp(name="Description", value=description)))
            return hits
        except Exception as e:
            LOG.error(traceback.format_exc())
            raise e
示例#13
0
class PassiveTotalSearcher(BaseComponent):
    """
    A custom threat service 'searcher' for Passive Total

    Test using 'curl':
        curl -v -k --header "Content-Type: application/json" --data-binary '{"type":"net.uri","value":"http://example.org"}' 'http://127.0.0.1:9000/cts/pst'
        curl -v 'http://127.0.0.1:9000/cts/example/f9acc1b7-6184-5746-873e-e385e6214261'

    """

    CONFIG_SECTION = "passivetotal"

    def __init__(self, opts):
        super(PassiveTotalSearcher, self).__init__(opts)
        self.options = opts.get(self.CONFIG_SECTION, {})

    # Register this as an async searcher for the URL /<root>/example
    channel = searcher_channel("pst")

    @handler("net.name", "net.uri", "net.ip")
    def _lookup(self, event, *args, **kwargs):
        """
        Handle lookups for artifacts of type 'net.name' (domain name artifact), 'net.uri' (URL) or 'net.ip' (IP address)
        """

        # Read configuration settings:

        self.passivetotal_api_key = self._get_value_from_options(
            "passivetotal_api_key")
        self.passivetotal_username = self._get_value_from_options(
            "passivetotal_username")
        self.passivetotal_base_url = self._get_value_from_options(
            "passivetotal_base_url")
        self.passivetotal_account_api_url = self._get_value_from_options(
            "passivetotal_account_api_url")
        self.passivetotal_actions_tags_api_url = self._get_value_from_options(
            "passivetotal_actions_tags_api_url")
        self.passivetotal_passive_dns_api_url = self._get_value_from_options(
            "passivetotal_passive_dns_api_url")
        self.passivetotal_actions_class_api_url = self._get_value_from_options(
            "passivetotal_actions_class_api_url")
        self.passivetotal_enrich_subdom_api_url = self._get_value_from_options(
            "passivetotal_enrich_subdom_api_url")
        self.passivetotal_community_url = self._get_value_from_options(
            "passivetotal_community_url")
        self.passivetotal_tags = self._get_value_from_options(
            "passivetotal_tags")

        # event.artifact is a ThreatServiceArtifactDTO
        artifact_type = event.artifact['type']
        artifact_value = event.artifact['value']
        LOG.info("_lookup started for Artifact Type {0} - Artifact Value {1}".
                 format(artifact_type, artifact_value))

        hits = self._query_passivetotal_api(artifact_value)
        yield hits

    def _get_value_from_options(self, app_config_setting_key):
        """
        Get value from options dict or raise ValueError for the mandatory config setting.
        :param app_config_setting_key key
        """
        if app_config_setting_key in self.options:
            return self.options[app_config_setting_key]
        else:
            error_msg = "Mandatory config setting '{}' not set.".format(
                app_config_setting_key)
            LOG.error(error_msg)
            raise ValueError(error_msg)

    def _query_passivetotal_api(self, artifact_value):
        """
        Validate user account if API call quota has exceeded, verify if tags are match, query RiskIQ PassiveTotal API
         for the given 'net.name' (domain name artifact), 'net.uri' (URL) or 'net.ip' (IP address) and generate Hits.
        :param artifact_value
        """
        hits = []

        if self._validate_user_account_exceeded():
            return hits

        validate_tags_match, tags_hits = self._validate_tag_match(
            artifact_value)
        if not validate_tags_match:
            # failure condition if the site doesn't match your definition
            LOG.info(
                "The site isn't currently listed as compromised according to your definition."
            )
            return hits

        LOG.info("Positive Threat Intel for %s", artifact_value)

        # Create the hits array to sent back to Resilient
        hits.append(self._generate_hit(artifact_value, tags_hits))

        return hits

    def _validate_user_account_exceeded(self):
        """
        Validate if user's API call quota has exceeded.
        Return True if user may not proceed.
        """

        account_metadata_response = self._passivetotal_get_response(
            self.passivetotal_account_api_url, '')
        if account_metadata_response.status_code == 200:
            account = account_metadata_response.json()
        else:
            LOG.info("No Account information found for username: {0}".format(
                self.passivetotal_username))
            LOG.debug(account_metadata_response.text)
            return True

        account_quota_exceeded = account.get("searchApiQuotaExceeded", None)
        if account_quota_exceeded:
            LOG.info("Your PassiveTotal Account has no API queries left.")
            LOG.debug(account_metadata_response)
            return True

        return False

    def _validate_tag_match(self, artifact_value):
        """
        Validate if returned PassiveTotal tag hits for given artifact_value include user's flagged tags from app.config file.
        Return True if tags intersect, there is a match. PT returns tags users has flagged.
        :param artifact_value
        """

        tags_response = self._passivetotal_get_response(
            self.passivetotal_actions_tags_api_url, artifact_value)
        if tags_response.status_code == 200:
            tags = tags_response.json()
        else:
            LOG.info("No Tag information found for artifact value: {0}".format(
                artifact_value))
            LOG.debug(tags_response.text)
            return False, None

        tags_hits = tags.get('tags', None)

        LOG.info("Comparing tags that result with a hit " + str(tags_hits) +
                 " with flagged tags in app.config file " +
                 self.passivetotal_tags)

        # Tests the site has tags you have flagged
        passive_tag_set = set(item.lower().strip()
                              for item in self.passivetotal_tags.split(","))
        tags_hit_set = set(item.lower() for item in tags_hits)

        return bool(passive_tag_set.intersection(tags_hit_set)), tags_hits

    def _passivetotal_get_response(self, path, query):
        """
        Get response from the given API for the given query.
        :param path PT API url
        :param query the domain or IP being queried
        """
        url = self.passivetotal_base_url + path
        data = {'query': query}
        auth = (self.passivetotal_username, self.passivetotal_api_key)

        response = requests.get(url, auth=auth, json=data)
        return response

    def _generate_hit(self, artifact_value, tags_hits_list):
        """
        Query RiskIQ PassiveTotal API for the given 'net.name' (domain name artifact), 'net.uri' (URL) or 'net.ip'
        (IP address) and generate a Hit.
        :param artifact_value
        :param tags_hits_list
        """
        # Passive DNS Results - Hits
        # We grab the totalRecords number and show the First Seen date to Last Seen date interval
        pdns_results_response = self._passivetotal_get_response(
            self.passivetotal_passive_dns_api_url, artifact_value)
        pdns_hit_number, pdns_first_seen, pdns_last_seen = None, None, None
        if pdns_results_response.status_code == 200:
            pdns_results = pdns_results_response.json()
            pdns_hit_number = pdns_results.get("totalRecords", None)
            pdns_first_seen = pdns_results.get("firstSeen", None)
            pdns_last_seen = pdns_results.get("lastSeen", None)
            LOG.info(pdns_hit_number)
            LOG.info(pdns_first_seen)
            LOG.info(pdns_last_seen)
        else:
            LOG.info(
                "No Passive DNS information found for artifact value: {0}".
                format(artifact_value))
            LOG.debug(pdns_results_response.text)

        # URL Classification - suspicious, malicious etc
        classification_results_response = self._passivetotal_get_response(
            self.passivetotal_actions_class_api_url, artifact_value)
        classification_hit = None
        if classification_results_response.status_code == 200:
            classification_results = classification_results_response.json()
            classification_hit = classification_results.get(
                "classification", None)
            LOG.info(classification_hit)
        else:
            LOG.info(
                "No URL classification found for artifact value: {0}".format(
                    artifact_value))
            LOG.debug(classification_results_response.text)

        # Count of subdomains
        subdomain_results_response = self._passivetotal_get_response(
            self.passivetotal_enrich_subdom_api_url, artifact_value)
        subdomain_hits_number, first_ten_subdomains = None, None
        if subdomain_results_response.status_code == 200:
            subdomain_results = subdomain_results_response.json()
            subdomain_hits = subdomain_results.get("subdomains", None)
            subdomain_hits_number = len(
                subdomain_hits) if subdomain_hits else None
            first_ten_subdomains = ', '.join(
                subdomain_hits[:10]) if subdomain_hits else None
            LOG.info(subdomain_hits_number)
            LOG.info(first_ten_subdomains)
        else:
            LOG.info("No subdomain information found for artifact value: {0}".
                     format(artifact_value))
            LOG.debug(subdomain_results_response.text)

        # Convert tags hits list to str
        tags_hits = ", ".join(tags_hits_list) if tags_hits_list else None

        # Construct url back to to PassiveThreat
        report_url = self.passivetotal_community_url + artifact_value

        return Hit(
            NumberProp(name="Number of Passive DNS Records",
                       value=pdns_hit_number),
            StringProp(name="First Seen", value=pdns_first_seen),
            StringProp(name="Last Seen", value=pdns_last_seen),
            NumberProp(name="Subdomains - All", value=subdomain_hits_number),
            StringProp(name="Subdomains - First ten Hostnames",
                       value=first_ten_subdomains),
            StringProp(name="Tags", value=tags_hits),
            StringProp(name="Classification", value=classification_hit),
            UriProp(name="Report Link", value=report_url))
class AbuseIPDBThreatFeedSearcher(ResilientComponent):
    """
    Example of a custom threat searcher component
    """
    def __init__(self, opts):
        super(AbuseIPDBThreatFeedSearcher, self).__init__(opts)

        self.options = opts.get("abuseipdb_cts", {})
        LOG.debug(opts)
        """check https://www.abuseipdb.com/categories for more information"""
        self.abuseipdb_categories = {
            3: "Fraud Orders",
            4: "DDoS Attack",
            5: "FTP Brute-Force",
            6: "Ping of Death",
            7: "Phishing",
            8: "Fraud VoIP",
            9: "Open Proxy",
            10: "Web Spam",
            11: "Email Spam",
            12: "Blog Spam",
            13: "VPN IP",
            14: "Port Scan",
            15: "Hacking",
            16: "SQL Injection",
            17: "Spoofing",
            18: "Brute-Force",
            19: "Bad Web Bot",
            20: "Exploited Host",
            21: "Web App Attack",
            22: "SSH",
            23: "IoT Targeted",
        }

    # Register this as an async searcher for the URL /<root>/example
    channel = searcher_channel("abuseipdb_threat_feed")

    @handler("net.ip")
    def _lookup_net_ip(self, event, *args, **kwargs):
        """Lookup an artifact"""

        # This is a generic handler - we only care about lookup events but might be sent others
        if not isinstance(event, ThreatServiceLookupEvent):
            return

        # event.artifact is a ThreatServiceArtifactDTO
        artifact_type = event.artifact['type']
        artifact_value = event.artifact['value']

        if not self.options.get("abuseipdb_key"):
            LOG.error(
                "AbuseIPDB api key not set. You must set an api key to run this CTS."
            )
            return

        LOG.info(
            "AbuseIPDB lookup started for Artifact Type {0} - Artifact Value {1}"
            .format(artifact_type, artifact_value))
        hits = self._query_abuseipdb(artifact_value)

        yield hits

    def _query_abuseipdb(self, artifact_value):
        hits = []
        ignore_white_listed = True
        if self.options.get("ignore_white_listed", "True").lower() != "true":
            ignore_white_listed = False
        try:
            url = "{0}/{1}/json?key={2}".format(
                self.options.get("abuseipdb_url",
                                 "https://www.abuseipdb.com/check"),
                artifact_value, self.options.get("abuseipdb_key"))

            adapter = requests_toolbelt.SSLAdapter("SSLv23")
            session = requests.Session()
            session.mount('https://', adapter)
            LOG.debug("Getting info from {0}".format(url))
            response = session.get(url)
            if response.status_code == 200:
                resp_json = json.loads(response.text)
                category_list = set([])
                number_of_reports = len(resp_json)
                country = resp_json[0]["country"]
                most_recent_report = resp_json[0]["created"]
                """First we clean list of duplicated categories
                We also check for whitelisted reports. 
                If the option to ignore is set to true, we ignore ALL reports if we found at least one white listed """
                for report in resp_json:
                    if report["isWhitelisted"] and ignore_white_listed:
                        LOG.info(
                            "Ignoring white listed reports for {0}".format(
                                artifact_value))
                        return hits

                    for category in report["category"]:
                        if category != 0:
                            category_list.add(category)
                """Then we build the string with the values"""
                categories_string = ""
                for cat in category_list:
                    if categories_string != "":
                        categories_string += ", "
                    categories_string += self.abuseipdb_categories[cat]

                # Return zero or more hits.  Here's one example.
                hits.append(
                    Hit(
                        NumberProp(name="Number of reports",
                                   value=number_of_reports),
                        StringProp(name="Country", value=country),
                        StringProp(name="Most Recent Report",
                                   value=most_recent_report),
                        StringProp(name="Categories",
                                   value=categories_string)))
            else:
                LOG.warn("Got response status {0} from AbuseIPDB".format(
                    response.status_code))

        except BaseException as e:
            LOG.exception(e.message)
        return hits