handle TAXII client/server errors according to decided policy
parent
3f80c07342
commit
2392912533
|
@ -51,7 +51,27 @@ class TAXIICollectionSink(DataSink):
|
||||||
"""
|
"""
|
||||||
def __init__(self, collection, allow_custom=False):
|
def __init__(self, collection, allow_custom=False):
|
||||||
super(TAXIICollectionSink, self).__init__()
|
super(TAXIICollectionSink, self).__init__()
|
||||||
self.collection = collection
|
try:
|
||||||
|
# we have to execute .can_write first in isolation because the
|
||||||
|
# attribute access could trigger a taxii2client.ValidationError which
|
||||||
|
# we catch here as a ValueError (its parent class). Later, we need to
|
||||||
|
# have the ability to also raise a different ValueError based on the
|
||||||
|
# value of .can_write
|
||||||
|
writeable = collection.can_write
|
||||||
|
|
||||||
|
except (HTTPError, ValueError) as e:
|
||||||
|
e.message = ("The underlying TAXII Collection resource defined in the supplied TAXII"
|
||||||
|
" Collection object provided could not be reached. TAXII Collection Error: "
|
||||||
|
+ e.message)
|
||||||
|
raise
|
||||||
|
|
||||||
|
if writeable:
|
||||||
|
# now past taxii2client possible exceptions, check value for local exceptions
|
||||||
|
self.collection = collection
|
||||||
|
else:
|
||||||
|
raise ValueError("The TAXII Collection object provided does not have write access"
|
||||||
|
" to the underlying linked Collection resource")
|
||||||
|
|
||||||
self.allow_custom = allow_custom
|
self.allow_custom = allow_custom
|
||||||
|
|
||||||
def add(self, stix_data, version=None):
|
def add(self, stix_data, version=None):
|
||||||
|
@ -111,7 +131,27 @@ class TAXIICollectionSource(DataSource):
|
||||||
"""
|
"""
|
||||||
def __init__(self, collection, allow_custom=True):
|
def __init__(self, collection, allow_custom=True):
|
||||||
super(TAXIICollectionSource, self).__init__()
|
super(TAXIICollectionSource, self).__init__()
|
||||||
self.collection = collection
|
try:
|
||||||
|
# we have to execute .can_read first in isolation because the
|
||||||
|
# attribute access could trigger a taxii2client.ValidationError which
|
||||||
|
# we catch here as a ValueError (its parent class). Later, we need to
|
||||||
|
# have the ability to also raise a different ValueError based on the
|
||||||
|
# value of .can_read
|
||||||
|
writeable = collection.can_read
|
||||||
|
|
||||||
|
except (HTTPError, ValueError) as e:
|
||||||
|
e.message = ("The underlying TAXII Collection resource defined in the supplied TAXII"
|
||||||
|
" Collection object provided could not be reached. TAXII Collection Error: "
|
||||||
|
+ e.message)
|
||||||
|
raise
|
||||||
|
|
||||||
|
if writeable:
|
||||||
|
# now past taxii2client possible exceptions, check value for local exceptions
|
||||||
|
self.collection = collection
|
||||||
|
else:
|
||||||
|
raise ValueError("The TAXII Collection object provided does not have read access"
|
||||||
|
" to the underlying linked Collection resource")
|
||||||
|
|
||||||
self.allow_custom = allow_custom
|
self.allow_custom = allow_custom
|
||||||
|
|
||||||
def get(self, stix_id, version=None, _composite_filters=None):
|
def get(self, stix_id, version=None, _composite_filters=None):
|
||||||
|
@ -145,9 +185,12 @@ class TAXIICollectionSource(DataSource):
|
||||||
stix_objs = self.collection.get_object(stix_id)["objects"]
|
stix_objs = self.collection.get_object(stix_id)["objects"]
|
||||||
stix_obj = list(apply_common_filters(stix_objs, query))
|
stix_obj = list(apply_common_filters(stix_objs, query))
|
||||||
|
|
||||||
except HTTPError:
|
except HTTPError as err:
|
||||||
# if resource not found or access is denied from TAXII server, return None
|
if err.response.status_code == 404:
|
||||||
stix_obj = []
|
# if resource not found or access is denied from TAXII server, return None
|
||||||
|
stix_obj = []
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
|
||||||
if len(stix_obj):
|
if len(stix_obj):
|
||||||
stix_obj = parse(stix_obj[0], allow_custom=self.allow_custom, version=version)
|
stix_obj = parse(stix_obj[0], allow_custom=self.allow_custom, version=version)
|
||||||
|
@ -231,13 +274,17 @@ class TAXIICollectionSource(DataSource):
|
||||||
# deduplicate data (before filtering as reduces wasted filtering)
|
# deduplicate data (before filtering as reduces wasted filtering)
|
||||||
all_data = deduplicate(all_data)
|
all_data = deduplicate(all_data)
|
||||||
|
|
||||||
# apply local (CompositeDataSource, TAXIICollectionSource and query) filters
|
# a pply local (CompositeDataSource, TAXIICollectionSource and query) filters
|
||||||
query.remove(taxii_filters)
|
query.remove(taxii_filters)
|
||||||
all_data = list(apply_common_filters(all_data, query))
|
all_data = list(apply_common_filters(all_data, query))
|
||||||
|
|
||||||
except HTTPError:
|
except HTTPError as err:
|
||||||
# if resources not found or access is denied from TAXII server, return empty list
|
# if resources not found or access is denied from TAXII server, return empty list
|
||||||
all_data = []
|
if err.response.status_code == 404:
|
||||||
|
err.message = ("The requested STIX objects for the TAXII Collection resource defined in"
|
||||||
|
" the supplied TAXII Collection object is either not found or access is"
|
||||||
|
" denied. Received error: " + err.message)
|
||||||
|
raise
|
||||||
|
|
||||||
# parse python STIX objects from the STIX object dicts
|
# parse python STIX objects from the STIX object dicts
|
||||||
stix_objs = [parse(stix_obj_dict, allow_custom=self.allow_custom, version=version) for stix_obj_dict in all_data]
|
stix_objs = [parse(stix_obj_dict, allow_custom=self.allow_custom, version=version) for stix_obj_dict in all_data]
|
||||||
|
|
|
@ -2,6 +2,8 @@ import json
|
||||||
|
|
||||||
from medallion.filters.basic_filter import BasicFilter
|
from medallion.filters.basic_filter import BasicFilter
|
||||||
import pytest
|
import pytest
|
||||||
|
from requests.exceptions import HTTPError
|
||||||
|
from requests.models import Response
|
||||||
from taxii2client import Collection, _filter_kwargs_to_query_params
|
from taxii2client import Collection, _filter_kwargs_to_query_params
|
||||||
|
|
||||||
from stix2 import (Bundle, TAXIICollectionSink, TAXIICollectionSource,
|
from stix2 import (Bundle, TAXIICollectionSink, TAXIICollectionSource,
|
||||||
|
@ -36,7 +38,12 @@ class MockTAXIICollectionEndpoint(Collection):
|
||||||
("id", "type", "version"),
|
("id", "type", "version"),
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
return Bundle(objects=objs)
|
if objs:
|
||||||
|
return Bundle(objects=objs)
|
||||||
|
else:
|
||||||
|
resp = Response()
|
||||||
|
resp.status_code = 404
|
||||||
|
resp.raise_for_status()
|
||||||
|
|
||||||
def get_object(self, id, version=None):
|
def get_object(self, id, version=None):
|
||||||
self._verify_can_read()
|
self._verify_can_read()
|
||||||
|
@ -51,7 +58,12 @@ class MockTAXIICollectionEndpoint(Collection):
|
||||||
("version",),
|
("version",),
|
||||||
[]
|
[]
|
||||||
)
|
)
|
||||||
return Bundle(objects=objs)
|
if objs:
|
||||||
|
return Bundle(objects=objs)
|
||||||
|
else:
|
||||||
|
resp = Response()
|
||||||
|
resp.status_code = 404
|
||||||
|
resp.raise_for_status()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
|
@ -71,6 +83,23 @@ def collection(stix_objs1):
|
||||||
return mock
|
return mock
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def collection_no_rw_access(stix_objs1):
|
||||||
|
mock = MockTAXIICollectionEndpoint(COLLECTION_URL, **{
|
||||||
|
"id": "91a7b528-80eb-42ed-a74d-c6fbd5a26116",
|
||||||
|
"title": "Not writeable or readable Collection",
|
||||||
|
"description": "This collection is a dropbox for submitting indicators",
|
||||||
|
"can_read": False,
|
||||||
|
"can_write": False,
|
||||||
|
"media_types": [
|
||||||
|
"application/vnd.oasis.stix+json; version=2.0"
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
mock.objects.extend(stix_objs1)
|
||||||
|
return mock
|
||||||
|
|
||||||
|
|
||||||
def test_ds_taxii(collection):
|
def test_ds_taxii(collection):
|
||||||
ds = TAXIICollectionSource(collection)
|
ds = TAXIICollectionSource(collection)
|
||||||
assert ds.collection is not None
|
assert ds.collection is not None
|
||||||
|
@ -292,3 +321,66 @@ def test_get_all_versions(collection):
|
||||||
indicators = ds.all_versions('indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f')
|
indicators = ds.all_versions('indicator--d81f86b9-975b-bc0b-775e-810c5ad45a4f')
|
||||||
# There are 3 indicators but 2 share the same 'modified' timestamp
|
# There are 3 indicators but 2 share the same 'modified' timestamp
|
||||||
assert len(indicators) == 2
|
assert len(indicators) == 2
|
||||||
|
|
||||||
|
|
||||||
|
def test_can_read_error(collection_no_rw_access):
|
||||||
|
"""create a TAXIICOllectionSource with a taxii2client.Collection
|
||||||
|
instance that does not have read access, check ValueError exception is raised"""
|
||||||
|
with pytest.raises(ValueError) as excinfo:
|
||||||
|
TAXIICollectionSource(collection_no_rw_access)
|
||||||
|
assert "Collection object provided does not have read access" in str(excinfo.value)
|
||||||
|
|
||||||
|
|
||||||
|
def test_can_write_error(collection_no_rw_access):
|
||||||
|
"""create a TAXIICOllectionSink with a taxii2client.Collection
|
||||||
|
instance that does not have write access, check ValueError exception is raised"""
|
||||||
|
with pytest.raises(ValueError) as excinfo:
|
||||||
|
TAXIICollectionSink(collection_no_rw_access)
|
||||||
|
assert "Collection object provided does not have write access" in str(excinfo.value)
|
||||||
|
|
||||||
|
|
||||||
|
def test_bad_collection():
|
||||||
|
"""this triggers a real connectivity issue (HTTPError: 503 ServerError) """
|
||||||
|
with pytest.raises(HTTPError) as excinfo:
|
||||||
|
mock = MockTAXIICollectionEndpoint("http://doenstexist118482.org", verify=False)
|
||||||
|
TAXIICollectionStore(mock)
|
||||||
|
assert "Collection object provided could not be reached. TAXII Collection Error:" in str(excinfo.value.message)
|
||||||
|
assert "HTTPError" in str(excinfo.type)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_404(collection):
|
||||||
|
"""a TAXIICollectionSource.get() call that receives an HTTP 404 response
|
||||||
|
code from the taxii2client should be be returned as None.
|
||||||
|
|
||||||
|
TAXII spec states that a TAXII server can return a 404 for
|
||||||
|
nonexistent resources or lack of access. Decided that None is acceptable
|
||||||
|
reponse to imply that state of the TAXII endpoint.
|
||||||
|
"""
|
||||||
|
ds = TAXIICollectionStore(collection)
|
||||||
|
|
||||||
|
# this will raise 404 from mock TAXII Client but TAXIICollectionStore
|
||||||
|
# should handle gracefully and return None
|
||||||
|
stix_obj = ds.get("indicator--1") # this will raise 404 from
|
||||||
|
assert stix_obj is None
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_versions_404(collection):
|
||||||
|
""" a TAXIICollectionSource.all_version() call that recieves an HTTP 404
|
||||||
|
response code from the taxii2client should be returned as an exception"""
|
||||||
|
ds = TAXIICollectionStore(collection)
|
||||||
|
with pytest.raises(HTTPError) as excinfo:
|
||||||
|
ds.all_versions("indicator--1")
|
||||||
|
assert "is either not found or access is denied" in str(excinfo.value.message)
|
||||||
|
assert "404" in str(excinfo.value)
|
||||||
|
|
||||||
|
|
||||||
|
def test_query_404(collection):
|
||||||
|
""" a TAXIICollectionSource.query() call that recieves an HTTP 404
|
||||||
|
response code from the taxii2client should be returned as an exception"""
|
||||||
|
ds = TAXIICollectionStore(collection)
|
||||||
|
query = [Filter("type", "=", "malware")]
|
||||||
|
|
||||||
|
with pytest.raises(HTTPError) as excinfo:
|
||||||
|
ds.query(query=query)
|
||||||
|
assert "is either not found or access is denied" in str(excinfo.value.message)
|
||||||
|
assert "404" in str(excinfo.value)
|
||||||
|
|
Loading…
Reference in New Issue