Skip to content

Commit 88f5fa8

Browse files
authored
Merge pull request #610 from chisholm/query-db-backend
relational data source: support db backends, db's which don't support array columns
2 parents 04f1c55 + 9b9ca63 commit 88f5fa8

File tree

3 files changed

+129
-37
lines changed

3 files changed

+129
-37
lines changed

stix2/datastore/relational_db/query.py

Lines changed: 118 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,22 @@ def _read_simple_properties(stix_id, core_table, type_table, conn):
9999
return obj_dict
100100

101101

102+
def _read_simple_array(fk_id, elt_column_name, array_table, conn):
103+
"""
104+
Read array elements from a given table.
105+
106+
:param fk_id: A foreign key value used to find the correct array elements
107+
:param elt_column_name: The name of the table column which contains the
108+
array elements
109+
:param array_table: A SQLAlchemy Table object containing the array data
110+
:param conn: An SQLAlchemy DB connection
111+
:return: The array, as a list
112+
"""
113+
stmt = sa.select(array_table.c[elt_column_name]).where(array_table.c.id == fk_id)
114+
refs = conn.scalars(stmt).all()
115+
return refs
116+
117+
102118
def _read_hashes(fk_id, hashes_table, conn):
103119
"""
104120
Read hashes from a table.
@@ -178,7 +194,7 @@ def _read_object_marking_refs(stix_id, stix_type_class, metadata, conn):
178194
return refs
179195

180196

181-
def _read_granular_markings(stix_id, stix_type_class, metadata, conn):
197+
def _read_granular_markings(stix_id, stix_type_class, metadata, conn, db_backend):
182198
"""
183199
Read granular markings from one of a couple special tables in the common
184200
schema.
@@ -189,6 +205,8 @@ def _read_granular_markings(stix_id, stix_type_class, metadata, conn):
189205
:param metadata: SQLAlchemy Metadata object containing all the table
190206
information
191207
:param conn: An SQLAlchemy DB connection
208+
:param db_backend: A backend object with information about how data is
209+
stored in the database
192210
:return: Granular markings as a list of dicts
193211
"""
194212

@@ -200,30 +218,43 @@ def _read_granular_markings(stix_id, stix_type_class, metadata, conn):
200218

201219
marking_table = metadata.tables["common." + marking_table_name]
202220

203-
stmt = sa.select(
204-
marking_table.c.lang,
205-
marking_table.c.marking_ref,
206-
marking_table.c.selectors,
207-
).where(marking_table.c.id == stix_id)
208-
209-
marking_dicts = conn.execute(stmt).mappings().all()
210-
return marking_dicts
221+
if db_backend.array_allowed():
222+
# arrays allowed: everything combined in the same table
223+
stmt = sa.select(
224+
marking_table.c.lang,
225+
marking_table.c.marking_ref,
226+
marking_table.c.selectors,
227+
).where(marking_table.c.id == stix_id)
211228

229+
marking_dicts = conn.execute(stmt).mappings().all()
212230

213-
def _read_simple_array(fk_id, elt_column_name, array_table, conn):
214-
"""
215-
Read array elements from a given table.
231+
else:
232+
# arrays not allowed: selectors are in their own table
233+
stmt = sa.select(
234+
marking_table.c.lang,
235+
marking_table.c.marking_ref,
236+
marking_table.c.selectors,
237+
).where(marking_table.c.id == stix_id)
238+
239+
marking_dicts = list(conn.execute(stmt).mappings())
240+
241+
for idx, marking_dict in enumerate(marking_dicts):
242+
# make a mutable shallow-copy of the row mapping
243+
marking_dicts[idx] = marking_dict = dict(marking_dict)
244+
selector_id = marking_dict.pop("selectors")
245+
246+
selector_table_name = f"{marking_table.fullname}_selector"
247+
selector_table = metadata.tables[selector_table_name]
248+
249+
selectors = _read_simple_array(
250+
selector_id,
251+
"selector",
252+
selector_table,
253+
conn
254+
)
255+
marking_dict["selectors"] = selectors
216256

