import demistomock as demisto # noqa: F401
from CommonServerPython import * # noqa: F401
import tempfile
""" IMPORTS """
import urllib3
import collections
import cabby
import requests
from lxml import etree
import dateutil.parser
from bs4 import BeautifulSoup, Tag
from netaddr import IPNetwork
from six import string_types
# TAXII11 import
import uuid
import dateutil
import pytz
# disable insecure warnings
urllib3.disable_warnings()
EPOCH = datetime.utcfromtimestamp(0).replace(tzinfo=pytz.UTC)
INTEGRATION_NAME = "TAXII1"
class AddressObject:
"""
Implements address object indicator decoding
based on: https://stixproject.github.io/data-model/1.2/AddressObj/AddressObjectType/
"""
@staticmethod
def decode(props, **kwargs):
result: List[dict[str, str]] = []
indicator = props.find("Address_Value")
if indicator is None or indicator.string is None:
return result
indicator = indicator.string.encode("ascii", "replace").decode()
category = props.get("category", None)
address_list = indicator.split("##comma##")
if category == "e-mail":
return [{"indicator": address, "type": "Email"} for address in address_list]
try:
for address in address_list:
ip = IPNetwork(address)
if ip.version == 4:
if len(address.split("/")) > 1:
type_ = "CIDR"
else:
type_ = "IP"
elif ip.version == 6:
if len(address.split("/")) > 1:
type_ = "IPv6CIDR"
else:
type_ = "IPv6"
else:
LOG(f"Unknown ip version: {ip.version!r}")
return []
result.append({"indicator": address, "type": type_})
except Exception:
return result
return result
class DomainNameObject:
"""
Implements domain object indicator decoding
based on: https://stixproject.github.io/data-model/1.2/DomainNameObj/DomainNameObjectType/
"""
@staticmethod
def decode(props, **kwargs):
dtype = props.get("type", "FQDN")
if dtype != "FQDN":
return []
raw_domain = props.find("Value")
if raw_domain is None or raw_domain.string is None:
return []
raw_domain = raw_domain.string.encode("ascii", "replace").decode()
domains_list = raw_domain.split("##comma##")
results = []
for domain in domains_list:
if "http" in domain:
domain = domain.replace("https://", "").replace("http://", "")
indicator_type = auto_detect_indicator_type(domain)
is_domain = ("Domain" in indicator_type) if indicator_type else False
demisto.debug(f"{indicator_type=}, {is_domain=}")
if len(domain.split(".")) > 1 or is_domain:
results.append({"indicator": domain, "type": "Domain"})
else:
demisto.debug(f"obj with value {domain} is not a domain, skipping.")
return results
class FileObject:
"""
Implements file object indicator decoding
based on: https://stixproject.github.io/data-model/1.2/FileObj/FileObjectType/
"""
@staticmethod
def _decode_basic_props(props):
result = {}
name = next((c for c in props if c.name == "File_Name"), None)
if name is not None:
result["stix_file_name"] = name.text
size = next((c for c in props if c.name == "File_Size"), None)
if size is not None:
result["stix_file_size"] = size.text
file_format = next((c for c in props if c.name == "File_Format"), None)
if file_format is not None:
result["stix_file_format"] = file_format.text
return result
@staticmethod
def decode(props, **kwargs):
result = []
bprops = FileObject._decode_basic_props(props)
hashes = props.find_all("Hash")
for h in hashes:
htype = h.find("Type")
if htype is None:
continue
htype = htype.string.lower()
if htype not in ["md5", "sha1", "sha256", "ssdeep"]:
continue
value = h.find("Simple_Hash_Value")
if value is None:
continue
value = value.string.lower()
file_list = value.split("##comma##")
for file in file_list:
result.append({"indicator": file, "htype": htype, "type": "File"})
for r in result:
for r2 in result:
if r["htype"] == r2["htype"]:
continue
r["stix_file_{}".format(r2["htype"])] = r2["indicator"]
r.update(bprops)
return result
class URIObject:
"""
Implements URI object indicator decoding
based on: https://stixproject.github.io/data-model/1.2/URIObj/URIObjectType/
"""
@staticmethod
def decode(props, **kwargs):
utype = props.get("type", "URL")
if utype == "URL":
type_ = "URL"
elif utype == "Domain Name":
type_ = "Domain"
else:
return []
raw_url = props.find("Value")
if raw_url is None or raw_url.string is None:
return []
raw_url = raw_url.string.encode("utf8", "replace").decode()
urls_list = raw_url.split("##comma##")
results = []
for url in urls_list:
if type_ == "URL" and auto_detect_indicator_type(url) == "URL":
results.append({"indicator": url, "type": type_})
elif type_ == "Domain":
domain = url.replace("https://", "").replace("http://", "")
indicator_type = auto_detect_indicator_type(domain)
is_domain = ("Domain" in indicator_type) if indicator_type else False
demisto.debug(f"{indicator_type=}, {is_domain=}")
if len(domain.split(".")) > 1 or is_domain:
results.append({"indicator": domain, "type": "Domain"})
else:
demisto.debug(f"obj with value {url} is not of type {type_}, skipping.")
else:
demisto.debug(f"obj with value {url} is not of type {type_}, skipping.")
return results
class SocketAddressObject:
"""
Implements socket address object indicator decoding
based on: https://stixproject.github.io/data-model/1.2/SocketAddressObj/SocketAddressObjectType/
"""
@staticmethod
def decode(props, **kwargs):
ip = props.get("ip_address", None)
if ip:
return AddressObject.decode(ip)
return []
class LinkObject:
"""
Implements link object indicator decoding
based on: https://stixproject.github.io/data-model/1.2/LinkObj/LinkObjectType/
"""
@staticmethod
def decode(props, **kwargs):
ltype = props.get("type", "URL")
if ltype != "URL":
LOG(f"Unhandled LinkObjectType type: {ltype}")
return []
value = props.get("value", None)
if value is None:
LOG("no value in observable LinkObject")
return []
if not isinstance(value, string_types):
value = value.get("value", None)
if value is None:
LOG("no value in observable LinkObject")
return []
links_list = value.split("##comma##")
return [{"indicator": link, "type": ltype} for link in links_list]
class HTTPSessionObject:
"""
Implements http session object indicator decoding
based on: https://stixproject.github.io/data-model/1.2/HTTPSessionObj/HTTPSessionObjectType/
"""
@staticmethod
def decode(props, **kwargs):
if "http_request_response" in props:
tmp = props["http_request_response"]
if len(tmp) == 1:
item = tmp[0]
http_client_request = item.get("http_client_request", None)
if http_client_request is not None:
http_request_header = http_client_request.get("http_request_header", None)
if http_request_header is not None:
raw_header = http_request_header.get("raw_header", None)
if raw_header is not None:
raw_header = raw_header.split("\n")[0]
headers_list = raw_header.split("##comma##")
return [
{
"indicator": header,
"type": "http-session", # we don't support this type natively in demisto
"header": header,
}
for header in headers_list
]
else:
LOG("multiple HTTPSessionObjectTypes not supported")
return []
class StixDecode:
"""
Decode STIX strings formatted as xml, and extract indicators from them
"""
DECODERS = {
"DomainNameObjectType": DomainNameObject.decode,
"FileObjectType": FileObject.decode,
"WindowsFileObjectType": FileObject.decode,
"URIObjectType": URIObject.decode,
"AddressObjectType": AddressObject.decode,
"SocketAddressObjectType": SocketAddressObject.decode,
"LinkObjectType": LinkObject.decode,
"HTTPSessionObjectType": HTTPSessionObject.decode,
}
@staticmethod
def object_extract_properties(props, kwargs):
type_ = props.get("xsi:type").rsplit(":")[-1]
demisto.debug(f"The type of the indicator is {type_=}")
if type_ not in StixDecode.DECODERS:
LOG(f"Unhandled cybox Object type: {type_!r} - {props!r}")
return []
return StixDecode.DECODERS[type_](props, **kwargs)
@staticmethod
def _parse_stix_timestamp(stix_timestamp):
dt = dateutil.parser.parse(stix_timestamp)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=pytz.UTC)
delta = dt - EPOCH
return int(delta.total_seconds() * 1000)
@staticmethod
def _deduplicate(indicators):
result = {}
for iv in indicators:
result["{}:{}".format(iv["indicator"], iv["type"])] = iv
return list(result.values())
@staticmethod
def decode(content, **kwargs):
observable_result = []
indicator_result: dict[str, dict] = {}
ttp_result: dict[str, dict] = {}
package = BeautifulSoup(content, "xml")
first_element = package.contents[0]
if not isinstance(first_element, Tag) or first_element.name != "STIX_Package":
return None, None, None, None
package = first_element
timestamp = package.get("timestamp", None)
if timestamp is not None:
timestamp = StixDecode._parse_stix_timestamp(timestamp)
# extract the Observable info
if observables := package.find_all("Observable"):
pprops = package_extract_properties(package)
for o in observables:
try:
gprops = observable_extract_properties(o)
obj = next((ob for ob in o if isinstance(ob, Tag) and ob.name == "Object"), None)
if obj is None:
continue
# main properties
properties = next((c for c in obj if isinstance(c, Tag) and c.name == "Properties"), None)
if properties is not None:
for r in StixDecode.object_extract_properties(properties, kwargs):
r.update(gprops)
r.update(pprops)
observable_result.append(r)
# then related objects
related = next((c for c in obj if isinstance(c, Tag) and c.name == "Related_Objects"), None)
if related is not None:
for robj in related:
if not isinstance(robj, Tag) or robj.name != "Related_Object":
continue
properties = next((c for c in robj if isinstance(c, Tag) and c.name == "Properties"), None)
if properties is None:
continue
for r in StixDecode.object_extract_properties(properties, kwargs):
r.update(gprops)
r.update(pprops)
observable_result.append(r)
except Exception as e:
demisto.error(f"Error for {str(o)} with message {str(e)}")
# extract the Indicator info
if (indicators := package.find_all("Indicator")) and observables:
indicator_ref = observables[0].get("idref")
if indicator_ref and isinstance(indicator_ref, str):
indicator_info = indicator_extract_properties(indicators[0])
indicator_result[indicator_ref] = indicator_info
# extract the TTP info
if ttp := package.find_all("TTP"):
ttp_info: dict[str, str] = {}
id_ref = ttp[0].get("id")
title = next((c for c in ttp[0] if isinstance(c, Tag) and c.name == "Title"), None)
if title is not None:
ttp_info["stix_ttp_title"] = title.text
description = next((c for c in ttp[0] if isinstance(c, Tag) and c.name == "Description"), None)
if description is not None:
ttp_info["ttp_description"] = description.text
if behavior := package.find_all("Behavior"):
if behavior[0].find_all("Malware"):
ttp_info.update(ttp_extract_properties(package.find_all("Malware_Instance")[0], "Malware"))
elif behavior[0].find_all("Attack_Patterns"):
ttp_info.update(ttp_extract_properties(package.find_all("Attack_Pattern")[0], "Attack Pattern"))
if isinstance(id_ref, str):
ttp_result[id_ref] = ttp_info
return timestamp, StixDecode._deduplicate(observable_result), indicator_result, ttp_result
class Taxii11:
"""
TAXII 1 client utilities class
"""
MESSAGE_BINDING = "urn:taxii.mitre.org:message:xml:1.1"
SERVICES = "urn:taxii.mitre.org:services:1.1"
PROTOCOLS = {"http": "urn:taxii.mitre.org:protocol:http:1.0", "https": "urn:taxii.mitre.org:protocol:https:1.0"}
# 2014-12-19T00:00:00Z
TAXII_DT_FORMAT = "%Y-%m-%dT%H:%M:%SZ"
@staticmethod
def new_message_id():
return str(uuid.uuid4())
@staticmethod
def discovery_request(message_id=None):
if message_id is None:
message_id = Taxii11.new_message_id()
return f""""""
@staticmethod
def collection_information_request(message_id=None):
if message_id is None:
message_id = Taxii11.new_message_id()
return f""""""
@staticmethod
def poll_request(collection_name, exclusive_begin_timestamp, inclusive_end_timestamp, message_id=None, subscription_id=None):
if message_id is None:
message_id = Taxii11.new_message_id()
exclusive_begin_timestamp = exclusive_begin_timestamp.strftime(Taxii11.TAXII_DT_FORMAT)
inclusive_end_timestamp = inclusive_end_timestamp.strftime(Taxii11.TAXII_DT_FORMAT)
result = [
'")
result.append(f"{exclusive_begin_timestamp}")
result.append(f"{inclusive_end_timestamp}")
if subscription_id is None:
result.append(
''
"FULL"
)
result.append("")
return "\n".join(result)
@staticmethod
def poll_fulfillment_request(result_id, result_part_number, collection_name, message_id=None):
if message_id is None:
message_id = Taxii11.new_message_id()
return f""""""
@staticmethod
def headers(content_type=None, accept=None, services=None, protocol=None):
if content_type is None:
content_type = Taxii11.MESSAGE_BINDING
if accept is None:
accept = Taxii11.MESSAGE_BINDING
if services is None:
services = Taxii11.SERVICES
if protocol is None:
protocol = "urn:taxii.mitre.org:protocol:http:1.0"
if protocol in Taxii11.PROTOCOLS:
protocol = Taxii11.PROTOCOLS[protocol]
return {
"Content-Type": "application/xml",
"X-TAXII-Content-Type": content_type,
"X-TAXII-Accept": accept,
"X-TAXII-Services": services,
"X-TAXII-Protocol": protocol,
"Accept": "application/xml",
}
@staticmethod
def parse_timestamp_label(timestamp_label):
try:
dt = dateutil.parser.parse(timestamp_label)
if dt.tzinfo is None:
dt = dt.replace(tzinfo=pytz.UTC)
delta = dt - EPOCH
return int(delta.total_seconds() * 1000)
except Exception:
return None
class TAXIIClient:
def __init__(
self,
insecure: bool = True,
polling_timeout: int = 20,
initial_interval: str = "1 day",
discovery_service: str = "",
poll_service: str = None,
collection: str = None,
credentials: dict = None,
creds_certificate: dict = {},
cert_text: str = None,
key_text: str = None,
feedTags: str = None,
tlp_color: str | None = None,
enrichmentExcluded: bool = False,
**kwargs,
):
"""
TAXII Client
:param insecure: Set to true to ignore https certificate
:param polling_timeout: Time before send request timeout
:param initial_interval: Interval between each read from TAXII server
:param discovery_service: TAXII server discovery service
:param poll_service: TAXII poll service
:param collection: TAXII collection
:param credentials: Username and password dict for basic auth
:param cert_text: Certificate File as Text
:param cert_key_text: Key File as Text - type 9 (credentials)
:param key_text: Key File as Text - type 4 (secret) - deprecated
:param kwargs:
"""
self.discovered_poll_service = None
self.last_taxii_run = demisto.getLastRun()
if isinstance(self.last_taxii_run, dict):
self.last_taxii_run = self.last_taxii_run.get("time")
self.last_stix_package_ts = None
self.last_taxii_content_ts = None
self.verify_cert = not insecure
self.polling_timeout = polling_timeout
try:
self.polling_timeout = int(self.polling_timeout)
except (ValueError, TypeError):
raise TypeError('Please provide a valid integer for "Polling Timeout"')
self.initial_interval = initial_interval
self.initial_interval = interval_in_sec(self.initial_interval)
if self.initial_interval is None:
self.initial_interval = 86400
self.discovery_service = discovery_service
self.poll_service = poll_service
self.collection = collection
self.api_key = None
self.api_header = None
self.username = None
self.password = None
self.crt = None
self.tags = argToList(feedTags)
self.tlp_color = tlp_color
self.ttps: dict[str, dict] = {}
self.enrichment_excluded = enrichmentExcluded or (tlp_color == "RED" and is_xsiam_or_xsoar_saas())
# authentication
if credentials:
if "_header:" in credentials.get("identifier", None):
self.api_header = credentials.get("identifier", None).split("_header:")[1]
self.api_key = credentials.get("password", None)
else:
self.username = credentials.get("identifier", None)
self.password = credentials.get("password", None)
cert_text = replace_spaces_in_credential(creds_certificate.get("identifier")) or cert_text
key_text = creds_certificate.get("password") or key_text
if (cert_text and not key_text) or (not cert_text and key_text):
raise Exception("You can not configure either certificate text or key, both are required.")
if cert_text and key_text:
cert_text_list = cert_text.split("-----")
# replace spaces with newline characters
cert_text_fixed = "-----".join(cert_text_list[:2] + [cert_text_list[2].replace(" ", "\n")] + cert_text_list[3:])
cf = tempfile.NamedTemporaryFile(delete=False)
cf.write(cert_text_fixed.encode())
cf.flush()
key_text_list = key_text.split("-----")
# replace spaces with newline characters
key_text_fixed = "-----".join(key_text_list[:2] + [key_text_list[2].replace(" ", "\n")] + key_text_list[3:])
kf = tempfile.NamedTemporaryFile(delete=False)
kf.write(key_text_fixed.encode())
kf.flush()
self.crt = (cf.name, kf.name)
if collection is None or collection == "":
all_collections = self.get_all_collections()
return_error(f"No collection set. Here is a list of all accessible collections: {str(all_collections)}")
def get_all_collections(self, is_raise_error=False):
"""Gets a list of all collections listed in the discovery service instance.
Args:
is_raise_error(bool): Whether to raise an error when one occurs.
Returns:
list. A list of all collection names in discovery service.
"""
if self.discovery_service:
taxii_client = cabby.create_client(discovery_path=self.discovery_service)
if self.username:
taxii_client.set_auth(username=str(self.username), password=self.password, verify_ssl=self.verify_cert)
elif self.api_key:
taxii_client.set_auth(username=str(self.api_key), verify_ssl=self.verify_cert)
else:
taxii_client.set_auth(verify_ssl=self.verify_cert)
try:
all_collections = taxii_client.get_collections()
return [collection.name for collection in all_collections]
except Exception as e:
if is_raise_error:
raise ConnectionError
return_error(f"{INTEGRATION_NAME} - An error occurred when trying to fetch available collections.\n{e}")
return []
def _send_request(self, url, headers, data, stream=False):
if self.api_key is not None and self.api_header is not None:
headers[self.api_header] = self.api_key
rkwargs = {
"stream": stream,
"verify": self.verify_cert,
"timeout": self.polling_timeout,
"headers": headers,
"cert": self.crt,
"data": data,
}
if self.username is not None and self.password is not None:
rkwargs["auth"] = (self.username, self.password)
r = requests.post(url, **rkwargs)
try:
r.raise_for_status()
except Exception:
demisto.debug(f"{INTEGRATION_NAME} - exception in request: {r.status_code!r} {r.content!r}")
raise
return r
@staticmethod
def _raise_for_taxii_error(response):
if response.contents[0].name != "Status_Message":
return
if response.contents[0]["status_type"] == "SUCCESS":
return
raise RuntimeError(
"{} - error returned by TAXII Server: {}".format(INTEGRATION_NAME, response.contents[0]["status_type"])
)
def _discover_poll_service(self):
# let's start from discovering the available services
req = Taxii11.discovery_request()
reqhdrs = Taxii11.headers(protocol=self.discovery_service.split(":", 1)[0])
result = self._send_request(url=self.discovery_service, headers=reqhdrs, data=req)
result = BeautifulSoup(result.text, "xml")
self._raise_for_taxii_error(result)
# from here we look for a good collection management service
coll_services = result.find_all("Service_Instance", service_type="COLLECTION_MANAGEMENT")
if len(coll_services) == 0:
raise RuntimeError(f"{INTEGRATION_NAME} - Collection management service not found")
selected_coll_service: str | None = None
for coll_service in coll_services:
address = coll_service.find("Address")
if address is None:
LOG(f"{INTEGRATION_NAME} - Collection management service with no address: {coll_service!r}")
continue
address_str = address.string
if selected_coll_service is None:
selected_coll_service = address_str
continue
msgbindings = coll_service.find_all("Message_Binding")
if len(msgbindings) != 0:
for msgbinding in msgbindings:
if msgbinding.string == Taxii11.MESSAGE_BINDING:
selected_coll_service = address_str
break
if selected_coll_service is None:
raise RuntimeError(f"{INTEGRATION_NAME} - Collection management service not found")
# from here we look for the correct poll service
req = Taxii11.collection_information_request()
reqhdrs = Taxii11.headers(protocol=selected_coll_service.split(":", 1)[0])
result = self._send_request(url=selected_coll_service, headers=reqhdrs, data=req)
result = BeautifulSoup(result.text, "xml")
self._raise_for_taxii_error(result)
# from here we look for the collection
assert self.collection is not None
collections_found = result.find_all("Collection", collection_name=self.collection)
if len(collections_found) == 0:
raise RuntimeError(f"{INTEGRATION_NAME} - collection {self.collection} not found")
# and the right poll service
poll_service = None
for coll in collections_found:
pservice = coll.find("Polling_Service")
if pservice is None:
LOG(f"{INTEGRATION_NAME} - Collection with no Polling_Service: {coll!r}")
continue
address = pservice.find("Address")
if address is None:
LOG(f"{INTEGRATION_NAME} - Collection with no Address: {coll!r}")
continue
address_str = address.string
if poll_service is None:
poll_service = address_str
continue
msgbindings = coll_service.find_all("Message_Binding")
if len(msgbindings) != 0:
for msgbinding in msgbindings:
if msgbinding.string == Taxii11.MESSAGE_BINDING:
poll_service = address_str
break
if poll_service is None:
raise RuntimeError(f"{INTEGRATION_NAME} - No valid Polling Service found")
return poll_service
def _poll_collection(self, poll_service, begin, end):
req = Taxii11.poll_request(collection_name=self.collection, exclusive_begin_timestamp=begin, inclusive_end_timestamp=end)
reqhdrs = Taxii11.headers(protocol=poll_service.split(":", 1)[0])
result = self._send_request(url=poll_service, headers=reqhdrs, data=req, stream=True)
result.raw.decode_content = True
while True:
result_part_number = None
result_id = None
more = None
tag_stack = collections.deque() # type: ignore
observables = []
indicators: dict[str, dict] = {}
try:
for action, element in etree.iterparse(
result.raw,
events=("start", "end"),
recover=True,
resolve_entities=False,
load_dtd=False,
no_network=True,
):
if action == "start":
tag_stack.append(element.tag)
else:
last_tag = tag_stack.pop()
if last_tag != element.tag:
raise RuntimeError(f"{INTEGRATION_NAME} - error parsing poll response, mismatched tags")
if action == "end" and element.tag.endswith("Status_Message") and len(tag_stack) == 0:
self._raise_for_taxii_error(BeautifulSoup(etree.tostring(element, encoding="unicode"), "xml"))
return
elif action == "end" and element.tag.endswith("Poll_Response") and len(tag_stack) == 0:
result_id = element.get("result_id", None)
more = element.get("more", None)
result_part_number = element.get("result_part_number", None)
if result_part_number is not None:
result_part_number = int(result_part_number)
elif action == "end" and element.tag.endswith("Content_Block") and len(tag_stack) == 1:
for c in element:
if c.tag.endswith("Content"):
if len(c) == 0:
continue
content = etree.tostring(c[0], encoding="unicode")
timestamp, observable, indicator, ttp = StixDecode.decode(content)
if observable:
observables.extend(observable)
if indicator:
indicators.update(indicator)
if ttp:
self.ttps.update(ttp)
if timestamp and (self.last_stix_package_ts is None or timestamp > self.last_stix_package_ts):
self.last_stix_package_ts = timestamp
elif c.tag.endswith("Timestamp_Label"):
timestamp = Taxii11.parse_timestamp_label(c.text)
if timestamp and (self.last_taxii_content_ts is None or timestamp > self.last_taxii_content_ts):
self.last_taxii_content_ts = timestamp
element.clear()
finally:
result.close()
if not more or more == "0" or more.lower() == "false":
break
if result_id is None or result_part_number is None:
break
req = Taxii11.poll_fulfillment_request(
collection_name=self.collection, result_id=result_id, result_part_number=result_part_number + 1
)
result = self._send_request(url=poll_service, headers=reqhdrs, data=req, stream=True)
for observable in observables:
if (indicator_ref := observable.get("indicator_ref")) and (indicator_info := indicators.get(indicator_ref)):
observable.update(indicator_info)
ttp_ref = observable.get("ttp_ref", [])
relationships = []
for reference in ttp_ref:
if relationship := self.ttps.get(reference):
relationships.append(relationship)
if relationships:
observable["relationships"] = relationships
yield observable
def _incremental_poll_collection(self, poll_service, begin, end):
"""Polls collection in increments of 10 days"""
cbegin = begin
dt = timedelta(days=10)
self.last_stix_package_ts = None
self.last_taxii_content_ts = None
while cbegin < end:
cend = min(end, cbegin + dt)
result = self._poll_collection(poll_service=poll_service, begin=cbegin, end=cend)
yield from result
if self.last_taxii_content_ts is not None:
self.last_taxii_run = self.last_taxii_content_ts
cbegin = cend
def build_iterator(self, now):
"""Creates an indicator iterator from the TAXII feed"""
if self.poll_service is not None:
discovered_poll_service = self.poll_service
else:
discovered_poll_service = self._discover_poll_service()
last_run = self.last_taxii_run
if last_run is None:
last_run = now - (self.initial_interval * 1000)
self.last_taxii_run = last_run
begin = datetime.utcfromtimestamp(last_run / 1000)
begin = begin.replace(microsecond=0, tzinfo=pytz.UTC)
end = datetime.utcfromtimestamp(now / 1000)
end = end.replace(tzinfo=pytz.UTC)
# lower time precision - solve issues with certain taxii servers
end = end.replace(second=0, microsecond=0)
begin = begin.replace(second=0, microsecond=0)
return self._incremental_poll_collection(discovered_poll_service, begin=begin, end=end)
""" Helper Methods """
def package_extract_properties(package):
"""Extracts properties from the STIX package"""
result: dict[str, str] = {}
header = package.find_all("STIX_Header")
if len(header) == 0:
return result
# share level
mstructures = header[0].find_all("Marking_Structure")
for ms in mstructures:
type_ = ms.get("xsi:type")
if type_ is result:
continue
color = ms.get("color")
if color is result:
continue
type_ = type_.lower()
if "tlpmarkingstructuretype" not in type_:
continue
result["share_level"] = color.lower() # To keep backward compatibility
result["TLP"] = color.upper() # https://www.us-cert.gov/tlp
break
# decode title
title = next((c for c in header[0] if c.name == "Title"), None)
if title is not None:
result["stix_package_title"] = title.text
# decode description
description = next((c for c in header[0] if c.name == "Description"), None)
if description is not None:
result["stix_package_description"] = description.text
# decode description
sdescription = next((c for c in header[0] if c.name == "Short_Description"), None)
if sdescription is not None:
result["stix_package_short_description"] = sdescription.text
# decode identity name from information_source
information_source = next((c for c in header[0] if c.name == "Information_Source"), None)
if information_source is not None:
identity = next((c for c in information_source if c.name == "Identity"), None)
if identity is not None:
name = next(c for c in identity if c.name == "Name")
if name is not None:
result["stix_package_information_source"] = name.text
return result
def observable_extract_properties(observable):
"""Extracts properties from observable"""
result: dict[str, str] = {}
if id_ref := observable.get("id"):
result["indicator_ref"] = id_ref
title = next((c for c in observable if c.name == "Title"), None)
if title is not None:
title = title.text
result["stix_title"] = title
description = next((c for c in observable if c.name == "Description"), None)
if description is not None:
description = description.text
result["stix_description"] = description
return result
def indicator_extract_properties(indicator) -> dict[str, Any]:
"""Extracts the Indicator properties
Args:
indicator (bs4.element.Tag): The Indicator content in xml.
Returns:
dict: The ttp properties in a dict {'property': 'value'}. (The value can be a list)
"""
result: dict[str, Any] = {}
title = next((c for c in indicator if c.name == "Title"), None)
if title is not None:
title = title.text
result["stix_indicator_name"] = title
description = next((c for c in indicator if c.name == "Description"), None)
if description is not None:
description = description.text
result["stix_indicator_description"] = description
confidence = next((c for c in indicator if c.name == "Confidence"), None)
if confidence is not None:
value = next((c for c in confidence if c.name == "Value"), None)
if value is not None:
value = value.text
result["confidence"] = value
if indicated_ttp := indicator.find_all("Indicated_TTP"):
result["ttp_ref"] = []
# Each indicator can be related to few ttps
for ttp_value in indicated_ttp:
ttp = next((c for c in ttp_value if c.name == "TTP"), None)
if ttp is not None:
value = ttp.get("idref")
result["ttp_ref"].append(value)
return result
def ttp_extract_properties(ttp, behavior) -> dict[str, str]:
"""Extracts the TTP properties
Args:
ttp (bs4.element.Tag): The TTP content in xml.
behavior (str): The TTP behavior ['Malware', 'Attack Pattern'].
Returns:
dict: The ttp properties in a dict {'property': 'value'}.
"""
result = {"type": behavior}
if behavior == "Malware":
type_ = next((c for c in ttp if c.name == "Type"), None)
if type_ is not None:
type_ = type_.text
result["malware_type"] = type_
name = next((c for c in ttp if c.name == "Name"), None)
if name is not None:
name = name.text
result["indicator"] = name
title = next((c for c in ttp if c.name == "Title"), None)
if title is not None:
title = title.text
result["title"] = title
if behavior == "Attack Pattern":
id_ref = next((c for c in ttp if c.name == "idref"), None)
if id_ref is not None:
id_ref = id_ref.text
result["stix_id_ref"] = id_ref
title = next((c for c in ttp if c.name == "Title"), None)
if title is not None:
title = title.text
result["indicator"] = title
description = next((c for c in ttp if c.name == "Description"), None)
if description is not None:
description = description.text
result["description"] = description
short_description = next((c for c in ttp if c.name == "Short_Description"), None)
if short_description is not None:
short_description = short_description.text
result["short_description"] = short_description
return result
def interval_in_sec(val):
"""Translates interval string to seconds int"""
if val is None:
return None
if isinstance(val, int):
return val
else:
range_split = val.split()
if len(range_split) != 2:
raise ValueError('Interval must be "number date_range_unit", examples: (2 hours, 4 minutes,6 months, 1 day.')
number = int(range_split[0])
range_unit = range_split[1].lower()
if range_unit not in ["minute", "minutes", "hour", "hours", "day", "days"]:
raise ValueError("The unit of Interval is invalid. Must be minutes, hours or days")
multipliers = {
"minute": 60,
"hour": 3600,
"day": 86400,
}
for m, m_value in multipliers.items():
if m in range_unit:
return number * m_value
return None
def create_relationships(indicator):
results = []
for relationship in indicator.get("relationships", {}):
if relationship.get("type") == "Malware":
name = "indicator-of"
relationship_type = "Malware"
else:
name = "related-to"
relationship_type = "Attack Pattern"
entity_relationship = EntityRelationship(
name=name,
entity_a=indicator.get("value"),
entity_a_type=indicator.get("type"),
entity_b=relationship.get("indicator"),
entity_b_type=relationship_type,
)
results.append(entity_relationship.to_indicator())
return results
def test_module(client, *_):
try:
all_collections = client.get_all_collections(is_raise_error=True)
except ConnectionError:
all_collections = [client.collection]
if client.collection not in all_collections:
return_error(
f"Collection could not be found at this time. Here is a list of all accessible collections:"
f" {str(all_collections)}"
)
client._discover_poll_service()
return "ok", {}, {}
def fetch_indicators_command(client):
indicators = []
# Create the indicators from the observables
iterator = client.build_iterator(date_to_timestamp(datetime.now()))
for item in iterator:
if indicator := item.get("indicator"):
item["value"] = indicator
indicator_obj = {
"value": indicator,
"type": item.get("type"),
"title": item.get("stix_title"),
"description": item.get("stix_description"),
"stixindicatorname": item.get("stix_indicator_name"),
"stixindicatordescription": item.get("stix_indicator_description"),
"confidence": item.get("confidence"),
}
fields: dict[str, str] = {}
for key, value in indicator_obj.items():
if key in client.tags:
fields[key] = value
indicator_obj["fields"] = fields
if item.get("relationships"):
indicator_obj["relationships"] = create_relationships(item)
if client.tlp_color:
indicator_obj["fields"]["trafficlightprotocol"] = client.tlp_color
if client.enrichment_excluded:
indicator_obj["enrichmentExcluded"] = client.enrichment_excluded
indicator_obj["rawJSON"] = item
indicators.append(indicator_obj)
# Create the indicators from the ttps
ttps = client.ttps
for item in ttps.values():
if indicator := item.get("indicator"):
item["value"] = indicator
indicator_obj = {
"value": indicator,
"type": item.get("type"),
"title": item.get("title"),
"description": item.get("description"),
"shortdescription": item.get("short_description"),
"stixindicatordescription": item.get("ttp_description"),
"stixttptitle": item.get("stix_ttp_title"),
}
if item.get("type") == "Malware":
indicator_obj["score"] = ThreatIntel.ObjectsScore.MALWARE
indicator_obj["stixmalwaretypes"] = item.get("malware_type", "").lower().replace(" ", "-")
else:
indicator_obj["score"] = ThreatIntel.ObjectsScore.ATTACK_PATTERN
ttp_fields: dict[str, str] = {}
for key, value in indicator_obj.items():
if key in client.tags and value:
ttp_fields[key] = value
indicator_obj["fields"] = ttp_fields
indicator_obj["rawJSON"] = item
indicators.append(indicator_obj)
return indicators
def get_indicators_command(client, args):
limit = int(args.get("limit", 10))
client.initial_interval = interval_in_sec(args.get("initial_interval"))
client.last_taxii_run = None
indicators_list = fetch_indicators_command(client)
entry_result = camelize(indicators_list[:limit])
hr = tableToMarkdown("Indicators", entry_result, headers=["Value", "Type", "Rawjson"])
return hr, {"TAXII.Indicator": entry_result}, indicators_list
def main():
# Write configure here
params = {key: value for key, value in demisto.params().items() if value is not None}
handle_proxy()
client = TAXIIClient(**params)
command = demisto.command()
demisto.info(f"Command being called is {command}")
# Switch case
commands = {"test-module": test_module, "get-indicators": get_indicators_command}
try:
if demisto.command() == "fetch-indicators":
indicators = fetch_indicators_command(client)
# we submit the indicators in batches
for b in batch(indicators, batch_size=2000):
demisto.createIndicators(b)
demisto.setLastRun({"time": client.last_taxii_run})
else:
readable_output, outputs, raw_response = commands[command](client, demisto.args()) # type: ignore
return_outputs(readable_output, outputs, raw_response)
except Exception as e:
err_msg = f"Error in {INTEGRATION_NAME} Integration [{e}]"
raise Exception(err_msg)
if __name__ in ("__builtin__", "builtins", "__main__"):
main()