Skip to content

Commit 9bdcbf8

Browse files
Feature/enrich threatactor (#7)
* add: relation for actor/country * add context information * add version
1 parent 582e3ec commit 9bdcbf8

5 files changed

Lines changed: 82 additions & 15 deletions

File tree

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "python-catalyst"
3-
version = "0.1.5"
3+
version = "0.1.6"
44
description = "Python client for the PRODAFT CATALYST API"
55
readme = "README.md"
66
license = { file = "LICENSE" }

python_catalyst/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"""PRODAFT CATALYST API client package."""
22

3-
__version__ = "0.1.5"
3+
__version__ = "0.1.6"
44

55
from .client import CatalystClient
66
from .enums import ObservableType, PostCategory, TLPLevel

python_catalyst/client.py

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ def _process_entity(
365365
collected_object_refs: List,
366366
entity_mappings: Dict,
367367
external_reference: stix2.ExternalReference = None,
368-
) -> None:
368+
) -> Tuple[List[Dict], List, Dict]:
369369
"""
370370
Process a single entity and add it to the report.
371371
@@ -398,6 +398,8 @@ def _process_entity(
398398
if self.logger:
399399
self.logger.debug(f"Added {entity_type}: {entity_value}")
400400

401+
return related_objects, collected_object_refs, entity_mappings
402+
401403
def _process_entities(
402404
self,
403405
entities: List[Dict],
@@ -407,7 +409,7 @@ def _process_entities(
407409
collected_object_refs: List,
408410
entity_mappings: Dict,
409411
external_reference: stix2.ExternalReference = None,
410-
) -> None:
412+
) -> Tuple[List[Dict], List, Dict]:
411413
"""
412414
Process a list of entities of the same type.
413415
@@ -421,7 +423,11 @@ def _process_entities(
421423
external_reference: Optional reference to the report
422424
"""
423425
for entity in entities:
424-
self._process_entity(
426+
(
427+
related_objects,
428+
collected_object_refs,
429+
entity_mappings,
430+
) = self._process_entity(
425431
entity,
426432
entity_type,
427433
converter_method,
@@ -431,14 +437,16 @@ def _process_entities(
431437
external_reference,
432438
)
433439

440+
return related_objects, collected_object_refs, entity_mappings
441+
434442
def _process_threat_actor(
435443
self,
436444
threat_actor: Dict,
437445
related_objects: List,
438446
collected_object_refs: List,
439447
entity_mappings: Dict,
440448
external_reference: stix2.ExternalReference = None,
441-
) -> None:
449+
) -> Tuple[List[Dict], List, Dict]:
442450
"""
443451
Process a threat actor entity with detailed information.
444452
@@ -461,14 +469,14 @@ def _process_threat_actor(
461469
f"Retrieved detailed information for threat actor: {entity_value}"
462470
)
463471

464-
ta_object = self.converter.create_detailed_threat_actor(
472+
ta_object, bundle = self.converter.create_detailed_threat_actor(
465473
detailed_threat_actor,
466474
context,
467475
report_reference=external_reference,
468476
)
469477

470478
is_abstract = detailed_threat_actor.get("is_abstract", False)
471-
related_objects.append(ta_object)
479+
related_objects.extend(bundle)
472480
collected_object_refs.append(ta_object.id)
473481

474482
if is_abstract:
@@ -482,6 +490,8 @@ def _process_threat_actor(
482490
if self.logger:
483491
self.logger.debug(f"Added threat actor: {entity_value}")
484492

493+
return related_objects, collected_object_refs, entity_mappings
494+
485495
except Exception as e:
486496
if self.logger:
487497
self.logger.warning(
@@ -501,6 +511,8 @@ def _process_threat_actor(
501511
if self.logger:
502512
self.logger.debug(f"Added threat actor: {entity_value}")
503513

514+
return related_objects, collected_object_refs, entity_mappings
515+
504516
def create_report_from_member_content_with_references(
505517
self, content: Dict
506518
) -> Tuple[Dict, List[Dict]]:
@@ -528,6 +540,7 @@ def create_report_from_member_content_with_references(
528540
content_id = content.get("id")
529541
slug = content.get("slug", "") # noqa: F841
530542
tlp = content.get("tlp", TLPLevel.CLEAR.value)
543+
topics = content.get("topics", [])
531544
self.converter = self.get_stix_converter(tlp)
532545

533546
if published_on:
@@ -547,6 +560,9 @@ def create_report_from_member_content_with_references(
547560
labels.append(content["category"])
548561
if content.get("sub_category") and content["sub_category"].get("name"):
549562
labels.append(content["sub_category"]["name"])
563+
if len(topics) > 0:
564+
for topic in topics:
565+
labels.append(topic["name"])
550566

551567
report_id = (
552568
f"report--{str(uuid.uuid5(uuid.NAMESPACE_URL, f'catalyst-{content_id}'))}"
@@ -606,14 +622,15 @@ def create_report_from_member_content_with_references(
606622
entity_id = observable.get("id")
607623
entity_value = observable.get("value")
608624
entity_type = observable.get("type")
609-
625+
entity_context = observable.get("context", "")
610626
if entity_id and entity_value and entity_type:
611627
observable_data = {
612628
"id": entity_id,
613629
"value": entity_value,
614630
"type": entity_type,
615631
"post_id": content_id,
616632
"tlp_marking": content_marking,
633+
"context": entity_context,
617634
}
618635

619636
(
@@ -657,7 +674,11 @@ def create_report_from_member_content_with_references(
657674
f"Skipping threat actor {threat_actor.get('value')} because user is not authenticated... This will be implemented in the future."
658675
)
659676
continue
660-
self._process_threat_actor(
677+
(
678+
related_objects,
679+
collected_object_refs,
680+
entity_mappings,
681+
) = self._process_threat_actor(
661682
threat_actor,
662683
related_objects,
663684
collected_object_refs,
@@ -684,7 +705,11 @@ def create_report_from_member_content_with_references(
684705
converter_method = processor
685706
mapping_type = entity_type
686707

687-
self._process_entities(
708+
(
709+
related_objects,
710+
collected_object_refs,
711+
entity_mappings,
712+
) = self._process_entities(
688713
all_entities.get(entity_type, []),
689714
mapping_type,
690715
converter_method,

python_catalyst/stix_converter.py

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1096,11 +1096,15 @@ def create_indicator_from_observable(
10961096
marking_ref = tlp_marking.id if tlp_marking else self.tlp_marking.id
10971097

10981098
created_by_ref = self.get_created_by_ref()
1099+
description = f"Indicator for {observable_type}: {value}"
1100+
if "context" in observable_data:
1101+
ctx = observable_data["context"]
1102+
description = f"{description}\n\n{ctx}"
10991103

11001104
return stix2.Indicator(
11011105
id=indicator_id,
11021106
name=indicator_name,
1103-
description=f"Indicator for {observable_type}: {value}",
1107+
description=description,
11041108
pattern=pattern,
11051109
pattern_type="stix",
11061110
created_by_ref=created_by_ref,
@@ -1210,6 +1214,7 @@ def create_detailed_threat_actor(
12101214
Returns:
12111215
STIX ThreatActor or IntrusionSet object with full details included
12121216
"""
1217+
bundle = []
12131218
external_references = []
12141219
if report_reference:
12151220
external_references = [report_reference]
@@ -1291,7 +1296,7 @@ def create_detailed_threat_actor(
12911296

12921297
if is_abstract:
12931298
# Create an Intrusion Set for abstract entities
1294-
return stix2.IntrusionSet(
1299+
actor = stix2.IntrusionSet(
12951300
id=IntrusionSet.generate_id(entity_value),
12961301
name=entity_value,
12971302
description=description,
@@ -1302,12 +1307,13 @@ def create_detailed_threat_actor(
13021307
external_references if external_references else None
13031308
),
13041309
custom_properties=custom_properties,
1310+
allow_custom=True,
13051311
)
13061312
else:
13071313
# Create a Threat Actor with the appropriate type
13081314
actor_type = "threat-actor-group" if is_group else "threat-actor-individual"
13091315
custom_properties["x_opencti_type"] = actor_type
1310-
return stix2.ThreatActor(
1316+
actor = stix2.ThreatActor(
13111317
id=ThreatActor.generate_id(entity_value, actor_type),
13121318
name=entity_value,
13131319
description=description,
@@ -1318,4 +1324,40 @@ def create_detailed_threat_actor(
13181324
external_references if external_references else None
13191325
),
13201326
custom_properties=custom_properties,
1327+
allow_custom=True,
13211328
)
1329+
bundle.append(actor)
1330+
1331+
suspected_origins = threat_actor_data.get("suspected_origins", [])
1332+
if isinstance(suspected_origins, list):
1333+
for origin in suspected_origins:
1334+
cname = origin.get("name")
1335+
ccode = origin.get("code")
1336+
if not cname:
1337+
continue
1338+
1339+
loc = stix2.Location(
1340+
name=cname,
1341+
country=cname,
1342+
custom_properties=(
1343+
{
1344+
"x_country_code": (
1345+
ccode.upper() if isinstance(ccode, str) else None
1346+
)
1347+
}
1348+
if ccode
1349+
else None
1350+
),
1351+
allow_custom=True,
1352+
)
1353+
rel_type = "originates-from" if is_abstract else "located-at"
1354+
rel = stix2.Relationship(
1355+
relationship_type=rel_type,
1356+
source_ref=actor.id,
1357+
target_ref=loc.id,
1358+
)
1359+
1360+
bundle.append(loc)
1361+
bundle.append(rel)
1362+
1363+
return actor, bundle

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
setup(
77
name="python-catalyst",
8-
version="0.1.5",
8+
version="0.1.6",
99
description="Python client for the PRODAFT CATALYST API",
1010
long_description=long_description,
1111
long_description_content_type="text/markdown",

0 commit comments

Comments
 (0)