From 9081cc956d9df71f28c943f8e95fdb79aa7f1bcf Mon Sep 17 00:00:00 2001 From: Hugo DUPRAS Date: Fri, 21 Aug 2026 09:50:11 +0200 Subject: [PATCH 1/8] feat(export-report-pdf): normalize config files for manager-supported migration (#7221) --- internal-export-file/export-report-pdf/src/config.yml.sample | 3 +-- internal-export-file/export-report-pdf/src/requirements.txt | 4 +++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/internal-export-file/export-report-pdf/src/config.yml.sample b/internal-export-file/export-report-pdf/src/config.yml.sample index bef467fd115..8bbc2ac85f1 100644 --- a/internal-export-file/export-report-pdf/src/config.yml.sample +++ b/internal-export-file/export-report-pdf/src/config.yml.sample @@ -3,8 +3,7 @@ opencti: token: 'ChangeMe' connector: - type: 'INTERNAL_EXPORT_FILE' - id: 'ExportReportPdf' + id: 'ChangeMe' name: 'ExportReportPdf' scope: 'application/pdf' log_level: 'info' diff --git a/internal-export-file/export-report-pdf/src/requirements.txt b/internal-export-file/export-report-pdf/src/requirements.txt index 78753ac8925..622f03d6ab4 100644 --- a/internal-export-file/export-report-pdf/src/requirements.txt +++ b/internal-export-file/export-report-pdf/src/requirements.txt @@ -5,4 +5,6 @@ pygal==3.0.5 wheel==0.46.2 CairoSVG==2.9.0 pygal_maps_world==1.0.2 -cmarkgfm==2024.11.20 \ No newline at end of file +cmarkgfm==2024.11.20 +pydantic >=2.8.2, <3 +connectors-sdk @ git+https://github.com/OpenCTI-Platform/connectors.git@master#subdirectory=connectors-sdk From b698450c8882f78672fafb1c6a6bec10bc43d539 Mon Sep 17 00:00:00 2001 From: Hugo DUPRAS Date: Fri, 21 Aug 2026 09:51:08 +0200 Subject: [PATCH 2/8] feat(export-report-pdf): add pydantic settings for manager-supported mode (#7221) --- .../export-report-pdf/src/__init__.py | 3 + .../export-report-pdf/src/config.yml.sample | 26 +-- .../src/export_report_pdf/config.py | 156 +++++++++--------- 3 files changed, 91 insertions(+), 94 deletions(-) create mode 100644 internal-export-file/export-report-pdf/src/__init__.py diff --git a/internal-export-file/export-report-pdf/src/__init__.py b/internal-export-file/export-report-pdf/src/__init__.py new file mode 100644 index 00000000000..bf7dc49582d --- /dev/null +++ b/internal-export-file/export-report-pdf/src/__init__.py @@ -0,0 +1,3 @@ +from export_report_pdf.config import ConnectorSettings + +__all__ = ["ConnectorSettings"] diff --git a/internal-export-file/export-report-pdf/src/config.yml.sample b/internal-export-file/export-report-pdf/src/config.yml.sample index 8bbc2ac85f1..e880e6497c2 100644 --- a/internal-export-file/export-report-pdf/src/config.yml.sample +++ b/internal-export-file/export-report-pdf/src/config.yml.sample @@ -4,18 +4,18 @@ opencti: connector: id: 'ChangeMe' - name: 'ExportReportPdf' - scope: 'application/pdf' - log_level: 'info' + # name: 'ExportReportPdf' + # scope: 'application/pdf' + # log_level: 'error' export_report_pdf: - primary_color: '#ff8c00' # The primary color for the output pdf - secondary_color: '#000000' # The secondary color for the output pdf - company_address_line_1: 'Example Name' # The first line of your company address - company_address_line_2: '123 Main Street' - company_address_line_3: 'Miami, FL 33101 USA' - company_phone_number: '888.888.8888' # The phone number of your company - company_email: 'intelligence_reports@example.com' # The email of your company - company_website: 'https://example.com' # The website of your company - indicators_only: false # Whether or not to only include Observables that are Indicators in the report - defang_urls: true # Replace http in Url observables with hxxp + # primary_color: '#ff8c00' # The primary color for the output pdf + # secondary_color: '#000000' # The secondary color for the output pdf + # company_address_line_1: 'Example Name' # The first line of your company address + # company_address_line_2: '123 Main Street' + # company_address_line_3: 'Miami, FL 33101 USA' + # company_phone_number: '888.888.8888' # The phone number of your company + # company_email: 'intelligence_reports@example.com' # The email of your company + # company_website: 'https://example.com' # The website of your company + # indicators_only: false # Whether or not to only include Observables that are Indicators in the report + # defang_urls: false # Replace http in Url observables with hxxp diff --git a/internal-export-file/export-report-pdf/src/export_report_pdf/config.py b/internal-export-file/export-report-pdf/src/export_report_pdf/config.py index 4739731bff9..bbaaf2f2d8b 100644 --- a/internal-export-file/export-report-pdf/src/export_report_pdf/config.py +++ b/internal-export-file/export-report-pdf/src/export_report_pdf/config.py @@ -1,88 +1,82 @@ -import os -from pathlib import Path +from connectors_sdk import ( + BaseConfigModel, + BaseConnectorSettings, + BaseInternalExportFileConnectorConfig, + ListFromString, +) +from pydantic import Field -import yaml -from pycti import get_config_variable +class InternalExportFileConnectorConfig(BaseInternalExportFileConnectorConfig): + """Override BaseInternalExportFileConnectorConfig with ExportReportPdf defaults. -class ConnectorConfig: - def __init__(self): - """ - Initialize the connector with necessary configurations - """ + Mirrors the connector's existing ``connector`` section variables one-to-one. + """ - # Load configuration file - self.load = self._load_config() - self._initialize_configurations() + name: str = Field( + description="The name of the connector.", + default="ExportReportPdf", + ) + scope: ListFromString = Field( + description="The scope of the connector, i.e. the MIME type of the exported files.", + default=["application/pdf"], + ) - @staticmethod - def _load_config() -> dict: - """ - Load the configuration from the YAML file - :return: Configuration dictionary - """ - config_file_path = Path(__file__).parents[1].joinpath("config.yml") - config = ( - yaml.load(open(config_file_path), Loader=yaml.FullLoader) - if os.path.isfile(config_file_path) - else {} - ) - return config +class ExportReportPdfConfig(BaseConfigModel): + """Config fields specific to the ExportReportPdf connector. - def _initialize_configurations(self) -> None: - """ - Connector configuration variables - :return: None - """ - # ExportReportPdf specific config settings - self.primary_color = get_config_variable( - "EXPORT_REPORT_PDF_PRIMARY_COLOR", - ["export_report_pdf", "primary_color"], - self.load, - ) - self.secondary_color = get_config_variable( - "EXPORT_REPORT_PDF_SECONDARY_COLOR", - ["export_report_pdf", "secondary_color"], - self.load, - ) - self.company_address_line_1 = get_config_variable( - "EXPORT_REPORT_PDF_COMPANY_ADDRESS_LINE_1", - ["export_report_pdf", "company_address_line_1"], - self.load, - ) - self.company_address_line_2 = get_config_variable( - "EXPORT_REPORT_PDF_COMPANY_ADDRESS_LINE_2", - ["export_report_pdf", "company_address_line_2"], - self.load, - ) - self.company_address_line_3 = get_config_variable( - "EXPORT_REPORT_PDF_COMPANY_ADDRESS_LINE_3", - ["export_report_pdf", "company_address_line_3"], - self.load, - ) - self.company_phone_number = get_config_variable( - "EXPORT_REPORT_PDF_COMPANY_PHONE_NUMBER", - ["export_report_pdf", "company_phone_number"], - self.load, - ) - self.company_email = get_config_variable( - "EXPORT_REPORT_PDF_COMPANY_EMAIL", - ["export_report_pdf", "company_email"], - self.load, - ) - self.company_website = get_config_variable( - "EXPORT_REPORT_PDF_COMPANY_WEBSITE", - ["export_report_pdf", "company_website"], - self.load, - ) - self.indicators_only = get_config_variable( - "EXPORT_REPORT_PDF_INDICATORS_ONLY", - ["export_report_pdf", "indicators_only"], - self.load, - ) - self.defang_urls = get_config_variable( - "EXPORT_REPORT_PDF_DEFANG_URLS", - ["export_report_pdf", "defang_urls"], - self.load, - ) + Mirrors the connector's existing ``export_report_pdf`` section variables one-to-one. + """ + + primary_color: str = Field( + description="The primary color for the output PDF (hex format, e.g. '#ff8c00').", + default="#ff8c00", + ) + secondary_color: str = Field( + description="The secondary color for the output PDF (hex format, e.g. '#000000').", + default="#000000", + ) + company_address_line_1: str | None = Field( + description="The first line of your company address (e.g. company name).", + default=None, + ) + company_address_line_2: str | None = Field( + description="The second line of your company address (e.g. street address).", + default=None, + ) + company_address_line_3: str | None = Field( + description="The third line of your company address (e.g. city, state, country).", + default=None, + ) + company_phone_number: str | None = Field( + description="The phone number of your company, displayed in the PDF footer.", + default=None, + ) + company_email: str | None = Field( + description="The email of your company, displayed in the PDF footer.", + default=None, + ) + company_website: str | None = Field( + description="The website of your company, displayed in the PDF footer.", + default=None, + ) + indicators_only: bool = Field( + description="Whether or not to only include Observables that are Indicators in the report.", + default=False, + ) + defang_urls: bool = Field( + description="Whether or not to replace 'http' in Url observables with 'hxxp'.", + default=False, + ) + + +class ConnectorSettings(BaseConnectorSettings): + """Global settings for the ExportReportPdf connector.""" + + connector: InternalExportFileConnectorConfig = Field( + default_factory=InternalExportFileConnectorConfig + ) + export_report_pdf: ExportReportPdfConfig = Field( + default_factory=ExportReportPdfConfig + ) From 5c0f07e36e874c154a37d5a60170903647b5b0e4 Mon Sep 17 00:00:00 2001 From: Hugo DUPRAS Date: Fri, 21 Aug 2026 09:51:33 +0200 Subject: [PATCH 3/8] feat(export-report-pdf): use pydantic settings in existing connector code (#7221) --- .../export-report-pdf/README.md | 1 + .../export-report-pdf/src/__init__.py | 2 +- .../src/export_report_pdf/connector.py | 126 ++++++++++-------- .../{config.py => settings.py} | 4 + .../export-report-pdf/src/main.py | 6 +- 5 files changed, 83 insertions(+), 56 deletions(-) rename internal-export-file/export-report-pdf/src/export_report_pdf/{config.py => settings.py} (94%) diff --git a/internal-export-file/export-report-pdf/README.md b/internal-export-file/export-report-pdf/README.md index 34a1367c191..d9ef393c7ed 100644 --- a/internal-export-file/export-report-pdf/README.md +++ b/internal-export-file/export-report-pdf/README.md @@ -49,6 +49,7 @@ The OpenCTI Export Report PDF connector allows exporting professional, branded P - OpenCTI Platform >= 5.6.1 - **For Windows**: GTK Runtime Environment (see [Known issues](#known-issues)) +- **For macOS**: you’ll have to install `cairo` and `libffi` (with Homebrew for example); ## Configuration variables diff --git a/internal-export-file/export-report-pdf/src/__init__.py b/internal-export-file/export-report-pdf/src/__init__.py index bf7dc49582d..c35f89bcc2c 100644 --- a/internal-export-file/export-report-pdf/src/__init__.py +++ b/internal-export-file/export-report-pdf/src/__init__.py @@ -1,3 +1,3 @@ -from export_report_pdf.config import ConnectorSettings +from export_report_pdf.settings import ConnectorSettings __all__ = ["ConnectorSettings"] diff --git a/internal-export-file/export-report-pdf/src/export_report_pdf/connector.py b/internal-export-file/export-report-pdf/src/export_report_pdf/connector.py index 6872ecb4dfb..d9849ddfff1 100644 --- a/internal-export-file/export-report-pdf/src/export_report_pdf/connector.py +++ b/internal-export-file/export-report-pdf/src/export_report_pdf/connector.py @@ -8,7 +8,7 @@ import cairosvg import cmarkgfm from cmarkgfm import Options as cmarkgfmOptions -from export_report_pdf.config import ConnectorConfig +from export_report_pdf.config import ConnectorSettings from jinja2 import Environment, FileSystemLoader from pycti import OpenCTIConnectorHelper, StixCyberObservableTypes from pygal_maps_world.i18n import COUNTRIES @@ -23,7 +23,9 @@ class Connector: - def __init__(self, config: ConnectorConfig, helper: OpenCTIConnectorHelper) -> None: + def __init__( + self, config: ConnectorSettings, helper: OpenCTIConnectorHelper + ) -> None: # Instantiate the connector helper from config self.config = config self.helper = helper @@ -161,12 +163,12 @@ def _process_list( "list_filters": str(main_filter), "list_marking": list_marking, "list_report_date": list_report_date, - "company_address_line_1": self.config.company_address_line_1, - "company_address_line_2": self.config.company_address_line_2, - "company_address_line_3": self.config.company_address_line_3, - "company_phone_number": self.config.company_phone_number, - "company_email": self.config.company_email, - "company_website": self.config.company_website, + "company_address_line_1": self.config.export_report_pdf.company_address_line_1, + "company_address_line_2": self.config.export_report_pdf.company_address_line_2, + "company_address_line_3": self.config.export_report_pdf.company_address_line_3, + "company_phone_number": self.config.export_report_pdf.company_phone_number, + "company_email": self.config.export_report_pdf.company_email, + "company_website": self.config.export_report_pdf.company_website, "entities": {}, "observables": {}, } @@ -178,7 +180,10 @@ def _process_list( ): # If only include indicators and # the observable doesn't have an indicator, skip it - if self.config.indicators_only and not entity["indicators"]: + if ( + self.config.export_report_pdf.indicators_only + and not entity["indicators"] + ): self.helper.log_info( f"Skipping {obj_entity_type} observable with value {entity['observable_value']} as it was not an Indicator." ) @@ -188,7 +193,10 @@ def _process_list( context["observables"][obj_entity_type] = [] # Defang urls - if self.config.defang_urls and obj_entity_type == "Url": + if ( + self.config.export_report_pdf.defang_urls + and obj_entity_type == "Url" + ): entity["observable_value"] = entity["observable_value"].replace( "http", "hxxp", 1 ) @@ -285,12 +293,12 @@ def _process_report(self, entity_id, file_name, file_markings, access_filter): "report_confidence": report_confidence, "report_external_refs": report_external_refs, "report_date": report_date, - "company_address_line_1": self.config.company_address_line_1, - "company_address_line_2": self.config.company_address_line_2, - "company_address_line_3": self.config.company_address_line_3, - "company_phone_number": self.config.company_phone_number, - "company_email": self.config.company_email, - "company_website": self.config.company_website, + "company_address_line_1": self.config.export_report_pdf.company_address_line_1, + "company_address_line_2": self.config.export_report_pdf.company_address_line_2, + "company_address_line_3": self.config.export_report_pdf.company_address_line_3, + "company_phone_number": self.config.export_report_pdf.company_phone_number, + "company_email": self.config.export_report_pdf.company_email, + "company_website": self.config.export_report_pdf.company_website, "entities": {}, "observables": {}, } @@ -316,7 +324,10 @@ def _process_report(self, entity_id, file_name, file_markings, access_filter): ): # If only include indicators and # the observable doesn't have an indicator, skip it - if self.config.indicators_only and not entity["indicators"]: + if ( + self.config.export_report_pdf.indicators_only + and not entity["indicators"] + ): self.helper.log_info( f"Skipping {obj_entity_type} observable with value {entity['observable_value']} as it was not an Indicator." ) @@ -326,7 +337,10 @@ def _process_report(self, entity_id, file_name, file_markings, access_filter): context["observables"][obj_entity_type] = [] # Defang urls - if self.config.defang_urls and obj_entity_type == "Url": + if ( + self.config.export_report_pdf.defang_urls + and obj_entity_type == "Url" + ): entity["observable_value"] = entity["observable_value"].replace( "http", "hxxp", 1 ) @@ -373,12 +387,12 @@ def _process_intrusion_set(self, entity_id, file_name, file_markings): "entities": {}, "target_map_country": None, "report_date": now_date, - "company_address_line_1": self.config.company_address_line_1, - "company_address_line_2": self.config.company_address_line_2, - "company_address_line_3": self.config.company_address_line_3, - "company_phone_number": self.config.company_phone_number, - "company_email": self.config.company_email, - "company_website": self.config.company_website, + "company_address_line_1": self.config.export_report_pdf.company_address_line_1, + "company_address_line_2": self.config.export_report_pdf.company_address_line_2, + "company_address_line_3": self.config.export_report_pdf.company_address_line_3, + "company_phone_number": self.config.export_report_pdf.company_phone_number, + "company_email": self.config.export_report_pdf.company_email, + "company_website": self.config.export_report_pdf.company_website, } # Get a bundle of all objects affiliated with the intrusion set @@ -474,12 +488,12 @@ def _process_threat_actor_group(self, entity_id, file_name, file_markings): "entities": {}, "target_map_country": None, "report_date": now_date, - "company_address_line_1": self.config.company_address_line_1, - "company_address_line_2": self.config.company_address_line_2, - "company_address_line_3": self.config.company_address_line_3, - "company_phone_number": self.config.company_phone_number, - "company_email": self.config.company_email, - "company_website": self.config.company_website, + "company_address_line_1": self.config.export_report_pdf.company_address_line_1, + "company_address_line_2": self.config.export_report_pdf.company_address_line_2, + "company_address_line_3": self.config.export_report_pdf.company_address_line_3, + "company_phone_number": self.config.export_report_pdf.company_phone_number, + "company_email": self.config.export_report_pdf.company_email, + "company_website": self.config.export_report_pdf.company_website, } # Get a bundle of all objects affiliated with the threat actor group @@ -575,12 +589,12 @@ def _process_threat_actor_individual(self, entity_id, file_name, file_markings): "entities": {}, "target_map_country": None, "report_date": now_date, - "company_address_line_1": self.config.company_address_line_1, - "company_address_line_2": self.config.company_address_line_2, - "company_address_line_3": self.config.company_address_line_3, - "company_phone_number": self.config.company_phone_number, - "company_email": self.config.company_email, - "company_website": self.config.company_website, + "company_address_line_1": self.config.export_report_pdf.company_address_line_1, + "company_address_line_2": self.config.export_report_pdf.company_address_line_2, + "company_address_line_3": self.config.export_report_pdf.company_address_line_3, + "company_phone_number": self.config.export_report_pdf.company_phone_number, + "company_email": self.config.export_report_pdf.company_email, + "company_website": self.config.export_report_pdf.company_website, } # Get a bundle of all objects affiliated with the threat actor individual @@ -717,12 +731,12 @@ def _process_case( "case_id": case_id, "case_external_refs": case_external_refs, "case_report_date": case_report_date, - "company_address_line_1": self.config.company_address_line_1, - "company_address_line_2": self.config.company_address_line_2, - "company_address_line_3": self.config.company_address_line_3, - "company_phone_number": self.config.company_phone_number, - "company_email": self.config.company_email, - "company_website": self.config.company_website, + "company_address_line_1": self.config.export_report_pdf.company_address_line_1, + "company_address_line_2": self.config.export_report_pdf.company_address_line_2, + "company_address_line_3": self.config.export_report_pdf.company_address_line_3, + "company_phone_number": self.config.export_report_pdf.company_phone_number, + "company_email": self.config.export_report_pdf.company_email, + "company_website": self.config.export_report_pdf.company_website, "tasks": case_tasks, "case_type": case_type, "case_priority": case_priority, @@ -753,7 +767,10 @@ def _process_case( ): # If only include indicators and # the observable doesn't have an indicator, skip it - if self.config.indicators_only and not entity["indicators"]: + if ( + self.config.export_report_pdf.indicators_only + and not entity["indicators"] + ): self.helper.log_info( f"Skipping {obj_entity_type} observable with value {entity['observable_value']} as it was not an Indicator." ) @@ -763,7 +780,10 @@ def _process_case( context["observables"][obj_entity_type] = [] # Defang urls - if self.config.defang_urls and obj_entity_type == "Url": + if ( + self.config.export_report_pdf.defang_urls + and obj_entity_type == "Url" + ): entity["observable_value"] = entity["observable_value"].replace( "http", "hxxp", 1 ) @@ -806,12 +826,12 @@ def _process_vulnerability(self, entity_id, file_name, file_markings): # Prepare our context context = { "report_date": now_date, - "company_address_line_1": self.config.company_address_line_1, - "company_address_line_2": self.config.company_address_line_2, - "company_address_line_3": self.config.company_address_line_3, - "company_phone_number": self.config.company_phone_number, - "company_email": self.config.company_email, - "company_website": self.config.company_website, + "company_address_line_1": self.config.export_report_pdf.company_address_line_1, + "company_address_line_2": self.config.export_report_pdf.company_address_line_2, + "company_address_line_3": self.config.export_report_pdf.company_address_line_3, + "company_phone_number": self.config.export_report_pdf.company_phone_number, + "company_email": self.config.export_report_pdf.company_email, + "company_website": self.config.export_report_pdf.company_website, # these will be filled in: "vulnerability": None, "softwares_impacted": [], @@ -898,10 +918,12 @@ def _set_colors(self): with open(os.path.join(root, file_name), "r") as f: new_css = f.read() new_css = new_css.replace( - "", self.config.primary_color + "", + self.config.export_report_pdf.primary_color, ) new_css = new_css.replace( - "", self.config.secondary_color + "", + self.config.export_report_pdf.secondary_color, ) file_name = file_name.replace(".template", "") diff --git a/internal-export-file/export-report-pdf/src/export_report_pdf/config.py b/internal-export-file/export-report-pdf/src/export_report_pdf/settings.py similarity index 94% rename from internal-export-file/export-report-pdf/src/export_report_pdf/config.py rename to internal-export-file/export-report-pdf/src/export_report_pdf/settings.py index bbaaf2f2d8b..3a78f97cd26 100644 --- a/internal-export-file/export-report-pdf/src/export_report_pdf/config.py +++ b/internal-export-file/export-report-pdf/src/export_report_pdf/settings.py @@ -13,6 +13,10 @@ class InternalExportFileConnectorConfig(BaseInternalExportFileConnectorConfig): Mirrors the connector's existing ``connector`` section variables one-to-one. """ + id: str = Field( + description="A UUID v4 to identify the connector in OpenCTI.", + default="5f4b1afc-fbf4-4ef6-bf19-a4c89bc2b726", + ) name: str = Field( description="The name of the connector.", default="ExportReportPdf", diff --git a/internal-export-file/export-report-pdf/src/main.py b/internal-export-file/export-report-pdf/src/main.py index c8be4c17163..3009ac843b5 100644 --- a/internal-export-file/export-report-pdf/src/main.py +++ b/internal-export-file/export-report-pdf/src/main.py @@ -1,14 +1,14 @@ import traceback -from export_report_pdf.config import ConnectorConfig +from export_report_pdf.settings import ConnectorSettings from export_report_pdf.connector import Connector from pycti import OpenCTIConnectorHelper def main() -> None: try: - config = ConnectorConfig() - helper = OpenCTIConnectorHelper(config=config.load) + config = ConnectorSettings() + helper = OpenCTIConnectorHelper(config=config.to_helper_config()) connector = Connector(config=config, helper=helper) connector.run() From 2da26b85ac800936b190e60c8822e8f40952b1f1 Mon Sep 17 00:00:00 2001 From: Hugo DUPRAS Date: Fri, 21 Aug 2026 09:51:39 +0200 Subject: [PATCH 4/8] feat(export-report-pdf): set manager_supported to true in connector manifest (#7221) --- .../export-report-pdf/__metadata__/connector_manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal-export-file/export-report-pdf/__metadata__/connector_manifest.json b/internal-export-file/export-report-pdf/__metadata__/connector_manifest.json index 9c5fd6f20d2..b279168630f 100644 --- a/internal-export-file/export-report-pdf/__metadata__/connector_manifest.json +++ b/internal-export-file/export-report-pdf/__metadata__/connector_manifest.json @@ -19,7 +19,7 @@ "support_version": ">=5.6.1", "subscription_link": null, "source_code": "https://github.com/OpenCTI-Platform/connectors/tree/master/internal-export-file/export-report-pdf", - "manager_supported": false, + "manager_supported": true, "container_version": "rolling", "container_image": "opencti/connector-export-report-pdf", "container_type": "INTERNAL_EXPORT_FILE" From 6a7850f96f345069eb07783c42d3a38acd6c4c1f Mon Sep 17 00:00:00 2001 From: Hugo DUPRAS Date: Fri, 21 Aug 2026 09:56:20 +0200 Subject: [PATCH 5/8] feat(export-report-pdf): generate connector config schema for manager-supported mode (#7221) --- .../export-report-pdf/README.md | 43 +------ .../__metadata__/CONNECTOR_CONFIG_DOC.md | 24 ++++ .../__metadata__/connector_config_schema.json | 107 ++++++++++++++++++ 3 files changed, 134 insertions(+), 40 deletions(-) create mode 100644 internal-export-file/export-report-pdf/__metadata__/CONNECTOR_CONFIG_DOC.md create mode 100644 internal-export-file/export-report-pdf/__metadata__/connector_config_schema.json diff --git a/internal-export-file/export-report-pdf/README.md b/internal-export-file/export-report-pdf/README.md index d9ef393c7ed..7b2235e8e46 100644 --- a/internal-export-file/export-report-pdf/README.md +++ b/internal-export-file/export-report-pdf/README.md @@ -11,9 +11,6 @@ - [Installation](#installation) - [Requirements](#requirements) - [Configuration variables](#configuration-variables) - - [OpenCTI environment variables](#opencti-environment-variables) - - [Base connector environment variables](#base-connector-environment-variables) - - [Connector specific environment variables](#connector-specific-environment-variables) - [Deployment](#deployment) - [Docker Deployment](#docker-deployment) - [Manual Deployment](#manual-deployment) @@ -53,44 +50,10 @@ The OpenCTI Export Report PDF connector allows exporting professional, branded P ## Configuration variables -There are a number of configuration options, which are set either in `docker-compose.yml` (for Docker) or in `config.yml` (for manual deployment). +Find all the configuration variables available here: [Connector Configurations](./__metadata__/CONNECTOR_CONFIG_DOC.md) -### OpenCTI environment variables - -Below are the parameters you'll need to set for OpenCTI: - -| Parameter | config.yml `opencti` | Docker environment variable | Default | Mandatory | Description | -|---------------|----------------------|-----------------------------|---------|-----------|------------------------------------------------------| -| OpenCTI URL | `url` | `OPENCTI_URL` | / | Yes | The URL of the OpenCTI platform. | -| OpenCTI Token | `token` | `OPENCTI_TOKEN` | / | Yes | The default admin token set in the OpenCTI platform. | - -### Base connector environment variables - -Below are the parameters you'll need to set for running the connector properly: - -| Parameter | config.yml `connector` | Docker environment variable | Default | Mandatory | Description | -|----------------|------------------------|-----------------------------|-----------------|-----------|----------------------------------------------------------------------------------------| -| Connector ID | `id` | `CONNECTOR_ID` | / | Yes | A unique `UUIDv4` identifier for this connector instance. | -| Connector Name | `name` | `CONNECTOR_NAME` | ExportReportPdf | No | Name of the connector. | -| Connector Scope| `scope` | `CONNECTOR_SCOPE` | application/pdf | Yes | The MIME type for PDF files. | -| Log Level | `log_level` | `CONNECTOR_LOG_LEVEL` | info | No | Determines the verbosity of the logs. Options are `debug`, `info`, `warn`, or `error`. | - -### Connector specific environment variables - -Below are the parameters specific to the Export Report PDF connector: - -| Parameter | config.yml `export_report_pdf` | Docker environment variable | Default | Mandatory | Description | -|-----------------------|--------------------------------|-----------------------------------------|---------|-----------|-----------------------------------------------------------------| -| Primary Color | `primary_color` | `EXPORT_REPORT_PDF_PRIMARY_COLOR` | / | No | Primary color for the PDF (hex format, e.g., `#ff8c00`). | -| Secondary Color | `secondary_color` | `EXPORT_REPORT_PDF_SECONDARY_COLOR` | / | No | Secondary color for the PDF (hex format, e.g., `#000000`). | -| Company Address Line 1| `company_address_line_1` | `EXPORT_REPORT_PDF_COMPANY_ADDRESS_LINE_1` | / | No | First line of company address (e.g., company name). | -| Company Address Line 2| `company_address_line_2` | `EXPORT_REPORT_PDF_COMPANY_ADDRESS_LINE_2` | / | No | Second line of company address (e.g., street address). | -| Company Address Line 3| `company_address_line_3` | `EXPORT_REPORT_PDF_COMPANY_ADDRESS_LINE_3` | / | No | Third line of company address (e.g., city, state, country). | -| Company Phone Number | `company_phone_number` | `EXPORT_REPORT_PDF_COMPANY_PHONE_NUMBER`| / | No | Company phone number for the PDF footer. | -| Company Email | `company_email` | `EXPORT_REPORT_PDF_COMPANY_EMAIL` | / | No | Company email for the PDF footer. | -| Company Website | `company_website` | `EXPORT_REPORT_PDF_COMPANY_WEBSITE` | / | No | Company website URL for the PDF footer. | -| Indicators Only | `indicators_only` | `EXPORT_REPORT_PDF_INDICATORS_ONLY` | false | No | If `true`, only include observables that have indicator status. | -| Defang URLs | `defang_urls` | `EXPORT_REPORT_PDF_DEFANG_URLS` | true | No | If `true`, replace `http` with `hxxp` in URL observables. | +_The `opencti` and `connector` options in the `docker-compose.yml` and `config.yml` are the same as for any other connector. +For more information regarding variables, please refer to [OpenCTI's documentation on connectors](https://docs.opencti.io/latest/deployment/connectors/)._ ## Deployment diff --git a/internal-export-file/export-report-pdf/__metadata__/CONNECTOR_CONFIG_DOC.md b/internal-export-file/export-report-pdf/__metadata__/CONNECTOR_CONFIG_DOC.md new file mode 100644 index 00000000000..546cfc3ad6b --- /dev/null +++ b/internal-export-file/export-report-pdf/__metadata__/CONNECTOR_CONFIG_DOC.md @@ -0,0 +1,24 @@ +# Connector Configurations + +Below is an exhaustive enumeration of all configurable parameters available, each accompanied by detailed explanations of their purposes, default behaviors, and usage guidelines to help you understand and utilize them effectively. + +### Type: `object` + +| Property | Type | Required | Possible values | Default | Description | +| -------- | ---- | -------- | --------------- | ------- | ----------- | +| OPENCTI_URL | `string` | ✅ | Format: [`uri`](https://json-schema.org/understanding-json-schema/reference/string#built-in-formats) | | The base URL of the OpenCTI instance. | +| OPENCTI_TOKEN | `string` | ✅ | Format: [`password`](https://json-schema.org/understanding-json-schema/reference/string#built-in-formats) | | The API token to connect to OpenCTI. | +| CONNECTOR_NAME | `string` | | string | `"ExportReportPdf"` | The name of the connector. | +| CONNECTOR_SCOPE | `array` | | string | `["application/pdf"]` | The scope of the connector, i.e. the MIME type of the exported files. | +| CONNECTOR_LOG_LEVEL | `string` | | `debug` `info` `warn` `warning` `error` | `"error"` | The minimum level of logs to display. | +| CONNECTOR_TYPE | `const` | | `INTERNAL_EXPORT_FILE` | `"INTERNAL_EXPORT_FILE"` | | +| EXPORT_REPORT_PDF_PRIMARY_COLOR | `string` | | string | `"#ff8c00"` | The primary color for the output PDF (hex format, e.g. '#ff8c00'). | +| EXPORT_REPORT_PDF_SECONDARY_COLOR | `string` | | string | `"#000000"` | The secondary color for the output PDF (hex format, e.g. '#000000'). | +| EXPORT_REPORT_PDF_COMPANY_ADDRESS_LINE_1 | `string` | | string | `null` | The first line of your company address (e.g. company name). | +| EXPORT_REPORT_PDF_COMPANY_ADDRESS_LINE_2 | `string` | | string | `null` | The second line of your company address (e.g. street address). | +| EXPORT_REPORT_PDF_COMPANY_ADDRESS_LINE_3 | `string` | | string | `null` | The third line of your company address (e.g. city, state, country). | +| EXPORT_REPORT_PDF_COMPANY_PHONE_NUMBER | `string` | | string | `null` | The phone number of your company, displayed in the PDF footer. | +| EXPORT_REPORT_PDF_COMPANY_EMAIL | `string` | | string | `null` | The email of your company, displayed in the PDF footer. | +| EXPORT_REPORT_PDF_COMPANY_WEBSITE | `string` | | string | `null` | The website of your company, displayed in the PDF footer. | +| EXPORT_REPORT_PDF_INDICATORS_ONLY | `boolean` | | boolean | `false` | Whether or not to only include Observables that are Indicators in the report. | +| EXPORT_REPORT_PDF_DEFANG_URLS | `boolean` | | boolean | `false` | Whether or not to replace 'http' in Url observables with 'hxxp'. | diff --git a/internal-export-file/export-report-pdf/__metadata__/connector_config_schema.json b/internal-export-file/export-report-pdf/__metadata__/connector_config_schema.json new file mode 100644 index 00000000000..40806793b46 --- /dev/null +++ b/internal-export-file/export-report-pdf/__metadata__/connector_config_schema.json @@ -0,0 +1,107 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://www.filigran.io/connectors/export-report-pdf_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.", + "format": "password", + "type": "string", + "writeOnly": true + }, + "CONNECTOR_NAME": { + "default": "ExportReportPdf", + "description": "The name of the connector.", + "type": "string" + }, + "CONNECTOR_SCOPE": { + "default": [ + "application/pdf" + ], + "description": "The scope of the connector, i.e. the MIME type of the exported files.", + "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": "INTERNAL_EXPORT_FILE", + "default": "INTERNAL_EXPORT_FILE", + "type": "string" + }, + "EXPORT_REPORT_PDF_PRIMARY_COLOR": { + "default": "#ff8c00", + "description": "The primary color for the output PDF (hex format, e.g. '#ff8c00').", + "type": "string" + }, + "EXPORT_REPORT_PDF_SECONDARY_COLOR": { + "default": "#000000", + "description": "The secondary color for the output PDF (hex format, e.g. '#000000').", + "type": "string" + }, + "EXPORT_REPORT_PDF_COMPANY_ADDRESS_LINE_1": { + "default": null, + "description": "The first line of your company address (e.g. company name).", + "type": "string" + }, + "EXPORT_REPORT_PDF_COMPANY_ADDRESS_LINE_2": { + "default": null, + "description": "The second line of your company address (e.g. street address).", + "type": "string" + }, + "EXPORT_REPORT_PDF_COMPANY_ADDRESS_LINE_3": { + "default": null, + "description": "The third line of your company address (e.g. city, state, country).", + "type": "string" + }, + "EXPORT_REPORT_PDF_COMPANY_PHONE_NUMBER": { + "default": null, + "description": "The phone number of your company, displayed in the PDF footer.", + "type": "string" + }, + "EXPORT_REPORT_PDF_COMPANY_EMAIL": { + "default": null, + "description": "The email of your company, displayed in the PDF footer.", + "type": "string" + }, + "EXPORT_REPORT_PDF_COMPANY_WEBSITE": { + "default": null, + "description": "The website of your company, displayed in the PDF footer.", + "type": "string" + }, + "EXPORT_REPORT_PDF_INDICATORS_ONLY": { + "default": false, + "description": "Whether or not to only include Observables that are Indicators in the report.", + "type": "boolean" + }, + "EXPORT_REPORT_PDF_DEFANG_URLS": { + "default": false, + "description": "Whether or not to replace 'http' in Url observables with 'hxxp'.", + "type": "boolean" + } + }, + "required": [ + "OPENCTI_URL", + "OPENCTI_TOKEN" + ], + "additionalProperties": true +} \ No newline at end of file From d2d9b32190b2342f242040d83fe8f64b4f212a2c Mon Sep 17 00:00:00 2001 From: Hugo DUPRAS Date: Fri, 21 Aug 2026 09:58:56 +0200 Subject: [PATCH 6/8] feat(export-report-pdf): add unit tests for manager-supported mode (#7221) --- .../tests/export_report_pdf/conftest.py | 8 +- .../tests/export_report_pdf/test_config.py | 201 ++++++++++++++++-- .../tests/export_report_pdf/test_connector.py | 27 +-- .../export-report-pdf/tests/test_main.py | 108 ++++++++++ 4 files changed, 309 insertions(+), 35 deletions(-) diff --git a/internal-export-file/export-report-pdf/tests/export_report_pdf/conftest.py b/internal-export-file/export-report-pdf/tests/export_report_pdf/conftest.py index 7fee2bfddf2..15a225e38b1 100644 --- a/internal-export-file/export-report-pdf/tests/export_report_pdf/conftest.py +++ b/internal-export-file/export-report-pdf/tests/export_report_pdf/conftest.py @@ -11,12 +11,11 @@ def fixture_config_dict() -> dict[str, Any]: return { "opencti": { - "url": "opencti-url", + "url": "http://localhost:8080", "token": "opencti-token", }, "connector": { "id": "export-report-pdf-connector-id", - "type": "INTERNAL_EXPORT_FILE", "name": "ExportReportPdf", "scope": "application/pdf", "log_level": "info", @@ -49,9 +48,4 @@ def mock_config(mocker: MockerFixture, config_dict: dict[str, Any]) -> None: @pytest.fixture(name="mocked_helper") def fixture_mocked_helper(mocker: MockerFixture) -> Mock: helper = mocker.patch("pycti.OpenCTIConnectorHelper", MagicMock()) - # helper.connect_id = "test-connector-id" - # helper.connect_name = "Test Connector" - # helper.api.work.initiate_work.return_value = "work-id" - # helper.get_state.return_value = {} - # helper.stix2_create_bundle.return_value = "bundle" return helper diff --git a/internal-export-file/export-report-pdf/tests/export_report_pdf/test_config.py b/internal-export-file/export-report-pdf/tests/export_report_pdf/test_config.py index c0e3316f4f7..104729f1769 100644 --- a/internal-export-file/export-report-pdf/tests/export_report_pdf/test_config.py +++ b/internal-export-file/export-report-pdf/tests/export_report_pdf/test_config.py @@ -1,15 +1,186 @@ -from export_report_pdf.config import ConnectorConfig - - -def test_config(mock_config, config_dict): - config = ConnectorConfig() - assert config.company_address_line_1 == "Company Address Line 1" - assert config.company_address_line_2 == "Company Address Line 2" - assert config.company_address_line_3 == "Company Address Line 3" - assert config.company_email == "export-report-pdf@email.com" - assert config.company_phone_number == "+1-234-567-8900" - assert config.company_website == "https://export-report-pdf.com" - assert config.defang_urls == True - assert config.indicators_only == False - assert config.primary_color == "#ff8c00" - assert config.secondary_color == "#000000" +from typing import Any + +import pytest +from connectors_sdk import BaseConfigModel, ConfigValidationError +from export_report_pdf.settings import ConnectorSettings + + +def test_config_should_load_from_environment(mock_config, config_dict) -> None: + """ + Test that ConnectorSettings loads all the connector's variables from the environment. + """ + config = ConnectorSettings() + + assert str(config.opencti.url) == "http://localhost:8080/" + assert config.opencti.token.get_secret_value() == "opencti-token" + assert config.connector.id == "export-report-pdf-connector-id" + assert config.connector.name == "ExportReportPdf" + assert config.connector.scope == ["application/pdf"] + assert config.connector.type == "INTERNAL_EXPORT_FILE" + assert config.export_report_pdf.company_address_line_1 == "Company Address Line 1" + assert config.export_report_pdf.company_address_line_2 == "Company Address Line 2" + assert config.export_report_pdf.company_address_line_3 == "Company Address Line 3" + assert config.export_report_pdf.company_email == "export-report-pdf@email.com" + assert config.export_report_pdf.company_phone_number == "+1-234-567-8900" + assert config.export_report_pdf.company_website == "https://export-report-pdf.com" + assert config.export_report_pdf.defang_urls is True + assert config.export_report_pdf.indicators_only is False + assert config.export_report_pdf.primary_color == "#ff8c00" + assert config.export_report_pdf.secondary_color == "#000000" + + +def test_config_should_apply_defaults() -> None: + """ + Test that ConnectorSettings falls back on the connector's default values + when only the required variables are set. + """ + + class FakeConnectorSettings(ConnectorSettings): + @classmethod + def _load_config_dict(cls, _, handler) -> dict[str, Any]: + return handler( + { + "opencti": { + "url": "http://localhost:8080", + "token": "test-token", + }, + "connector": {"id": "connector-id"}, + "export_report_pdf": {}, + } + ) + + config = FakeConnectorSettings() + + assert config.connector.name == "ExportReportPdf" + assert config.connector.scope == ["application/pdf"] + assert config.connector.log_level == "error" + assert config.export_report_pdf.primary_color == "#ff8c00" + assert config.export_report_pdf.secondary_color == "#000000" + assert config.export_report_pdf.company_address_line_1 is None + assert config.export_report_pdf.company_address_line_2 is None + assert config.export_report_pdf.company_address_line_3 is None + assert config.export_report_pdf.company_phone_number is None + assert config.export_report_pdf.company_email is None + assert config.export_report_pdf.company_website is None + assert config.export_report_pdf.indicators_only is False + assert config.export_report_pdf.defang_urls is False + + +@pytest.mark.parametrize( + "settings_dict", + [ + pytest.param( + { + "opencti": { + "url": "http://localhost:8080", + "token": "test-token", + }, + "connector": { + "id": "connector-id", + "name": "ExportReportPdf", + "scope": "application/pdf", + "log_level": "error", + }, + "export_report_pdf": { + "primary_color": "#ff8c00", + "secondary_color": "#000000", + "company_address_line_1": "Example Name", + "company_address_line_2": "123 Main Street", + "company_address_line_3": "Miami, FL 33101 USA", + "company_phone_number": "888.888.8888", + "company_email": "intelligence_reports@example.com", + "company_website": "https://example.com", + "indicators_only": False, + "defang_urls": True, + }, + }, + id="full_valid_settings_dict", + ), + pytest.param( + { + "opencti": { + "url": "http://localhost:8080", + "token": "test-token", + }, + "connector": {"id": "connector-id"}, + "export_report_pdf": {}, + }, + id="minimal_valid_settings_dict", + ), + ], +) +def test_settings_should_accept_valid_input(settings_dict) -> None: + """ + Test that ConnectorSettings accepts valid input. + _load_config_dict is overridden to return a fake but valid dict. + """ + + class FakeConnectorSettings(ConnectorSettings): + @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.export_report_pdf, BaseConfigModel) is True + + +@pytest.mark.parametrize( + "settings_dict, field_name", + [ + pytest.param( + {}, + "settings", + id="empty_settings_dict", + ), + pytest.param( + { + "opencti": {"url": "http://localhost:8080"}, + "connector": {"id": "connector-id"}, + "export_report_pdf": {}, + }, + "opencti.token", + id="missing_opencti_token", + ), + pytest.param( + { + "opencti": { + "url": "http://localhost:8080", + "token": "test-token", + }, + "connector": {"id": 42}, + "export_report_pdf": {}, + }, + "connector.id", + id="invalid_connector_id", + ), + pytest.param( + { + "opencti": { + "url": "http://localhost:8080", + "token": "test-token", + }, + "connector": {"id": "connector-id"}, + "export_report_pdf": {"indicators_only": "not-a-boolean"}, + }, + "export_report_pdf.indicators_only", + id="invalid_indicators_only", + ), + ], +) +def test_settings_should_raise_when_invalid_input(settings_dict, field_name) -> None: + """ + Test that ConnectorSettings raises on invalid input. + _load_config_dict is overridden to return a fake and invalid dict. + """ + + class FakeConnectorSettings(ConnectorSettings): + @classmethod + def _load_config_dict(cls, _, handler) -> dict[str, Any]: + return handler(settings_dict) + + with pytest.raises(ConfigValidationError) as err: + FakeConnectorSettings() + assert "Error validating configuration" in str(err) diff --git a/internal-export-file/export-report-pdf/tests/export_report_pdf/test_connector.py b/internal-export-file/export-report-pdf/tests/export_report_pdf/test_connector.py index 5289edaa4de..ebb94ec2afe 100644 --- a/internal-export-file/export-report-pdf/tests/export_report_pdf/test_connector.py +++ b/internal-export-file/export-report-pdf/tests/export_report_pdf/test_connector.py @@ -1,26 +1,27 @@ import pytest -from export_report_pdf.config import ConnectorConfig +from export_report_pdf.settings import ConnectorSettings from export_report_pdf.connector import Connector from pycti import OpenCTIConnectorHelper @pytest.mark.usefixtures("mock_config", "mocked_helper") def test_connector_config(mocked_helper: OpenCTIConnectorHelper) -> None: - connector = Connector(config=ConnectorConfig(), helper=mocked_helper) - assert connector.config.company_address_line_1 == "Company Address Line 1" - assert connector.config.company_address_line_2 == "Company Address Line 2" - assert connector.config.company_address_line_3 == "Company Address Line 3" - assert connector.config.company_email == "export-report-pdf@email.com" - assert connector.config.company_phone_number == "+1-234-567-8900" - assert connector.config.company_website == "https://export-report-pdf.com" - assert connector.config.defang_urls == True - assert connector.config.indicators_only == False - assert connector.config.primary_color == "#ff8c00" - assert connector.config.secondary_color == "#000000" + connector = Connector(config=ConnectorSettings(), helper=mocked_helper) + config = connector.config.export_report_pdf + assert config.company_address_line_1 == "Company Address Line 1" + assert config.company_address_line_2 == "Company Address Line 2" + assert config.company_address_line_3 == "Company Address Line 3" + assert config.company_email == "export-report-pdf@email.com" + assert config.company_phone_number == "+1-234-567-8900" + assert config.company_website == "https://export-report-pdf.com" + assert config.defang_urls is True + assert config.indicators_only is False + assert config.primary_color == "#ff8c00" + assert config.secondary_color == "#000000" @pytest.mark.usefixtures("mock_config", "mocked_helper") def test_connector_start(mocked_helper: OpenCTIConnectorHelper) -> None: - connector = Connector(config=ConnectorConfig(), helper=mocked_helper) + connector = Connector(config=ConnectorSettings(), helper=mocked_helper) connector.run() mocked_helper.listen.assert_called_once() diff --git a/internal-export-file/export-report-pdf/tests/test_main.py b/internal-export-file/export-report-pdf/tests/test_main.py index b2e008f7c78..248d7d43099 100644 --- a/internal-export-file/export-report-pdf/tests/test_main.py +++ b/internal-export-file/export-report-pdf/tests/test_main.py @@ -1,9 +1,117 @@ +from typing import Any +from unittest.mock import MagicMock + +import pytest +from export_report_pdf.config import ConnectorSettings +from export_report_pdf.connector import Connector from main import main +from pycti import OpenCTIConnectorHelper from pytest_mock import MockerFixture +@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 for testing purpose. + Overrides _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": "ExportReportPdf", + "scope": "application/pdf", + "log_level": "error", + }, + "export_report_pdf": { + "primary_color": "#ff8c00", + "secondary_color": "#000000", + "company_address_line_1": "Example Name", + "company_address_line_2": "123 Main Street", + "company_address_line_3": "Miami, FL 33101 USA", + "company_phone_number": "888.888.8888", + "company_email": "intelligence_reports@example.com", + "company_website": "https://example.com", + "indicators_only": False, + "defang_urls": False, + }, + } + ) + + +def test_connector_settings_is_instantiated() -> None: + """ + Test that ConnectorSettings 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 + """ + settings = StubConnectorSettings() + + assert isinstance(settings, ConnectorSettings) + assert isinstance(settings.to_helper_config(), dict) + + +def test_opencti_connector_helper_is_instantiated( + mock_opencti_connector_helper, +) -> None: + """ + Test that OpenCTIConnectorHelper 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 + """ + 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 == "ExportReportPdf" + assert helper.connect_scope == "application/pdf" + assert helper.log_level == "ERROR" + + +def test_connector_is_instantiated( + mock_opencti_connector_helper, mocker: MockerFixture +) -> None: + """ + 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 + """ + mocker.patch("export_report_pdf.connector.Connector._set_colors") + + settings = StubConnectorSettings() + helper = OpenCTIConnectorHelper(config=settings.to_helper_config()) + + connector = Connector(config=settings, helper=helper) + + assert connector.config == settings + assert connector.helper == helper + + def test_main(mocker: MockerFixture) -> None: # Make sure the main starts without errors + mocker.patch("main.ConnectorSettings", StubConnectorSettings) mocker.patch("main.OpenCTIConnectorHelper") mocker.patch("export_report_pdf.connector.Connector._set_colors") main() From 9809ff2017f1fd4619b0a10d4d39b8665bb18f17 Mon Sep 17 00:00:00 2001 From: Hugo DUPRAS Date: Fri, 21 Aug 2026 10:40:21 +0200 Subject: [PATCH 7/8] feat(export-report-pdf): set a unique default UUID for the connector id (#7221) --- .../tests/export_report_pdf/test_config.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/internal-export-file/export-report-pdf/tests/export_report_pdf/test_config.py b/internal-export-file/export-report-pdf/tests/export_report_pdf/test_config.py index 104729f1769..b626f9a94d3 100644 --- a/internal-export-file/export-report-pdf/tests/export_report_pdf/test_config.py +++ b/internal-export-file/export-report-pdf/tests/export_report_pdf/test_config.py @@ -1,4 +1,5 @@ from typing import Any +from uuid import UUID import pytest from connectors_sdk import BaseConfigModel, ConfigValidationError @@ -66,6 +67,32 @@ def _load_config_dict(cls, _, handler) -> dict[str, Any]: assert config.export_report_pdf.defang_urls is False +def test_config_should_default_connector_id() -> None: + """ + Test that the connector's id falls back on its unique default UUID v4 + when CONNECTOR_ID is not provided. + """ + + class FakeConnectorSettings(ConnectorSettings): + @classmethod + def _load_config_dict(cls, _, handler) -> dict[str, Any]: + return handler( + { + "opencti": { + "url": "http://localhost:8080", + "token": "test-token", + }, + "connector": {}, + "export_report_pdf": {}, + } + ) + + config = FakeConnectorSettings() + + assert config.connector.id == "5f4b1afc-fbf4-4ef6-bf19-a4c89bc2b726" + assert UUID(config.connector.id).version == 4 + + @pytest.mark.parametrize( "settings_dict", [ From 55ecb1da452b9bbd117ccbc29a341c952219ef11 Mon Sep 17 00:00:00 2001 From: Hugo DUPRAS Date: Fri, 21 Aug 2026 15:08:21 +0200 Subject: [PATCH 8/8] fix(export-report-pdf): repoint imports at the renamed settings module (#7221) --- .../export-report-pdf/src/export_report_pdf/connector.py | 2 +- internal-export-file/export-report-pdf/src/main.py | 2 +- .../export-report-pdf/tests/export_report_pdf/test_connector.py | 2 +- internal-export-file/export-report-pdf/tests/test_main.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal-export-file/export-report-pdf/src/export_report_pdf/connector.py b/internal-export-file/export-report-pdf/src/export_report_pdf/connector.py index d9849ddfff1..26119dfd6cf 100644 --- a/internal-export-file/export-report-pdf/src/export_report_pdf/connector.py +++ b/internal-export-file/export-report-pdf/src/export_report_pdf/connector.py @@ -8,7 +8,7 @@ import cairosvg import cmarkgfm from cmarkgfm import Options as cmarkgfmOptions -from export_report_pdf.config import ConnectorSettings +from export_report_pdf.settings import ConnectorSettings from jinja2 import Environment, FileSystemLoader from pycti import OpenCTIConnectorHelper, StixCyberObservableTypes from pygal_maps_world.i18n import COUNTRIES diff --git a/internal-export-file/export-report-pdf/src/main.py b/internal-export-file/export-report-pdf/src/main.py index 3009ac843b5..32adaa7e6c5 100644 --- a/internal-export-file/export-report-pdf/src/main.py +++ b/internal-export-file/export-report-pdf/src/main.py @@ -1,7 +1,7 @@ import traceback -from export_report_pdf.settings import ConnectorSettings from export_report_pdf.connector import Connector +from export_report_pdf.settings import ConnectorSettings from pycti import OpenCTIConnectorHelper diff --git a/internal-export-file/export-report-pdf/tests/export_report_pdf/test_connector.py b/internal-export-file/export-report-pdf/tests/export_report_pdf/test_connector.py index ebb94ec2afe..828cb13b24a 100644 --- a/internal-export-file/export-report-pdf/tests/export_report_pdf/test_connector.py +++ b/internal-export-file/export-report-pdf/tests/export_report_pdf/test_connector.py @@ -1,6 +1,6 @@ import pytest -from export_report_pdf.settings import ConnectorSettings from export_report_pdf.connector import Connector +from export_report_pdf.settings import ConnectorSettings from pycti import OpenCTIConnectorHelper diff --git a/internal-export-file/export-report-pdf/tests/test_main.py b/internal-export-file/export-report-pdf/tests/test_main.py index 248d7d43099..d2f0855b55a 100644 --- a/internal-export-file/export-report-pdf/tests/test_main.py +++ b/internal-export-file/export-report-pdf/tests/test_main.py @@ -2,8 +2,8 @@ from unittest.mock import MagicMock import pytest -from export_report_pdf.config import ConnectorSettings from export_report_pdf.connector import Connector +from export_report_pdf.settings import ConnectorSettings from main import main from pycti import OpenCTIConnectorHelper from pytest_mock import MockerFixture