Skip to content

Commit 002c9e7

Browse files
committed
Add support for lists as dict values. Code attempts to support
array columns and auxiliary tables (for those DBs which don't support array columns), but it is only tested with postgres, which supports array columns. The aux table code is still untested.
1 parent 9653bd7 commit 002c9e7

File tree

1 file changed

+73
-11
lines changed

1 file changed

+73
-11
lines changed

stix2/datastore/relational_db/query.py

Lines changed: 73 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import collections
12
import inspect
23

34
import sqlalchemy as sa
@@ -280,7 +281,15 @@ def _read_kill_chain_phases(stix_id, type_table, metadata, conn):
280281
return kill_chain_phases
281282

282283

283-
def _read_dictionary_property(stix_id, type_table, prop_name, prop_instance, metadata, conn):
284+
def _read_dictionary_property(
285+
stix_id,
286+
type_table,
287+
prop_name,
288+
prop_instance,
289+
metadata,
290+
conn,
291+
db_backend
292+
):
284293
"""
285294
Read a dictionary from a table.
286295
@@ -292,23 +301,57 @@ def _read_dictionary_property(stix_id, type_table, prop_name, prop_instance, met
292301
:param metadata: SQLAlchemy Metadata object containing all the table
293302
information
294303
:param conn: An SQLAlchemy DB connection
304+
:param db_backend: A backend object with information about how data is
305+
stored in the database
295306
:return: The dictionary, or None if no dictionary entries were found
296307
"""
297308
dict_table_name = f"{type_table.fullname}_{prop_name}"
298309
dict_table = metadata.tables[dict_table_name]
299310

300311
if len(prop_instance.valid_types) == 1:
301-
stmt = sa.select(
302-
dict_table.c.name, dict_table.c.value,
303-
).where(
304-
dict_table.c.id == stix_id,
305-
)
312+
valid_type = prop_instance.valid_types[0]
313+
314+
if isinstance(valid_type, stix2.properties.ListProperty):
315+
if db_backend.array_allowed():
316+
stmt = sa.select(
317+
dict_table.c.name, dict_table.c["values"],
318+
).where(
319+
dict_table.c.id == stix_id,
320+
)
306321

307-
results = conn.execute(stmt)
308-
dict_value = dict(results.all())
322+
results = conn.execute(stmt)
323+
dict_value = dict(results.all())
324+
325+
else:
326+
# Dict contains a list, but array columns are not supported.
327+
# So query from an auxiliary table.
328+
list_table_name = f"{dict_table_name}_values"
329+
list_table = metadata.tables[list_table_name]
330+
stmt = sa.select(
331+
dict_table.c.name, list_table.c.value
332+
).select_from(dict_table).join(
333+
list_table, list_table.c.id == dict_table.c.values
334+
).where(
335+
dict_table.c.id == stix_id
336+
)
337+
338+
results = conn.execute(stmt)
339+
dict_value = collections.defaultdict(list)
340+
for key, value in results:
341+
dict_value[key].append(value)
342+
343+
else:
344+
stmt = sa.select(
345+
dict_table.c.name, dict_table.c.value,
346+
).where(
347+
dict_table.c.id == stix_id,
348+
)
349+
350+
results = conn.execute(stmt)
351+
dict_value = dict(results.all())
309352

310353
else:
311-
# In this case, we get one column per valid type
354+
# In this case, we get one column per valid type (assume no lists here)
312355
type_cols = (col for col in dict_table.c if col.key not in ("id", "name"))
313356
stmt = sa.select(dict_table.c.name, *type_cols).where(dict_table.c.id == stix_id)
314357
results = conn.execute(stmt)
@@ -437,7 +480,15 @@ def _read_embedded_object_list(fk_id, join_table, embedded_type, metadata, conn)
437480
return obj_list
438481

439482

440-
def _read_complex_property_value(obj_id, prop_name, prop_instance, obj_table, metadata, conn):
483+
def _read_complex_property_value(
484+
obj_id,
485+
prop_name,
486+
prop_instance,
487+
obj_table,
488+
metadata,
489+
conn,
490+
db_backend
491+
):
441492
"""
442493
Read property values which require auxiliary tables to store. These are
443494
idiosyncratic and just require a lot of special cases. This function has
@@ -456,6 +507,8 @@ def _read_complex_property_value(obj_id, prop_name, prop_instance, obj_table, me
456507
:param metadata: SQLAlchemy Metadata object containing all the table
457508
information
458509
:param conn: An SQLAlchemy DB connection
510+
:param db_backend: A backend object with information about how data is
511+
stored in the database
459512
:return: The property value
460513
"""
461514

@@ -523,7 +576,15 @@ def _read_complex_property_value(obj_id, prop_name, prop_instance, obj_table, me
523576
elif isinstance(prop_instance, stix2.properties.DictionaryProperty):
524577
# ExtensionsProperty/HashesProperty subclasses DictionaryProperty, so
525578
# this must come after those
526-
prop_value = _read_dictionary_property(obj_id, obj_table, prop_name, prop_instance, metadata, conn)
579+
prop_value = _read_dictionary_property(
580+
obj_id,
581+
obj_table,
582+
prop_name,
583+
prop_instance,
584+
metadata,
585+
conn,
586+
db_backend
587+
)
527588

528589
elif isinstance(prop_instance, stix2.properties.EmbeddedObjectProperty):
529590
prop_value = _read_embedded_object(
@@ -611,6 +672,7 @@ def _read_complex_top_level_property_value(
611672
type_table,
612673
metadata,
613674
conn,
675+
db_backend
614676
)
615677

616678
return prop_value

0 commit comments

Comments
 (0)