217-
:param fk_id: A foreign key value used to find the correct array elements
218-
:param elt_column_name: The name of the table column which contains the
219-
array elements
220-
:param array_table: A SQLAlchemy Table object containing the array data
221-
:param conn: An SQLAlchemy DB connection
222-
:return: The array, as a list
223-
"""
224-
stmt = sa.select(array_table.c[elt_column_name]).where(array_table.c.id == fk_id)
225-
refs = conn.scalars(stmt).all()
226-
return refs
257+
return marking_dicts
227258

228259

229260
def _read_kill_chain_phases(stix_id, type_table, metadata, conn):
@@ -437,10 +468,26 @@ def _read_complex_property_value(obj_id, prop_name, prop_instance, obj_table, me
437468
ref_table = metadata.tables[ref_table_name]
438469
prop_value = _read_simple_array(obj_id, "ref_id", ref_table, conn)
439470

440-
elif isinstance(prop_instance.contained, stix2.properties.EnumProperty):
441-
enum_table_name = f"{obj_table.fullname}_{prop_name}"
442-
enum_table = metadata.tables[enum_table_name]
443-
prop_value = _read_simple_array(obj_id, prop_name, enum_table, conn)
471+
elif isinstance(prop_instance.contained, (
472+
# Most of these list-of-simple-type cases would occur when array
473+
# columns are disabled.
474+
stix2.properties.BinaryProperty,
475+
stix2.properties.BooleanProperty,
476+
stix2.properties.EnumProperty,
477+
stix2.properties.HexProperty,
478+
stix2.properties.IntegerProperty,
479+
stix2.properties.FloatProperty,
480+
stix2.properties.StringProperty,
481+
stix2.properties.TimestampProperty,
482+
)):
483+
array_table_name = f"{obj_table.fullname}_{prop_name}"
484+
array_table = metadata.tables[array_table_name]
485+
prop_value = _read_simple_array(
486+
obj_id,
487+
prop_name,
488+
array_table,
489+
conn
490+
)
444491

445492
elif isinstance(prop_instance.contained, stix2.properties.EmbeddedObjectProperty):
446493
join_table_name = f"{obj_table.fullname}_{prop_name}"
@@ -494,7 +541,16 @@ def _read_complex_property_value(obj_id, prop_name, prop_instance, obj_table, me
494541
return prop_value
495542

496543

497-
def _read_complex_top_level_property_value(stix_id, stix_type_class, prop_name, prop_instance, type_table, metadata, conn):
544+
def _read_complex_top_level_property_value(
545+
stix_id,
546+
stix_type_class,
547+
prop_name,
548+
prop_instance,
549+
type_table,
550+
metadata,
551+
conn,
552+
db_backend
553+
):
498554
"""
499555
Read property values which require auxiliary tables to store. These
500556
require a lot of special cases. This function has additional support for
@@ -511,6 +567,8 @@ def _read_complex_top_level_property_value(stix_id, stix_type_class, prop_name,
511567
:param metadata: SQLAlchemy Metadata object containing all the table
512568
information
513569
:param conn: An SQLAlchemy DB connection
570+
:param db_backend: A backend object with information about how data is
571+
stored in the database
514572
:return: The property value
515573
"""
516574

@@ -519,26 +577,53 @@ def _read_complex_top_level_property_value(stix_id, stix_type_class, prop_name,
519577
prop_value = _read_external_references(stix_id, metadata, conn)
520578

521579
elif prop_name == "object_marking_refs":
522-
prop_value = _read_object_marking_refs(stix_id, stix_type_class, metadata, conn)
580+
prop_value = _read_object_marking_refs(
581+
stix_id,
582+
stix_type_class,
583+
metadata,
584+
conn
585+
)
523586

524587
elif prop_name == "granular_markings":
525-
prop_value = _read_granular_markings(stix_id, stix_type_class, metadata, conn)
588+
prop_value = _read_granular_markings(
589+
stix_id,
590+
stix_type_class,
591+
metadata,
592+
conn,
593+
db_backend
594+
)
595+
596+
# Will apply when array columns are unsupported/disallowed by the backend
597+
elif prop_name == "labels":
598+
label_table = metadata.tables[
599+
f"common.core_{stix_type_class.name.lower()}_labels"
600+
]
601+
prop_value = _read_simple_array(stix_id, "label", label_table, conn)
526602

527603
else:
528604
# Other properties use specific table patterns depending on property type
529-
prop_value = _read_complex_property_value(stix_id, prop_name, prop_instance, type_table, metadata, conn)
605+
prop_value = _read_complex_property_value(
606+
stix_id,
607+
prop_name,
608+
prop_instance,
609+
type_table,
610+
metadata,
611+
conn
612+
)
530613

531614
return prop_value
532615

533616

534-
def read_object(stix_id, metadata, conn):
617+
def read_object(stix_id, metadata, conn, db_backend):
535618
"""
536619
Read a STIX object from the database, identified by a STIX ID.
537620
538621
:param stix_id: A STIX ID
539622
:param metadata: SQLAlchemy Metadata object containing all the table
540623
information
541624
:param conn: An SQLAlchemy DB connection
625+
:param db_backend: A backend object with information about how data is
626+
stored in the database
542627
:return: A STIX object
543628
"""
544629
_check_support(stix_id)
@@ -554,7 +639,7 @@ def read_object(stix_id, metadata, conn):
554639
if type_table.schema == "common":
555640
# Applies to extension-definition SMO, whose data is stored in the
556641
# common schema; it does not get its own. This type class is used to
557-
# determine which markings tables to use; its markings are
642+
# determine which common tables to use; its markings are
558643
# in the *_sdo tables.
559644
stix_type_class = stix2.utils.STIXTypeClass.SDO
560645
else:
@@ -578,6 +663,7 @@ def read_object(stix_id, metadata, conn):
578663
type_table,
579664
metadata,
580665
conn,
666+
db_backend
581667
)
582668

583669
if prop_value is not None:

stix2/datastore/relational_db/relational_db.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,10 +195,11 @@ def __init__(
195195
Initialize this source. Only one of stix_object_classes and metadata
196196
should be given: if the latter is given, assume table schemas are
197197
already created. Instances of this class do not create the actual
198-
database tables; see the source/sink for that.
198+
database tables; see the store/sink for that.
199199
200200
Args:
201-
database_connection_or_url: An SQLAlchemy engine object, or URL
201+
db_backend: A database backend object
202+
allow_custom: TODO: unused so far
202203
*stix_object_classes: STIX object classes to map into table schemas.
203204
This can be used to limit which schemas are created, if one is
204205
only working with a subset of STIX types. If not given,
@@ -230,6 +231,7 @@ def get(self, stix_id, version=None, _composite_filters=None):
230231
stix_id,
231232
self.metadata,
232233
conn,
234+
self.db_backend,
233235
)
234236

235237
return stix_obj

stix2/test/v21/test_datastore_relational_db.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77

88
import stix2
99
from stix2.datastore import DataSourceError
10+
from stix2.datastore.relational_db.database_backends.postgres_backend import (
11+
PostgresBackend,
12+
)
1013
from stix2.datastore.relational_db.relational_db import RelationalDBStore
1114
import stix2.properties
1215
import stix2.registry
@@ -15,7 +18,7 @@
1518
_DB_CONNECT_URL = f"postgresql://{os.getenv('POSTGRES_USER', 'postgres')}:{os.getenv('POSTGRES_PASSWORD', 'postgres')}@0.0.0.0:5432/postgres"
1619

1720
store = RelationalDBStore(
18-
_DB_CONNECT_URL,
21+
PostgresBackend(_DB_CONNECT_URL, True),
1922
True,
2023
None,
2124
False,
@@ -878,7 +881,7 @@ def test_property(object_variation):
878881
ensure schemas can be created and values can be stored and retrieved.
879882
"""
880883
rdb_store = RelationalDBStore(
881-
_DB_CONNECT_URL,
884+
PostgresBackend(_DB_CONNECT_URL, True),
882885
True,
883886
None,
884887
True,
@@ -918,7 +921,7 @@ def test_dictionary_property_complex():
918921
)
919922

920923
rdb_store = RelationalDBStore(
921-
_DB_CONNECT_URL,
924+
PostgresBackend(_DB_CONNECT_URL, True),
922925
True,
923926
None,
924927
True,
@@ -934,6 +937,7 @@ def test_dictionary_property_complex():
934937
def test_extension_definition():
935938
obj = stix2.ExtensionDefinition(
936939
created_by_ref="identity--8a5fb7e4-aabe-4635-8972-cbcde1fa4792",
940+
labels=["label1", "label2"],
937941
name="test",
938942
schema="a schema",
939943
version="1.2.3",

0 commit comments

Comments
 (0)