From b101eb427aa5f11f824480bf935df91e7a962f59 Mon Sep 17 00:00:00 2001 From: Romain GUIGNARD Date: Sat, 22 Aug 2026 16:00:37 +0200 Subject: [PATCH 1/2] fix(google-dtm): guard optional fields when building the alert content (#7360) --- .../google_dtm_connector/converter_to_stix.py | 82 ++++++----- .../tests_connector/test_converter_to_stix.py | 136 ++++++++++++++++++ 2 files changed, 179 insertions(+), 39 deletions(-) create mode 100644 external-import/google-dtm/tests/tests_connector/test_converter_to_stix.py diff --git a/external-import/google-dtm/src/google_dtm_connector/converter_to_stix.py b/external-import/google-dtm/src/google_dtm_connector/converter_to_stix.py index 3798dc34de0..bedfacb6362 100644 --- a/external-import/google-dtm/src/google_dtm_connector/converter_to_stix.py +++ b/external-import/google-dtm/src/google_dtm_connector/converter_to_stix.py @@ -64,16 +64,16 @@ def create_channel(self, dtm_channel: dict) -> CustomObjectChannel: :param dtm_channel: :return: """ - channel_type = dtm_channel.get("messenger", {}).get("name") - channel_name = dtm_channel.get("name") - channel_description = dtm_channel.get("channel_info").get("description") + channel_type = dtm_channel.get("messenger", {}).get("name") or "" + channel_name = dtm_channel.get("name") or "" + channel_description = dtm_channel.get("channel_info", {}).get("description") channel_url = dtm_channel.get("channel_url") - formatted_channel_name = "[" + channel_type + "] - " + channel_name + formatted_channel_name = f"[{channel_type}] - {channel_name}" external_refs = [] if channel_url: external_ref = stix2.ExternalReference( - source_name=channel_type + " - " + channel_name, url=channel_url + source_name=f"{channel_type} - {channel_name}", url=channel_url ) external_refs.append(external_ref) channel = CustomObjectChannel( @@ -128,22 +128,23 @@ def convert_document_analysis_alert_to_markdown_content( :return: """ metadata_part = self.get_common_content_metadata_part(dtm_alert) + dtm_alert_doc = dtm_alert.get("doc", {}) markdown_content = f""" {metadata_part} ### Source Information -- **Author**: {dtm_alert.get("doc").get("source_url")} -- **Collected**: {dtm_alert.get("doc").get("ingested")} -- **Published**: {dtm_alert.get("doc").get("timestamp")} -- **Source File**: {dtm_alert.get("doc").get("filename")} -- **MD5**: {dtm_alert.get("doc").get("file_hashes").get("md5")} -- **SHA1**: {dtm_alert.get("doc").get("file_hashes").get("sha1")} -- **SHA256**: {dtm_alert.get("doc").get("file_hashes").get("sha256")} -- **Source**: {dtm_alert.get("doc").get("source")} -- **Source URL**: {dtm_alert.get("doc").get("source_url")} +- **Author**: {dtm_alert_doc.get("source_url")} +- **Collected**: {dtm_alert_doc.get("ingested")} +- **Published**: {dtm_alert_doc.get("timestamp")} +- **Source File**: {dtm_alert_doc.get("filename")} +- **MD5**: {dtm_alert_doc.get("file_hashes", {}).get("md5")} +- **SHA1**: {dtm_alert_doc.get("file_hashes", {}).get("sha1")} +- **SHA256**: {dtm_alert_doc.get("file_hashes", {}).get("sha256")} +- **Source**: {dtm_alert_doc.get("source")} +- **Source URL**: {dtm_alert_doc.get("source_url")} ### Content ``` -{dtm_alert.get("doc").get("raw_text")} +{dtm_alert_doc.get("raw_text")} ``` """ return markdown_content @@ -154,18 +155,19 @@ def convert_paste_alert_to_markdown_content(self, dtm_alert: dict) -> str: :return: """ metadata_part = self.get_common_content_metadata_part(dtm_alert) + dtm_alert_doc = dtm_alert.get("doc", {}) markdown_content = f""" {metadata_part} ### Source Information -- **Created**: {dtm_alert.get("doc").get("timestamp")} -- **Paste Id**: {dtm_alert.get("doc").get("paste_id")} -- **URL**: {dtm_alert.get("doc").get("source_location", {}).get("url", "")} -- **Author**: {dtm_alert.get("doc").get("author", {}).get("identity", {}).get("name", "")} -- **Title**: {dtm_alert.get("doc").get("title", "")} +- **Created**: {dtm_alert_doc.get("timestamp")} +- **Paste Id**: {dtm_alert_doc.get("paste_id")} +- **URL**: {dtm_alert_doc.get("source_location", {}).get("url", "")} +- **Author**: {dtm_alert_doc.get("author", {}).get("identity", {}).get("name", "")} +- **Title**: {dtm_alert_doc.get("title", "")} ### Content ``` -{dtm_alert.get("doc").get("body")} +{dtm_alert_doc.get("body")} ``` """ return markdown_content @@ -185,16 +187,16 @@ def convert_account_discovery_alert_to_markdown_content( - **Source URL**: {dtm_alert_doc.get("source_url")} - **Collected**: {dtm_alert_doc.get("ingested")} - **Published**: {dtm_alert_doc.get("timestamp")} -- **Source File**: {dtm_alert_doc.get("source_file").get("filename")} -- **MD5**: {dtm_alert_doc.get("source_file").get("hashes").get("md5")} -- **SHA1**: {dtm_alert_doc.get("source_file").get("hashes").get("sha1")} -- **SHA256**: {dtm_alert_doc.get("source_file").get("hashes").get("sha256")} +- **Source File**: {dtm_alert_doc.get("source_file", {}).get("filename")} +- **MD5**: {dtm_alert_doc.get("source_file", {}).get("hashes", {}).get("md5")} +- **SHA1**: {dtm_alert_doc.get("source_file", {}).get("hashes", {}).get("sha1")} +- **SHA256**: {dtm_alert_doc.get("source_file", {}).get("hashes", {}).get("sha256")} ### Content -- **Service URL**: {dtm_alert_doc.get("service_account").get("service").get("inet_location").get("domain")} -- **Service Domain**: {dtm_alert_doc.get("service_account").get("service").get("inet_location").get("url")} -- **Email Domain**: {dtm_alert_doc.get("service_account").get("email_domain")} -- **Login**: {dtm_alert_doc.get("service_account").get("login")} -- **Password**: {dtm_alert_doc.get("service_account").get("password").get("plain_text")} +- **Service URL**: {dtm_alert_doc.get("service_account", {}).get("service", {}).get("inet_location", {}).get("domain")} +- **Service Domain**: {dtm_alert_doc.get("service_account", {}).get("service", {}).get("inet_location", {}).get("url")} +- **Email Domain**: {dtm_alert_doc.get("service_account", {}).get("email_domain")} +- **Login**: {dtm_alert_doc.get("service_account", {}).get("login")} +- **Password**: {dtm_alert_doc.get("service_account", {}).get("password", {}).get("plain_text")} """ return markdown_content @@ -209,11 +211,11 @@ def convert_message_type_alert_to_markdown_content(self, dtm_alert: dict) -> str {metadata_part} ### Source Information - **Created**: {dtm_alert_doc.get("ingested")} -- **Channel**: {dtm_alert_doc.get("channel").get("name")} -- **Channel URL**: {dtm_alert_doc.get("channel").get("channel_url")} -- **Channel Description**: {dtm_alert_doc.get("channel").get("channel_info").get("description")} -- **Messenger**: {dtm_alert_doc.get("channel").get("messenger").get("name")} -- **Author**: {dtm_alert_doc.get("sender").get("identity").get("name")} +- **Channel**: {dtm_alert_doc.get("channel", {}).get("name")} +- **Channel URL**: {dtm_alert_doc.get("channel", {}).get("channel_url")} +- **Channel Description**: {dtm_alert_doc.get("channel", {}).get("channel_info", {}).get("description")} +- **Messenger**: {dtm_alert_doc.get("channel", {}).get("messenger", {}).get("name")} +- **Author**: {dtm_alert_doc.get("sender", {}).get("identity", {}).get("name")} - **Message Id**: {dtm_alert_doc.get("message_id")} ### Content @@ -294,7 +296,7 @@ def convert_alert_to_markdown_content(self, dtm_alert: dict) -> str: {metadata_part} ### Post ``` -{dtm_alert.get("doc").get("raw_text")} +{dtm_alert.get("doc", {}).get("raw_text")} ``` """ return markdown_content @@ -321,7 +323,7 @@ def create_incident(self, dtm_alert: dict) -> list: incident_type = dtm_alert.get("alert_type") # generate a content based on alert useful information - doc_type = dtm_alert.get("doc").get("__type") + doc_type = dtm_alert.get("doc", {}).get("__type") files = [] try: if doc_type == "message": @@ -397,8 +399,10 @@ def create_incident(self, dtm_alert: dict) -> list: ) stix_objects.append(stix_incident) - if "channel" in dtm_alert.get("doc"): - stix_channel = self.create_channel(dtm_alert.get("doc").get("channel")) + if "channel" in dtm_alert.get("doc", {}): + stix_channel = self.create_channel( + dtm_alert.get("doc", {}).get("channel", {}) + ) stix_objects.append(stix_channel) # create relation between incident and channel diff --git a/external-import/google-dtm/tests/tests_connector/test_converter_to_stix.py b/external-import/google-dtm/tests/tests_connector/test_converter_to_stix.py new file mode 100644 index 00000000000..8194500f265 --- /dev/null +++ b/external-import/google-dtm/tests/tests_connector/test_converter_to_stix.py @@ -0,0 +1,136 @@ +"""Tests for the alert content generated by `ConverterToStix`. + +DTM omits optional fields instead of returning them null, so these tests pin +that an incomplete document still produces its `alert.md` attachment and, for +`create_channel`, that it does not abort the whole run. +""" + +import base64 +from unittest.mock import MagicMock + +import pytest +from src.google_dtm_connector.converter_to_stix import ConverterToStix + +_ACCOUNT_DISCOVERY_ALERT = { + "id": "da1pueokorjs73af1j90", + "title": "Credential leak on acme.com", + "created_at": "2026-08-22T01:00:00.000Z", + "updated_at": "2026-08-22T01:30:00.000Z", + "severity": "high", + "alert_type": "Compromised Credentials", + "doc": { + "__type": "account_discovery", + "source_url": "https://forum.invalid/thread/1", + "source_file": { + "filename": "combo.txt", + "hashes": {"md5": "d41d8cd98f00b204e9800998ecf8427e"}, + }, + "service_account": { + "login": "jdoe@acme.com", + "email_domain": "acme.com", + "service": {"inet_location": {"domain": "acme.com"}}, + }, + }, +} + +_MESSAGE_ALERT = { + "id": "da1pueokorjs73af1j91", + "title": "Mention on a Telegram channel", + "created_at": "2026-08-22T01:00:00.000Z", + "updated_at": "2026-08-22T01:30:00.000Z", + "severity": "medium", + "alert_type": "Forum", + "doc": { + "__type": "message", + "body": "acme.com is up for sale", + # No channel_info, no messenger, no sender. + "channel": {"name": "leaks", "channel_url": "https://t.me/leaks"}, + }, +} + + +@pytest.fixture +def converter() -> ConverterToStix: + return ConverterToStix(MagicMock(), tlp="clear") + + +def _attachments(stix_objects): + incident = next(obj for obj in stix_objects if obj["type"] == "incident") + return incident.get("x_opencti_files", []) + + +def _alert_markdown(stix_objects): + (attachment,) = _attachments(stix_objects) + assert attachment["name"] == "alert.md" + return base64.b64decode(attachment["data"]).decode("utf-8") + + +# -------------------------------------------------------------------------- +# A missing optional field no longer costs the attachment +# -------------------------------------------------------------------------- + + +def test_account_discovery_without_a_plaintext_password(converter): + """The reported bug: service_account.password is absent, .get() raised.""" + stix_objects = converter.create_incident(_ACCOUNT_DISCOVERY_ALERT) + + markdown = _alert_markdown(stix_objects) + converter.helper.connector_logger.error.assert_not_called() + # The absent value is rendered empty, the ones that are there are kept. + assert "- **Password**: None\n" in markdown + assert "- **Login**: jdoe@acme.com\n" in markdown + assert "- **MD5**: d41d8cd98f00b204e9800998ecf8427e\n" in markdown + assert "- **SHA256**: None\n" in markdown + + +def test_message_alert_without_channel_info(converter): + stix_objects = converter.create_incident(_MESSAGE_ALERT) + + markdown = _alert_markdown(stix_objects) + converter.helper.connector_logger.error.assert_not_called() + assert "- **Channel**: leaks\n" in markdown + assert "- **Channel Description**: None\n" in markdown + assert "- **Author**: None\n" in markdown + + +@pytest.mark.parametrize( + "doc", + [ + {"__type": "account_discovery"}, + {"__type": "message"}, + {"__type": "paste"}, + {"__type": "document_analysis"}, + {"__type": "shop_listing"}, + {"__type": "domain_discovery"}, + {"__type": "web_content_publish"}, + {"__type": "something_new"}, + ], +) +def test_an_empty_document_still_produces_an_attachment(converter, doc): + """Every alert type must survive a document reduced to its type.""" + stix_objects = converter.create_incident({**_ACCOUNT_DISCOVERY_ALERT, "doc": doc}) + + assert len(_attachments(stix_objects)) == 1 + converter.helper.connector_logger.error.assert_not_called() + + +# -------------------------------------------------------------------------- +# create_channel sits outside create_incident's try/except +# -------------------------------------------------------------------------- + + +def test_a_channel_without_channel_info_does_not_abort_the_run(converter): + """`create_channel` is called outside the try: it used to kill the run.""" + stix_objects = converter.create_incident(_MESSAGE_ALERT) + + channel = next(obj for obj in stix_objects if obj["type"] == "channel") + assert channel["name"] == "[] - leaks" + assert channel["external_references"][0]["url"] == "https://t.me/leaks" + + +def test_a_channel_reduced_to_nothing_does_not_raise(converter): + channel = converter.create_channel({}) + + assert channel["type"] == "channel" + # stix2 drops the empty list rather than storing it. + assert "external_references" not in channel From edcb9028f6e389ff18a7abb69c46b08eeedb086c Mon Sep 17 00:00:00 2001 From: Romain GUIGNARD Date: Mon, 24 Aug 2026 08:44:27 +0200 Subject: [PATCH 2/2] fixes GH copilot review --- external-import/google-dtm/docker-compose.yml | 2 +- .../google_dtm_connector/converter_to_stix.py | 106 +++++++++--------- .../tests_connector/test_converter_to_stix.py | 20 +++- 3 files changed, 70 insertions(+), 58 deletions(-) diff --git a/external-import/google-dtm/docker-compose.yml b/external-import/google-dtm/docker-compose.yml index 6f41049737c..6bfcc580813 100644 --- a/external-import/google-dtm/docker-compose.yml +++ b/external-import/google-dtm/docker-compose.yml @@ -1,7 +1,7 @@ version: '3' services: connector-google-dtm: - image: opencti/connector-google-dtm:6.9.10 + image: opencti/connector-google-dtm:latest environment: # OpenCTI connection parameters - OPENCTI_URL=http://localhost diff --git a/external-import/google-dtm/src/google_dtm_connector/converter_to_stix.py b/external-import/google-dtm/src/google_dtm_connector/converter_to_stix.py index bedfacb6362..824ec4a1bb9 100644 --- a/external-import/google-dtm/src/google_dtm_connector/converter_to_stix.py +++ b/external-import/google-dtm/src/google_dtm_connector/converter_to_stix.py @@ -93,11 +93,11 @@ def generate_incident_description(dtm_alert: dict) -> str: :return: """ markdown_description = f""" -**Alert Id**: {dtm_alert.get("id")}\n +**Alert Id**: {dtm_alert.get("id", "")}\n -**Alert Summary**: {dtm_alert.get("alert_summary")}\n +**Alert Summary**: {dtm_alert.get("alert_summary", "")}\n -**Summary from Gemini**: {dtm_alert.get("ai_doc_summary")} +**Summary from Gemini**: {dtm_alert.get("ai_doc_summary", "")} """ return markdown_description @@ -109,11 +109,11 @@ def get_common_content_metadata_part(dtm_alert: dict) -> str: """ markdown_content = f""" ### Metadata -- **Alert Id**: {dtm_alert.get("id")} -- **Monitor Id**: {dtm_alert.get("monitor_id")} -- **Created**: {dtm_alert.get("created_at")} -- **Type**: {dtm_alert.get("alert_type")} -- **Severity**: {dtm_alert.get("severity")} +- **Alert Id**: {dtm_alert.get("id", "")} +- **Monitor Id**: {dtm_alert.get("monitor_id", "")} +- **Created**: {dtm_alert.get("created_at", "")} +- **Type**: {dtm_alert.get("alert_type", "")} +- **Severity**: {dtm_alert.get("severity", "")} ### Summary from Gemini {dtm_alert.get("ai_doc_summary", "N/A")} @@ -132,19 +132,19 @@ def convert_document_analysis_alert_to_markdown_content( markdown_content = f""" {metadata_part} ### Source Information -- **Author**: {dtm_alert_doc.get("source_url")} -- **Collected**: {dtm_alert_doc.get("ingested")} -- **Published**: {dtm_alert_doc.get("timestamp")} -- **Source File**: {dtm_alert_doc.get("filename")} -- **MD5**: {dtm_alert_doc.get("file_hashes", {}).get("md5")} -- **SHA1**: {dtm_alert_doc.get("file_hashes", {}).get("sha1")} -- **SHA256**: {dtm_alert_doc.get("file_hashes", {}).get("sha256")} -- **Source**: {dtm_alert_doc.get("source")} -- **Source URL**: {dtm_alert_doc.get("source_url")} +- **Author**: {dtm_alert_doc.get("source_url", "")} +- **Collected**: {dtm_alert_doc.get("ingested", "")} +- **Published**: {dtm_alert_doc.get("timestamp", "")} +- **Source File**: {dtm_alert_doc.get("filename", "")} +- **MD5**: {dtm_alert_doc.get("file_hashes", {}).get("md5", "")} +- **SHA1**: {dtm_alert_doc.get("file_hashes", {}).get("sha1", "")} +- **SHA256**: {dtm_alert_doc.get("file_hashes", {}).get("sha256", "")} +- **Source**: {dtm_alert_doc.get("source", "")} +- **Source URL**: {dtm_alert_doc.get("source_url", "")} ### Content ``` -{dtm_alert_doc.get("raw_text")} +{dtm_alert_doc.get("raw_text", "")} ``` """ return markdown_content @@ -159,15 +159,15 @@ def convert_paste_alert_to_markdown_content(self, dtm_alert: dict) -> str: markdown_content = f""" {metadata_part} ### Source Information -- **Created**: {dtm_alert_doc.get("timestamp")} -- **Paste Id**: {dtm_alert_doc.get("paste_id")} +- **Created**: {dtm_alert_doc.get("timestamp", "")} +- **Paste Id**: {dtm_alert_doc.get("paste_id", "")} - **URL**: {dtm_alert_doc.get("source_location", {}).get("url", "")} - **Author**: {dtm_alert_doc.get("author", {}).get("identity", {}).get("name", "")} - **Title**: {dtm_alert_doc.get("title", "")} ### Content ``` -{dtm_alert_doc.get("body")} +{dtm_alert_doc.get("body", "")} ``` """ return markdown_content @@ -184,19 +184,19 @@ def convert_account_discovery_alert_to_markdown_content( markdown_content = f""" {metadata_part} ### Source Information -- **Source URL**: {dtm_alert_doc.get("source_url")} -- **Collected**: {dtm_alert_doc.get("ingested")} -- **Published**: {dtm_alert_doc.get("timestamp")} -- **Source File**: {dtm_alert_doc.get("source_file", {}).get("filename")} -- **MD5**: {dtm_alert_doc.get("source_file", {}).get("hashes", {}).get("md5")} -- **SHA1**: {dtm_alert_doc.get("source_file", {}).get("hashes", {}).get("sha1")} -- **SHA256**: {dtm_alert_doc.get("source_file", {}).get("hashes", {}).get("sha256")} +- **Source URL**: {dtm_alert_doc.get("source_url", "")} +- **Collected**: {dtm_alert_doc.get("ingested", "")} +- **Published**: {dtm_alert_doc.get("timestamp", "")} +- **Source File**: {dtm_alert_doc.get("source_file", {}).get("filename", "")} +- **MD5**: {dtm_alert_doc.get("source_file", {}).get("hashes", {}).get("md5", "")} +- **SHA1**: {dtm_alert_doc.get("source_file", {}).get("hashes", {}).get("sha1", "")} +- **SHA256**: {dtm_alert_doc.get("source_file", {}).get("hashes", {}).get("sha256", "")} ### Content -- **Service URL**: {dtm_alert_doc.get("service_account", {}).get("service", {}).get("inet_location", {}).get("domain")} -- **Service Domain**: {dtm_alert_doc.get("service_account", {}).get("service", {}).get("inet_location", {}).get("url")} -- **Email Domain**: {dtm_alert_doc.get("service_account", {}).get("email_domain")} -- **Login**: {dtm_alert_doc.get("service_account", {}).get("login")} -- **Password**: {dtm_alert_doc.get("service_account", {}).get("password", {}).get("plain_text")} +- **Service URL**: {dtm_alert_doc.get("service_account", {}).get("service", {}).get("inet_location", {}).get("domain", "")} +- **Service Domain**: {dtm_alert_doc.get("service_account", {}).get("service", {}).get("inet_location", {}).get("url", "")} +- **Email Domain**: {dtm_alert_doc.get("service_account", {}).get("email_domain", "")} +- **Login**: {dtm_alert_doc.get("service_account", {}).get("login", "")} +- **Password**: {dtm_alert_doc.get("service_account", {}).get("password", {}).get("plain_text", "")} """ return markdown_content @@ -210,17 +210,17 @@ def convert_message_type_alert_to_markdown_content(self, dtm_alert: dict) -> str markdown_content = f""" {metadata_part} ### Source Information -- **Created**: {dtm_alert_doc.get("ingested")} -- **Channel**: {dtm_alert_doc.get("channel", {}).get("name")} -- **Channel URL**: {dtm_alert_doc.get("channel", {}).get("channel_url")} -- **Channel Description**: {dtm_alert_doc.get("channel", {}).get("channel_info", {}).get("description")} -- **Messenger**: {dtm_alert_doc.get("channel", {}).get("messenger", {}).get("name")} -- **Author**: {dtm_alert_doc.get("sender", {}).get("identity", {}).get("name")} -- **Message Id**: {dtm_alert_doc.get("message_id")} +- **Created**: {dtm_alert_doc.get("ingested", "")} +- **Channel**: {dtm_alert_doc.get("channel", {}).get("name", "")} +- **Channel URL**: {dtm_alert_doc.get("channel", {}).get("channel_url", "")} +- **Channel Description**: {dtm_alert_doc.get("channel", {}).get("channel_info", {}).get("description", "")} +- **Messenger**: {dtm_alert_doc.get("channel", {}).get("messenger", {}).get("name", "")} +- **Author**: {dtm_alert_doc.get("sender", {}).get("identity", {}).get("name", "")} +- **Message Id**: {dtm_alert_doc.get("message_id", "")} ### Content ``` -{dtm_alert_doc.get("body")} +{dtm_alert_doc.get("body", "")} ``` """ return markdown_content @@ -235,13 +235,13 @@ def convert_web_content_alert_to_markdown_content(self, dtm_alert: dict) -> str: markdown_content = f""" {metadata_part} ### Source Information -- **Created**: {dtm_alert_doc.get("timestamp")} -- **Title**: {dtm_alert_doc.get("title")} +- **Created**: {dtm_alert_doc.get("timestamp", "")} +- **Title**: {dtm_alert_doc.get("title", "")} - **URL**: {dtm_alert_doc.get("inet_location", {}).get("url", "")} ### Content ``` -{dtm_alert_doc.get("text") if "text" in dtm_alert_doc else dtm_alert_doc.get("raw_text")} +{dtm_alert_doc.get("text", "") if "text" in dtm_alert_doc else dtm_alert_doc.get("raw_text", "")} ``` """ return markdown_content @@ -258,9 +258,9 @@ def convert_domain_discovery_alert_to_markdown_content( markdown_content = f""" {metadata_part} ### Source Information -- **Created**: {dtm_alert_doc.get("timestamp")} -- **Domain**: {dtm_alert_doc.get("domain")} -- **Source**: {dtm_alert_doc.get("source")} +- **Created**: {dtm_alert_doc.get("timestamp", "")} +- **Domain**: {dtm_alert_doc.get("domain", "")} +- **Source**: {dtm_alert_doc.get("source", "")} """ return markdown_content @@ -274,15 +274,15 @@ def convert_shop_list_alert_to_markdown_content(self, dtm_alert: dict) -> str: markdown_content = f""" {metadata_part} ### Source Information -- **Created**: {dtm_alert_doc.get("ingested")} -- **URL**: {dtm_alert_doc.get("listing_url", {}).get("url")} -- **Shop Name**: {dtm_alert_doc.get("shop", {}).get("name")} +- **Created**: {dtm_alert_doc.get("ingested", "")} +- **URL**: {dtm_alert_doc.get("listing_url", {}).get("url", "")} +- **Shop Name**: {dtm_alert_doc.get("shop", {}).get("name", "")} - **Price**: {str(dtm_alert_doc.get("price", "")) + dtm_alert_doc.get("currency", "")} - **Quantity**: {dtm_alert_doc.get("item_qty", "")} -- **Seller**: {dtm_alert_doc.get("seller", {}).get("identity", {}).get("name")} +- **Seller**: {dtm_alert_doc.get("seller", {}).get("identity", {}).get("name", "")} - **Listing ID**: {dtm_alert_doc.get("listing_id", "")} - **Listing URL**: {dtm_alert_doc.get("listing_url", {}).get("url", "")} -- **Item Type**: {dtm_alert_doc.get("item_type")} +- **Item Type**: {dtm_alert_doc.get("item_type", "")} """ return markdown_content @@ -296,7 +296,7 @@ def convert_alert_to_markdown_content(self, dtm_alert: dict) -> str: {metadata_part} ### Post ``` -{dtm_alert.get("doc", {}).get("raw_text")} +{dtm_alert.get("doc", {}).get("raw_text", "")} ``` """ return markdown_content diff --git a/external-import/google-dtm/tests/tests_connector/test_converter_to_stix.py b/external-import/google-dtm/tests/tests_connector/test_converter_to_stix.py index 8194500f265..37fe1e1a093 100644 --- a/external-import/google-dtm/tests/tests_connector/test_converter_to_stix.py +++ b/external-import/google-dtm/tests/tests_connector/test_converter_to_stix.py @@ -77,10 +77,10 @@ def test_account_discovery_without_a_plaintext_password(converter): markdown = _alert_markdown(stix_objects) converter.helper.connector_logger.error.assert_not_called() # The absent value is rendered empty, the ones that are there are kept. - assert "- **Password**: None\n" in markdown + assert "- **Password**: \n" in markdown assert "- **Login**: jdoe@acme.com\n" in markdown assert "- **MD5**: d41d8cd98f00b204e9800998ecf8427e\n" in markdown - assert "- **SHA256**: None\n" in markdown + assert "- **SHA256**: \n" in markdown def test_message_alert_without_channel_info(converter): @@ -89,8 +89,8 @@ def test_message_alert_without_channel_info(converter): markdown = _alert_markdown(stix_objects) converter.helper.connector_logger.error.assert_not_called() assert "- **Channel**: leaks\n" in markdown - assert "- **Channel Description**: None\n" in markdown - assert "- **Author**: None\n" in markdown + assert "- **Channel Description**: \n" in markdown + assert "- **Author**: \n" in markdown @pytest.mark.parametrize( @@ -112,6 +112,18 @@ def test_an_empty_document_still_produces_an_attachment(converter, doc): assert len(_attachments(stix_objects)) == 1 converter.helper.connector_logger.error.assert_not_called() + # A missing value is an empty one, never the literal "None". + assert "None" not in _alert_markdown(stix_objects) + + +@pytest.mark.parametrize( + "alert", [_ACCOUNT_DISCOVERY_ALERT, _MESSAGE_ALERT], ids=["account", "message"] +) +def test_a_missing_value_is_never_rendered_as_none(converter, alert): + """`None` in alert.md is a placeholder no analyst can interpret.""" + markdown = _alert_markdown(converter.create_incident(alert)) + + assert "None" not in markdown # --------------------------------------------------------------------------