Skip to content
This repository was archived by the owner on Dec 5, 2025. It is now read-only.

Commit 6cf1fed

Browse files
author
Samuel Hassine
committed
#6 Handle observables export as STIX2 indicators
1 parent c9f4496 commit 6cf1fed

File tree

4 files changed

+99
-20
lines changed

4 files changed

+99
-20
lines changed

pycti/opencti.py

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import uuid
99
import base64
1010

11-
from pycti.stix2 import Stix2
11+
from pycti.opencti_stix2 import OpenCTIStix2
1212

1313

1414
class OpenCTI:
@@ -574,9 +574,22 @@ def get_stix_observable_by_id(self, id):
574574
query StixObservable($id: String!) {
575575
stixObservable(id: $id) {
576576
id
577+
name
578+
description
577579
stix_id
578580
entity_type
579581
observable_value
582+
created_at
583+
updated_at
584+
stixRelations {
585+
edges {
586+
node {
587+
id
588+
first_seen
589+
last_seen
590+
}
591+
}
592+
}
580593
}
581594
}
582595
"""
@@ -2781,7 +2794,10 @@ def get_stix_observable(self, id):
27812794
stix_id
27822795
entity_type
27832796
name
2797+
description
27842798
observable_value
2799+
created_at
2800+
updated_at
27852801
createdByRef {
27862802
node {
27872803
id
@@ -2863,7 +2879,10 @@ def get_stix_observable_by_value(self, observable_value):
28632879
stix_id
28642880
entity_type
28652881
name
2882+
description
28662883
observable_value
2884+
created_at
2885+
updated_at
28672886
createdByRef {
28682887
node {
28692888
id
@@ -2934,7 +2953,10 @@ def get_stix_observables(self, limit=10000):
29342953
stix_id
29352954
entity_type
29362955
name
2956+
description
29372957
observable_value
2958+
created_at
2959+
updated_at
29382960
createdByRef {
29392961
node {
29402962
id
@@ -3517,16 +3539,16 @@ def stix2_import_bundle_from_file(self, file_path, update=False, types=[]):
35173539
with open(os.path.join(file_path)) as file:
35183540
data = json.load(file)
35193541

3520-
stix2 = Stix2(self)
3542+
stix2 = OpenCTIStix2(self)
35213543
stix2.import_bundle(data, update, types)
35223544

35233545
def stix2_import_bundle(self, json_data, update=False, types=[]):
35243546
data = json.loads(json_data)
3525-
stix2 = Stix2(self)
3547+
stix2 = OpenCTIStix2(self)
35263548
stix2.import_bundle(data, update, types)
35273549

35283550
def stix2_export_entity(self, entity_type, entity_id, mode='simple'):
3529-
stix2 = Stix2(self)
3551+
stix2 = OpenCTIStix2(self)
35303552
bundle = {
35313553
'type': 'bundle',
35323554
'id': 'bundle--' + str(uuid.uuid4()),
@@ -3541,7 +3563,7 @@ def stix2_export_entity(self, entity_type, entity_id, mode='simple'):
35413563
return bundle
35423564

35433565
def stix2_export_bundle(self, types=[]):
3544-
stix2 = Stix2(self)
3566+
stix2 = OpenCTIStix2(self)
35453567
uuids = []
35463568
bundle = {
35473569
'type': 'bundle',

pycti/stix2.py renamed to pycti/opencti_stix2.py

Lines changed: 68 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,15 @@
44
import datetime
55
import datefinder
66
import dateutil.parser
7+
import pytz
78

89
datefinder.ValueError = ValueError, OverflowError
10+
from stix2 import ObjectPath, ObservationExpression, EqualityComparisonExpression, HashConstant
911

12+
utc = pytz.UTC
1013

11-
class Stix2:
14+
15+
class OpenCTIStix2:
1216
"""
1317
Python API for Stix2 in OpenCTI
1418
:param opencti: OpenCTI instance
@@ -29,7 +33,7 @@ def convert_markdown(self, text):
2933
def filter_objects(self, uuids, objects):
3034
result = []
3135
for object in objects:
32-
if object['id'] not in uuids:
36+
if 'id' in object and object['id'] not in uuids:
3337
result.append(object)
3438
return result
3539

