Skip to content

Commit c4fed72

Browse files
author
Samuel Hassine
committed
[client] New STIX 2.1 splitter
1 parent b629ed9 commit c4fed72

File tree

4 files changed

+284
-9
lines changed

4 files changed

+284
-9
lines changed

examples/import_stix2_file.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
opencti_api_client = OpenCTIApiClient(api_url, api_token)
1111

1212
# File to import
13-
file_to_import = "./test.json"
13+
file_to_import = "./enterprise-attack.json"
1414

1515
# Import the bundle
1616
opencti_api_client.stix2.import_bundle_from_file(file_to_import, True)

pycti/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
from .entities.opencti_indicator import Indicator
4242

4343
from .utils.opencti_stix2 import OpenCTIStix2
44+
from .utils.opencti_stix2_splitter import OpenCTIStix2Splitter
4445
from .utils.constants import StixCyberObservableTypes
4546

4647
__all__ = [
@@ -79,5 +80,6 @@
7980
"Opinion",
8081
"Indicator",
8182
"OpenCTIStix2",
83+
"OpenCTIStix2Splitter",
8284
"StixCyberObservableTypes",
8385
]

pycti/connector/opencti_connector_helper.py

+9-8
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from pika.exceptions import UnroutableError, NackError
1313
from pycti.api.opencti_api_client import OpenCTIApiClient
1414
from pycti.connector.opencti_connector import OpenCTIConnector
15-
15+
from pycti.utils.opencti_stix2_splitter import OpenCTIStix2Splitter
1616

