Repository: OpenCTI-Platform/connectors

Issue:
[Intezer Connector] Error in message processing
## Description

The bug is present when using the Intezer connector. There is an error raised - see  log:

`Traceback (most recent call last):

  File "/usr/local/lib/python3.10/site-packages/pycti/connector/opencti_connector_helper.py", line 178, in _data_handler

    message = self.callback(json_data["event"])

  File "/opt/opencti-connector-intezer-sandbox/intezer_sandbox.py", line 183, in _process_message

    return self._process_observable(observable)

  File "/opt/opencti-connector-intezer-sandbox/intezer_sandbox.py", line 160, in _process_observable

    return self._process_file(observable)

  File "/opt/opencti-connector-intezer-sandbox/intezer_sandbox.py", line 118, in _process_file

    raise ValueError(f"No files found for {observable['observable_value']}")

ValueError: No files found for 475835b6703aade008c297f726c4cbe8fe29ac4d67bcf28d3761aeab59d3e929

INFO:root:Reporting work update_received opencti-work--5fb6c917-1b18-4870-9f9b-a8961f1661d2

INFO:root:Message (delivery_tag=43) processed, thread terminated

INFO:root:Reporting work update_received opencti-work--ec7a2988-8df2-4f3a-8d7c-1802b18fdf01

INFO:root:Reading StixCyberObservable {8d79573b-0720-40b8-b92b-89b58fb64a6f}.

INFO:root:Processing the observable d486d807f4e11918a935df4673c32731fb292a8fa77158741aee2a16fb5b298f

ERROR:root:Error in message processing, reporting error to API

Traceback (most recent call last):

  File "/usr/local/lib/python3.10/site-packages/pycti/connector/opencti_connector_helper.py", line 178, in _data_handler

    message = self.callback(json_data["event"])

  File "/opt/opencti-connector-intezer-sandbox/intezer_sandbox.py", line 183, in _process_message

    return self._process_observable(observable)

  File "/opt/opencti-connector-intezer-sandbox/intezer_sandbox.py", line 160, in _process_observable

    return self._process_file(observable)

  File "/opt/opencti-connector-intezer-sandbox/intezer_sandbox.py", line 127, in _process_file

    result_url = self.intezer_client.upload_file(

  File "/opt/opencti-connector-intezer-sandbox/intezer_api.py", line 39, in upload_file

    assert response.status_code == 201

AssertionError

INFO:root:Reporting work update_received opencti-work--ec7a2988-8df2-4f3a-8d7c-1802b18fdf01

INFO:root:Message (delivery_tag=44) processed, thread terminated

INFO:root:Reporting work update_received opencti-work--0acea19e-6b8b-4602-a713-92476294c4d5

INFO:root:Reading StixCyberObservable {0e37c49b-6ae5-49b8-b984-26284e308604}.

INFO:root:Processing the observable 2a42f058cb8189ad5133b20a42dc23de418e6107926243132264a8d44d3f32f2

ERROR:root:Error in message processing, reporting error to API

Traceback (most recent call last):

  File "/usr/local/lib/python3.10/site-packages/pycti/connector/opencti_connector_helper.py", line 178, in _data_handler

    message = self.callback(json_data["event"])

  File "/opt/opencti-connector-intezer-sandbox/intezer_sandbox.py", line 183, in _process_message

    return self._process_observable(observable)

  File "/opt/opencti-connector-intezer-sandbox/intezer_sandbox.py", line 160, in _process_observable

    return self._process_file(observable)

  File "/opt/opencti-connector-intezer-sandbox/intezer_sandbox.py", line 118, in _process_file

    raise ValueError(f"No files found for {observable['observable_value']}")

ValueError: No files found for 2a42f058cb8189ad5133b20a42dc23de418e6107926243132264a8d44d3f32f2

INFO:root:Reporting work update_received opencti-work--0acea19e-6b8b-4602-a713-92476294c4d5

INFO:root:Message (delivery_tag=45) processed, thread terminated

INFO:root:Reporting work update_received opencti-work--04f265d8-a9f0-4750-b2a7-319d0b97fea6

INFO:root:Reading StixCyberObservable {6e44f30d-dab5-41af-86cc-dc4cf45616b6}.

INFO:root:Processing the observable d1a998486349214703d5aa3c13fe200e1098e5d0c8cf73561275e8211026a15c

ERROR:root:Error in message processing, reporting error to API

Traceback (most recent call last):

  File "/usr/local/lib/python3.10/site-packages/pycti/connector/opencti_connector_helper.py", line 178, in _data_handler

    message = self.callback(json_data["event"])

  File "/opt/opencti-connector-intezer-sandbox/intezer_sandbox.py", line 183, in _process_message

    return self._process_observable(observable)

  File "/opt/opencti-connector-intezer-sandbox/intezer_sandbox.py", line 160, in _process_observable

    return self._process_file(observable)

  File "/opt/opencti-connector-intezer-sandbox/intezer_sandbox.py", line 127, in _process_file

    result_url = self.intezer_client.upload_file(

  File "/opt/opencti-connector-intezer-sandbox/intezer_api.py", line 39, in upload_file

    assert response.status_code == 201

AssertionError

INFO:root:Reporting work update_received opencti-work--04f265d8-a9f0-4750-b2a7-319d0b97fea6

INFO:root:Message (delivery_tag=46) processed, thread terminated

INFO:root:Reporting work update_received opencti-work--6dbfdccc-597a-45d8-827d-4e571ec1ac0b

INFO:root:Reading StixCyberObservable {668236c5-7957-485a-9b9a-24354812be89}.

INFO:root:Processing the observable 09681462d42abbc451fc6d3b27d68a67dec8f999a17da7cc422b5240a4ee487a

ERROR:root:Error in message processing, reporting error to API

Traceback (most recent call last):

  File "/usr/local/lib/python3.10/site-packages/pycti/connector/opencti_connector_helper.py", line 178, in _data_handler

    message = self.callback(json_data["event"])

  File "/opt/opencti-connector-intezer-sandbox/intezer_sandbox.py", line 183, in _process_message

    return self._process_observable(observable)

  File "/opt/opencti-connector-intezer-sandbox/intezer_sandbox.py", line 160, in _process_observable

    return self._process_file(observable)

  File "/opt/opencti-connector-intezer-sandbox/intezer_sandbox.py", line 127, in _process_file

    result_url = self.intezer_client.upload_file(

  File "/opt/opencti-connector-intezer-sandbox/intezer_api.py", line 39, in upload_file

    assert response.status_code == 201

AssertionError

INFO:root:Reporting work update_received opencti-work--6dbfdccc-597a-45d8-827d-4e571ec1ac0b

INFO:root:Message (delivery_tag=47) processed, thread terminated

INFO:root:Reporting work update_received opencti-work--229fa15e-909a-4b82-a38e-937ae9de5a73

INFO:root:Reading StixCyberObservable {49e5ef94-ae0b-49f9-bf64-03c184d2ee43}.

INFO:root:Processing the observable 8cb5d6867d8460fbba1b3cf83e5ab95d22b9302f399b5094d20cb66565c240dd

ERROR:root:Error in message processing, reporting error to API

Traceback (most recent call last):

  File "/usr/local/lib/python3.10/site-packages/pycti/connector/opencti_connector_helper.py", line 178, in _data_handler

    message = self.callback(json_data["event"])

  File "/opt/opencti-connector-intezer-sandbox/intezer_sandbox.py", line 183, in _process_message

    return self._process_observable(observable)

  File "/opt/opencti-connector-intezer-sandbox/intezer_sandbox.py", line 160, in _process_observable

    return self._process_file(observable)

  File "/opt/opencti-connector-intezer-sandbox/intezer_sandbox.py", line 127, in _process_file

    result_url = self.intezer_client.upload_file(

  File "/opt/opencti-connector-intezer-sandbox/intezer_api.py", line 39, in upload_file

    assert response.status_code == 201

AssertionError

INFO:root:Reporting work update_received opencti-work--229fa15e-909a-4b82-a38e-937ae9de5a73`

