From 4dc8f5ad34d6576767385db34751bd89595d3d62 Mon Sep 17 00:00:00 2001 From: Romain GUIGNARD Date: Thu, 16 Jul 2026 13:03:59 +0200 Subject: [PATCH 1/4] feat(malwarebazaar): add external references to indicators linking to MalwareBazaar (#6940) --- .../converter_to_stix.py | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/external-import/malwarebazaar/src/malwarebazaar_connector/converter_to_stix.py b/external-import/malwarebazaar/src/malwarebazaar_connector/converter_to_stix.py index 2a43cc12f50..36356b87a5d 100644 --- a/external-import/malwarebazaar/src/malwarebazaar_connector/converter_to_stix.py +++ b/external-import/malwarebazaar/src/malwarebazaar_connector/converter_to_stix.py @@ -57,6 +57,19 @@ def _create_tlp_marking(level) -> str: } return mapping[level] + @staticmethod + def create_external_reference(sha256_hash: str) -> stix2.ExternalReference: + """ + Create an External Reference pointing to the MalwareBazaar sample page + :param sha256_hash: SHA256 hash of the sample + :return: External Reference STIX2 object + """ + return stix2.ExternalReference( + source_name="MalwareBazaar", + url=f"https://bazaar.abuse.ch/sample/{sha256_hash}/", + description="MalwareBazaar sample page", + ) + def create_file_observable(self, entity: dict, labels: list) -> dict: """ Creates File Observable object based on MalwareBazaar Entity Data @@ -64,6 +77,7 @@ def create_file_observable(self, entity: dict, labels: list) -> dict: :param labels: List of Labels/Tags to apply :return: File Observable STIX2 object """ + external_reference = self.create_external_reference(entity["sha256_hash"]) file_observable = stix2.File( name=entity["file_name"], size=entity["file_size"], @@ -77,7 +91,7 @@ def create_file_observable(self, entity: dict, labels: list) -> dict: object_marking_refs=self.tlp_marking, custom_properties={ "x_opencti_created_by_ref": self.author["id"], - # "x_opencti_external_references": self.external_reference, + "x_opencti_external_references": [external_reference], "x_opencti_additional_names": [ entity["sha256_hash"], # entity["file_name"], @@ -99,6 +113,7 @@ def create_indicator(self, entity: dict, labels: list) -> dict: """ indicator_pattern = f"[file:hashes.'SHA-256' = '{entity['sha256_hash']}']" indicator_description = f"Indicator for hash SHA256 {entity['sha256_hash']}" + external_reference = self.create_external_reference(entity["sha256_hash"]) indicator = stix2.Indicator( id=Indicator.generate_id(indicator_pattern), name=f"{entity['sha256_hash']}", @@ -108,6 +123,7 @@ def create_indicator(self, entity: dict, labels: list) -> dict: labels=labels, created_by_ref=self.author["id"], object_marking_refs=self.tlp_marking, + external_references=[external_reference], custom_properties={ "x_opencti_score": int(self.config.malwarebazaar.x_opencti_score), "x_opencti_main_observable_type": "File", From ecfae7b7201793fc9b165a835c7b6b4702780bc7 Mon Sep 17 00:00:00 2001 From: Romain GUIGNARD Date: Thu, 16 Jul 2026 13:27:07 +0200 Subject: [PATCH 2/4] feat(malwarebazaar): create malware from signature and link it to indicator (#2044) --- external-import/malwarebazaar/README.md | 19 +++++++-- .../src/malwarebazaar_connector/connector.py | 5 +++ .../converter_to_stix.py | 41 ++++++++++++++++++- 3 files changed, 60 insertions(+), 5 deletions(-) diff --git a/external-import/malwarebazaar/README.md b/external-import/malwarebazaar/README.md index edaf31d95ac..43c7c6179fc 100644 --- a/external-import/malwarebazaar/README.md +++ b/external-import/malwarebazaar/README.md @@ -2,7 +2,7 @@ | Status | Date | Comment | |--------|------|---------| -| Community | - | - | +| Verified | - | - | The MalwareBazaar connector imports malware samples and their hashes from abuse.ch MalwareBazaar into OpenCTI. @@ -29,7 +29,7 @@ The MalwareBazaar connector imports malware samples and their hashes from abuse. MalwareBazaar is a project from abuse.ch with the goal of sharing malware samples with the infosec community, AV vendors, and threat intelligence providers. The platform collects malware samples submitted by security researchers and organizations. -This connector fetches recent malware sample metadata from MalwareBazaar API and imports them as File observables with associated hashes (MD5, SHA-1, SHA-256) into OpenCTI. +This connector fetches recent malware sample metadata from MalwareBazaar API and imports them as File observables with associated hashes (MD5, SHA-1, SHA-256), along with a related Indicator and, when available, the detected Malware family, into OpenCTI. ## Installation @@ -124,11 +124,14 @@ graph LR subgraph OpenCTI direction LR File[File Observable] + Indicator[Indicator] Malware[Malware] end API --> File - File -- related-to --> Malware + API --> Indicator + Indicator -- based-on --> File + Indicator -- indicates --> Malware ``` ### Entity Mapping @@ -141,7 +144,9 @@ graph LR | file_name | File.name | Original filename | | file_size | File.size | File size in bytes | | file_type | File.mime_type | MIME type | +| sha256_hash | Indicator | STIX indicator based on the SHA-256 hash | | signature | Malware | Detected malware family | +| sha256_hash | External Reference | Link to the MalwareBazaar sample page | | tags | Labels | MalwareBazaar tags | | reporter | Description | Sample submitter | @@ -152,8 +157,14 @@ graph LR - `x_opencti_score`: Default 100 (high confidence) - Labels from configuration - TLP marking + - External reference to the MalwareBazaar sample page -2. **Malware Relationship**: If signature is available, creates relationship to malware family +2. **Indicator**: A STIX indicator is created from the SHA-256 hash, linked to the File + observable with a `based-on` relationship, and carries the same TLP marking and an + external reference to the MalwareBazaar sample page. + +3. **Malware**: If a signature (malware family) is available, a Malware entity is created + and linked to the Indicator with an `indicates` relationship. ### Filtering Options diff --git a/external-import/malwarebazaar/src/malwarebazaar_connector/connector.py b/external-import/malwarebazaar/src/malwarebazaar_connector/connector.py index 627d311ffd0..6ec9e083acd 100644 --- a/external-import/malwarebazaar/src/malwarebazaar_connector/connector.py +++ b/external-import/malwarebazaar/src/malwarebazaar_connector/connector.py @@ -93,6 +93,11 @@ def _collect_intelligence(self) -> list: stix_objects.append(entity_set_to_stix["FILE_OBSERVABLE"]) stix_objects.append(entity_set_to_stix["INDICATOR_4_FILE"]) stix_objects.append(entity_set_to_stix["FILE_2_INDICATOR_RELATIONSHIP"]) + if entity_set_to_stix["MALWARE"] is not None: + stix_objects.append(entity_set_to_stix["MALWARE"]) + stix_objects.append( + entity_set_to_stix["INDICATOR_2_MALWARE_RELATIONSHIP"] + ) if len(stix_objects): stix_objects.append(self.converter_to_stix.author) stix_objects.append(self.converter_to_stix.tlp_marking) diff --git a/external-import/malwarebazaar/src/malwarebazaar_connector/converter_to_stix.py b/external-import/malwarebazaar/src/malwarebazaar_connector/converter_to_stix.py index 36356b87a5d..f589727767b 100644 --- a/external-import/malwarebazaar/src/malwarebazaar_connector/converter_to_stix.py +++ b/external-import/malwarebazaar/src/malwarebazaar_connector/converter_to_stix.py @@ -1,6 +1,12 @@ import stix2 import validators -from pycti import Identity, Indicator, MarkingDefinition, StixCoreRelationship +from pycti import ( + Identity, + Indicator, + Malware, + MarkingDefinition, + StixCoreRelationship, +) class ConverterToStix: @@ -132,6 +138,21 @@ def create_indicator(self, entity: dict, labels: list) -> dict: return indicator + def create_malware(self, signature: str) -> dict: + """ + Creates Malware object based on MalwareBazaar signature (malware family) + :param signature: Malware family/signature name from MalwareBazaar + :return: Malware STIX2 object + """ + malware = stix2.Malware( + id=Malware.generate_id(signature), + name=signature, + is_family=True, + created_by_ref=self.author["id"], + object_marking_refs=self.tlp_marking, + ) + return malware + def create_relationship( self, source_id: str, relationship_type: str, target_id: str, labels: list ) -> dict: @@ -215,6 +236,8 @@ def create_obs(self, entity: dict) -> dict: "FILE_OBSERVABLE": None, "INDICATOR_4_FILE": None, "FILE_2_INDICATOR_RELATIONSHIP": None, + "MALWARE": None, + "INDICATOR_2_MALWARE_RELATIONSHIP": None, } if ( self._is_valid_md5(entity["md5_hash"]) is True @@ -250,8 +273,24 @@ def create_obs(self, entity: dict) -> dict: "FILE_OBSERVABLE": file_observable, "INDICATOR_4_FILE": indicator, "FILE_2_INDICATOR_RELATIONSHIP": relationship, + "MALWARE": None, + "INDICATOR_2_MALWARE_RELATIONSHIP": None, } + # Create Malware from the MalwareBazaar signature (malware family) if present + signature = entity.get("signature") + if signature and len(signature) >= 1: + malware = self.create_malware(signature=signature) + indicator_2_malware = self.create_relationship( + source_id=indicator["id"], + relationship_type="indicates", + target_id=malware["id"], + labels=tags_as_labels, + ) + file_observable_set["MALWARE"] = malware + file_observable_set["INDICATOR_2_MALWARE_RELATIONSHIP"] = ( + indicator_2_malware + ) else: self.helper.connector_logger.error( "This observable entity is not a valid for ingest, missing one or more required fields (md5_hash, sha1_hash, sha256_hash, reporter, or filename): ", From f7d7ea47a7773c1d6cc639b25585f32fefbe52ae Mon Sep 17 00:00:00 2001 From: Romain GUIGNARD Date: Tue, 28 Jul 2026 11:09:39 +0200 Subject: [PATCH 3/4] fix object_marking_refs --- .../src/malwarebazaar_connector/converter_to_stix.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/external-import/malwarebazaar/src/malwarebazaar_connector/converter_to_stix.py b/external-import/malwarebazaar/src/malwarebazaar_connector/converter_to_stix.py index f589727767b..6880f8374ca 100644 --- a/external-import/malwarebazaar/src/malwarebazaar_connector/converter_to_stix.py +++ b/external-import/malwarebazaar/src/malwarebazaar_connector/converter_to_stix.py @@ -94,7 +94,7 @@ def create_file_observable(self, entity: dict, labels: list) -> dict: "SHA-256": entity["sha256_hash"], # "SHA-512": entity["sha512_hash"], # API for MalwareBazaar currently does NOT provide a SHA512 }, - object_marking_refs=self.tlp_marking, + object_marking_refs=[self.tlp_marking], custom_properties={ "x_opencti_created_by_ref": self.author["id"], "x_opencti_external_references": [external_reference], @@ -128,7 +128,7 @@ def create_indicator(self, entity: dict, labels: list) -> dict: description=indicator_description, labels=labels, created_by_ref=self.author["id"], - object_marking_refs=self.tlp_marking, + object_marking_refs=[self.tlp_marking], external_references=[external_reference], custom_properties={ "x_opencti_score": int(self.config.malwarebazaar.x_opencti_score), @@ -149,7 +149,7 @@ def create_malware(self, signature: str) -> dict: name=signature, is_family=True, created_by_ref=self.author["id"], - object_marking_refs=self.tlp_marking, + object_marking_refs=[self.tlp_marking] ) return malware @@ -173,7 +173,7 @@ def create_relationship( target_ref=target_id, labels=labels, created_by_ref=self.author["id"], - object_marking_refs=self.tlp_marking, + object_marking_refs=[self.tlp_marking] # external_references=self.external_reference, ) return relationship From ed490999db76e2e21dba32bb0a637efc49f0bf3f Mon Sep 17 00:00:00 2001 From: Thibaut Rouxel Date: Thu, 30 Jul 2026 11:55:58 +0200 Subject: [PATCH 4/4] feat(malwarebazaar): add tests --- .../converter_to_stix.py | 4 +- .../tests_connector/test_converter_to_stix.py | 146 ++++++++++++++++++ 2 files changed, 148 insertions(+), 2 deletions(-) create mode 100644 external-import/malwarebazaar/tests/tests_connector/test_converter_to_stix.py diff --git a/external-import/malwarebazaar/src/malwarebazaar_connector/converter_to_stix.py b/external-import/malwarebazaar/src/malwarebazaar_connector/converter_to_stix.py index 6880f8374ca..8a0cb670ca7 100644 --- a/external-import/malwarebazaar/src/malwarebazaar_connector/converter_to_stix.py +++ b/external-import/malwarebazaar/src/malwarebazaar_connector/converter_to_stix.py @@ -149,7 +149,7 @@ def create_malware(self, signature: str) -> dict: name=signature, is_family=True, created_by_ref=self.author["id"], - object_marking_refs=[self.tlp_marking] + object_marking_refs=[self.tlp_marking], ) return malware @@ -173,7 +173,7 @@ def create_relationship( target_ref=target_id, labels=labels, created_by_ref=self.author["id"], - object_marking_refs=[self.tlp_marking] + object_marking_refs=[self.tlp_marking], # external_references=self.external_reference, ) return relationship diff --git a/external-import/malwarebazaar/tests/tests_connector/test_converter_to_stix.py b/external-import/malwarebazaar/tests/tests_connector/test_converter_to_stix.py new file mode 100644 index 00000000000..ac86ef2f8ed --- /dev/null +++ b/external-import/malwarebazaar/tests/tests_connector/test_converter_to_stix.py @@ -0,0 +1,146 @@ +from unittest.mock import MagicMock + +import pytest +import stix2 +from malwarebazaar_connector.converter_to_stix import ConverterToStix + +SHA256 = "a" * 64 +MD5 = "b" * 32 +SHA1 = "c" * 40 + + +@pytest.fixture +def config(): + """Minimal config exposing the `malwarebazaar` attributes used by the converter.""" + config = MagicMock() + config.malwarebazaar.tlp_level = "clear" + config.malwarebazaar.x_opencti_score = 100 + config.malwarebazaar.labels = ["malware-bazaar"] + return config + + +@pytest.fixture +def helper(): + helper = MagicMock() + helper.connect_name = "Malwarebazaar" + return helper + + +@pytest.fixture +def converter(helper, config): + return ConverterToStix(helper=helper, config=config) + + +@pytest.fixture +def entity(): + return { + "md5_hash": MD5, + "sha1_hash": SHA1, + "sha256_hash": SHA256, + "file_name": "sample.exe", + "file_size": 1234, + "file_type_mime": "application/x-dosexec", + "reporter": "abuse_ch", + "tags": ["exe"], + "signature": "Emotet", + } + + +def test_create_external_reference(converter): + """The external reference MUST point to the MalwareBazaar sample page for the given SHA256.""" + external_reference = converter.create_external_reference(SHA256) + + assert isinstance(external_reference, stix2.ExternalReference) + assert external_reference.source_name == "MalwareBazaar" + assert external_reference.url == f"https://bazaar.abuse.ch/sample/{SHA256}/" + assert external_reference.description == "MalwareBazaar sample page" + + +def test_create_file_observable_has_external_reference(converter, entity): + """The File observable MUST embed the MalwareBazaar external reference.""" + file_observable = converter.create_file_observable(entity, labels=["exe"]) + + external_references = file_observable["x_opencti_external_references"] + assert len(external_references) == 1 + assert external_references[0].url == f"https://bazaar.abuse.ch/sample/{SHA256}/" + + +def test_create_file_observable_marking_is_a_list(converter, entity): + """`object_marking_refs` MUST be a list to comply with STIX2.""" + file_observable = converter.create_file_observable(entity, labels=["exe"]) + + assert isinstance(file_observable["object_marking_refs"], list) + + +def test_create_indicator_has_external_reference(converter, entity): + """The Indicator MUST embed the MalwareBazaar external reference.""" + indicator = converter.create_indicator(entity, labels=["exe"]) + + assert len(indicator["external_references"]) == 1 + assert ( + indicator["external_references"][0].url + == f"https://bazaar.abuse.ch/sample/{SHA256}/" + ) + assert isinstance(indicator["object_marking_refs"], list) + + +def test_create_malware(converter): + """`create_malware` MUST return a Malware family with a deterministic id.""" + malware = converter.create_malware("Emotet") + + assert isinstance(malware, stix2.Malware) + assert malware["name"] == "Emotet" + assert malware["is_family"] is True + assert isinstance(malware["object_marking_refs"], list) + + +def test_create_obs_with_signature_creates_malware(converter, entity): + """When the entity has a signature, the resulting set MUST include the Malware + and the Indicator -> Malware `indicates` relationship.""" + result = converter.create_obs(entity) + + assert result["MALWARE"] is not None + assert result["MALWARE"]["name"] == "Emotet" + + relationship = result["INDICATOR_2_MALWARE_RELATIONSHIP"] + assert relationship is not None + assert relationship["relationship_type"] == "indicates" + assert relationship["source_ref"] == result["INDICATOR_4_FILE"]["id"] + assert relationship["target_ref"] == result["MALWARE"]["id"] + + +def test_create_obs_without_signature_has_no_malware(converter, entity): + """When the entity has no signature, no Malware nor relationship MUST be created.""" + entity["signature"] = None + + result = converter.create_obs(entity) + + assert result["FILE_OBSERVABLE"] is not None + assert result["MALWARE"] is None + assert result["INDICATOR_2_MALWARE_RELATIONSHIP"] is None + + +def test_create_obs_with_empty_signature_has_no_malware(converter, entity): + """An empty signature string MUST NOT produce a Malware object.""" + entity["signature"] = "" + + result = converter.create_obs(entity) + + assert result["MALWARE"] is None + assert result["INDICATOR_2_MALWARE_RELATIONSHIP"] is None + + +def test_create_obs_invalid_entity_returns_empty_set(converter, entity): + """An entity missing required fields MUST return the empty set with no objects.""" + entity["md5_hash"] = "not-a-valid-hash" + + result = converter.create_obs(entity) + + assert result == { + "FILE_OBSERVABLE": None, + "INDICATOR_4_FILE": None, + "FILE_2_INDICATOR_RELATIONSHIP": None, + "MALWARE": None, + "INDICATOR_2_MALWARE_RELATIONSHIP": None, + } + converter.helper.connector_logger.error.assert_called_once()