Skip to content

Commit a1fd916

Browse files
author
Samuel Hassine
committed
[client] Upgrade the Python client with new indicator type (OpenCTI-Platform#40)
1 parent a47e39a commit a1fd916

14 files changed

+750
-801
lines changed

LICENSE

+201-661
Large diffs are not rendered by default.

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
[![Website](https://img.shields.io/badge/website-opencti.io-blue.svg)](https://www.opencti.io)
44
[![CircleCI](https://circleci.com/gh/OpenCTI-Platform/client-python.svg?style=shield)](https://circleci.com/gh/OpenCTI-Platform/client-python/tree/master)
55
[![GitHub release](https://img.shields.io/github/release/OpenCTI-Platform/client-python.svg)](https://github.com/OpenCTI-Platform/client-python/releases/latest)
6+
[![Number of PyPI downloads](https://img.shields.io/pypi/dm/pycti.svg)](https://pypi.python.org/pypi/pycti/)
67
[![Slack Status](https://slack.luatix.org/badge.svg)](https://slack.luatix.org)
78

89
The official OpenCTI Python client helps developers to use the OpenCTI API by providing easy to use methods and utils. This client is also used by some OpenCTI components.

examples/add_organization_to_sector.py

-2
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@
2929
toId=sector['id'],
3030
relationship_type='gathering',
3131
description='BNP Paribas is part of the sector Banking institutions.',
32-
first_seen=datetime.datetime.today().strftime('%Y-%m-%dT%H:%M:%SZ'),
33-
last_seen=datetime.datetime.today().strftime('%Y-%m-%dT%H:%M:%SZ'),
3432
ignore_dates=True
3533
)
3634

examples/create_incident_with_ttps_and_observables.py examples/create_incident_with_ttps_and_indicators.py

+30-14
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from pycti import OpenCTIApiClient
77

88
# Variables
9-
api_url = 'http://localhost:4000'
9+
api_url = 'https://demo.opencti.io'
1010
api_token = 'c2d944bb-aea6-4bd6-b3d7-6c10451e2256'
1111

1212
# OpenCTI initialization
@@ -17,6 +17,7 @@
1717

1818
# Prepare all the elements of the report
1919
object_refs = []
20+
observable_refs = []
2021

2122
# Create the incident
2223
incident = opencti_api_client.incident.create(
@@ -26,7 +27,6 @@
2627
)
2728
print(incident)
2829
object_refs.append(incident['id'])
29-
3030
# Create the associated report
3131
report = opencti_api_client.report.create(
3232
name="Report about my new incident",
@@ -58,29 +58,36 @@
5858
kill_chain_phase_id=kill_chain_phase_id
5959
)
6060

61+
# Create the observable and indicator and indicates to the relation
6162
# Create the observable
6263
observable_ttp1 = opencti_api_client.stix_observable.create(
6364
type='Email-Address',
6465
observable_value='phishing@mail.com'
6566
)
67+
print(observable_ttp1)
68+
# Get the indicator
69+
indicator_ttp1 = observable_ttp1['indicators'][0]
70+
print(indicator_ttp1)
6671
# Indicates the relation Incident => uses => TTP
67-
observable_ttp1_relation = opencti_api_client.stix_relation.create(
68-
fromType='Stix-Observable',
69-
fromId=observable_ttp1['id'],
72+
indicator_ttp1_relation = opencti_api_client.stix_relation.create(
73+
fromType='Indicator',
74+
fromId=indicator_ttp1['id'],
7075
toType='stix_relation',
7176
toId=ttp1_relation['id'],
7277
relationship_type='indicates',
7378
description='This email address is the sender of the spearphishing.',
7479
first_seen=date,
7580
last_seen=date
7681
)
77-
# Elements for the report
82+
83+
# Prepare elements for the report
7884
object_refs.extend([
7985
ttp1['id'],
8086
ttp1_relation['id'],
81-
observable_ttp1['id'],
82-
observable_ttp1_relation['id']
87+
indicator_ttp1['id'],
88+
indicator_ttp1_relation['id']
8389
])
90+
observable_refs.append(observable_ttp1['id'])
8491

8592
# Registry Run Keys / Startup Folder
8693
ttp2 = opencti_api_client.attack_pattern.read(filters=[{'key': 'external_id', 'values': ['T1060']}])
@@ -102,15 +109,21 @@
102109
id=ttp2_relation['id'],
103110
kill_chain_phase_id=kill_chain_phase_id
104111
)
105-
# Add observables to the relation
112+
113+
# Create the observable and indicator and indicates to the relation
114+
# Create the observable
106115
observable_ttp2 = opencti_api_client.stix_observable.create(
107116
type='Registry-Key',
108117
observable_value='Disk security'
109118
)
119+
print(observable_ttp2)
120+
# Get the indicator
121+
indicator_ttp2 = observable_ttp2['indicators'][0]
122+
print(indicator_ttp2)
110123
# Indicates the relation Incident => uses => TTP
111-
observable_ttp2_relation = opencti_api_client.stix_relation.create(
112-
fromType='Stix-Observable',
113-
fromId=observable_ttp2['id'],
124+
indicator_ttp2_relation = opencti_api_client.stix_relation.create(
125+
fromType='Indicator',
126+
fromId=indicator_ttp2['id'],
114127
toType='stix_relation',
115128
toId=ttp2_relation['id'],
116129
relationship_type='indicates',
@@ -122,9 +135,10 @@
122135
object_refs.extend([
123136
ttp2['id'],
124137
ttp2_relation['id'],
125-
observable_ttp2['id'],
126-
observable_ttp2_relation['id']
138+
indicator_ttp2['id'],
139+
indicator_ttp2_relation['id']
127140
])
141+
observable_refs.append(observable_ttp2['id'])
128142

129143
# Data Encrypted
130144
ttp3 = opencti_api_client.attack_pattern.read(filters=[{'key': 'external_id', 'values': ['T1022']}])
@@ -151,3 +165,5 @@
151165
# Add all element to the report
152166
for object_ref in object_refs:
153167
opencti_api_client.report.add_stix_entity(id=report['id'], report=report, entity_id=object_ref)
168+
for observable_ref in observable_refs:
169+
opencti_api_client.report.add_stix_observable(id=report['id'], report=report, entity_id=observable_ref)

pycti/__init__.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from pycti.connector.opencti_connector import ConnectorType
66
from pycti.connector.opencti_connector import OpenCTIConnector
7-
from pycti.connector.opencti_connector_helper import OpenCTIConnectorHelper
7+
from pycti.connector.opencti_connector_helper import OpenCTIConnectorHelper, get_config_variable
88

99
from pycti.entities.opencti_marking_definition import MarkingDefinition
1010
from pycti.entities.opencti_external_reference import ExternalReference
@@ -25,6 +25,7 @@
2525
from pycti.entities.opencti_attack_pattern import AttackPattern
2626
from pycti.entities.opencti_course_of_action import CourseOfAction
2727
from pycti.entities.opencti_report import Report
28+
from pycti.entities.opencti_indicator import Indicator
2829

2930
from pycti.utils.opencti_stix2 import OpenCTIStix2
3031
from pycti.utils.constants import ObservableTypes

pycti/api/opencti_api_client.py

+9-7
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from pycti.entities.opencti_attack_pattern import AttackPattern
3636
from pycti.entities.opencti_course_of_action import CourseOfAction
3737
from pycti.entities.opencti_report import Report
38+
from pycti.entities.opencti_indicator import Indicator
3839

3940
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
4041

@@ -96,6 +97,7 @@ def __init__(self, url, token, log_level='info', ssl_verify=False):
9697
self.attack_pattern = AttackPattern(self)
9798
self.course_of_action = CourseOfAction(self)
9899
self.report = Report(self)
100+
self.indicator = Indicator(self)
99101

100102
# Check if openCTI is available
101103
if not self.health_check():
@@ -275,6 +277,9 @@ def process_multiple_fields(self, data):
275277
if 'stixRelations' in data:
276278
data['stixRelations'] = self.process_multiple(data['stixRelations'])
277279
data['stixRelationsIds'] = self.process_multiple_ids(data['stixRelations'])
280+
if 'indicators' in data:
281+
data['indicators'] = self.process_multiple(data['indicators'])
282+
data['indicatorsIds'] = self.process_multiple_ids(data['indicators'])
278283
return data
279284

280285
@deprecated(version='2.1.0', reason="Replaced by the StixDomainEntity class in pycti")
@@ -1754,11 +1759,9 @@ def resolve_role(self, relation_type, from_type, to_type):
17541759

17551760
relation_type = relation_type.lower()
17561761
from_type = from_type.lower()
1757-
from_type = 'observable' if (
1758-
(ObservableTypes.has_value(from_type) and (
1759-
relation_type == 'indicates' or relation_type == 'localization' or relation_type == 'gathering')
1760-
) or from_type == 'stix-observable'
1761-
) else from_type
1762+
from_type = 'observable' if ((ObservableTypes.has_value(from_type) and (
1763+
relation_type == 'localization' or relation_type == 'gathering')) or from_type == 'stix-observable'
1764+
) else from_type
17621765
to_type = to_type.lower()
17631766
mapping = {
17641767
'uses': {
@@ -1903,11 +1906,10 @@ def resolve_role(self, relation_type, from_type, to_type):
19031906
},
19041907
},
19051908
'indicates': {
1906-
'observable': {
1909+
'indicator': {
19071910
'threat-actor': {'from_role': 'indicator', 'to_role': 'characterize'},
19081911
'intrusion-set': {'from_role': 'indicator', 'to_role': 'characterize'},
19091912
'campaign': {'from_role': 'indicator', 'to_role': 'characterize'},
1910-
'incident': {'from_role': 'indicator', 'to_role': 'characterize'},
19111913
'malware': {'from_role': 'indicator', 'to_role': 'characterize'},
19121914
'tool': {'from_role': 'indicator', 'to_role': 'characterize'},
19131915
'stix_relation': {'from_role': 'indicator', 'to_role': 'characterize'},

pycti/connector/opencti_connector_helper.py

+36-11
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,27 @@
1414
from pycti.connector.opencti_connector import OpenCTIConnector
1515

1616

17+
def get_config_variable(envvar, yaml_path, config={}, isNumber=False):
18+
if os.getenv(envvar) is not None:
19+
result = os.getenv(envvar)
20+
elif yaml_path is not None:
21+
if yaml_path[0] in config and yaml_path[1] in config[yaml_path[0]]:
22+
result = config[yaml_path[0]][yaml_path[1]]
23+
else:
24+
return None
25+
else:
26+
return None
27+
28+
if result == 'yes' or result == 'true' or result == 'True':
29+
return True
30+
elif result == 'no' or result == 'false' or result == 'False':
31+
return False
32+
elif isNumber:
33+
return int(result)
34+
else:
35+
return result
36+
37+
1738
class ListenQueue(threading.Thread):
1839
def __init__(self, helper, config, callback):
1940
threading.Thread.__init__(self)
@@ -104,18 +125,22 @@ class OpenCTIConnectorHelper:
104125

105126
def __init__(self, config: dict):
106127
# Load API config
107-
self.opencti_url = os.getenv('OPENCTI_URL') or config['opencti']['url']
108-
self.opencti_token = os.getenv('OPENCTI_TOKEN') or config['opencti']['token']
128+
self.opencti_url = get_config_variable('OPENCTI_URL', ['opencti', 'url'], config)
129+
self.opencti_token = get_config_variable('OPENCTI_TOKEN', ['opencti', 'token'], config)
109130
# Load connector config
110-
self.connect_id = os.getenv('CONNECTOR_ID') or config['connector']['id']
111-
self.connect_type = os.getenv('CONNECTOR_TYPE') or config['connector']['type']
112-
self.connect_name = os.getenv('CONNECTOR_NAME') or config['connector']['name']
113-
self.connect_confidence_level = int(
114-
os.getenv('CONNECTOR_CONFIDENCE_LEVEL') or config['connector']['confidence_level'] or 2)
115-
self.connect_scope = os.getenv('CONNECTOR_SCOPE') or config['connector']['scope']
116-
self.log_level = os.getenv('CONNECTOR_LOG_LEVEL') or config['connector']['log_level'] or 'info'
117-
118-
# Configure logger²
131+
self.connect_id = get_config_variable('CONNECTOR_ID', ['connector', 'id'], config)
132+
self.connect_type = get_config_variable('CONNECTOR_TYPE', ['connector', 'type'], config)
133+
self.connect_name = get_config_variable('CONNECTOR_NAME', ['connector', 'name'], config)
134+
self.connect_confidence_level = get_config_variable(
135+
'CONNECTOR_CONFIDENCE_LEVEL',
136+
['connector', 'confidence_level'],
137+
config,
138+
True
139+
)
140+
self.connect_scope = get_config_variable('CONNECTOR_SCOPE', ['connector', 'scope'], config)
141+
self.log_level = get_config_variable('CONNECTOR_LOG_LEVEL', ['connector', 'log_level'], config)
142+
143+
# Configure logger
119144
numeric_level = getattr(logging, self.log_level.upper(), None)
120145
if not isinstance(numeric_level, int):
121146
raise ValueError('Invalid log level: ' + self.log_level)

pycti/entities/opencti_attack_pattern.py

+5
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,8 @@ def create(self, **kwargs):
255255
stix_id_key=stix_id_key,
256256
name=name
257257
)
258+
if object_result is None and external_id is not None:
259+
object_result = self.read(filters=[{'key': 'external_id', 'values': [external_id]}])
258260
if object_result is not None:
259261
if update:
260262
# name
@@ -326,6 +328,8 @@ def import_from_stix2(self, **kwargs):
326328
if stix_object is not None:
327329
# Extract external ID
328330
external_id = None
331+
if CustomProperties.EXTERNAL_ID in stix_object:
332+
external_id = stix_object[CustomProperties.EXTERNAL_ID]
329333
if 'external_references' in stix_object:
330334
for external_reference in stix_object['external_references']:
331335
if external_reference['source_name'] == 'mitre-attack' or external_reference[
@@ -368,6 +372,7 @@ def to_stix2(self, **kwargs):
368372
attack_pattern = dict()
369373
attack_pattern['id'] = entity['stix_id_key']
370374
attack_pattern['type'] = 'attack-pattern'
375+
if self.opencti.not_empty(entity['external_id']): attack_pattern[CustomProperties.EXTERNAL_ID] = entity['external_id']
371376
attack_pattern['name'] = entity['name']
372377
if self.opencti.not_empty(entity['stix_label']):
373378
attack_pattern['labels'] = entity['stix_label']

0 commit comments

Comments
 (0)