Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions TheHiveV5/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

## 2026-01-23 - 1.1.0

### Added

- Add optional `ca_certificate` configuration parameter for custom PKI/internal CA support in TLS connections
- Implement CA certificate file caching to avoid creating duplicate temp files for the same certificate
- Add robust cleanup mechanism for temporary CA files at process exit

## 2025-12-22 - 1.0.9

### Fixed
Expand Down
9 changes: 7 additions & 2 deletions TheHiveV5/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@
"description": "Is the server certificate verified",
"type": "boolean",
"default": true
},
"ca_certificate": {
"title": "CA Certificate",
"description": "PEM-encoded CA certificate for TLS verification (optional, for internal PKI)",
"type": "string"
}
},
"required": [
Expand All @@ -36,8 +41,8 @@
"name": "The Hive V5",
"uuid": "d6c96586-707c-451f-b9f6-d31b3291f87d",
"slug": "thehivev5",
"version": "1.0.9",
"version": "1.1.0",
"categories": [
"Collaboration Tools"
]
}
}
69 changes: 68 additions & 1 deletion TheHiveV5/tests/test_thehiveconnector.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import os
import pytest
import sys
import unittest.mock as mock
import requests_mock

from thehive.thehiveconnector import TheHiveConnector, key_exists
from thehive.thehiveconnector import (
TheHiveConnector,
key_exists,
prepare_verify_param,
_ca_file_cache,
_cleanup_ca_files,
)
from thehive4py.errors import TheHiveError


Expand All @@ -29,6 +36,66 @@ def test_connector_init_without_apikey():
assert "API key is required" in str(exc_info.value)


class TestPrepareVerifyParam:
"""Tests for the prepare_verify_param helper function"""

def test_verify_false_returns_false(self):
"""When verify is False, should return False regardless of ca_certificate"""
assert prepare_verify_param(verify=False) is False
assert prepare_verify_param(verify=False, ca_certificate="some cert") is False

def test_verify_true_without_ca_returns_true(self):
"""When verify is True and no CA, should return True (use system CA store)"""
assert prepare_verify_param(verify=True) is True
assert prepare_verify_param(verify=True, ca_certificate=None) is True

def test_verify_true_with_ca_returns_file_path(self):
"""When verify is True and CA provided, should return path to temp file"""
ca_cert = "-----BEGIN CERTIFICATE-----\nTEST_UNIQUE_1\n-----END CERTIFICATE-----"
result = prepare_verify_param(verify=True, ca_certificate=ca_cert)

assert isinstance(result, str)
assert result.endswith(".pem")
assert os.path.exists(result)

# Verify content
with open(result, "r") as f:
assert f.read() == ca_cert

def test_same_ca_returns_cached_file(self):
"""Same CA certificate content should return the same cached file path"""
ca_cert = "-----BEGIN CERTIFICATE-----\nTEST_CACHE\n-----END CERTIFICATE-----"
result1 = prepare_verify_param(verify=True, ca_certificate=ca_cert)
result2 = prepare_verify_param(verify=True, ca_certificate=ca_cert)

assert result1 == result2
assert os.path.exists(result1)

def test_different_ca_returns_different_files(self):
"""Different CA certificates should return different file paths"""
ca_cert1 = "-----BEGIN CERTIFICATE-----\nTEST_DIFF_1\n-----END CERTIFICATE-----"
ca_cert2 = "-----BEGIN CERTIFICATE-----\nTEST_DIFF_2\n-----END CERTIFICATE-----"
result1 = prepare_verify_param(verify=True, ca_certificate=ca_cert1)
result2 = prepare_verify_param(verify=True, ca_certificate=ca_cert2)

assert result1 != result2
assert os.path.exists(result1)
assert os.path.exists(result2)

def test_cleanup_function_removes_files(self):
"""The cleanup function should remove all cached CA files"""
ca_cert = "-----BEGIN CERTIFICATE-----\nTEST_CLEANUP\n-----END CERTIFICATE-----"
result = prepare_verify_param(verify=True, ca_certificate=ca_cert)

assert os.path.exists(result)

# Call cleanup
_cleanup_ca_files()

# File should be removed
assert not os.path.exists(result)
Comment on lines +84 to +95
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test calls _cleanup_ca_files() directly but doesn't restore the cache state afterward. This could cause issues in subsequent tests if they expect the cache to be in a certain state. Consider clearing _ca_file_cache after calling cleanup, or use a pytest fixture to ensure proper test isolation and cleanup between tests.

Copilot uses AI. Check for mistakes.


def test_connector_safe_call_with_thehive_error():
"""Test that _safe_call properly handles TheHiveError"""
connector = TheHiveConnector("http://localhost:9000", "APIKEY123", "TESTORG")
Expand Down
1 change: 1 addition & 0 deletions TheHiveV5/thehive/add_commment.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ def run(self, arguments: dict[str, Any]) -> Optional[OutputComment]:
self.module.configuration["apikey"],
organisation=self.module.configuration["organisation"],
verify=self.module.configuration.get("verify_certificate", True),
ca_certificate=self.module.configuration.get("ca_certificate"),
)

arg_alert_id = arguments["alert_id"]
Expand Down
1 change: 1 addition & 0 deletions TheHiveV5/thehive/add_observable.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ def run(self, arguments: dict[str, Any]) -> Optional[Dict[str, List]]:
self.module.configuration["apikey"],
organisation=self.module.configuration["organisation"],
verify=self.module.configuration.get("verify_certificate", True),
ca_certificate=self.module.configuration.get("ca_certificate"),
)