@@ -59,6 +63,7 @@ def prepare_export(self, entity, stix_object, mode='simple'):
5963
created_by_ref['created'] = self.format_date(entity_created_by_ref['created'])
6064
created_by_ref['modified'] = self.format_date(entity_created_by_ref['modified'])
6165
if self.not_empty(entity_created_by_ref['alias']): created_by_ref['x_opencti_aliases'] = entity_created_by_ref['alias']
66+
created_by_ref['x_opencti_identity_type'] = entity_created_by_ref['entity_type']
6267
created_by_ref['x_opencti_id'] = entity_created_by_ref['id']
6368

6469
stix_object['created_by_ref'] = created_by_ref['id']
@@ -129,7 +134,9 @@ def prepare_export(self, entity, stix_object, mode='simple'):
129134

130135
result.append(stix_object)
131136

132-
uuids = [x['id'] for x in result]
137+
uuids = []
138+
for x in result:
139+
uuids.append(x['id'])
133140
if mode == 'full' and len(objects_to_get) > 0:
134141
for entity_object in objects_to_get:
135142
entity_object_data = None
@@ -250,6 +257,7 @@ def prepare_export(self, entity, stix_object, mode='simple'):
250257
return result
251258

252259
def import_object(self, stix_object, update=False):
260+
self.opencti.log('Importing a ' + stix_object['type'])
253261
# Reports
254262
reports = {}
255263
# Created By Ref
@@ -396,6 +404,7 @@ def import_object(self, stix_object, update=False):
396404
'attack-pattern': self.create_attack_pattern,
397405
'course-of-action': self.create_course_of_action,
398406
'report': self.create_report,
407+
'indicator': self.create_indicator,
399408
}
400409
do_import = importer.get(stix_object['type'], lambda stix_object, update: self.unknown_type(stix_object, update))
401410
stix_object_result = do_import(stix_object, update)
@@ -816,8 +825,7 @@ def create_report(self, stix_object, update=False):
816825
stix_object['published'] if 'published' in stix_object else '',
817826
stix_object['x_opencti_report_class'] if 'x_opencti_report_class' in stix_object else 'external',
818827
stix_object['x_opencti_object_status'] if 'x_opencti_object_status' in stix_object else 0,
819-
stix_object[
820-
'x_opencti_source_confidence_level'] if 'x_opencti_source_confidence_level' in stix_object else 3,
828+
stix_object['x_opencti_source_confidence_level'] if 'x_opencti_source_confidence_level' in stix_object else 3,
821829
stix_object['x_opencti_graph_data'] if 'x_opencti_graph_data' in stix_object else '',
822830
stix_object['x_opencti_id'] if 'x_opencti_id' in stix_object else None,
823831
stix_object['id'] if 'id' in stix_object else None,
@@ -826,12 +834,39 @@ def create_report(self, stix_object, update=False):
826834
)
827835

828836
def export_stix_observable(self, entity):
829-
stix_observable = {
830-
'id': entity['stix_id'],
831-
'type': entity['entity_type'],
832-
'value': entity['observable_value']
833-
}
834-
return self.prepare_export(entity, stix_observable)
837+
stix_observable = dict()
838+
stix_observable['id'] = entity['stix_id']
839+
stix_observable['type'] = 'indicator'
840+
stix_observable['name'] = 'Indicator'
841+
if self.not_empty(entity['description']): stix_observable['description'] = entity['description']
842+
stix_observable['labels'] = ['indicator']
843+
stix_observable['created'] = self.format_date(entity['created_at'])
844+
stix_observable['modified'] = self.format_date(entity['updated_at'])
845+
stix_observable['x_opencti_observable_type'] = entity['entity_type']
846+
stix_observable['x_opencti_observable_value'] = entity['observable_value']
847+
stix_observable['x_opencti_id'] = entity['id']
848+
if len(entity['stixRelations']) > 0:
849+
first_seen = utc.localize(datetime.datetime.utcnow())
850+
for relation in entity['stixRelations']:
851+
relation_first_seen = dateutil.parser.parse(relation['first_seen'])
852+
if relation_first_seen < first_seen:
853+
first_seen = relation_first_seen
854+
stix_observable['valid_from'] = self.format_date(first_seen)
855+
final_stix_observable = self.prepare_observable(entity, stix_observable)
856+
return self.prepare_export(entity, final_stix_observable)
857+
858+
def create_indicator(self, stix_object, update=False):
859+
if 'x_opencti_observable_type' in stix_object and 'x_opencti_observable_value' in stix_object:
860+
return self.opencti.create_stix_observable_if_not_exists(
861+
stix_object['x_opencti_observable_type'],
862+
stix_object['x_opencti_observable_value'],
863+
self.convert_markdown(stix_object['description']) if 'description' in stix_object else '',
864+
stix_object['id'] if 'id' in stix_object else None,
865+
stix_object['created'] if 'created' in stix_object else None,
866+
stix_object['modified'] if 'modified' in stix_object else None,
867+
)
868+
# TODO: Implement extraction of observables from STIX2 patterns
869+
return None
835870

