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

Commit 49951ec

Browse files
convert to singleton pattern
1 parent 40a0924 commit 49951ec

File tree

2 files changed

+203
-126
lines changed

2 files changed

+203
-126
lines changed

pycti/api/opencti_api_client.py

Lines changed: 124 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import shutil
99
import signal
1010
import tempfile
11+
import threading
1112
from typing import Dict, Tuple, Union
1213

1314
import magic
@@ -83,6 +84,12 @@
8384
from pycti.utils.opencti_stix2 import OpenCTIStix2
8485
from pycti.utils.opencti_stix2_utils import OpenCTIStix2Utils
8586

87+
# Global singleton variables for proxy certificate management
88+
_PROXY_CERT_BUNDLE = None
89+
_PROXY_CERT_DIR = None
90+
_PROXY_CERT_LOCK = threading.Lock()
91+
_PROXY_SIGNAL_HANDLERS_REGISTERED = False
92+
8693

8794
def build_request_headers(token: str, custom_headers: str, app_logger):
8895
headers_dict = {
@@ -171,18 +178,9 @@ def __init__(
171178
self.app_logger = self.logger_class("api")
172179
self.admin_logger = self.logger_class("admin")
173180

174-
# Initialize temp certificate directory tracker
175-
self.temp_cert_dir = None
176-
177181
# Setup proxy certificates if provided
178182
self._setup_proxy_certificates()
179183

180-
# Register cleanup handlers for temp certificates
181-
if self.temp_cert_dir:
182-
atexit.register(self._cleanup_temp_certificates)
183-
signal.signal(signal.SIGTERM, self._signal_handler)
184-
signal.signal(signal.SIGINT, self._signal_handler)
185-
186184
# Define API
187185
self.api_token = token
188186
self.api_url = url + "/graphql"
@@ -272,74 +270,101 @@ def _setup_proxy_certificates(self):
272270
Detects HTTPS_CA_CERTIFICATES environment variable and combines
273271
proxy certificates with system certificates for SSL verification.
274272
Supports both inline certificate content and file paths.
273+
274+
Uses a singleton pattern to ensure only one certificate bundle is created
275+
across all instances, avoiding resource leaks and conflicts.
275276
"""
277+
global _PROXY_CERT_BUNDLE, _PROXY_CERT_DIR, _PROXY_SIGNAL_HANDLERS_REGISTERED
278+
276279
https_ca_certificates = os.getenv("HTTPS_CA_CERTIFICATES")
277280
if not https_ca_certificates:
278281
return
279282

280-
try:
281-
# Create secure temporary directory
282-
cert_dir = tempfile.mkdtemp(prefix="opencti_proxy_certs_")
283-
self.temp_cert_dir = cert_dir
284-
285-
# Determine if HTTPS_CA_CERTIFICATES contains inline content or file path
286-
cert_content = self._get_certificate_content(https_ca_certificates)
287-
if not cert_content:
288-
self.app_logger.warning(
289-
"Invalid HTTPS_CA_CERTIFICATES: not a valid certificate or file path",
290-
{
291-
"value": (
292-
https_ca_certificates[:50] + "..."
293-
if len(https_ca_certificates) > 50
294-
else https_ca_certificates
295-
)
296-
},
283+
# Thread-safe check and setup
284+
with _PROXY_CERT_LOCK:
285+
# If already configured, reuse existing bundle
286+
if _PROXY_CERT_BUNDLE is not None:
287+
self.ssl_verify = _PROXY_CERT_BUNDLE
288+
self.app_logger.debug(
289+
"Reusing existing proxy certificate bundle",
290+
{"cert_bundle": _PROXY_CERT_BUNDLE},
297291
)
298292
return
299293

300-
# Write proxy certificate to temp file
301-
proxy_cert_file = os.path.join(cert_dir, "proxy-ca.crt")
302-
with open(proxy_cert_file, "w") as f:
303-
f.write(cert_content)
304-
305-
# Find system certificates
306-
system_cert_paths = [
307-
"/etc/ssl/certs/ca-certificates.crt", # Debian/Ubuntu
308-
"/etc/pki/tls/certs/ca-bundle.crt", # RHEL/CentOS
309-
"/etc/ssl/cert.pem", # Alpine/BSD
310-
]
311-
312-
# Create combined certificate bundle
313-
combined_cert_file = os.path.join(cert_dir, "combined-ca-bundle.crt")
314-
with open(combined_cert_file, "w") as combined:
315-
# Add system certificates first
316-
for system_path in system_cert_paths:
317-
if os.path.exists(system_path):
318-
with open(system_path, "r") as sys_certs:
319-
combined.write(sys_certs.read())
320-
combined.write("\n")
321-
break
322-
323-
# Add proxy certificate
324-
combined.write(cert_content)
325-
326-
# Update ssl_verify to use combined certificate bundle
327-
self.ssl_verify = combined_cert_file
328-
329-
# Set environment variables for urllib and other libraries
330-
os.environ["REQUESTS_CA_BUNDLE"] = combined_cert_file
331-
os.environ["SSL_CERT_FILE"] = combined_cert_file
332-
333-
self.app_logger.info(
334-
"Proxy certificates configured",
335-
{"cert_bundle": combined_cert_file},
336-
)
294+
# First initialization - create the certificate bundle
295+
try:
296+
# Create secure temporary directory
297+
cert_dir = tempfile.mkdtemp(prefix="opencti_proxy_certs_")
298+
299+
# Determine if HTTPS_CA_CERTIFICATES contains inline content or file path
300+
cert_content = self._get_certificate_content(https_ca_certificates)
301+
if not cert_content:
302+
self.app_logger.warning(
303+
"Invalid HTTPS_CA_CERTIFICATES: not a valid certificate or file path",
304+
{
305+
"value": (
306+
https_ca_certificates[:50] + "..."
307+
if len(https_ca_certificates) > 50
308+
else https_ca_certificates
309+
)
310+
},
311+
)
312+
return
313+
314+
# Write proxy certificate to temp file
315+
proxy_cert_file = os.path.join(cert_dir, "proxy-ca.crt")
316+
with open(proxy_cert_file, "w") as f:
317+
f.write(cert_content)
318+
319+
# Find system certificates
320+
system_cert_paths = [
321+
"/etc/ssl/certs/ca-certificates.crt", # Debian/Ubuntu
322+
"/etc/pki/tls/certs/ca-bundle.crt", # RHEL/CentOS
323+
"/etc/ssl/cert.pem", # Alpine/BSD
324+
]
325+
326+
# Create combined certificate bundle
327+
combined_cert_file = os.path.join(cert_dir, "combined-ca-bundle.crt")
328+
with open(combined_cert_file, "w") as combined:
329+
# Add system certificates first
330+
for system_path in system_cert_paths:
331+
if os.path.exists(system_path):
332+
with open(system_path, "r") as sys_certs:
333+
combined.write(sys_certs.read())
334+
combined.write("\n")
335+
break
336+
337+
# Add proxy certificate
338+
combined.write(cert_content)
339+
340+
# Update global singleton variables
341+
_PROXY_CERT_BUNDLE = combined_cert_file
342+
_PROXY_CERT_DIR = cert_dir
343+
self.ssl_verify = combined_cert_file
344+
345+
# Set environment variables for urllib and other libraries
346+
os.environ["REQUESTS_CA_BUNDLE"] = combined_cert_file
347+
os.environ["SSL_CERT_FILE"] = combined_cert_file
348+
349+
# Register cleanup handlers only once
350+
atexit.register(_cleanup_proxy_certificates)
351+
352+
# Register signal handlers only once
353+
if not _PROXY_SIGNAL_HANDLERS_REGISTERED:
354+
signal.signal(signal.SIGTERM, _signal_handler_proxy_cleanup)
355+
signal.signal(signal.SIGINT, _signal_handler_proxy_cleanup)
356+
_PROXY_SIGNAL_HANDLERS_REGISTERED = True
357+
358+
self.app_logger.info(
359+
"Proxy certificates configured",
360+
{"cert_bundle": combined_cert_file},
361+
)
337362

338-
except Exception as e:
339-
self.app_logger.error(
340-
"Failed to setup proxy certificates", {"error": str(e)}
341-
)
342-
raise
363+
except Exception as e:
364+
self.app_logger.error(
365+
"Failed to setup proxy certificates", {"error": str(e)}
366+
)
367+
raise
343368

344369
def _get_certificate_content(self, https_ca_certificates):
345370
"""Extract certificate content from environment variable.
@@ -353,7 +378,7 @@ def _get_certificate_content(self, https_ca_certificates):
353378
"""
354379
# Strip whitespace once at the beginning
355380
stripped_https_ca_certificates = https_ca_certificates.strip()
356-
381+
357382
# Check if it's inline certificate content (starts with PEM header)
358383
if stripped_https_ca_certificates.startswith("-----BEGIN CERTIFICATE-----"):
359384
self.app_logger.debug(
@@ -390,43 +415,6 @@ def _get_certificate_content(self, https_ca_certificates):
390415
# Neither inline content nor valid file path
391416
return None
392417

393-
def _cleanup_temp_certificates(self):
394-
"""Clean up temporary certificate directory.
395-
396-
This method is called on normal program exit via atexit
397-
or when receiving termination signals (SIGTERM/SIGINT).
398-
"""
399-
if self.temp_cert_dir and os.path.exists(self.temp_cert_dir):
400-
try:
401-
shutil.rmtree(self.temp_cert_dir)
402-
self.app_logger.debug(
403-
"Cleaned up temporary certificates",
404-
{"cert_dir": self.temp_cert_dir}
405-
)
406-
except Exception as e:
407-
self.app_logger.warning(
408-
"Failed to cleanup temporary certificates",
409-
{"cert_dir": self.temp_cert_dir, "error": str(e)}
410-
)
411-
finally:
412-
self.temp_cert_dir = None
413-
414-
def _signal_handler(self, signum, frame):
415-
"""Handle termination signals (SIGTERM/SIGINT).
416-
417-
Performs cleanup and then raises SystemExit to allow
418-
normal shutdown procedures to complete.
419-
420-
:param signum: Signal number
421-
:param frame: Current stack frame
422-
"""
423-
self.app_logger.info(
424-
"Received termination signal, cleaning up",
425-
{"signal": signum}
426-
)
427-
self._cleanup_temp_certificates()
428-
raise SystemExit(0)
429-
430418
def set_applicant_id_header(self, applicant_id):
431419
self.request_headers["opencti-applicant-id"] = applicant_id
432420

@@ -1062,3 +1050,33 @@ def get_attribute_in_mitre_extension(key, object) -> any:
10621050
"extension-definition--322b8f77-262a-4cb8-a915-1e441e00329b"
10631051
][key]
10641052
return None
1053+
1054+
1055+
# Global cleanup functions for proxy certificates singleton
1056+
def _cleanup_proxy_certificates():
1057+
"""Clean up temporary certificate directory for proxy certificates.
1058+
1059+
This function is called on normal program exit via atexit.
1060+
"""
1061+
global _PROXY_CERT_DIR
1062+
if _PROXY_CERT_DIR and os.path.exists(_PROXY_CERT_DIR):
1063+
try:
1064+
shutil.rmtree(_PROXY_CERT_DIR)
1065+
except Exception:
1066+
# Silently fail cleanup - best effort
1067+
pass
1068+
finally:
1069+
_PROXY_CERT_DIR = None
1070+
1071+
1072+
def _signal_handler_proxy_cleanup(signum, frame):
1073+
"""Handle termination signals (SIGTERM/SIGINT) for proxy certificate cleanup.
1074+
1075+
Performs cleanup and then raises SystemExit to allow
1076+
normal shutdown procedures to complete.
1077+
1078+
:param signum: Signal number
1079+
:param frame: Current stack frame
1080+
"""
1081+
_cleanup_proxy_certificates()
1082+
raise SystemExit(0)

0 commit comments

Comments
 (0)