Skip to content

Commit 3803e4b

Browse files
authored
Merge pull request #343 from chisholm/sco_tlo_filesystemstore
Fix the filesystem store to support the new top-level 2.1 SCOs.
2 parents 67548a8 + 4c67142 commit 3803e4b

File tree

3 files changed

+133
-40
lines changed

3 files changed

+133
-40
lines changed

stix2/datastore/filesystem.py

Lines changed: 70 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
DataSink, DataSource, DataSourceError, DataStoreMixin,
1616
)
1717
from stix2.datastore.filters import Filter, FilterSet, apply_common_filters
18-
from stix2.utils import format_datetime, get_type_from_id, is_marking
18+
from stix2.utils import format_datetime, get_type_from_id
1919

2020

2121
def _timestamp2filename(timestamp):
@@ -329,11 +329,50 @@ def _check_object_from_file(query, filepath, allow_custom, version, encoding):
329329
return result
330330

331331

332+
def _is_versioned_type_dir(type_path, type_name):
333+
"""
334+
Try to detect whether the given directory is for a versioned type of STIX
335+
object. This is done by looking for a directory whose name is a STIX ID
336+
of the appropriate type. If found, treat this type as versioned. This
337+
doesn't work when a versioned type directory is empty (it will be
338+
mis-classified as unversioned), but this detection is only necessary when
339+
reading/querying data. If a directory is empty, you'll get no results
340+
either way.
341+
342+
Args:
343+
type_path: A path to a directory containing one type of STIX object.
344+
type_name: The STIX type name.
345+
346+
Returns:
347+
True if the directory looks like it contains versioned objects; False
348+
if not.
349+
350+
Raises:
351+
OSError: If there are errors accessing directory contents or stat()'ing
352+
files
353+
"""
354+
id_regex = re.compile(
355+
r"^" + re.escape(type_name) +
356+
r"--[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}"
357+
r"-[0-9a-f]{12}$",
358+
re.I,
359+
)
360+
361+
for entry in os.listdir(type_path):
362+
s = os.stat(os.path.join(type_path, entry))
363+
if stat.S_ISDIR(s.st_mode) and id_regex.match(entry):
364+
is_versioned = True
365+
break
366+
else:
367+
is_versioned = False
368+
369+
return is_versioned
370+
371+
332372
def _search_versioned(query, type_path, auth_ids, allow_custom, version, encoding):
333373
"""
334374
Searches the given directory, which contains data for STIX objects of a
335-
particular versioned type (i.e. not markings), and return any which match
336-
the query.
375+
particular versioned type, and return any which match the query.
337376
338377
Args:
339378
query: The query to match against
@@ -390,36 +429,24 @@ def _search_versioned(query, type_path, auth_ids, allow_custom, version, encodin
390429

391430
# For backward-compatibility, also search for plain files named after
392431
# object IDs, in the type directory.
393-
id_files = _get_matching_dir_entries(
394-
type_path, auth_ids, stat.S_ISREG,
395-
".json",
432+
backcompat_results = _search_unversioned(
433+
query, type_path, auth_ids, allow_custom, version, encoding,
396434
)
397-
for id_file in id_files:
398-
id_path = os.path.join(type_path, id_file)
399-
400-
try:
401-
stix_obj = _check_object_from_file(
402-
query, id_path, allow_custom,
403-
version, encoding,
404-
)
405-
if stix_obj:
406-
results.append(stix_obj)
407-
except IOError as e:
408-
if e.errno != errno.ENOENT:
409-
raise
410-
# else, file-not-found is ok, just skip
435+
results.extend(backcompat_results)
411436

412437
return results
413438

414439

415-
def _search_markings(query, markings_path, auth_ids, allow_custom, version, encoding):
440+
def _search_unversioned(
441+
query, type_path, auth_ids, allow_custom, version, encoding,
442+
):
416443
"""
417-
Searches the given directory, which contains markings data, and return any
418-
which match the query.
444+
Searches the given directory, which contains unversioned data, and return
445+
any objects which match the query.
419446
420447
Args:
421448
query: The query to match against
422-
markings_path: The directory with STIX markings files
449+
type_path: The directory with STIX files of unversioned type
423450
auth_ids: Search optimization based on object ID
424451
allow_custom (bool): Whether to allow custom properties as well unknown
425452
custom objects.
@@ -441,11 +468,11 @@ def _search_markings(query, markings_path, auth_ids, allow_custom, version, enco
441468
"""
442469
results = []
443470
id_files = _get_matching_dir_entries(
444-
markings_path, auth_ids, stat.S_ISREG,
471+
type_path, auth_ids, stat.S_ISREG,
445472
".json",
446473
)
447474
for id_file in id_files:
448-
id_path = os.path.join(markings_path, id_file)
475+
id_path = os.path.join(type_path, id_file)
449476

450477
try:
451478
stix_obj = _check_object_from_file(
@@ -530,12 +557,14 @@ def _check_path_and_write(self, stix_obj, encoding='utf-8'):
530557
"""Write the given STIX object to a file in the STIX file directory.
531558
"""
532559
type_dir = os.path.join(self._stix_dir, stix_obj["type"])
533-
if is_marking(stix_obj):
534-
filename = stix_obj["id"]
535-
obj_dir = type_dir
536-
else:
560+
561+
# All versioned objects should have a "modified" property.
562+
if "modified" in stix_obj:
537563
filename = _timestamp2filename(stix_obj["modified"])
538564
obj_dir = os.path.join(type_dir, stix_obj["id"])
565+
else:
566+
filename = stix_obj["id"]
567+
obj_dir = type_dir
539568

540569
file_path = os.path.join(obj_dir, filename + ".json")
541570

@@ -649,12 +678,14 @@ def get(self, stix_id, version=None, _composite_filters=None):
649678
all_data = self.all_versions(stix_id, version=version, _composite_filters=_composite_filters)
650679

651680
if all_data:
652-
if is_marking(stix_id):
653-
# Markings are unversioned; there shouldn't be more than one
654-
# result.
655-
stix_obj = all_data[0]
656-
else:
681+
# Simple check for a versioned STIX type: see if the objects have a
682+
# "modified" property. (Need only check one, since they are all of
683+
# the same type.)
684+
is_versioned = "modified" in all_data[0]
685+
if is_versioned:
657686
stix_obj = sorted(all_data, key=lambda k: k['modified'])[-1]
687+
else:
688+
stix_obj = all_data[0]
658689
else:
659690
stix_obj = None
660691

@@ -720,14 +751,15 @@ def query(self, query=None, version=None, _composite_filters=None):
720751
)
721752
for type_dir in type_dirs:
722753
type_path = os.path.join(self._stix_dir, type_dir)
723-
if type_dir == "marking-definition":
724-
type_results = _search_markings(
754+
type_is_versioned = _is_versioned_type_dir(type_path, type_dir)
755+
if type_is_versioned:
756+
type_results = _search_versioned(
725757
query, type_path, auth_ids,
726758
self.allow_custom, version,
727759
self.encoding,
728760
)
729761
else:
730-
type_results = _search_versioned(
762+
type_results = _search_unversioned(
731763
query, type_path, auth_ids,
732764
self.allow_custom, version,
733765
self.encoding,
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"ctime": "2020-10-06T01:54:32.000Z",
3+
"contains_refs": [
4+
"directory--80539e31-85f3-4304-bd14-e2e8c10859a5",
5+
"file--e9e03175-0357-41b5-a2aa-eb99b455cd0c",
6+
"directory--f6c54233-027b-4464-8126-da1324d8f66c"
7+
],
8+
"path": "/performance/Democrat.gif",
9+
"type": "directory",
10+
"id": "directory--572827aa-e0cd-44fd-afd5-a717a7585f39"
11+
}

stix2/test/v21/test_datastore_filesystem.py

Lines changed: 52 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,16 @@ def test_filesystem_source_backward_compatible(fs_source):
221221
assert result.malware_types == ["version four"]
222222

223223

224+
def test_filesystem_source_sco(fs_source):
225+
results = fs_source.query([stix2.Filter("type", "=", "directory")])
226+
227+
assert len(results) == 1
228+
result = results[0]
229+
assert result["type"] == "directory"
230+
assert result["id"] == "directory--572827aa-e0cd-44fd-afd5-a717a7585f39"
231+
assert result["path"] == "/performance/Democrat.gif"
232+
233+
224234
def test_filesystem_sink_add_python_stix_object(fs_sink, fs_source):
225235
# add python stix object
226236
camp1 = stix2.v21.Campaign(
@@ -435,6 +445,24 @@ def test_filesystem_sink_marking(fs_sink):
435445
os.remove(marking_filepath)
436446

437447

448+
def test_filesystem_sink_sco(fs_sink):
449+
file_sco = {
450+
"type": "file",
451+
"id": "file--decfcc48-31b3-45f5-87c8-1b3a5d71a307",
452+
"name": "cats.png",
453+
}
454+
455+
fs_sink.add(file_sco)
456+
sco_filepath = os.path.join(
457+
FS_PATH, "file", file_sco["id"] + ".json",
458+
)
459+
460+
assert os.path.exists(sco_filepath)
461+
462+
os.remove(sco_filepath)
463+
os.rmdir(os.path.dirname(sco_filepath))
464+
465+
438466
def test_filesystem_store_get_stored_as_bundle(fs_store):
439467
coa = fs_store.get("course-of-action--95ddb356-7ba0-4bd9-a889-247262b8946f")
440468
assert coa.id == "course-of-action--95ddb356-7ba0-4bd9-a889-247262b8946f"
@@ -473,9 +501,10 @@ def test_filesystem_store_query_single_filter(fs_store):
473501

474502
def test_filesystem_store_empty_query(fs_store):
475503
results = fs_store.query() # returns all
476-
assert len(results) == 30
504+
assert len(results) == 31
477505
assert "tool--242f3da3-4425-4d11-8f5c-b842886da966" in [obj.id for obj in results]
478506
assert "marking-definition--fa42a846-8d90-4e51-bc29-71d5b4802168" in [obj.id for obj in results]
507+
assert "directory--572827aa-e0cd-44fd-afd5-a717a7585f39" in [obj.id for obj in results]
479508

480509

481510
def test_filesystem_store_query_multiple_filters(fs_store):
@@ -487,7 +516,7 @@ def test_filesystem_store_query_multiple_filters(fs_store):
487516

488517
def test_filesystem_store_query_dont_include_type_folder(fs_store):
489518
results = fs_store.query(stix2.Filter("type", "!=", "tool"))
490-
assert len(results) == 28
519+
assert len(results) == 29
491520

492521

493522
def test_filesystem_store_add(fs_store):
@@ -574,6 +603,26 @@ def test_filesystem_store_add_marking(fs_store):
574603
os.remove(marking_filepath)
575604

576605

606+
def test_filesystem_store_add_sco(fs_store):
607+
sco = stix2.v21.EmailAddress(
608+
value="jdoe@example.com",
609+
)
610+
611+
fs_store.add(sco)
612+
sco_filepath = os.path.join(
613+
FS_PATH, "email-addr", sco["id"] + ".json",
614+
)
615+
616+
assert os.path.exists(sco_filepath)
617+
618+
sco_r = fs_store.get(sco["id"])
619+
assert sco_r["id"] == sco["id"]
620+
assert sco_r["value"] == sco["value"]
621+
622+
os.remove(sco_filepath)
623+
os.rmdir(os.path.dirname(sco_filepath))
624+
625+
577626
def test_filesystem_object_with_custom_property(fs_store):
578627
camp = stix2.v21.Campaign(
579628
name="Scipio Africanus",
@@ -1024,6 +1073,7 @@ def test_search_auth_set_black_empty(rel_fs_store):
10241073
"attack-pattern",
10251074
"campaign",
10261075
"course-of-action",
1076+
"directory",
10271077
"identity",
10281078
"indicator",
10291079
"intrusion-set",

0 commit comments

Comments
 (0)