836871
def export_stix_relation(self, entity):
837872
stix_relation = dict()
@@ -851,6 +886,7 @@ def export_stix_relation(self, entity):
851886
if self.not_empty(entity['weight']): stix_relation['x_opencti_weight'] = entity['weight']
852887
if self.not_empty(entity['role_played']): stix_relation['x_opencti_role_played'] = entity['role_played']
853888
if self.not_empty(entity['score']): stix_relation['x_opencti_score'] = entity['score']
889+
stix_relation['x_opencti_id'] = entity['id']
854890
return self.prepare_export(entity, stix_relation)
855891

856892
def import_relationship(self, stix_relation, update=False):
@@ -862,15 +898,15 @@ def import_relationship(self, stix_relation, update=False):
862898
# Check entities
863899
if stix_relation['source_ref'] in self.mapping_cache:
864900
source_id = self.mapping_cache[stix_relation['source_ref']]['id']
865-
source_type = self.mapping_cache[stix_relation['source_ref']]['type']
901+
source_type = self.mapping_cache[stix_relation['source_ref']]['type'] if stix_relation['relationship_type'] != 'indicates' else 'observable'
866902
else:
867903
if 'x_opencti_source_ref' in stix_relation:
868904
stix_object_result = self.opencti.get_stix_domain_entity_by_id(stix_relation['x_opencti_source_ref'])
869905
else:
870906
stix_object_result = self.opencti.get_stix_domain_entity_by_stix_id(stix_relation['source_ref'])
871907
if stix_object_result is not None:
872908
source_id = stix_object_result['id']
873-
source_type = stix_object_result['entity_type']
909+
source_type = stix_object_result['entity_type'] if stix_relation['relationship_type'] != 'indicates' else 'observable'
874910
else:
875911
self.opencti.log('Source ref of the relationship not found, doing nothing...')
876912
return None
@@ -1032,6 +1068,23 @@ def resolve_author(self, title):
10321068
return self.get_author('The MITRE Corporation')
10331069
return None
10341070

1071+
def prepare_observable(self, entity, stix_observable):
1072+
if 'file' in entity['entity_type']:
1073+
observable_type = 'file'
1074+
elif entity['entity_type'] == 'domain':
1075+
observable_type = 'domain-name'
1076+
else:
1077+
observable_type = entity['entity_type']
1078+
1079+
if observable_type == 'file':
1080+
lhs = ObjectPath(observable_type, ['hashes', entity['entity_type'].split('-')[1].upper()])
1081+
ece = ObservationExpression(EqualityComparisonExpression(lhs, HashConstant(entity['observable_value'], entity['entity_type'].split('-')[1].upper())))
1082+
if observable_type == 'ipv4-addr' or observable_type == 'ipv6-addr' or observable_type == 'domain_name' or observable_type == 'url':
1083+
lhs = ObjectPath(observable_type, ["value"])
1084+
ece = ObservationExpression(EqualityComparisonExpression(lhs, entity['observable_value']))
1085+
stix_observable['pattern'] = str(ece)
1086+
return stix_observable
1087+
10351088
def get_author(self, name):
10361089
if name in self.mapping_cache:
10371090
return self.mapping_cache[name]
@@ -1041,6 +1094,8 @@ def get_author(self, name):
10411094
return author_id
10421095

10431096
def format_date(self, date):
1097+
if isinstance(date, datetime.date):
1098+
return date.isoformat(timespec='milliseconds').replace('+00:00', 'Z')
10441099
if date is not None:
10451100
return dateutil.parser.parse(date).isoformat(timespec='milliseconds').replace('+00:00', 'Z')
10461101
else:

requirements.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,6 @@ PyYAML
33
setuptools
44
pypandoc
55
python-dateutil
6-
datefinder
6+
datefinder
7+
stix2
8+
pytz

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
print("warning: pypandoc module not found, could not convert Markdown to RST")
1313
read_md = lambda f: open(f, 'r').read()
1414

15-
VERSION = "1.2.2"
15+
VERSION = "1.2.3"
1616

1717
class VerifyVersionCommand(install):
1818
description = 'verify that the git tag matches our version'

0 commit comments

Comments
 (0)