1717
def get_config_variable(
1818
env_var: str, yaml_path: list, config: Dict = {}, isNumber: Optional[bool] = False
@@ -312,7 +312,8 @@ def send_stix2_bundle(
312312
if entities_types is None:
313313
entities_types = []
314314
if split:
315-
bundles = self.split_stix2_bundle(bundle)
315+
stix2_splitter = OpenCTIStix2Splitter()
316+
bundles = stix2_splitter.split_bundle(bundle)
316317
if len(bundles) == 0:
317318
raise ValueError("Nothing to import")
318319
pika_connection = pika.BlockingConnection(
@@ -459,13 +460,13 @@ def stix2_get_embedded_objects(self, item) -> dict:
459460
if object_marking_ref in self.cache_index:
460461
object_marking_refs.append(self.cache_index[object_marking_ref])
461462
# Created by ref
462-
created_by = None
463-
if "created_by" in item and item["created_by"] in self.cache_index:
464-
created_by = self.cache_index[item["created_by"]]
463+
created_by_ref = None
464+
if "created_by_ref" in item and item["created_by_ref"] in self.cache_index:
465+
created_by_ref = self.cache_index[item["created_by_ref"]]
465466

466467
return {
467468
"object_marking_refs": object_marking_refs,
468-
"created_by": created_by,
469+
"created_by_ref": created_by_ref,
469470
}
470471

471472
def stix2_get_entity_objects(self, entity) -> list:
@@ -481,8 +482,8 @@ def stix2_get_entity_objects(self, entity) -> list:
481482
# Get embedded objects
482483
embedded_objects = self.stix2_get_embedded_objects(entity)
483484
# Add created by ref
484-
if embedded_objects["created_by"] is not None:
485-
items.append(embedded_objects["created_by"])
485+
if embedded_objects["created_by_ref"] is not None:
486+
items.append(embedded_objects["created_by_ref"])
486487
# Add marking definitions
487488
if len(embedded_objects["object_marking_refs"]) > 0:
488489
items = items + embedded_objects["object_marking_refs"]

pycti/utils/opencti_stix2_splitter.py

+272
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
import json
2+
import uuid
3+
4+
5+
class OpenCTIStix2Splitter:
6+
def __init__(self):
7+
self.cache_index = {}
8+
self.cache_added = []
9+
self.entities = []
10+
self.relationships = []
11+
12+
def enlist_entity_element(self, item_id, raw_data):
13+
nb_deps = 0
14+
item = raw_data[item_id]
15+
is_marking = item["id"].startswith('marking-definition--')
16+
if "created_by_ref" in item and is_marking is False and self.cache_index.get(item["created_by_ref"]) is None:
17+
nb_deps += 1
18+
self.enlist_entity_element(item["created_by_ref"], raw_data)
19+
20+
if "object_refs" in item:
21+
for object_ref in item["object_refs"]:
22+
nb_deps += 1
23+
if self.cache_index.get(object_ref) is None:
24+
self.enlist_entity_element(object_ref, raw_data)
25+
26+
if "object_marking_refs" in item:
27+
for object_marking_ref in item["object_marking_refs"]:
28+
nb_deps += 1
29+
if self.cache_index.get(object_marking_ref) is None:
30+
self.enlist_entity_element(object_marking_ref, raw_data)
31+
32+
item["nb_deps"] = nb_deps
33+
self.entities.append(item)
34+
self.cache_index[item_id] = item # Put in cache
35+
36+
def enlist_relation_element(self, item_id, raw_data):
37+
nb_deps = 0
38+
item = raw_data[item_id]
39+
source = item["source_ref"]
40+
target = item["target_ref"]
41+
if source.startswith('relationship--'):
42+
nb_deps += 1
43+
if self.cache_index.get(source) is None:
44+
self.enlist_entity_element(target, raw_data)
45+
if target.startswith('relationship--'):
46+
nb_deps += 1
47+
if self.cache_index.get(target) is None:
48+
self.enlist_entity_element(target, raw_data)
49+
item["nb_deps"] = nb_deps
50+
self.relationships.append(item)
51+
self.cache_index[item_id] = item # Put in cache
52+
53+
def split_bundle(self, bundle) -> list:
54+
"""splits a valid stix2 bundle into a list of bundles
55+
56+
:param bundle: valid stix2 bundle
57+
:type bundle:
58+
:raises Exception: if data is not valid JSON
59+
:return: returns a list of bundles
60+
:rtype: list
61+
"""
62+
try:
63+
bundle_data = json.loads(bundle)
64+
except:
65+
raise Exception("File data is not a valid JSON")
66+
67+
raw_data = {}
68+
for item in bundle_data["objects"]:
69+
raw_data[item["id"]] = item
70+
71+
for item in bundle_data["objects"]:
72+
is_entity = item["type"] != "relationship"
73+
if is_entity:
74+
self.enlist_entity_element(item["id"], raw_data)
75+
76+
for item in bundle_data["objects"]:
77+
is_relation = item["type"] == "relationship"
78+
if is_relation:
79+
self.enlist_relation_element(item["id"], raw_data)
80+
81+
bundles = []
82+
for entity in self.entities:
83+
bundles.append(self.stix2_create_bundle([entity]))
84+
for relationship in self.relationships:
85+
bundles.append(self.stix2_create_bundle([relationship]))
86+
return bundles
87+
88+
# @deprecated
89+
def split_stix2_bundle(self, bundle) -> list:
90+
"""splits a valid stix2 bundle into a list of bundles
91+
92+
:param bundle: valid stix2 bundle
93+
:type bundle:
94+
:raises Exception: if data is not valid JSON
95+
:return: returns a list of bundles
96+
:rtype: list
97+
"""
98+
try:
99+
bundle_data = json.loads(bundle)
100+
except:
101+
raise Exception("File data is not a valid JSON")
102+
103+
# validation = validate_parsed_json(bundle_data)
104+
# if not validation.is_valid:
105+
# raise ValueError('The bundle is not a valid STIX2 JSON:' + bundle)
106+
107+
# Index all objects by id
108+
for item in bundle_data["objects"]:
109+
self.cache_index[item["id"]] = item
110+
111+
bundles = []
112+
# Reports must be handled because of object_refs
113+
for item in bundle_data["objects"]:
114+
if item["type"] == "report":
115+
items_to_send = self.stix2_deduplicate_objects(
116+
self.stix2_get_report_objects(item)
117+
)
118+
for item_to_send in items_to_send:
119+
self.cache_added.append(item_to_send["id"])
120+
bundles.append(self.stix2_create_bundle(items_to_send))
121+
122+
# Relationships not added in previous reports
123+
for item in bundle_data["objects"]:
124+
if item["type"] == "relationship" and item["id"] not in self.cache_added:
125+
items_to_send = self.stix2_deduplicate_objects(
126+
self.stix2_get_relationship_objects(item)
127+
)
128+
for item_to_send in items_to_send:
129+
self.cache_added.append(item_to_send["id"])
130+
bundles.append(self.stix2_create_bundle(items_to_send))
131+
132+
# Entities not added in previous reports and relationships
133+
for item in bundle_data["objects"]:
134+
if item["type"] != "relationship" and item["id"] not in self.cache_added:
135+
items_to_send = self.stix2_deduplicate_objects(
136+
self.stix2_get_entity_objects(item)
137+
)
138+
for item_to_send in items_to_send:
139+
self.cache_added.append(item_to_send["id"])
140+
bundles.append(self.stix2_create_bundle(items_to_send))
141+
142+
return bundles
143+
144+
@staticmethod
145+
def stix2_deduplicate_objects(items) -> list:
146+
"""deduplicate stix2 items
147+
148+
:param items: valid stix2 items
149+
:type items:
150+
:return: de-duplicated list of items
151+
:rtype: list
152+
"""
153+
154+
ids = []
155+
final_items = []
156+
for item in items:
157+
if item["id"] not in ids:
158+
final_items.append(item)
159+
ids.append(item["id"])
160+
return final_items
161+
162+
def stix2_get_report_objects(self, report) -> list:
163+
"""get a list of items for a stix2 report object
164+
165+
:param report: valid stix2 report object
166+
:type report:
167+
:return: list of items for a stix2 report object
168+
:rtype: list
169+
"""
170+
171+
items = [report]
172+
# Add all object refs
173+
for object_ref in report["object_refs"]:
174+
items.append(self.cache_index[object_ref])
175+
for item in items:
176+
if item["type"] == "relationship":
177+
items = items + self.stix2_get_relationship_objects(item)
178+
else:
179+
items = items + self.stix2_get_entity_objects(item)
180+
return items
181+
182+
@staticmethod
183+
def stix2_create_bundle(items):
184+
"""create a stix2 bundle with items
185+
186+
:param items: valid stix2 items
187+
:type items:
188+
:return: JSON of the stix2 bundle
189+
:rtype:
190+
"""
191+
192+
bundle = {
193+
"type": "bundle",
194+
"id": "bundle--" + str(uuid.uuid4()),
195+
"spec_version": "2.0",
196+
"objects": items,
197+
}
198+
return json.dumps(bundle)
199+
200+
def stix2_get_relationship_objects(self, relationship) -> list:
201+
"""get a list of relations for a stix2 relationship object
202+
203+
:param relationship: valid stix2 relationship
204+
:type relationship:
205+
:return: list of relations objects
206+
:rtype: list
207+
"""
208+
209+
items = [relationship]
210+
# Get source ref
211+
if relationship["source_ref"] in self.cache_index:
212+
items.append(self.cache_index[relationship["source_ref"]])
213+
214+
# Get target ref
215+
if relationship["target_ref"] in self.cache_index:
216+
items.append(self.cache_index[relationship["target_ref"]])
217+
218+
# Get embedded objects
219+
embedded_objects = self.stix2_get_embedded_objects(relationship)
220+
# Add created by ref
221+
if embedded_objects["created_by_ref"] is not None:
222+
items.append(embedded_objects["created_by_ref"])
223+
# Add marking definitions
224+
if len(embedded_objects["object_marking_refs"]) > 0:
225+
items = items + embedded_objects["object_marking_refs"]
226+
227+
return items
228+
229+
def stix2_get_embedded_objects(self, item) -> dict:
230+
"""gets created and marking refs for a stix2 item
231+
232+
:param item: valid stix2 item
233+
:type item:
234+
:return: returns a dict of created_by_ref of object_marking_refs
235+
:rtype: dict
236+
"""
237+
# Marking definitions
238+
object_marking_refs = []
239+
if "object_marking_refs" in item:
240+
for object_marking_ref in item["object_marking_refs"]:
241+
if object_marking_ref in self.cache_index:
242+
object_marking_refs.append(self.cache_index[object_marking_ref])
243+
# Created by ref
244+
created_by_ref = None
245+
if "created_by_ref" in item and item["created_by_ref"] in self.cache_index:
246+
created_by_ref = self.cache_index[item["created_by_ref"]]
247+
248+
return {
249+
"object_marking_refs": object_marking_refs,
250+
"created_by_ref": created_by_ref,
251+
}
252+
253+
def stix2_get_entity_objects(self, entity) -> list:
254+
"""process a stix2 entity
255+
256+
:param entity: valid stix2 entity
257+
:type entity:
258+
:return: entity objects as list
259+
:rtype: list
260+
"""
261+
262+
items = [entity]
263+
# Get embedded objects
264+
embedded_objects = self.stix2_get_embedded_objects(entity)
265+
# Add created by ref
266+
if embedded_objects["created_by_ref"] is not None:
267+
items.append(embedded_objects["created_by_ref"])
268+
# Add marking definitions
269+
if len(embedded_objects["object_marking_refs"]) > 0:
270+
items = items + embedded_objects["object_marking_refs"]
271+
272+
return items

0 commit comments

Comments
 (0)