From a0dcf820d2a84b8a0b1add3467b0d48d10f82b81 Mon Sep 17 00:00:00 2001 From: "M. Bilgehan Ertan" Date: Thu, 16 Oct 2025 14:16:14 +0200 Subject: [PATCH 1/3] add: relation for actor/country --- python_catalyst/client.py | 36 ++++++++++++++++++++------ python_catalyst/stix_converter.py | 43 +++++++++++++++++++++++++++++-- 2 files changed, 69 insertions(+), 10 deletions(-) diff --git a/python_catalyst/client.py b/python_catalyst/client.py index 4350675..711e23f 100644 --- a/python_catalyst/client.py +++ b/python_catalyst/client.py @@ -365,7 +365,7 @@ def _process_entity( collected_object_refs: List, entity_mappings: Dict, external_reference: stix2.ExternalReference = None, - ) -> None: + ) -> Tuple[List[Dict], List, Dict]: """ Process a single entity and add it to the report. @@ -398,6 +398,8 @@ def _process_entity( if self.logger: self.logger.debug(f"Added {entity_type}: {entity_value}") + return related_objects, collected_object_refs, entity_mappings + def _process_entities( self, entities: List[Dict], @@ -407,7 +409,7 @@ def _process_entities( collected_object_refs: List, entity_mappings: Dict, external_reference: stix2.ExternalReference = None, - ) -> None: + ) -> Tuple[List[Dict], List, Dict]: """ Process a list of entities of the same type. @@ -421,7 +423,11 @@ def _process_entities( external_reference: Optional reference to the report """ for entity in entities: - self._process_entity( + ( + related_objects, + collected_object_refs, + entity_mappings, + ) = self._process_entity( entity, entity_type, converter_method, @@ -431,6 +437,8 @@ def _process_entities( external_reference, ) + return related_objects, collected_object_refs, entity_mappings + def _process_threat_actor( self, threat_actor: Dict, @@ -438,7 +446,7 @@ def _process_threat_actor( collected_object_refs: List, entity_mappings: Dict, external_reference: stix2.ExternalReference = None, - ) -> None: + ) -> Tuple[List[Dict], List, Dict]: """ Process a threat actor entity with detailed information. @@ -461,14 +469,14 @@ def _process_threat_actor( f"Retrieved detailed information for threat actor: {entity_value}" ) - ta_object = self.converter.create_detailed_threat_actor( + ta_object, bundle = self.converter.create_detailed_threat_actor( detailed_threat_actor, context, report_reference=external_reference, ) is_abstract = detailed_threat_actor.get("is_abstract", False) - related_objects.append(ta_object) + related_objects.extend(bundle) collected_object_refs.append(ta_object.id) if is_abstract: @@ -482,6 +490,8 @@ def _process_threat_actor( if self.logger: self.logger.debug(f"Added threat actor: {entity_value}") + return related_objects, collected_object_refs, entity_mappings + except Exception as e: if self.logger: self.logger.warning( @@ -501,6 +511,8 @@ def _process_threat_actor( if self.logger: self.logger.debug(f"Added threat actor: {entity_value}") + return related_objects, collected_object_refs, entity_mappings + def create_report_from_member_content_with_references( self, content: Dict ) -> Tuple[Dict, List[Dict]]: @@ -657,7 +669,11 @@ def create_report_from_member_content_with_references( f"Skipping threat actor {threat_actor.get('value')} because user is not authenticated... This will be implemented in the future." ) continue - self._process_threat_actor( + ( + related_objects, + collected_object_refs, + entity_mappings, + ) = self._process_threat_actor( threat_actor, related_objects, collected_object_refs, @@ -684,7 +700,11 @@ def create_report_from_member_content_with_references( converter_method = processor mapping_type = entity_type - self._process_entities( + ( + related_objects, + collected_object_refs, + entity_mappings, + ) = self._process_entities( all_entities.get(entity_type, []), mapping_type, converter_method, diff --git a/python_catalyst/stix_converter.py b/python_catalyst/stix_converter.py index 4113d7a..afb3715 100644 --- a/python_catalyst/stix_converter.py +++ b/python_catalyst/stix_converter.py @@ -696,6 +696,7 @@ def create_threat_actor( Returns: STIX Threat Actor object """ + print(context) external_references = [] if report_reference: external_references = [report_reference] @@ -1210,6 +1211,7 @@ def create_detailed_threat_actor( Returns: STIX ThreatActor or IntrusionSet object with full details included """ + bundle = [] external_references = [] if report_reference: external_references = [report_reference] @@ -1291,7 +1293,7 @@ def create_detailed_threat_actor( if is_abstract: # Create an Intrusion Set for abstract entities - return stix2.IntrusionSet( + actor = stix2.IntrusionSet( id=IntrusionSet.generate_id(entity_value), name=entity_value, description=description, @@ -1302,12 +1304,13 @@ def create_detailed_threat_actor( external_references if external_references else None ), custom_properties=custom_properties, + allow_custom=True, ) else: # Create a Threat Actor with the appropriate type actor_type = "threat-actor-group" if is_group else "threat-actor-individual" custom_properties["x_opencti_type"] = actor_type - return stix2.ThreatActor( + actor = stix2.ThreatActor( id=ThreatActor.generate_id(entity_value, actor_type), name=entity_value, description=description, @@ -1318,4 +1321,40 @@ def create_detailed_threat_actor( external_references if external_references else None ), custom_properties=custom_properties, + allow_custom=True, ) + bundle.append(actor) + + suspected_origins = threat_actor_data.get("suspected_origins", []) + if isinstance(suspected_origins, list): + for origin in suspected_origins: + cname = origin.get("name") + ccode = origin.get("code") + if not cname: + continue + + loc = stix2.Location( + name=cname, + country=cname, + custom_properties=( + { + "x_country_code": ( + ccode.upper() if isinstance(ccode, str) else None + ) + } + if ccode + else None + ), + allow_custom=True, + ) + rel_type = "originates-from" if is_abstract else "located-at" + rel = stix2.Relationship( + relationship_type=rel_type, + source_ref=actor.id, + target_ref=loc.id, + ) + + bundle.append(loc) + bundle.append(rel) + + return actor, bundle From 937d0ce8de34abc55b11e88599e7c147f5b17d91 Mon Sep 17 00:00:00 2001 From: "M. Bilgehan Ertan" Date: Thu, 16 Oct 2025 17:17:08 +0200 Subject: [PATCH 2/3] add context information --- python_catalyst/client.py | 7 ++++++- python_catalyst/stix_converter.py | 7 +++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/python_catalyst/client.py b/python_catalyst/client.py index 711e23f..03a6527 100644 --- a/python_catalyst/client.py +++ b/python_catalyst/client.py @@ -540,6 +540,7 @@ def create_report_from_member_content_with_references( content_id = content.get("id") slug = content.get("slug", "") # noqa: F841 tlp = content.get("tlp", TLPLevel.CLEAR.value) + topics = content.get("topics", []) self.converter = self.get_stix_converter(tlp) if published_on: @@ -559,6 +560,9 @@ def create_report_from_member_content_with_references( labels.append(content["category"]) if content.get("sub_category") and content["sub_category"].get("name"): labels.append(content["sub_category"]["name"]) + if len(topics) > 0: + for topic in topics: + labels.append(topic["name"]) report_id = ( f"report--{str(uuid.uuid5(uuid.NAMESPACE_URL, f'catalyst-{content_id}'))}" @@ -618,7 +622,7 @@ def create_report_from_member_content_with_references( entity_id = observable.get("id") entity_value = observable.get("value") entity_type = observable.get("type") - + entity_context = observable.get("context", "") if entity_id and entity_value and entity_type: observable_data = { "id": entity_id, @@ -626,6 +630,7 @@ def create_report_from_member_content_with_references( "type": entity_type, "post_id": content_id, "tlp_marking": content_marking, + "context": entity_context, } ( diff --git a/python_catalyst/stix_converter.py b/python_catalyst/stix_converter.py index afb3715..7af5ef1 100644 --- a/python_catalyst/stix_converter.py +++ b/python_catalyst/stix_converter.py @@ -696,7 +696,6 @@ def create_threat_actor( Returns: STIX Threat Actor object """ - print(context) external_references = [] if report_reference: external_references = [report_reference] @@ -1097,11 +1096,15 @@ def create_indicator_from_observable( marking_ref = tlp_marking.id if tlp_marking else self.tlp_marking.id created_by_ref = self.get_created_by_ref() + description = f"Indicator for {observable_type}: {value}" + if "context" in observable_data: + ctx = observable_data["context"] + description = f"{description}\n\n{ctx}" return stix2.Indicator( id=indicator_id, name=indicator_name, - description=f"Indicator for {observable_type}: {value}", + description=description, pattern=pattern, pattern_type="stix", created_by_ref=created_by_ref, From 8987fc19c3db2c3f2264f6d72d899d53e595b82c Mon Sep 17 00:00:00 2001 From: "M. Bilgehan Ertan" Date: Thu, 16 Oct 2025 17:26:06 +0200 Subject: [PATCH 3/3] add version --- pyproject.toml | 2 +- python_catalyst/__init__.py | 2 +- setup.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f7f7578..e46ac42 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "python-catalyst" -version = "0.1.5" +version = "0.1.6" description = "Python client for the PRODAFT CATALYST API" readme = "README.md" license = { file = "LICENSE" } diff --git a/python_catalyst/__init__.py b/python_catalyst/__init__.py index 88496c2..4927ccd 100644 --- a/python_catalyst/__init__.py +++ b/python_catalyst/__init__.py @@ -1,6 +1,6 @@ """PRODAFT CATALYST API client package.""" -__version__ = "0.1.5" +__version__ = "0.1.6" from .client import CatalystClient from .enums import ObservableType, PostCategory, TLPLevel diff --git a/setup.py b/setup.py index dbbf6ed..c9fb60c 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="python-catalyst", - version="0.1.5", + version="0.1.6", description="Python client for the PRODAFT CATALYST API", long_description=long_description, long_description_content_type="text/markdown",