import json
import pytest
import demistomock as demisto
from FeedTAXII import TAXIIClient, fetch_indicators_command, Taxii11
""" helper functions """
def get_files_in_dir(mypath, only_with_ext=None):
from os import listdir
from os.path import isfile, join
files_list = [f for f in listdir(mypath) if isfile(join(mypath, f))]
if only_with_ext:
return [f for f in files_list if f.endswith(only_with_ext)]
return files_list
class TestStixDecode:
FILE_PATH = "test_data/StixDecodeTest"
def test_decode(self):
"""Test decode on all files"""
from FeedTAXII import StixDecode
xml_files_names = get_files_in_dir(self.FILE_PATH, "xml")
for xml_f_name in xml_files_names:
file_path = f"{self.FILE_PATH}/{xml_f_name}"
with open(file_path) as xml_f:
stix_str = xml_f.read()
res = StixDecode.decode(stix_str)
res_path = f'{file_path.rstrip(".xml")}-result.json'
with open(res_path) as res_f:
expected_res = json.load(res_f)
assert expected_res == list(res[1])
class TestUtilFunctions:
multipliers = {
"minute": 60,
"hour": 3600,
"day": 86400,
}
def test_interval_in_sec_1(self):
"""Empty"""
from FeedTAXII import interval_in_sec
assert interval_in_sec(None) is None
def test_interval_in_sec_2(self):
"""Integer"""
from FeedTAXII import interval_in_sec
val = 25
assert interval_in_sec(val) == val
def test_interval_in_sec_3(self):
"""Str with len < 2"""
from FeedTAXII import interval_in_sec
val = "25"
with pytest.raises(ValueError):
interval_in_sec(val)
def test_interval_in_sec_4(self):
"""Str with len > 2"""
from FeedTAXII import interval_in_sec
val = "25 minutes ok"
with pytest.raises(ValueError):
interval_in_sec(val)
def test_interval_in_sec_5(self):
"""Invalid str with len == 2"""
from FeedTAXII import interval_in_sec
val = "25 minu"
with pytest.raises(ValueError):
interval_in_sec(val)
def test_interval_in_sec_6(self):
"""Valid str"""
from FeedTAXII import interval_in_sec
# Minutes
val = "25 minutes"
assert interval_in_sec(val) == 25 * self.multipliers["minute"]
# Hours
val = "30 hours"
assert interval_in_sec(val) == 30 * self.multipliers["hour"]
# Days
val = "40 hours"
assert interval_in_sec(val) == 40 * self.multipliers["hour"]
class TestCommands:
def test_fetch_indicators(self, mocker):
client = TAXIIClient(collection="a collection")
with open("test_data/raw_indicators.json") as f:
raw_indicators = json.load(f)
mocker.patch.object(client, "build_iterator", return_value=raw_indicators)
res = fetch_indicators_command(client)
with open("test_data/indicators_results.json") as exp_f:
expected = json.load(exp_f)
assert res == expected
def test_poll_collection(mocker):
"""
Given:
- A collection of indicators in STIX format where there are more than one indicator for some blocks.
When:
- fetch_indicators_command is running.
Then:
- Validate the indicator extract as expected.
"""
import requests_mock
from FeedTAXII import fetch_indicators_command
client = TAXIIClient(collection="a collection", poll_service="http://example/taxii-data")
with open("test_data/collection_example.xml", "rb") as xml_f:
stix_content = xml_f.read()
with requests_mock.Mocker() as m:
m.post("http://example/taxii-data", content=stix_content)
res = fetch_indicators_command(client)
with open("test_data/indicators_example.json") as json_f:
expected_result = json.load(json_f)
assert res == expected_result
@pytest.mark.parametrize("tags", (["title", "description"], []))
def test_tags_parameter(mocker, tags):
"""
Given:
- tags parameters
When:
- Executing any command on feed
Then:
- Validate the tags supplied exists in the indicators
"""
client = TAXIIClient(collection="a collection", feedTags=json.dumps(tags))
with open("test_data/raw_indicators.json") as f:
raw_indicators = json.load(f)
mocker.patch.object(client, "build_iterator", return_value=raw_indicators)
res = fetch_indicators_command(client)
assert tags == list(res[0]["fields"].keys())
def test_client_headers():
"""
Given:
- TAXII1 client
When:
- Getting the client headers
Then:
- Validate the headers contain the Accept header
"""
client = Taxii11()
headers = client.headers()
assert headers["Accept"] == "application/xml"
assert headers["X-TAXII-Content-Type"] == client.MESSAGE_BINDING
assert headers["X-TAXII-Accept"] == client.MESSAGE_BINDING
assert headers["X-TAXII-Services"] == client.SERVICES
assert headers["X-TAXII-Protocol"] == "urn:taxii.mitre.org:protocol:http:1.0"
headers = client.headers(protocol="https")
assert headers["X-TAXII-Protocol"] == "urn:taxii.mitre.org:protocol:https:1.0"
def test_fetch_enrichment_excluded(mocker):
"""
Given:
- A TAXII client with the enrichmentExcluded parameter set to True.
When:
- Calling the fetch_indicators command
Then:
- The indicators returned should have enrichmentExcluded set to True.
"""
client = TAXIIClient(collection="a collection", enrichmentExcluded=True)
with open("test_data/raw_indicators.json") as f:
raw_indicators = json.load(f)
mocker.patch.object(client, "build_iterator", return_value=raw_indicators)
res = fetch_indicators_command(client)
with open("test_data/indicators_results.json") as exp_f:
expected = json.load(exp_f)
for ind in expected:
ind["enrichmentExcluded"] = True
assert res == expected
def test_client_enrichment_excluded_with_tlp_red(mocker):
"""
Given: integration params with enrichment excluded set to False and tlp_color set to RED
When: creating client
Then: validate the enrichment_excluded is set to True
"""
from FeedTAXII import TAXIIClient
mocker.patch("FeedTAXII.is_xsiam_or_xsoar_saas", return_value=True)
client = TAXIIClient(collection="test", enrichmentExcluded=False, tlp_color="RED")
assert client.enrichment_excluded is True
def test_decoding_domain():
"""
Given: domain indicator type without a protocol
When: decoding the indicator
Then: validate return of the indicator
"""
from bs4 import BeautifulSoup
from FeedTAXII import DomainNameObject
xml = 'www.a.com'
soup = BeautifulSoup(xml, "xml")
props = soup.find("DomainName")
indicator = DomainNameObject.decode(props)[0]
assert indicator.get("indicator") == "www.a.com"
assert indicator.get("type") == "Domain"
def test_decoding_url():
"""
Given: domain indicator type with a protocol
When: decoding the indicator
Then: validate return of the indicator
"""
from bs4 import BeautifulSoup
from FeedTAXII import URIObject
xml = 'https://www.a.com'
soup = BeautifulSoup(xml, "xml")
props = soup.find("URI")
indicator = URIObject.decode(props)[0]
assert indicator.get("indicator") == "www.a.com"
assert indicator.get("type") == "Domain"
def test_poll_collection_uses_safe_xml_parser_settings(mocker):
"""
Given:
- A TAXIIClient that polls a collection.
When:
- The _poll_collection method invokes etree.iterparse.
Then:
- Verify that safe XML parser settings are used:
resolve_entities=False, load_dtd=False, no_network=True.
"""
from datetime import datetime, UTC
mock_iterparse = mocker.patch("FeedTAXII.etree.iterparse", return_value=iter([]))
mocker.patch.object(demisto, "getLastRun", return_value={})
mock_response = mocker.MagicMock()
mock_response.raw = mocker.MagicMock()
mock_response.raw.decode_content = True
client = TAXIIClient(
poll_service="https://test.com/poll",
collection="test_collection",
credentials={"identifier": "user", "password": "pass"},
cert_text=None,
key_text=None,
insecure=True,
)
mocker.patch.object(client, "_send_request", return_value=mock_response)
begin = datetime(2024, 1, 1, tzinfo=UTC)
end = datetime(2024, 1, 2, tzinfo=UTC)
# _poll_collection is a generator; exhaust it to trigger iterparse call
list(client._poll_collection("https://test.com/poll", begin, end))
mock_iterparse.assert_called_once()
call_kwargs = mock_iterparse.call_args.kwargs
assert call_kwargs.get("resolve_entities") is False, "resolve_entities should be False"
assert call_kwargs.get("load_dtd") is False, "load_dtd should be False"
assert call_kwargs.get("no_network") is True, "no_network should be True"