diff --git a/stream/microsoft-defender-intel/__metadata__/connector_config_schema.json b/stream/microsoft-defender-intel/__metadata__/connector_config_schema.json new file mode 100644 index 00000000000..b3167f6d8df --- /dev/null +++ b/stream/microsoft-defender-intel/__metadata__/connector_config_schema.json @@ -0,0 +1,124 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://www.filigran.io/connectors/microsoft-defender-intel_config.schema.json", + "type": "object", + "properties": { + "OPENCTI_URL": { + "description": "The base URL of the OpenCTI instance.", + "format": "uri", + "maxLength": 2083, + "minLength": 1, + "type": "string" + }, + "OPENCTI_TOKEN": { + "description": "The API token to connect to OpenCTI.", + "type": "string" + }, + "CONNECTOR_NAME": { + "default": "MicrosoftDefenderIntel", + "description": "The name of the connector.", + "type": "string" + }, + "CONNECTOR_SCOPE": { + "description": "The scope of the connector, e.g. 'flashpoint'.", + "items": { + "type": "string" + }, + "type": "array" + }, + "CONNECTOR_LOG_LEVEL": { + "default": "error", + "description": "The minimum level of logs to display.", + "enum": [ + "debug", + "info", + "warn", + "warning", + "error" + ], + "type": "string" + }, + "CONNECTOR_TYPE": { + "const": "STREAM", + "default": "STREAM", + "type": "string" + }, + "CONNECTOR_LIVE_STREAM_ID": { + "default": "live", + "description": "The ID of the live stream to connect to.", + "type": "string" + }, + "CONNECTOR_LIVE_STREAM_LISTEN_DELETE": { + "default": true, + "description": "Whether to listen for delete events on the live stream.", + "type": "boolean" + }, + "CONNECTOR_LIVE_STREAM_NO_DEPENDENCIES": { + "default": true, + "description": "Whether to ignore dependencies when processing events from the live stream.", + "type": "boolean" + }, + "MICROSOFT_DEFENDER_INTEL_TENANT_ID": { + "description": "Your Azure App Tenant ID, see the screenshot to help you find this information.", + "type": "string" + }, + "MICROSOFT_DEFENDER_INTEL_CLIENT_ID": { + "description": "Your Azure App Client ID, see the screenshot to help you find this information.", + "type": "string" + }, + "MICROSOFT_DEFENDER_INTEL_CLIENT_SECRET": { + "description": "Your Azure App Client secret, See the screenshot to help you find this information.", + "format": "password", + "type": "string", + "writeOnly": true + }, + "MICROSOFT_DEFENDER_INTEL_LOGIN_URL": { + "default": "https://login.microsoft.com", + "description": "Login URL for Microsoft which is `https://login.microsoft.com`", + "type": "string" + }, + "MICROSOFT_DEFENDER_INTEL_BASE_URL": { + "default": "https://api.securitycenter.microsoft.com", + "description": "The resource the API will use which is `https://api.securitycenter.microsoft.com`", + "type": "string" + }, + "MICROSOFT_DEFENDER_INTEL_RESOURCE_PATH": { + "default": "/api/indicators", + "description": "The request URL that will be used which is `/api/indicators`", + "type": "string" + }, + "MICROSOFT_DEFENDER_INTEL_EXPIRE_TIME": { + "default": 30, + "description": "Number of days for your indicator to expire in Sentinel. Suggestion of `30` as a default", + "type": "integer" + }, + "MICROSOFT_DEFENDER_INTEL_ACTION": { + "default": "Alert", + "description": "The action to apply if the indicator is matched from within the targetProduct security tool. Possible values are: `Warn`, `Block`, `Audit`, `Alert`, `AlertAndBlock`, `BlockAndRemediate`, `Allowed`. `BlockAndRemediate` is not compatible with network indicators (see: https://learn.microsoft.com/en-us/defender-endpoint/indicator-manage)", + "enum": [ + "Warn", + "Block", + "Audit", + "Alert", + "AlertAndBlock", + "BlockAndRemediate", + "Allowed" + ], + "type": "string" + }, + "MICROSOFT_DEFENDER_INTEL_PASSIVE_ONLY": { + "default": false, + "description": "Determines if the indicator should trigger an event that is visible to an end-user. When set to `True` security tools will not notify the end user that a \u00e2\u20ac\u02dchit\u00e2\u20ac\u2122 has occurred. This is most often treated as audit or silent mode by security products where they will simply log that a match occurred but will not perform the action. Default value is `False`.", + "type": "boolean" + } + }, + "required": [ + "OPENCTI_URL", + "OPENCTI_TOKEN", + "CONNECTOR_SCOPE", + "MICROSOFT_DEFENDER_INTEL_TENANT_ID", + "MICROSOFT_DEFENDER_INTEL_CLIENT_ID", + "MICROSOFT_DEFENDER_INTEL_CLIENT_SECRET" + ], + "additionalProperties": true +} \ No newline at end of file diff --git a/stream/microsoft-defender-intel/docker-compose.yml b/stream/microsoft-defender-intel/docker-compose.yml index 92a5888d021..239ba9f5181 100644 --- a/stream/microsoft-defender-intel/docker-compose.yml +++ b/stream/microsoft-defender-intel/docker-compose.yml @@ -11,4 +11,4 @@ services: - MICROSOFT_DEFENDER_INTEL_TENANT_ID=ChangeMe - MICROSOFT_DEFENDER_INTEL_CLIENT_ID=ChangeMe - MICROSOFT_DEFENDER_INTEL_CLIENT_SECRET=ChangeMe - restart: unless-stopped \ No newline at end of file + restart: unless-stopped diff --git a/stream/microsoft-defender-intel/src/config.yml.sample b/stream/microsoft-defender-intel/src/config.yml.sample index 80671d1bfa8..dc7897b1744 100644 --- a/stream/microsoft-defender-intel/src/config.yml.sample +++ b/stream/microsoft-defender-intel/src/config.yml.sample @@ -11,4 +11,4 @@ connector: microsoft_defender_intel: tenant_id: 'ChangeMe' client_id: 'ChangeMe' - client_secret: 'ChangeMe' \ No newline at end of file + client_secret: 'ChangeMe' diff --git a/stream/microsoft-defender-intel/src/main.py b/stream/microsoft-defender-intel/src/main.py index 358c211a0ca..bb1bddfe1ef 100644 --- a/stream/microsoft-defender-intel/src/main.py +++ b/stream/microsoft-defender-intel/src/main.py @@ -1,6 +1,10 @@ import traceback -from microsoft_defender_intel_connector import MicrosoftDefenderIntelConnector +from microsoft_defender_intel_connector import ( + ConnectorSettings, + MicrosoftDefenderIntelConnector, +) +from pycti import OpenCTIConnectorHelper if __name__ == "__main__": """ @@ -13,7 +17,10 @@ It signals to the operating system and any calling processes that the program did not complete successfully. """ try: - connector = MicrosoftDefenderIntelConnector() + settings = ConnectorSettings() + helper = OpenCTIConnectorHelper(config=settings.to_helper_config()) + + connector = MicrosoftDefenderIntelConnector(config=settings, helper=helper) connector.run() except Exception: traceback.print_exc() diff --git a/stream/microsoft-defender-intel/src/microsoft_defender_intel_connector/__init__.py b/stream/microsoft-defender-intel/src/microsoft_defender_intel_connector/__init__.py index 2c220019e65..85c23a89d66 100644 --- a/stream/microsoft-defender-intel/src/microsoft_defender_intel_connector/__init__.py +++ b/stream/microsoft-defender-intel/src/microsoft_defender_intel_connector/__init__.py @@ -1,3 +1,3 @@ -from .connector import MicrosoftDefenderIntelConnector - -__all__ = ["MicrosoftDefenderIntelConnector"] +__all__ = ["MicrosoftDefenderIntelConnector", "ConnectorSettings"] +from microsoft_defender_intel_connector.connector import MicrosoftDefenderIntelConnector +from microsoft_defender_intel_connector.settings import ConnectorSettings diff --git a/stream/microsoft-defender-intel/src/microsoft_defender_intel_connector/connector.py b/stream/microsoft-defender-intel/src/microsoft_defender_intel_connector/connector.py index 2b9c2ac931f..65ea590ee81 100644 --- a/stream/microsoft-defender-intel/src/microsoft_defender_intel_connector/connector.py +++ b/stream/microsoft-defender-intel/src/microsoft_defender_intel_connector/connector.py @@ -1,15 +1,11 @@ import json from json import JSONDecodeError +from microsoft_defender_intel_connector.settings import ConnectorSettings from pycti import OpenCTIConnectorHelper from .api_handler import DefenderApiHandler, DefenderApiHandlerError -from .config_variables import ConfigConnector -from .utils import ( - FILE_HASH_TYPES_MAPPER, - is_observable, - is_stix_indicator, -) +from .utils import FILE_HASH_TYPES_MAPPER, is_observable, is_stix_indicator class MicrosoftDefenderIntelConnector: @@ -39,15 +35,13 @@ class MicrosoftDefenderIntelConnector: """ - def __init__(self): + def __init__(self, config: ConnectorSettings, helper: OpenCTIConnectorHelper): """ Initialize the Connector with necessary configurations """ - - # Load configuration file and connection helper - self.config = ConfigConnector() - self.helper = OpenCTIConnectorHelper(self.config.load) - self.api = DefenderApiHandler(self.helper, self.config) + self.config = config + self.helper = helper + self.api = DefenderApiHandler(self.helper, self.config.microsoft_defender_intel) def _check_stream_id(self) -> None: """ @@ -73,7 +67,6 @@ def _convert_indicator_to_observables(self, data) -> list[dict]: "observable_values", data ) if parsed_observables: - # Iterate over the parsed observables for observable in parsed_observables: observable_data = {} observable_data.update(data) @@ -92,7 +85,6 @@ def _convert_indicator_to_observables(self, data) -> list[dict]: observable_data["type"] = "file" observable_data["hashes"] = file observables.append(observable_data) - return observables except: indicator_opencti_id = OpenCTIConnectorHelper.get_attribute_in_extension( @@ -117,13 +109,11 @@ def _create_defender_indicator(self, observable_data): "[CREATE] Indicator created", {"defender_id": result["id"], "opencti_id": observable_opencti_id}, ) - # Update OpenCTI SDO external references external_reference = self.helper.api.external_reference.create( source_name="Microsoft Defender", external_id=result["id"], description="Intel within the Microsoft platform.", ) - # If observable was built from an OpenCTI Indicator if "pattern" in observable_data: self.helper.api.stix_domain_object.add_external_reference( id=observable_opencti_id, @@ -184,10 +174,7 @@ def _handle_update_event(self, data): did_update = True self.helper.connector_logger.info( "[UPDATE] Indicator updated", - { - "defender_id": result[0]["id"], - "opencti_id": opencti_id, - }, + {"defender_id": result[0]["id"], "opencti_id": opencti_id}, ) elif is_observable(data): result = self.api.find_indicators(data["value"]) @@ -196,10 +183,7 @@ def _handle_update_event(self, data): did_update = True self.helper.connector_logger.info( "[UPDATE] Indicator updated", - { - "defender_id": result[0]["id"], - "opencti_id": opencti_id, - }, + {"defender_id": result[0]["id"], "opencti_id": opencti_id}, ) if not did_update: self.helper.connector_logger.info( @@ -265,23 +249,14 @@ def _handle_delete_event(self, data): did_delete = True self.helper.connector_logger.info( "[DELETE] Indicator deleted", - { - "defender_id": indicator_result["id"], - "opencti_id": opencti_id, - }, + {"defender_id": indicator_result["id"], "opencti_id": opencti_id}, ) external_reference = self.helper.api.external_reference.read( filters={ "mode": "and", "filters": [ - { - "key": "source_name", - "values": ["Microsoft Defender"], - }, - { - "key": "external_id", - "values": [indicator_result["id"]], - }, + {"key": "source_name", "values": ["Microsoft Defender"]}, + {"key": "external_id", "values": [indicator_result["id"]]}, ], "filterGroups": [], } @@ -319,20 +294,16 @@ def process_message(self, msg) -> None: """ try: self._check_stream_id() - parsed_msg = self.validate_json(msg) data = parsed_msg["data"] - if msg.event == "create": self._handle_create_event(data) if msg.event == "update": self._handle_update_event(data) if msg.event == "delete": self._handle_delete_event(data) - except DefenderApiHandlerError as err: self.helper.connector_logger.error(err.msg, err.metadata) - except Exception as err: self.helper.connector_logger.error( "[ERROR] Failed processing data {" + str(err) + "}" diff --git a/stream/microsoft-defender-intel/src/microsoft_defender_intel_connector/settings.py b/stream/microsoft-defender-intel/src/microsoft_defender_intel_connector/settings.py new file mode 100644 index 00000000000..13cbe28d648 --- /dev/null +++ b/stream/microsoft-defender-intel/src/microsoft_defender_intel_connector/settings.py @@ -0,0 +1,83 @@ +from typing import Literal + +from connectors_sdk import ( + BaseConfigModel, + BaseConnectorSettings, + BaseStreamConnectorConfig, +) +from pydantic import Field, SecretStr + + +class StreamConnectorConfig(BaseStreamConnectorConfig): + """ + Override the `BaseStreamConnectorConfig` to add parameters and/or defaults + to the configuration for connectors of type `STREAM`. + """ + + name: str = Field( + description="The name of the connector.", + default="MicrosoftDefenderIntel", + ) + live_stream_id: str = Field( + description="The ID of the live stream to connect to.", + default="live", # listen the global stream (not filtered) + ) + + +class MicrosoftDefenderIntelConfig(BaseConfigModel): + """ + Define parameters and/or defaults for the configuration specific to the `MicrosoftDefenderIntelConnector`. + """ + + tenant_id: str = Field( + description="Your Azure App Tenant ID, see the screenshot to help you find this information.", + ) + client_id: str = Field( + description="Your Azure App Client ID, see the screenshot to help you find this information.", + ) + client_secret: SecretStr = Field( + description="Your Azure App Client secret, See the screenshot to help you find this information.", + ) + login_url: str = Field( + description="Login URL for Microsoft which is `https://login.microsoft.com`", + default="https://login.microsoft.com", + ) + base_url: str = Field( + description="The resource the API will use which is `https://api.securitycenter.microsoft.com`", + default="https://api.securitycenter.microsoft.com", + ) + resource_path: str = Field( + description="The request URL that will be used which is `/api/indicators`", + default="/api/indicators", + ) + expire_time: int = Field( + description="Number of days for your indicator to expire in Sentinel. Suggestion of `30` as a default", + default=30, + ) + action: Literal[ + "Warn", + "Block", + "Audit", + "Alert", + "AlertAndBlock", + "BlockAndRemediate", + "Allowed", + ] = Field( + description="The action to apply if the indicator is matched from within the targetProduct security tool. Possible values are: `Warn`, `Block`, `Audit`, `Alert`, `AlertAndBlock`, `BlockAndRemediate`, `Allowed`. `BlockAndRemediate` is not compatible with network indicators (see: https://learn.microsoft.com/en-us/defender-endpoint/indicator-manage)", + default="Alert", + ) + passive_only: bool = Field( + description="Determines if the indicator should trigger an event that is visible to an end-user. When set to `True` security tools will not notify the end user that a ‘hit’ has occurred. This is most often treated as audit or silent mode by security products where they will simply log that a match occurred but will not perform the action. Default value is `False`.", + default=False, + ) + + +class ConnectorSettings(BaseConnectorSettings): + """ + Override `BaseConnectorSettings` to include `StreamConnectorConfig` and `MicrosoftDefenderIntelConfig`. + """ + + connector: StreamConnectorConfig = Field(default_factory=StreamConnectorConfig) + microsoft_defender_intel: MicrosoftDefenderIntelConfig = Field( + default_factory=MicrosoftDefenderIntelConfig + ) diff --git a/stream/microsoft-defender-intel/src/requirements.txt b/stream/microsoft-defender-intel/src/requirements.txt index 8602cca8498..9e7b707a91b 100644 --- a/stream/microsoft-defender-intel/src/requirements.txt +++ b/stream/microsoft-defender-intel/src/requirements.txt @@ -1 +1,3 @@ -pycti==6.8.15 \ No newline at end of file +pycti==6.8.15 +pydantic >=2.8.2, <3 +connectors-sdk @ git+https://github.com/OpenCTI-Platform/connectors.git@master#subdirectory=connectors-sdk diff --git a/stream/microsoft-defender-intel/tests/conftest.py b/stream/microsoft-defender-intel/tests/conftest.py new file mode 100644 index 00000000000..5ee8fc0e226 --- /dev/null +++ b/stream/microsoft-defender-intel/tests/conftest.py @@ -0,0 +1,4 @@ +import os +import sys + +sys.path.append(os.path.join(os.path.dirname(__file__), "..", "src")) diff --git a/stream/microsoft-defender-intel/tests/test-requirements.txt b/stream/microsoft-defender-intel/tests/test-requirements.txt new file mode 100644 index 00000000000..bdef682113c --- /dev/null +++ b/stream/microsoft-defender-intel/tests/test-requirements.txt @@ -0,0 +1,2 @@ +-r ../src/requirements.txt +pytest==8.4.2 diff --git a/stream/microsoft-defender-intel/tests/test_main.py b/stream/microsoft-defender-intel/tests/test_main.py new file mode 100644 index 00000000000..57bc4b8518b --- /dev/null +++ b/stream/microsoft-defender-intel/tests/test_main.py @@ -0,0 +1,112 @@ +from typing import Any +from unittest.mock import MagicMock + +import pytest +from microsoft_defender_intel_connector import ( + ConnectorSettings, + MicrosoftDefenderIntelConnector, +) +from pycti import OpenCTIConnectorHelper + + +@pytest.fixture +def mock_opencti_connector_helper(monkeypatch): + """Mock all heavy dependencies of OpenCTIConnectorHelper, typically API calls to OpenCTI.""" + + module_import_path = "pycti.connector.opencti_connector_helper" + monkeypatch.setattr(f"{module_import_path}.killProgramHook", MagicMock()) + monkeypatch.setattr(f"{module_import_path}.sched.scheduler", MagicMock()) + monkeypatch.setattr(f"{module_import_path}.ConnectorInfo", MagicMock()) + monkeypatch.setattr(f"{module_import_path}.OpenCTIApiClient", MagicMock()) + monkeypatch.setattr(f"{module_import_path}.OpenCTIConnector", MagicMock()) + monkeypatch.setattr(f"{module_import_path}.OpenCTIMetricHandler", MagicMock()) + monkeypatch.setattr(f"{module_import_path}.PingAlive", MagicMock()) + + +class StubConnectorSettings(ConnectorSettings): + """ + Subclass of `ConnectorSettings` (implementation of `BaseConnectorSettings`) for testing purpose. + It overrides `BaseConnectorSettings._load_config_dict` to return a fake but valid config dict. + """ + + @classmethod + def _load_config_dict(cls, _, handler) -> dict[str, Any]: + return handler( + { + "opencti": { + "url": "http://localhost:8080", + "token": "test-token", + }, + "connector": { + "id": "connector-id", + "name": "Test Connector", + "scope": "test, connector", + "log_level": "error", + "live_stream_id": "live", + "live_stream_listen_delete": True, + "live_stream_no_dependencies": True, + }, + "microsoft_defender_intel": { + "tenant_id": "str", + "client_id": "str", + "client_secret": "SecretStr", + "login_url": "https://login.microsoft.com", + "base_url": "https://api.securitycenter.microsoft.com", + "resource_path": "/api/indicators", + "expire_time": 30, + "action": "Alert", + "passive_only": False, + }, + } + ) + + +def test_connector_settings_is_instantiated(): + """ + Test that the implementation of `BaseConnectorSettings` (from `connectors-sdk`) can be instantiated successfully: + - the implemented class MUST have a method `to_helper_config` (inherited from `BaseConnectorSettings`) + - the method `to_helper_config` MUST return a dict (as in base class) + """ + settings = StubConnectorSettings() + + assert isinstance(settings, ConnectorSettings) + assert isinstance(settings.to_helper_config(), dict) + + +def test_opencti_connector_helper_is_instantiated(mock_opencti_connector_helper): + """ + Test that `OpenCTIConnectorHelper` (from `pycti`) can be instantiated successfully: + - the value of `settings.to_helper_config` MUST be the expected dict for `OpenCTIConnectorHelper` + - the helper MUST be able to get its instance's attributes from the config dict + + :param mock_opencti_connector_helper: `OpenCTIConnectorHelper` is mocked during this test to avoid any external calls to OpenCTI API + """ + settings = StubConnectorSettings() + helper = OpenCTIConnectorHelper(config=settings.to_helper_config()) + + assert helper.opencti_url == "http://localhost:8080/" + assert helper.opencti_token == "test-token" + assert helper.connect_id == "connector-id" + assert helper.connect_name == "Test Connector" + assert helper.connect_scope == "test,connector" + assert helper.log_level == "ERROR" + assert helper.connect_live_stream_id == "live" + assert helper.connect_live_stream_listen_delete == True + assert helper.connect_live_stream_no_dependencies == True + + +def test_connector_is_instantiated(mock_opencti_connector_helper): + """ + Test that the connector's main class can be instantiated successfully: + - the connector's main class MUST be able to access env/config vars through `self.config` + - the connector's main class MUST be able to access `pycti` API through `self.helper` + + :param mock_opencti_connector_helper: `OpenCTIConnectorHelper` is mocked during this test to avoid any external calls to OpenCTI API + """ + settings = StubConnectorSettings() + helper = OpenCTIConnectorHelper(config=settings.to_helper_config()) + + connector = MicrosoftDefenderIntelConnector(config=settings, helper=helper) + + assert connector.config == settings + assert connector.helper == helper diff --git a/stream/microsoft-defender-intel/tests/tests_connector/test_settings.py b/stream/microsoft-defender-intel/tests/tests_connector/test_settings.py new file mode 100644 index 00000000000..ced4e182ed4 --- /dev/null +++ b/stream/microsoft-defender-intel/tests/tests_connector/test_settings.py @@ -0,0 +1,154 @@ +from typing import Any + +import pytest +from connectors_sdk import BaseConfigModel, ConfigValidationError +from microsoft_defender_intel_connector import ConnectorSettings + + +@pytest.mark.parametrize( + "settings_dict", + [ + pytest.param( + { + "opencti": {"url": "http://localhost:8080", "token": "test-token"}, + "connector": { + "id": "connector-id", + "name": "Test Connector", + "scope": "test, connector", + "log_level": "error", + "live_stream_id": "live", + "live_stream_listen_delete": True, + "live_stream_no_dependencies": True, + }, + "microsoft_defender_intel": { + "tenant_id": "str", + "client_id": "str", + "client_secret": "SecretStr", + "login_url": "https://login.microsoft.com", + "base_url": "https://api.securitycenter.microsoft.com", + "resource_path": "/api/indicators", + "expire_time": 30, + "action": "Alert", + "passive_only": False, + }, + }, + id="full_valid_settings_dict", + ), + pytest.param( + { + "opencti": {"url": "http://localhost:8080", "token": "test-token"}, + "connector": { + "id": "connector-id", + "scope": "test, connector", + "log_level": "error", + }, + "microsoft_defender_intel": { + "tenant_id": "str", + "client_id": "str", + "client_secret": "SecretStr", + }, + }, + id="minimal_valid_settings_dict", + ), + ], +) +def test_settings_should_accept_valid_input(settings_dict): + """ + Test that `ConnectorSettings` (implementation of `BaseConnectorSettings` from `connectors-sdk`) accepts valid input. + For the test purpose, `BaseConnectorSettings._load_config_dict` is overridden to return + a fake but valid dict (instead of the env/config vars parsed from `config.yml`, `.env` or env vars). + + :param settings_dict: The dict to use as `ConnectorSettings` input + """ + + class FakeConnectorSettings(ConnectorSettings): + """ + Subclass of `ConnectorSettings` (implementation of `BaseConnectorSettings`) for testing purpose. + It overrides `BaseConnectorSettings._load_config_dict` to return a fake but valid config dict. + """ + + @classmethod + def _load_config_dict(cls, _, handler) -> dict[str, Any]: + return handler(settings_dict) + + settings = FakeConnectorSettings() + assert isinstance(settings.opencti, BaseConfigModel) is True + assert isinstance(settings.connector, BaseConfigModel) is True + assert isinstance(settings.microsoft_defender_intel, BaseConfigModel) is True + + +@pytest.mark.parametrize( + "settings_dict, field_name", + [ + pytest.param({}, "settings", id="empty_settings_dict"), + pytest.param( + { + "opencti": {"url": "http://localhost:PORT", "token": "test-token"}, + "connector": { + "id": "connector-id", + "name": "Test Connector", + "scope": "test, connector", + "log_level": "error", + "live_stream_id": "live", + "live_stream_listen_delete": True, + "live_stream_no_dependencies": True, + }, + "microsoft_defender_intel": { + "tenant_id": "str", + "client_id": "str", + "client_secret": "SecretStr", + "login_url": "https://login.microsoft.com", + "base_url": "https://api.securitycenter.microsoft.com", + "resource_path": "/api/indicators", + "expire_time": 30, + "action": "Alert", + "passive_only": False, + }, + }, + "opencti.url", + id="invalid_opencti_url", + ), + pytest.param( + { + "opencti": {"url": "http://localhost:8080", "token": "test-token"}, + "connector": { + "name": "Test Connector", + "scope": "test, connector", + "log_level": "error", + "live_stream_id": "live", + "live_stream_listen_delete": True, + "live_stream_no_dependencies": True, + }, + "microsoft_defender_intel": { + "tenant_id": "str", + "client_id": "str", + "client_secret": "SecretStr", + }, + }, + "connector.id", + id="missing_connector_id", + ), + ], +) +def test_settings_should_raise_when_invalid_input(settings_dict, field_name): + """ + Test that `ConnectorSettings` (implementation of `BaseConnectorSettings` from `connectors-sdk`) raises on invalid input. + For the test purpose, `BaseConnectorSettings._load_config_dict` is overridden to return + a fake and invalid dict (instead of the env/config vars parsed from `config.yml`, `.env` or env vars). + + :param settings_dict: The dict to use as `ConnectorSettings` input + """ + + class FakeConnectorSettings(ConnectorSettings): + """ + Subclass of `ConnectorSettings` (implementation of `BaseConnectorSettings`) for testing purpose. + It overrides `BaseConnectorSettings._load_config_dict` to return a fake but valid config dict. + """ + + @classmethod + def _load_config_dict(cls, _, handler) -> dict[str, Any]: + return handler(settings_dict) + + with pytest.raises(ConfigValidationError) as err: + FakeConnectorSettings() + assert str("Error validating configuration") in str(err)