## Environment

1. Ubuntu 20.04
2. OpenCTI version: 5.3.7

## Reproducible Steps
Install openCTI
Add Intezer connectorr

## Expected Output

Data enrichment.




Discussion:
@YungBinary Any idea about this one? Thanks a lot!
@SamuelHassine It looks like the assertion is failing and the response status code is not 201. This may have changed in the API. See https://github.com/OpenCTI-Platform/connectors/blob/14c9e0670c170dd1da912e11974b2401cd8db2b5/internal-enrichment/intezer-sandbox/src/intezer_api.py#L39

I'll play around with it when I get a chance to see what's going on.
@fugitive101 Do you have an API key setup? According to the documentation, 201 response is returned upon success (https://analyze.intezer.com/api-docs.html#/paths/analyze/post). You may be getting a 403 response which indicates you don't have authorization to call the API.

Resolving patch:
diff --git a/internal-enrichment/intezer-sandbox/src/intezer_api.py b/internal-enrichment/intezer-sandbox/src/intezer_api.py
--- a/internal-enrichment/intezer-sandbox/src/intezer_api.py
+++ b/internal-enrichment/intezer-sandbox/src/intezer_api.py
@@ -36,8 +36,7 @@ def upload_file(self, file_name, file_contents):
         file_obj = io.BytesIO(file_contents)
         files = {"file": ("file_name", file_obj)}
         response = self._session.post(f"{self.base_url}/analyze", files=files)
-        assert response.status_code == 201
-
+        response.raise_for_status()
         return response.json()["result_url"]
 
     def get_analysis_report(self, result_url):
diff --git a/internal-export-file/export-report-pdf/src/export-report-pdf.py b/internal-export-file/export-report-pdf/src/export-report-pdf.py
--- a/internal-export-file/export-report-pdf/src/export-report-pdf.py
+++ b/internal-export-file/export-report-pdf/src/export-report-pdf.py
@@ -4,7 +4,6 @@
 import os
 import sys
 import time
-
 import cairosvg
 import yaml
 from jinja2 import Environment, FileSystemLoader
@@ -92,11 +91,9 @@ def _get_readable_date_time(self, str_date_time):
 
     def _process_message(self, data):
         file_name = data["file_name"]
-        # TODO this can be implemented to filter every entity and observable
-        # max_marking = data["max_marking"]
         if "entity_type" not in data or "entity_id" not in data:
             raise ValueError(
-                'This Connector currently only handles direct export (single entity and no list) of the following entity types: "Report" and "Intrusion-Set'
+                'This Connector currently only handles direct export (single entity and no list) of the following entity types: "Report", "Intrusion-Set", and "Threat-Actor"'
             )
         entity_type = data["entity_type"]
         entity_id = data["entity_id"]
@@ -105,9 +102,11 @@ def _process_message(self, data):
             self._process_report(entity_id, file_name)
         elif entity_type == "Intrusion-Set":
             self._process_intrusion_set(entity_id, file_name)
+        elif entity_type == "Threat-Actor":
+            self._process_threat_actor(entity_id, file_name)
         else:
             raise ValueError(
-                f'This Connector currently only handles the entity types: "Report" and "Intrusion-Set", not "{entity_type}".'
+                f'This connector currently only handles the entity types: "Report", "Intrusion-Set", "Threat-Actor", not "{entity_type}".'
             )
 
         return "Export done"
@@ -269,18 +268,18 @@ def _process_intrusion_set(self, entity_id, file_name):
             targeted_countries = []
             for relationship in context["entities"]["relationship"]:
                 if (
-                    relationship["relationship_type"] != "targets"
-                    and relationship["to"]["entity_type"] != "Country"
+                    relationship["entity_type"] == "targets"
+                    and relationship["relationship_type"] == "targets"
+                    and relationship["to"]["entity_type"] == "Country"
                 ):
-                    continue
-
-                country_name = relationship["to"]["name"]
-                country_code = self._get_country_code(country_name)
-                if not country_code:
-                    self.helper.log_error(f"{country_name} is not a supported country")
-                    continue
+                    country_code = relationship["to"]["name"].lower()
+                    if not self._validate_country_code(country_code):
+                        self.helper.log_warning(
+                            f"{country_code} is not a supported country code, skipping..."
+                        )
+                        continue
 
-                targeted_countries.append(country_code)
+                    targeted_countries.append(country_code)
 
             # Build targeted countries image
             if targeted_countries:
@@ -310,6 +309,100 @@ def _process_intrusion_set(self, entity_id, file_name):
             entity_id, file_name, pdf_contents, "application/pdf"
         )
 