arg_alert_id = arguments["alert_id"]
Expand Down
8 changes: 7 additions & 1 deletion TheHiveV5/thehive/create_alert.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,20 @@
from requests import HTTPError
from posixpath import join as urljoin

from .thehiveconnector import prepare_verify_param


class TheHiveCreateAlertV5(Action):
def run(self, arguments: dict[str, Any]) -> Optional[OutputAlert]:
verify_param = prepare_verify_param(
self.module.configuration.get("verify_certificate", True),
self.module.configuration.get("ca_certificate"),
)
api = TheHiveApi(
self.module.configuration["base_url"],
self.module.configuration["apikey"],
organisation=self.module.configuration["organisation"],
verify=self.module.configuration.get("verify_certificate", True),
verify=verify_param,
)
Comment on lines +14 to 23
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The create_alert.py action uses prepare_verify_param directly and creates TheHiveApi, while other actions (add_observable.py, add_commment.py, upload_logs.py) pass ca_certificate to TheHiveConnector which then calls prepare_verify_param. This inconsistency makes the code harder to maintain and could lead to divergent behavior. Consider refactoring create_alert.py to use TheHiveConnector like the other actions, or document why this action needs a different approach.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@CharlesLR-sekoia good remarks from copilot if you can check


arg_sekoia_server = arguments.get("sekoia_base_url", "https://app.sekoia.io")
Expand Down
69 changes: 66 additions & 3 deletions TheHiveV5/thehive/thehiveconnector.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,12 @@
- Adds basic error handling and logging
"""

import atexit
import hashlib
import logging
from typing import Optional, Dict, List, Any
import os
import tempfile
from typing import Optional, Dict, List, Any, Union

from thehive4py import TheHiveApi
from thehive4py.errors import TheHiveError
Expand Down Expand Up @@ -173,6 +177,62 @@ def key_exists(mapping: dict, key_to_check: str) -> bool:
return key_to_check in mapping


# Cache for CA certificate files to avoid creating duplicates
_ca_file_cache: Dict[str, str] = {}
_atexit_registered = False
Comment on lines +180 to +182
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The global variables _ca_file_cache and _atexit_registered are not thread-safe. In a multi-threaded environment, concurrent calls to prepare_verify_param could result in race conditions when checking and updating the cache or registering the cleanup handler. Consider using a threading.Lock to protect access to these shared resources. This is especially important in automation systems where multiple actions might be executed concurrently.

Copilot uses AI. Check for mistakes.


def _cleanup_ca_files() -> None:
"""Clean up all cached CA certificate files at process exit."""
for ca_file in _ca_file_cache.values():
try:
if os.path.exists(ca_file):
os.unlink(ca_file)
except OSError:
logger.warning("Failed to clean up temporary CA file: %s", ca_file)


def prepare_verify_param(verify: bool, ca_certificate: Optional[str] = None) -> Union[bool, str]:
"""
Prepare the verify parameter for requests/thehive4py.

Args:
verify: Whether to verify the certificate
ca_certificate: PEM-encoded CA certificate content (optional)

Returns:
- False if verify is False
- Path to temp CA file if ca_certificate is provided
- True otherwise (use system CA store)
"""
global _atexit_registered

if not verify:
return False

if ca_certificate:
# Use hash of certificate content as cache key to avoid duplicates
ca_hash = hashlib.sha256(ca_certificate.encode()).hexdigest()

if ca_hash in _ca_file_cache and os.path.exists(_ca_file_cache[ca_hash]):
return _ca_file_cache[ca_hash]

with tempfile.NamedTemporaryFile(mode="w", suffix=".pem", delete=False) as f:
f.write(ca_certificate)
ca_file = f.name
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no validation that the ca_certificate parameter contains valid PEM-encoded certificate data before writing it to a file. If invalid data is provided, it will be written to disk and passed to the TLS verification layer, potentially causing confusing errors later. Consider adding basic validation (e.g., checking for "BEGIN CERTIFICATE" and "END CERTIFICATE" markers) or documenting that validation is delegated to the underlying requests/TLS library.

Copilot uses AI. Check for mistakes.

_ca_file_cache[ca_hash] = ca_file

# Register cleanup only once
if not _atexit_registered:
atexit.register(_cleanup_ca_files)
_atexit_registered = True

return ca_file

return True


class TheHiveConnector:
"""
Minimal TheHive Alert Connector.
Expand All @@ -181,11 +241,14 @@ class TheHiveConnector:
res = connector.alert_get("ALERT-ID")
"""

def __init__(self, url: str, api_key: str, organisation: str, verify: bool = True):
def __init__(
self, url: str, api_key: str, organisation: str, verify: bool = True, ca_certificate: Optional[str] = None
):
if not api_key:
raise ValueError("API key is required")

self.api = TheHiveApi(url=url, apikey=api_key, organisation=organisation, verify=verify)
verify_param = prepare_verify_param(verify, ca_certificate)
self.api = TheHiveApi(url=url, apikey=api_key, organisation=organisation, verify=verify_param)

def _safe_call(self, fn, *args, **kwargs):
try:
Expand Down
1 change: 1 addition & 0 deletions TheHiveV5/thehive/upload_logs.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ def run(self, arguments: dict[str, Any]) -> dict[str, list[dict[str, Any]]]:
self.module.configuration["apikey"],
organisation=self.module.configuration["organisation"],
verify=self.module.configuration.get("verify_certificate", True),
ca_certificate=self.module.configuration.get("ca_certificate"),
)

arg_alert_id = arguments["alert_id"]
Expand Down
Loading