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

Commit 271ea97

Browse files
author
Samuel Hassine
committed
[connector] Add the connector helper class (#8)
1 parent ca7151c commit 271ea97

File tree

8 files changed

+113
-31
lines changed

8 files changed

+113
-31
lines changed

examples/config.yml.sample

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,3 @@
11
opencti:
22
api_url: 'http://opencti.example.com'
3-
api_key : 'XXxxXxXXxxX'
4-
verbose: true
5-
6-
mitre:
7-
repository_path_cti: '/home/user/git/cti'
3+
token : 'ChangeMe'

examples/stix2/export.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@
1010
config = yaml.load(open(os.path.dirname(__file__) + '/../config.yml'))
1111

1212
# Export file
13-
export_file = './exports/report.json'
13+
export_file = './report.json'
1414

1515
# OpenCTI initialization
16-
opencti = OpenCTI(config['opencti']['api_url'], config['opencti']['api_key'], config['opencti']['log_file'], config['opencti']['verbose'])
16+
opencti = OpenCTI(config['opencti']['api_url'], config['opencti']['token'])
1717

1818
# Import the bundle
1919
bundle = opencti.stix2_export_entity('report', '{REPORT_ID}', 'full')

examples/stix2/import.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@
99
config = yaml.load(open(os.path.dirname(__file__) + '/../config.yml'))
1010

1111
# File to import
12-
file_to_import = config['mitre']['repository_path_cti'] + '/apt1.json'
12+
file_to_import = './enterprise-attack.json'
1313

1414
# OpenCTI initialization
15-
opencti = OpenCTI(config['opencti']['api_url'], config['opencti']['api_key'], config['opencti']['log_file'], config['opencti']['verbose'])
15+
opencti = OpenCTI(config['opencti']['api_url'], config['opencti']['token'])
1616

1717
# Import the bundle
1818
opencti.stix2_import_bundle_from_file(file_to_import, False)

pycti/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1-
from .opencti import OpenCTI
1+
from .opencti import OpenCTI
2+
from .opencti_connector import OpenCTIConnector

pycti/opencti.py

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import json
88
import uuid
99
import base64
10+
import logging
1011

1112
from pycti.opencti_stix2 import OpenCTIStix2
1213

@@ -15,28 +16,18 @@ class OpenCTI:
1516
"""
1617
Python API for OpenCTI
1718
:param url: OpenCTI URL
18-
:param key: The API key
19-
:param verbose: Log all requests. Defaults to None
20-
:param stdout: Display log to stdout. Defaults to None
19+
:param token: The API key
2120
"""
2221

23-
def __init__(self, url, key, log_file='', verbose=True, stdout=True):
22+
def __init__(self, url, token):
2423
self.api_url = url + '/graphql'
25-
self.log_file = log_file
26-
self.verbose = verbose
27-
self.stdout = stdout
2824
self.request_headers = {
29-
'Authorization': 'Bearer ' + key,
25+
'Authorization': 'Bearer ' + token,
3026
'Content-Type': 'application/json'
3127
}
3228

3329
def log(self, message):
34-
if self.stdout:
35-
print(message)
36-
if self.verbose and len(self.log_file) > 0:
37-
file = open(self.log_file, 'a')
38-
file.write('[' + datetime.datetime.today().strftime('%Y-%m-%d %H:%M:%S') + '] ' + message + "\n")
39-
file.close()
30+
logging.info('[' + datetime.datetime.today().strftime('%Y-%m-%d %H:%M:%S') + '] ' + message + "\n")
4031

4132
def query(self, query, variables={}):
4233
r = requests.post(self.api_url, json={'query': query, 'variables': variables}, headers=self.request_headers)
@@ -409,7 +400,8 @@ def get_stix_relation_by_id(self, id):
409400
result = self.query(query, {'id': id})
410401
return result['data']['stixRelation']
411402

412-
def get_stix_relations(self, from_id=None, to_id=None, type='stix_relation', first_seen=None, last_seen=None, inferred=False):
403+
def get_stix_relations(self, from_id=None, to_id=None, type='stix_relation', first_seen=None, last_seen=None,
404+
inferred=False):
413405
self.log('Getting relations, from: ' + from_id + ', to: ' + to_id + '...')
414406
if type == 'revoked-by':
415407
return []
@@ -1717,8 +1709,10 @@ def create_incident_if_not_exists(self,
17171709
object_result = self.check_existing_stix_domain_entity(stix_id, name, 'Incident')
17181710
if object_result is not None:
17191711
self.update_stix_domain_entity_field(object_result['id'], 'name', name)
1720-
description is not None and self.update_stix_domain_entity_field(object_result['id'], 'description', description)
1721-
first_seen is not None and self.update_stix_domain_entity_field(object_result['id'], 'first_seen', first_seen)
1712+
description is not None and self.update_stix_domain_entity_field(object_result['id'], 'description',
1713+
description)
1714+
first_seen is not None and self.update_stix_domain_entity_field(object_result['id'], 'first_seen',
1715+
first_seen)
17221716
last_seen is not None and self.update_stix_domain_entity_field(object_result['id'], 'last_seen', last_seen)
17231717
return object_result
17241718
else:
@@ -2509,7 +2503,8 @@ def create_course_of_action(self, name, description, id=None, stix_id=None, crea
25092503
})
25102504
return result['data']['courseOfActionAdd']
25112505

2512-
def create_course_of_action_if_not_exists(self, name, description, id=None, stix_id=None, created=None, modified=None):
2506+
def create_course_of_action_if_not_exists(self, name, description, id=None, stix_id=None, created=None,
2507+
modified=None):
25132508
object_result = self.check_existing_stix_domain_entity(stix_id, name, 'Course-Of-Action')
25142509
if object_result is not None:
25152510
return object_result

pycti/opencti_connector.py

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# coding: utf-8
2+
3+
import pika
4+
import logging
5+
import json
6+
import base64
7+
from stix2validator import validate_string
8+
9+
EXCHANGE_NAME = 'amqp.opencti'
10+
11+
12+
class OpenCTIConnector:
13+
"""
14+
Python API for OpenCTI connector
15+
:param identifier: Connector identifier
16+
:param config: Connector configuration
17+
:param rabbitmq_hostname: RabbitMQ hostname
18+
:param rabbitmq_port: RabbitMQ hostname
19+
:param rabbitmq_username: RabbitMQ hostname
20+
:param rabbitmq_password: RabbitMQ password
21+
"""
22+
23+
def __init__(self, identifier, config, rabbitmq_hostname, rabbitmq_port, rabbitmq_username, rabbitmq_password):
24+
# Initialize configuration
25+
self.connection = None
26+
self.identifier = identifier
27+
self.config = config
28+
self.rabbitmq_hostname = rabbitmq_hostname
29+
self.rabbitmq_port = rabbitmq_port
30+
self.rabbitmq_username = rabbitmq_username
31+
self.rabbitmq_password = rabbitmq_password
32+
self.queue_name = 'import-connectors-' + self.identifier
33+
self.routing_key = 'import.connectors.' + self.identifier
34+
35+
# Encode the configuration
36+
config_encoded = base64.b64encode(json.dumps(self.config).encode('utf-8')).decode('utf-8')
37+
38+
# Declare the queue for the connector
39+
channel = self._connect()
40+
channel.queue_delete(self.queue_name)
41+
channel.queue_declare(self.queue_name, durable=True, arguments={'config': config_encoded})
42+
channel.queue_bind(queue=self.queue_name, exchange=EXCHANGE_NAME, routing_key=self.routing_key)
43+
channel.close()
44+
self._disconnect()
45+
46+
def _connect(self):
47+
try:
48+
credentials = pika.PlainCredentials(self.rabbitmq_username, self.rabbitmq_password)
49+
parameters = pika.ConnectionParameters(self.rabbitmq_hostname, self.rabbitmq_port, '/', credentials)
50+
self.connection = pika.BlockingConnection(parameters)
51+
channel = self.connection.channel()
52+
channel.exchange_declare(exchange=EXCHANGE_NAME, exchange_type='direct', durable=True)
53+
return channel
54+
except:
55+
logging.error('Unable to connect to RabbitMQ with the given parameters')
56+
57+
def _disconnect(self):
58+
if self.connection is not None and not self.connection.is_closed:
59+
self.connection.close()
60+
61+
def send_stix2_bundle(self, bundle):
62+
"""
63+
This method send a STIX2 bundle to RabbitMQ to be consumed by workers
64+
:param bundle: A valid STIX2 bundle
65+
"""
66+
logging.info('Sending a STI2 bundle to RabbitMQ...')
67+
68+
# Validate the STIX 2 bundle
69+
validation = validate_string(bundle)
70+
if not validation.is_valid:
71+
raise ValueError('The bundle is not a valid STIX2 JSON')
72+
73+
# Prepare the message
74+
message = {
75+
'type': 'stix2-bundle',
76+
'content': base64.b64encode(bundle.encode('utf-8')).decode('utf-8')
77+
}
78+
79+
# Send the message
80+
channel = self._connect()
81+
channel.basic_publish(EXCHANGE_NAME, self.routing_key, json.dumps(message))
82+
channel.close()
83+
self._disconnect()
84+
logging.info('STIX2 bundle has been sent')

requirements.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,6 @@ pypandoc
55
python-dateutil
66
datefinder
77
stix2
8-
pytz
8+
pytz
9+
pika
10+
stix2-validator

setup.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@
77

88
try:
99
from pypandoc import convert
10+
1011
read_md = lambda f: convert(f, 'rst')
1112
except ImportError:
1213
print("warning: pypandoc module not found, could not convert Markdown to RST")
1314
read_md = lambda f: open(f, 'r').read()
1415

15-
VERSION = "1.2.7"
16+
VERSION = "1.2.8"
17+
1618

1719
class VerifyVersionCommand(install):
1820
description = 'verify that the git tag matches our version'
@@ -25,6 +27,7 @@ def run(self):
2527
)
2628
sys.exit(info)
2729

30+
2831
setup(
2932
name='pycti',
3033
version=VERSION,
@@ -50,7 +53,8 @@ def run(self):
5053
'Topic :: Software Development :: Libraries :: Python Modules'
5154
],
5255
include_package_data=True,
53-
install_requires=['requests', 'PyYAML', 'python-dateutil', 'datefinder', 'stix2', 'pytz'],
56+
install_requires=['requests', 'PyYAML', 'python-dateutil', 'datefinder', 'stix2', 'stix2-validator', 'pytz',
57+
'pika'],
5458
cmdclass={
5559
'verify': VerifyVersionCommand,
5660
}

0 commit comments

Comments
 (0)