+    def _process_threat_actor(self, entity_id, file_name):
+        """
+        Process a Threat Actor entity and upload as pdf.
+        """
+
+        now_date = datetime.datetime.now().strftime("%b %d %Y")
+
+        # Store context for usage in html template
+        context = {
+            "entities": {},
+            "target_map_country": None,
+            "report_date": now_date,
+            "company_address_line_1": self.company_address_line_1,
+            "company_address_line_2": self.company_address_line_2,
+            "company_address_line_3": self.company_address_line_3,
+            "company_phone_number": self.company_phone_number,
+            "company_email": self.company_email,
+            "company_website": self.company_website,
+        }
+
+        # Get a bundle of all objects affiliated with the threat actor
+        bundle = self.helper.api.stix2.export_entity("Threat-Actor", entity_id, "full")
+
+        for bundle_obj in bundle["objects"]:
+            obj_id = bundle_obj["id"]
+            obj_entity_type = bundle_obj["type"]
+
+            reader_func = self._get_reader(obj_entity_type)
+            if reader_func is None:
+                self.helper.log_error(
+                    f'Could not find a function to read entity with type "{obj_entity_type}"'
+                )
+                continue
+
+            time.sleep(0.3)
+            entity_dict = reader_func(id=obj_id)
+
+            # Key names cannot have - in them for jinja2 templating
+            obj_entity_type = obj_entity_type.replace("-", "_")
+            if obj_entity_type not in context["entities"]:
+                context["entities"][obj_entity_type] = []
+
+            context["entities"][obj_entity_type].append(entity_dict)
+
+        # Generate the svg img contents for the targets map
+        if "relationship" in context["entities"]:
+
+            # Create world map
+            world_map = World()
+            world_map.title = "Targeted Countries"
+            targeted_countries = []
+            for relationship in context["entities"]["relationship"]:
+                if (
+                    relationship["entity_type"] == "targets"
+                    and relationship["relationship_type"] == "targets"
+                    and relationship["to"]["entity_type"] == "Country"
+                ):
+                    country_code = relationship["to"]["name"].lower()
+                    if not self._validate_country_code(country_code):
+                        self.helper.log_warning(
+                            f"{country_code} is not a supported country code, skipping..."
+                        )
+                        continue
+
+                    targeted_countries.append(country_code)
+
+            # Build targeted countries image
+            if targeted_countries:
+                world_map.add("Targeted Countries", targeted_countries)
+                # Convert the svg to base64 png
+                svg_bytes = world_map.render()
+                png_bytes = io.BytesIO()
+                cairosvg.svg2png(bytestring=svg_bytes, write_to=png_bytes)
+                base64_png = base64.b64encode(png_bytes.getvalue()).decode()
+                context["target_map_country"] = f"data:image/png;base64, {base64_png}"
+
+        # Render html with input variables
+        env = Environment(
+            loader=FileSystemLoader(self.current_dir), finalize=self._finalize
+        )
+        template = env.get_template("resources/threat-actor.html")
+        html_string = template.render(context)
+
+        # Generate pdf from html string
+        pdf_contents = HTML(
+            string=html_string, base_url=f"{self.current_dir}/resources"
+        ).write_pdf()
+
+        # Upload the output pdf
+        self.helper.log_info(f"Uploading: {file_name}")
+        self.helper.api.stix_domain_object.push_entity_export(
+            entity_id, file_name, pdf_contents, "application/pdf"
+        )
+
     def _set_colors(self):
         for root, dirs, files in os.walk(self.current_dir):
             for file_name in files:
@@ -325,10 +418,13 @@ def _set_colors(self):
                     with open(os.path.join(root, file_name), "w") as f:
                         f.write(new_css)
 
-    def _get_country_code(self, country_name):
-        for code, name in COUNTRIES.items():
-            if country_name.lower() in name.lower():
-                return code
+    def _validate_country_code(self, country_code):
+        """
+        Returns a boolean indicating whether or not the country code is valid.
+        """
+        if country_code in COUNTRIES:
+            return True
+        return False
 
     def _finalize(self, data):
         """


Test patch: