Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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":
Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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

Comment on lines +73 to +84

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
Loading