Skip to content

Commit 11f658b

Browse files
committed
Implement log shipping to Graylog via GELF
1 parent 5261ddd commit 11f658b

File tree

8 files changed

+89
-3
lines changed

8 files changed

+89
-3
lines changed

pycti/api/opencti_api_client.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,18 @@ class OpenCTIApiClient:
9898
:type cert: str, tuple, optional
9999
:param auth: Add a AuthBase class with custom authentication for you OpenCTI infrastructure.
100100
:type auth: requests.auth.AuthBase, optional
101+
:param graylog_host: Graylog host name or IP address
102+
:type graylog_host: str, optional
103+
:param graylog_port: Graylog port
104+
:type graylog_port: int, optional
105+
:param graylog_adapter: the Graylog adapter to use. Valid values are "udp" and "tcp". Uses UDP by default.
106+
:type graylog_adapter: str, optional
107+
:param log_shipping_level: log level when shipping logs remotely
108+
:type log_shipping_level: str, optional
109+
:param log_shipping_env_var_prefix: The prefix used to match environment variables. Matching variables will be added
110+
as meta info to the log data. The value of this property will be stripped from the name of the environment
111+
variable.
112+
:type log_shipping_env_var_prefix: str, optional
101113
"""
102114

103115
def __init__(
@@ -112,6 +124,11 @@ def __init__(
112124
cert=None,
113125
auth=None,
114126
perform_health_check=True,
127+
graylog_host=None,
128+
graylog_port=None,
129+
graylog_adapter=None,
130+
log_shipping_level=None,
131+
log_shipping_env_var_prefix=None,
115132
):
116133
"""Constructor method"""
117134

@@ -126,7 +143,9 @@ def __init__(
126143
raise ValueError("A TOKEN must be set")
127144

128145
# Configure logger
129-
self.logger_class = logger(log_level.upper(), json_logging)
146+
self.logger_class = logger(log_level.upper(), json_logging, graylog_host, graylog_port, graylog_adapter,
147+
log_shipping_level.upper() if log_shipping_level is not None else None,
148+
log_shipping_env_var_prefix)
130149
self.app_logger = self.logger_class("api")
131150

132151
# Define API

pycti/connector/opencti_connector_helper.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -878,6 +878,21 @@ def __init__(self, config: Dict, playbook_compatible=False) -> None:
878878
self.log_level = get_config_variable(
879879
"CONNECTOR_LOG_LEVEL", ["connector", "log_level"], config, default="INFO"
880880
).upper()
881+
self.graylog_host = get_config_variable(
882+
"CONNECTOR_GRAYLOG_HOST", ["connector", "graylog_host"], config
883+
)
884+
self.graylog_port = get_config_variable(
885+
"CONNECTOR_GRAYLOG_PORT", ["connector", "graylog_port"], config, True, 12201
886+
)
887+
self.graylog_adapter = get_config_variable(
888+
"CONNECTOR_GRAYLOG_ADAPTER", ["connector", "graylog_adapter"], config, default="udp"
889+
)
890+
self.log_shipping_level = get_config_variable(
891+
"CONNECTOR_LOG_SHIPPING_LEVEL", ["connector", "log_shipping_level"], config, default="INFO"
892+
).upper()
893+
self.log_shipping_env_var_prefix = get_config_variable(
894+
"CONNECTOR_LOG_SHIPPING_ENV_VAR_PREFIX", ["connector", "log_shipping_env_var_prefix"], config
895+
)
881896
self.connect_run_and_terminate = get_config_variable(
882897
"CONNECTOR_RUN_AND_TERMINATE",
883898
["connector", "run_and_terminate"],
@@ -915,6 +930,11 @@ def __init__(self, config: Dict, playbook_compatible=False) -> None:
915930
self.opencti_ssl_verify,
916931
json_logging=self.opencti_json_logging,
917932
bundle_send_to_queue=self.bundle_send_to_queue,
933+
graylog_host=self.graylog_host,
934+
graylog_port=self.graylog_port,
935+
graylog_adapter=self.graylog_adapter,
936+
log_shipping_level=self.log_shipping_level,
937+
log_shipping_env_var_prefix=self.log_shipping_env_var_prefix,
918938
)
919939
# - Impersonate API that will use applicant id
920940
# Behave like standard api if applicant not found
@@ -925,6 +945,11 @@ def __init__(self, config: Dict, playbook_compatible=False) -> None:
925945
self.opencti_ssl_verify,
926946
json_logging=self.opencti_json_logging,
927947
bundle_send_to_queue=self.bundle_send_to_queue,
948+
graylog_host=self.graylog_host,
949+
graylog_port=self.graylog_port,
950+
graylog_adapter=self.graylog_adapter,
951+
log_shipping_level=self.log_shipping_level,
952+
log_shipping_env_var_prefix=self.log_shipping_env_var_prefix,
928953
)
929954
self.connector_logger = self.api.logger_class(self.connect_name)
930955
# For retro compatibility

pycti/utils/opencti_logger.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import datetime
22
import logging
3+
import os
34

5+
from pygelf import GelfUdpHandler, GelfTcpHandler
46
from pythonjsonlogger import jsonlogger
57

68

@@ -17,7 +19,23 @@ def add_fields(self, log_record, record, message_dict):
1719
log_record["level"] = record.levelname
1820

1921

20-
def logger(level, json_logging=True):
22+
class ContextFilter(logging.Filter):
23+
def __init__(self, context_vars):
24+
"""
25+
:param context_vars: the extra properties to add to the LogRecord
26+
:type context_vars: list[tuple[str, str]]
27+
"""
28+
super().__init__()
29+
self.context_vars = context_vars
30+
31+
def filter(self, record):
32+
for key, value in self.context_vars:
33+
setattr(record, key, value)
34+
return True
35+
36+
37+
def logger(level, json_logging=True, graylog_host=None, graylog_port=None, graylog_adapter=None,
38+
log_shipping_level=None, log_shipping_env_var_prefix=None):
2139
# Exceptions
2240
logging.getLogger("urllib3").setLevel(logging.WARNING)
2341
logging.getLogger("pika").setLevel(logging.ERROR)
@@ -31,6 +49,21 @@ def logger(level, json_logging=True):
3149
else:
3250
logging.basicConfig(level=level)
3351

52+
if graylog_host is not None:
53+
if graylog_adapter == "tcp":
54+
shipping_handler = GelfTcpHandler(host=graylog_host, port=graylog_port, include_extra_fields=True)
55+
else:
56+
shipping_handler = GelfUdpHandler(host=graylog_host, port=graylog_port, include_extra_fields=True)
57+
shipping_handler.setLevel(log_shipping_level)
58+
59+
if log_shipping_env_var_prefix is not None:
60+
filtered_env = [(k.removeprefix(log_shipping_env_var_prefix), v) for k, v in os.environ.items()
61+
if k.startswith(log_shipping_env_var_prefix)]
62+
shipping_filter = ContextFilter(filtered_env)
63+
shipping_handler.addFilter(shipping_filter)
64+
65+
logging.getLogger().addHandler(shipping_handler)
66+
3467
class AppLogger:
3568
def __init__(self, name):
3669
self.local_logger = logging.getLogger(name)

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,5 @@ opentelemetry-sdk~=1.22.0
1515
deprecation~=2.1.0
1616
# OpenCTI
1717
filigran-sseclient~=1.0.0
18-
stix2~=3.0.1
18+
stix2~=3.0.1
19+
pygelf~=0.4.2

setup.cfg

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ install_requires =
5252
# OpenCTI
5353
filigran-sseclient~=1.0.0
5454
stix2~=3.0.1
55+
pygelf~=0.4.2
5556

5657
[options.extras_require]
5758
dev =

tests/cases/connectors.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ def __init__(self, config_file_path: str, api_client: OpenCTIApiClient, data: Di
6363
os.environ["OPENCTI_JSON_LOGGING"] = "true"
6464
os.environ["CONNECTOR_EXPOSE_METRICS"] = "true"
6565
os.environ["CONNECTOR_METRICS_PORT"] = "9096"
66+
os.environ["GRAYLOG_DUMMY_VAR"] = "dummy_value"
6667

6768
config = (
6869
yaml.load(open(config_file_path), Loader=yaml.FullLoader)

tests/data/external_import_config.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ connector:
66
confidence_level: 80 # From 0 (Unknown) to 100 (Fully trusted)
77
update_existing_data: True
88
log_level: 'debug'
9+
graylog_host: '127.0.0.1'
10+
graylog_port: 12201
11+
graylog_adapter: 'tcp'
12+
log_shipping_level: 'warn'
13+
log_shipping_env_var_prefix: 'GRAYLOG_'
914

1015
test:
1116
interval: 1

tests/data/internal_import_config.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ connector:
77
only_contextual: true # Only extract data related to an entity (a report, a threat actor, etc.)
88
confidence_level: 15 # From 0 (Unknown) to 100 (Fully trusted)
99
log_level: 'debug'
10+
graylog_host: '127.0.0.1'

0 commit comments

Comments
 (0)