From 00d73010320100cf37ae726650c2d1725d792f46 Mon Sep 17 00:00:00 2001 From: Julien Richard Date: Sat, 25 Jan 2025 19:40:02 +0100 Subject: [PATCH 01/11] [client] Introduce background tasks support --- pycti/entities/opencti_stix_core_object.py | 46 ++++++++++++++++++++++ pycti/utils/opencti_stix2.py | 12 ++++++ pycti/utils/opencti_stix2_splitter.py | 4 +- 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/pycti/entities/opencti_stix_core_object.py b/pycti/entities/opencti_stix_core_object.py index 4846cc16b..c827a197c 100644 --- a/pycti/entities/opencti_stix_core_object.py +++ b/pycti/entities/opencti_stix_core_object.py @@ -1682,6 +1682,52 @@ def reports(self, **kwargs): self.opencti.app_logger.error("Missing parameters: id") return None + """ + Apply rule to Stix-Core-Object object + + :param element_id: the Stix-Core-Object id + :param rule_id: the rule to apply + :return void + """ + + def rule_apply(self, **kwargs): + rule_id = kwargs.get("rule_id", None) + element_id = kwargs.get("element_id", None) + if element_id is not None and rule_id is not None: + self.opencti.app_logger.info("Apply rule stix_core_object", {"id": element_id}) + query = """ + mutation StixCoreApplyRule($elementId: ID!, $ruleId: ID!) { + ruleApply(elementId: $elementId, ruleId: $ruleId) + } + """ + self.opencti.query(query, {"elementId": element_id, "ruleId": rule_id}) + else: + self.opencti.app_logger.error("[stix_core_object] Missing parameters: id") + return None + + """ + Apply rule clear to Stix-Core-Object object + + :param element_id: the Stix-Core-Object id + :param rule_id: the rule to apply + :return void + """ + + def rule_clear(self, **kwargs): + rule_id = kwargs.get("rule_id", None) + element_id = kwargs.get("element_id", None) + if element_id is not None and rule_id is not None: + self.opencti.app_logger.info("Apply rule clear stix_core_object", {"id": element_id}) + query = """ + mutation StixCoreClearRule($elementId: ID!, $ruleId: ID!) { + ruleClear(elementId: $elementId, ruleId: $ruleId) + } + """ + self.opencti.query(query, {"elementId": element_id, "ruleId": rule_id}) + else: + self.opencti.app_logger.error("[stix_core_object] Missing parameters: id") + return None + """ Delete a Stix-Core-Object object diff --git a/pycti/utils/opencti_stix2.py b/pycti/utils/opencti_stix2.py index 2eeb6fcfa..8ab373535 100644 --- a/pycti/utils/opencti_stix2.py +++ b/pycti/utils/opencti_stix2.py @@ -2458,6 +2458,14 @@ def apply_patch(self, item): ) self.apply_patch_files(item) + def rule_apply(self, item): + rule_id = item["opencti_rule"] + self.opencti.stix_core_object.rule_apply(element_id=item["id"], rule_id=rule_id) + + def rule_clear(self, item): + rule_id = item["opencti_rule"] + self.opencti.stix_core_object.rule_clear(element_id=item["id"], rule_id=rule_id) + def import_item( self, item, @@ -2479,6 +2487,10 @@ def import_item( self.opencti.stix.merge(id=target_id, object_ids=source_ids) elif item["opencti_operation"] == "patch": self.apply_patch(item=item) + elif item["opencti_operation"] == "rule_apply": + self.rule_apply(item=item) + elif item["opencti_operation"] == "rule_clear": + self.rule_clear(item=item) else: raise ValueError("Not supported opencti_operation") elif item["type"] == "relationship": diff --git a/pycti/utils/opencti_stix2_splitter.py b/pycti/utils/opencti_stix2_splitter.py index 10a655908..fe95a65d0 100644 --- a/pycti/utils/opencti_stix2_splitter.py +++ b/pycti/utils/opencti_stix2_splitter.py @@ -176,11 +176,11 @@ def enlist_element( # Put in cache if self.cache_index.get(item_id) is None: # enlist only if compatible - if item["type"] == "relationship": + if item["type"] == "relationship" and item.get("opencti_operation") is None: is_compatible = ( item["source_ref"] is not None and item["target_ref"] is not None ) - elif item["type"] == "sighting": + elif item["type"] == "sighting" and item.get("opencti_operation") is None: is_compatible = ( item["sighting_of_ref"] is not None and len(item["where_sighted_refs"]) > 0 From 8407aaff12e35bcf169fa3120a398334f1aa58f6 Mon Sep 17 00:00:00 2001 From: Julien Richard Date: Sun, 26 Jan 2025 23:14:20 +0100 Subject: [PATCH 02/11] [client] Introduce new functions share/unshare and rescan --- pycti/entities/opencti_stix_core_object.py | 83 +++++++++++++++++++++- pycti/utils/opencti_stix2.py | 22 +++++- 2 files changed, 102 insertions(+), 3 deletions(-) diff --git a/pycti/entities/opencti_stix_core_object.py b/pycti/entities/opencti_stix_core_object.py index c827a197c..66c1dee81 100644 --- a/pycti/entities/opencti_stix_core_object.py +++ b/pycti/entities/opencti_stix_core_object.py @@ -1694,7 +1694,9 @@ def rule_apply(self, **kwargs): rule_id = kwargs.get("rule_id", None) element_id = kwargs.get("element_id", None) if element_id is not None and rule_id is not None: - self.opencti.app_logger.info("Apply rule stix_core_object", {"id": element_id}) + self.opencti.app_logger.info( + "Apply rule stix_core_object", {"id": element_id} + ) query = """ mutation StixCoreApplyRule($elementId: ID!, $ruleId: ID!) { ruleApply(elementId: $elementId, ruleId: $ruleId) @@ -1717,7 +1719,9 @@ def rule_clear(self, **kwargs): rule_id = kwargs.get("rule_id", None) element_id = kwargs.get("element_id", None) if element_id is not None and rule_id is not None: - self.opencti.app_logger.info("Apply rule clear stix_core_object", {"id": element_id}) + self.opencti.app_logger.info( + "Apply rule clear stix_core_object", {"id": element_id} + ) query = """ mutation StixCoreClearRule($elementId: ID!, $ruleId: ID!) { ruleClear(elementId: $elementId, ruleId: $ruleId) @@ -1728,6 +1732,81 @@ def rule_clear(self, **kwargs): self.opencti.app_logger.error("[stix_core_object] Missing parameters: id") return None + """ + Apply rules rescan to Stix-Core-Object object + + :param element_id: the Stix-Core-Object id + :return void + """ + + def rules_rescan(self, **kwargs): + element_id = kwargs.get("element_id", None) + if element_id is not None: + self.opencti.app_logger.info( + "Apply rules rescan stix_core_object", {"id": element_id} + ) + query = """ + mutation StixCoreRescanRules($elementId: ID!) { + rulesRescan(elementId: $elementId) + } + """ + self.opencti.query(query, {"elementId": element_id}) + else: + self.opencti.app_logger.error("[stix_core_object] Missing parameters: id") + return None + + """ + Share element to multiple organizations + + :param entity_id: the Stix-Core-Object id + :param organization_id:s the organization to share with + :return void + """ + + def organization_share(self, entity_id, organization_ids): + query = """ + mutation StixCoreObjectEdit($id: ID!, $organizationId: [ID!]!) { + stixCoreObjectEdit(id: $id) { + restrictionOrganizationAdd(organizationId: $organizationId) { + id + } + } + } + """ + self.opencti.query( + query, + { + "id": entity_id, + "organizationId": organization_ids, + }, + ) + + """ + Unshare element from multiple organizations + + :param entity_id: the Stix-Core-Object id + :param organization_id:s the organization to share with + :return void + """ + + def organization_unshare(self, entity_id, organization_ids): + query = """ + mutation StixCoreObjectEdit($id: ID!, $organizationId: [ID!]!) { + stixCoreObjectEdit(id: $id) { + restrictionOrganizationDelete(organizationId: $organizationId) { + id + } + } + } + """ + self.opencti.query( + query, + { + "id": entity_id, + "organizationId": organization_ids, + }, + ) + """ Delete a Stix-Core-Object object diff --git a/pycti/utils/opencti_stix2.py b/pycti/utils/opencti_stix2.py index 8ab373535..f1ae74811 100644 --- a/pycti/utils/opencti_stix2.py +++ b/pycti/utils/opencti_stix2.py @@ -2466,6 +2466,17 @@ def rule_clear(self, item): rule_id = item["opencti_rule"] self.opencti.stix_core_object.rule_clear(element_id=item["id"], rule_id=rule_id) + def rules_rescan(self, item): + self.opencti.stix_core_object.rules_rescan(element_id=item["id"]) + + def organization_share(self, item): + organization_ids = item["organization_ids"] + self.opencti.stix_core_object.organization_share(item["id"], organization_ids) + + def organization_unshare(self, item): + organization_ids = item["organization_ids"] + self.opencti.stix_core_object.organization_unshare(item["id"], organization_ids) + def import_item( self, item, @@ -2491,8 +2502,17 @@ def import_item( self.rule_apply(item=item) elif item["opencti_operation"] == "rule_clear": self.rule_clear(item=item) + elif item["opencti_operation"] == "rules_rescan": + self.rules_rescan(item=item) + elif item["opencti_operation"] == "share": + self.organization_share(item=item) + elif item["opencti_operation"] == "unshare": + self.organization_unshare(item=item) else: - raise ValueError("Not supported opencti_operation") + raise ValueError( + "Not supported opencti_operation", + {"operation": item["opencti_operation"]}, + ) elif item["type"] == "relationship": # Import relationship self.import_relationship(item, update, types) From 02aaae91cd69b83b2d6ea77fa2514c05080eebe7 Mon Sep 17 00:00:00 2001 From: Julien Richard Date: Tue, 28 Jan 2025 16:41:39 +0100 Subject: [PATCH 03/11] [client] Add enrichment api --- pycti/entities/opencti_stix.py | 7 +++--- pycti/entities/opencti_stix_core_object.py | 28 ++++++++++++++++++++++ pycti/utils/opencti_stix2.py | 13 ++++++++-- 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/pycti/entities/opencti_stix.py b/pycti/entities/opencti_stix.py index ce8aca565..ffd3e1a18 100644 --- a/pycti/entities/opencti_stix.py +++ b/pycti/entities/opencti_stix.py @@ -11,16 +11,17 @@ def __init__(self, opencti): def delete(self, **kwargs): id = kwargs.get("id", None) + force_delete = kwargs.get("force_delete", True) if id is not None: self.opencti.app_logger.info("Deleting Stix element", {"id": id}) query = """ - mutation StixEdit($id: ID!) { + mutation StixEdit($id: ID!, $forceDelete: Boolean) { stixEdit(id: $id) { - delete + delete(forceDelete: $forceDelete) } } """ - self.opencti.query(query, {"id": id}) + self.opencti.query(query, {"id": id, "forceDelete": force_delete}) else: self.opencti.app_logger.error("[opencti_stix] Missing parameters: id") return None diff --git a/pycti/entities/opencti_stix_core_object.py b/pycti/entities/opencti_stix_core_object.py index 66c1dee81..134c91310 100644 --- a/pycti/entities/opencti_stix_core_object.py +++ b/pycti/entities/opencti_stix_core_object.py @@ -1755,6 +1755,34 @@ def rules_rescan(self, **kwargs): self.opencti.app_logger.error("[stix_core_object] Missing parameters: id") return None + """ + Ask enrichment with multiple connectors + + :param element_id: the Stix-Core-Object id + :param connector_ids the connectors + :return void + """ + + def ask_enrichment(self, **kwargs): + element_id = kwargs.get("element_id", None) + connector_ids = kwargs.get("connector_ids", None) + query = """ + mutation StixCoreObjectEdit($id: ID!, $connectorId: [ID!]!) { + stixCoreObjectEdit(id: $id) { + askEnrichment(connectorId: $connectorId) { + id + } + } + } + """ + self.opencti.query( + query, + { + "id": element_id, + "connectorId": connector_ids, + }, + ) + """ Share element to multiple organizations diff --git a/pycti/utils/opencti_stix2.py b/pycti/utils/opencti_stix2.py index f1ae74811..29b32963c 100644 --- a/pycti/utils/opencti_stix2.py +++ b/pycti/utils/opencti_stix2.py @@ -2489,9 +2489,13 @@ def import_item( try: self.opencti.set_retry_number(processing_count) if "opencti_operation" in item: - if item["opencti_operation"] == "delete": + if ( + item["opencti_operation"] == "delete" + or item["opencti_operation"] == "delete-force" + ): delete_id = item["id"] - self.opencti.stix.delete(id=delete_id) + force_delete = item["opencti_operation"] == "delete-force" + self.opencti.stix.delete(id=delete_id, force_delete=force_delete) elif item["opencti_operation"] == "merge": target_id = item["merge_target_id"] source_ids = item["merge_source_ids"] @@ -2508,6 +2512,11 @@ def import_item( self.organization_share(item=item) elif item["opencti_operation"] == "unshare": self.organization_unshare(item=item) + elif item["opencti_operation"] == "enrichment": + connector_ids = item["connector_ids"] + self.opencti.stix_core_object.ask_enrichment( + element_id=item["id"], connector_ids=connector_ids + ) else: raise ValueError( "Not supported opencti_operation", From 304b18952a865d7777021bd539ef1f2c827d612e Mon Sep 17 00:00:00 2001 From: Julien Richard Date: Wed, 29 Jan 2025 10:19:27 +0100 Subject: [PATCH 04/11] [client] Adapt sharing and start restrict access --- pycti/entities/opencti_stix_core_object.py | 43 +++++++++++++++++++--- pycti/utils/opencti_stix2.py | 18 +++++++-- 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/pycti/entities/opencti_stix_core_object.py b/pycti/entities/opencti_stix_core_object.py index 134c91310..c5c02833f 100644 --- a/pycti/entities/opencti_stix_core_object.py +++ b/pycti/entities/opencti_stix_core_object.py @@ -1755,6 +1755,33 @@ def rules_rescan(self, **kwargs): self.opencti.app_logger.error("[stix_core_object] Missing parameters: id") return None + """ + Ask clear restriction + + :param element_id: the Stix-Core-Object id + :return void + """ + + def clear_access_restriction(self, **kwargs): + element_id = kwargs.get("element_id", None) + if element_id is not None: + query = """ + mutation StixCoreObjectEdit($id: ID!) { + stixCoreObjectEdit(id: $id) { + clearAccessRestriction + } + } + """ + self.opencti.query( + query, + { + "id": element_id, + }, + ) + else: + self.opencti.app_logger.error("[stix_core_object] Missing parameters: id") + return None + """ Ask enrichment with multiple connectors @@ -1791,11 +1818,11 @@ def ask_enrichment(self, **kwargs): :return void """ - def organization_share(self, entity_id, organization_ids): + def organization_share(self, entity_id, organization_ids, sharing_direct_container): query = """ - mutation StixCoreObjectEdit($id: ID!, $organizationId: [ID!]!) { + mutation StixCoreObjectEdit($id: ID!, $organizationId: [ID!]!, $directContainerSharing: Boolean) { stixCoreObjectEdit(id: $id) { - restrictionOrganizationAdd(organizationId: $organizationId) { + restrictionOrganizationAdd(organizationId: $organizationId, directContainerSharing: $directContainerSharing) { id } } @@ -1806,6 +1833,7 @@ def organization_share(self, entity_id, organization_ids): { "id": entity_id, "organizationId": organization_ids, + "directContainerSharing": sharing_direct_container, }, ) @@ -1817,11 +1845,13 @@ def organization_share(self, entity_id, organization_ids): :return void """ - def organization_unshare(self, entity_id, organization_ids): + def organization_unshare( + self, entity_id, organization_ids, sharing_direct_container + ): query = """ - mutation StixCoreObjectEdit($id: ID!, $organizationId: [ID!]!) { + mutation StixCoreObjectEdit($id: ID!, $organizationId: [ID!]!, $directContainerSharing: Boolean) { stixCoreObjectEdit(id: $id) { - restrictionOrganizationDelete(organizationId: $organizationId) { + restrictionOrganizationDelete(organizationId: $organizationId, directContainerSharing: $directContainerSharing) { id } } @@ -1832,6 +1862,7 @@ def organization_unshare(self, entity_id, organization_ids): { "id": entity_id, "organizationId": organization_ids, + "directContainerSharing": sharing_direct_container, }, ) diff --git a/pycti/utils/opencti_stix2.py b/pycti/utils/opencti_stix2.py index 29b32963c..9467b25c8 100644 --- a/pycti/utils/opencti_stix2.py +++ b/pycti/utils/opencti_stix2.py @@ -2470,12 +2470,18 @@ def rules_rescan(self, item): self.opencti.stix_core_object.rules_rescan(element_id=item["id"]) def organization_share(self, item): - organization_ids = item["organization_ids"] - self.opencti.stix_core_object.organization_share(item["id"], organization_ids) + organization_ids = item["sharing_organization_ids"] + sharing_direct_container = item["sharing_direct_container"] + self.opencti.stix_core_object.organization_share( + item["id"], organization_ids, sharing_direct_container + ) def organization_unshare(self, item): - organization_ids = item["organization_ids"] - self.opencti.stix_core_object.organization_unshare(item["id"], organization_ids) + organization_ids = item["sharing_organization_ids"] + sharing_direct_container = item["sharing_direct_container"] + self.opencti.stix_core_object.organization_unshare( + item["id"], organization_ids, sharing_direct_container + ) def import_item( self, @@ -2512,6 +2518,10 @@ def import_item( self.organization_share(item=item) elif item["opencti_operation"] == "unshare": self.organization_unshare(item=item) + elif item["opencti_operation"] == "clear_access_restriction": + self.opencti.stix_core_object.clear_access_restriction( + element_id=item["id"] + ) elif item["opencti_operation"] == "enrichment": connector_ids = item["connector_ids"] self.opencti.stix_core_object.ask_enrichment( From 868623c6a1eaea88e63e0e063e148a8f96889f3d Mon Sep 17 00:00:00 2001 From: Julien Richard Date: Thu, 30 Jan 2025 13:44:09 +0100 Subject: [PATCH 05/11] [client] Fix remove members support --- pycti/entities/opencti_stix_core_object.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pycti/entities/opencti_stix_core_object.py b/pycti/entities/opencti_stix_core_object.py index c5c02833f..99621fa62 100644 --- a/pycti/entities/opencti_stix_core_object.py +++ b/pycti/entities/opencti_stix_core_object.py @@ -1768,7 +1768,9 @@ def clear_access_restriction(self, **kwargs): query = """ mutation StixCoreObjectEdit($id: ID!) { stixCoreObjectEdit(id: $id) { - clearAccessRestriction + clearAccessRestriction { + id + } } } """ From 401a5b33cefaeb7fb750124fc6a097530ddb2726 Mon Sep 17 00:00:00 2001 From: Julien Richard Date: Thu, 30 Jan 2025 22:59:13 +0100 Subject: [PATCH 06/11] [client] Add support for trash restore --- pycti/api/opencti_api_client.py | 2 ++ pycti/api/opencti_api_trash.py | 18 ++++++++++++++++++ pycti/utils/opencti_stix2.py | 2 ++ pycti/utils/opencti_stix2_splitter.py | 1 + 4 files changed, 23 insertions(+) create mode 100644 pycti/api/opencti_api_trash.py diff --git a/pycti/api/opencti_api_client.py b/pycti/api/opencti_api_client.py index 5826aef4a..d318daa3e 100644 --- a/pycti/api/opencti_api_client.py +++ b/pycti/api/opencti_api_client.py @@ -11,6 +11,7 @@ from pycti import __version__ from pycti.api.opencti_api_connector import OpenCTIApiConnector from pycti.api.opencti_api_playbook import OpenCTIApiPlaybook +from pycti.api.opencti_api_trash import OpenCTIApiTrash from pycti.api.opencti_api_work import OpenCTIApiWork from pycti.entities.opencti_attack_pattern import AttackPattern from pycti.entities.opencti_campaign import Campaign @@ -151,6 +152,7 @@ def __init__( # Define the dependencies self.work = OpenCTIApiWork(self) + self.trash = OpenCTIApiTrash(self) self.playbook = OpenCTIApiPlaybook(self) self.connector = OpenCTIApiConnector(self) self.stix2 = OpenCTIStix2(self) diff --git a/pycti/api/opencti_api_trash.py b/pycti/api/opencti_api_trash.py new file mode 100644 index 000000000..18d909a45 --- /dev/null +++ b/pycti/api/opencti_api_trash.py @@ -0,0 +1,18 @@ +class OpenCTIApiTrash: + """OpenCTIApiTrash""" + + def __init__(self, api): + self.api = api + + def delete_operation_restore(self, operation_id: str): + query = """ + mutation DeleteOperationRestore($id: ID!) { + deleteOperationRestore(id: $id) + } + """ + self.api.query( + query, + { + "id": operation_id, + }, + ) diff --git a/pycti/utils/opencti_stix2.py b/pycti/utils/opencti_stix2.py index 9467b25c8..12e727092 100644 --- a/pycti/utils/opencti_stix2.py +++ b/pycti/utils/opencti_stix2.py @@ -2502,6 +2502,8 @@ def import_item( delete_id = item["id"] force_delete = item["opencti_operation"] == "delete-force" self.opencti.stix.delete(id=delete_id, force_delete=force_delete) + elif item["opencti_operation"] == "restore": + self.opencti.trash.delete_operation_restore(item["id"]) elif item["opencti_operation"] == "merge": target_id = item["merge_target_id"] source_ids = item["merge_source_ids"] diff --git a/pycti/utils/opencti_stix2_splitter.py b/pycti/utils/opencti_stix2_splitter.py index fe95a65d0..bf6890df6 100644 --- a/pycti/utils/opencti_stix2_splitter.py +++ b/pycti/utils/opencti_stix2_splitter.py @@ -19,6 +19,7 @@ SUPPORTED_STIX_ENTITY_OBJECTS # entities + list(STIX_CYBER_OBSERVABLE_MAPPING.keys()) # observables + ["relationship", "sighting"] # relationships + + ["deleteoperation"] # OpenCTI specific type for operation ) From 964bfcb8e059cd55d0676e598aa1e0fe7b6b7243 Mon Sep 17 00:00:00 2001 From: Julien Richard Date: Mon, 10 Feb 2025 18:06:25 +0100 Subject: [PATCH 07/11] [client] add draft api --- pycti/api/opencti_api_client.py | 2 ++ pycti/api/opencti_api_draft.py | 18 +++++++++++++++++ pycti/entities/opencti_stix_core_object.py | 23 ++++++++++++++++++++++ pycti/utils/opencti_stix2.py | 4 ++++ 4 files changed, 47 insertions(+) create mode 100644 pycti/api/opencti_api_draft.py diff --git a/pycti/api/opencti_api_client.py b/pycti/api/opencti_api_client.py index d318daa3e..d30402d2d 100644 --- a/pycti/api/opencti_api_client.py +++ b/pycti/api/opencti_api_client.py @@ -10,6 +10,7 @@ from pycti import __version__ from pycti.api.opencti_api_connector import OpenCTIApiConnector +from pycti.api.opencti_api_draft import OpenCTIApiDraft from pycti.api.opencti_api_playbook import OpenCTIApiPlaybook from pycti.api.opencti_api_trash import OpenCTIApiTrash from pycti.api.opencti_api_work import OpenCTIApiWork @@ -153,6 +154,7 @@ def __init__( # Define the dependencies self.work = OpenCTIApiWork(self) self.trash = OpenCTIApiTrash(self) + self.draft = OpenCTIApiDraft(self) self.playbook = OpenCTIApiPlaybook(self) self.connector = OpenCTIApiConnector(self) self.stix2 = OpenCTIStix2(self) diff --git a/pycti/api/opencti_api_draft.py b/pycti/api/opencti_api_draft.py new file mode 100644 index 000000000..e73dca833 --- /dev/null +++ b/pycti/api/opencti_api_draft.py @@ -0,0 +1,18 @@ +class OpenCTIApiDraft: + """OpenCTIApiDraft""" + + def __init__(self, api): + self.api = api + + def delete(self, draft_id: str): + query = """ + mutation DraftWorkspaceDelete($id: ID!) { + draftWorkspaceDelete(id: $id) + } + """ + self.api.query( + query, + { + "id": draft_id, + }, + ) diff --git a/pycti/entities/opencti_stix_core_object.py b/pycti/entities/opencti_stix_core_object.py index 99621fa62..6603856dd 100644 --- a/pycti/entities/opencti_stix_core_object.py +++ b/pycti/entities/opencti_stix_core_object.py @@ -1890,3 +1890,26 @@ def delete(self, **kwargs): else: self.opencti.app_logger.error("[stix_core_object] Missing parameters: id") return None + + """ + Remove a Stix-Core-Object object from draft (revert) + + :param id: the Stix-Core-Object id + :return void + """ + + def remove_from_draft(self, **kwargs): + id = kwargs.get("id", None) + if id is not None: + self.opencti.app_logger.info("Draft remove stix_core_object", {"id": id}) + query = """ + mutation StixCoreObjectEditDraftRemove($id: ID!) { + stixCoreObjectEdit(id: $id) { + removeFromDraft + } + } + """ + self.opencti.query(query, {"id": id}) + else: + self.opencti.app_logger.error("[stix_core_object] Missing parameters: id") + return None \ No newline at end of file diff --git a/pycti/utils/opencti_stix2.py b/pycti/utils/opencti_stix2.py index 12e727092..8fe656f16 100644 --- a/pycti/utils/opencti_stix2.py +++ b/pycti/utils/opencti_stix2.py @@ -2502,6 +2502,10 @@ def import_item( delete_id = item["id"] force_delete = item["opencti_operation"] == "delete-force" self.opencti.stix.delete(id=delete_id, force_delete=force_delete) + elif item["opencti_operation"] == "delete-draft": + self.opencti.draft.delete(item["id"]) + elif item["opencti_operation"] == "revert-draft": + self.opencti.stix_core_object.remove_from_draft(id=item["id"]) elif item["opencti_operation"] == "restore": self.opencti.trash.delete_operation_restore(item["id"]) elif item["opencti_operation"] == "merge": From a7c33756f824ce8aedaf6291bf57a589286f020d Mon Sep 17 00:00:00 2001 From: Julien Richard Date: Tue, 11 Feb 2025 23:42:49 +0100 Subject: [PATCH 08/11] [client] Introduce new class --- pycti/api/opencti_api_client.py | 4 +++ pycti/api/opencti_api_draft.py | 5 +-- pycti/api/opencti_api_playbook.py | 14 ++++++++ pycti/api/opencti_api_public_dashboard.py | 19 ++++++++++ pycti/api/opencti_api_trash.py | 25 +++++++++++++- pycti/api/opencti_api_work.py | 14 ++++++++ pycti/api/opencti_api_workspace.py | 19 ++++++++++ pycti/entities/opencti_group.py | 6 +++- pycti/entities/opencti_stix_core_object.py | 2 +- pycti/utils/constants.py | 2 +- pycti/utils/opencti_stix2.py | 40 ++++++++++++++++++---- pycti/utils/opencti_stix2_splitter.py | 3 +- pycti/utils/opencti_stix2_utils.py | 21 ++++++++++++ 13 files changed, 161 insertions(+), 13 deletions(-) create mode 100644 pycti/api/opencti_api_public_dashboard.py create mode 100644 pycti/api/opencti_api_workspace.py diff --git a/pycti/api/opencti_api_client.py b/pycti/api/opencti_api_client.py index d30402d2d..266135716 100644 --- a/pycti/api/opencti_api_client.py +++ b/pycti/api/opencti_api_client.py @@ -12,8 +12,10 @@ from pycti.api.opencti_api_connector import OpenCTIApiConnector from pycti.api.opencti_api_draft import OpenCTIApiDraft from pycti.api.opencti_api_playbook import OpenCTIApiPlaybook +from pycti.api.opencti_api_public_dashboard import OpenCTIApiPublicDashboard from pycti.api.opencti_api_trash import OpenCTIApiTrash from pycti.api.opencti_api_work import OpenCTIApiWork +from pycti.api.opencti_api_workspace import OpenCTIApiWorkspace from pycti.entities.opencti_attack_pattern import AttackPattern from pycti.entities.opencti_campaign import Campaign from pycti.entities.opencti_capability import Capability @@ -155,6 +157,8 @@ def __init__( self.work = OpenCTIApiWork(self) self.trash = OpenCTIApiTrash(self) self.draft = OpenCTIApiDraft(self) + self.workspace = OpenCTIApiWorkspace(self) + self.public_dashboard = OpenCTIApiPublicDashboard(self) self.playbook = OpenCTIApiPlaybook(self) self.connector = OpenCTIApiConnector(self) self.stix2 = OpenCTIStix2(self) diff --git a/pycti/api/opencti_api_draft.py b/pycti/api/opencti_api_draft.py index e73dca833..9869c9619 100644 --- a/pycti/api/opencti_api_draft.py +++ b/pycti/api/opencti_api_draft.py @@ -4,7 +4,8 @@ class OpenCTIApiDraft: def __init__(self, api): self.api = api - def delete(self, draft_id: str): + def delete(self, **kwargs): + id = kwargs.get("id", None) query = """ mutation DraftWorkspaceDelete($id: ID!) { draftWorkspaceDelete(id: $id) @@ -13,6 +14,6 @@ def delete(self, draft_id: str): self.api.query( query, { - "id": draft_id, + "id": id, }, ) diff --git a/pycti/api/opencti_api_playbook.py b/pycti/api/opencti_api_playbook.py index f02bf6df8..8b6c71ec5 100644 --- a/pycti/api/opencti_api_playbook.py +++ b/pycti/api/opencti_api_playbook.py @@ -32,3 +32,17 @@ def playbook_step_execution(self, playbook: dict, bundle: str): "bundle": bundle, }, ) + + def delete(self, **kwargs): + id = kwargs.get("id", None) + query = """ + mutation PlaybookDelete($id: ID!) { + playbookDelete(id: $id) + } + """ + self.api.query( + query, + { + "id": id, + }, + ) diff --git a/pycti/api/opencti_api_public_dashboard.py b/pycti/api/opencti_api_public_dashboard.py new file mode 100644 index 000000000..767b5379a --- /dev/null +++ b/pycti/api/opencti_api_public_dashboard.py @@ -0,0 +1,19 @@ +class OpenCTIApiPublicDashboard: + """OpenCTIApiPublicDashboard""" + + def __init__(self, api): + self.api = api + + def delete(self, **kwargs): + id = kwargs.get("id", None) + query = """ + mutation PublicDashboardDelete($id: ID!) { + publicDashboardDelete(id: $id) + } + """ + self.api.query( + query, + { + "id": id, + }, + ) diff --git a/pycti/api/opencti_api_trash.py b/pycti/api/opencti_api_trash.py index 18d909a45..ee90c9bdd 100644 --- a/pycti/api/opencti_api_trash.py +++ b/pycti/api/opencti_api_trash.py @@ -4,7 +4,7 @@ class OpenCTIApiTrash: def __init__(self, api): self.api = api - def delete_operation_restore(self, operation_id: str): + def restore(self, operation_id: str): query = """ mutation DeleteOperationRestore($id: ID!) { deleteOperationRestore(id: $id) @@ -16,3 +16,26 @@ def delete_operation_restore(self, operation_id: str): "id": operation_id, }, ) + + def delete(self, **kwargs): + """Delete a role given its ID + + :param id: ID for the role on the platform. + :type id: str + """ + id = kwargs.get("id", None) + if id is None: + self.api.admin_logger.error("[opencti_role] Missing parameter: id") + return None + + query = """ + mutation DeleteOperationConfirm($id: ID!) { + deleteOperationConfirm(id: $id) { + } + """ + self.api.query( + query, + { + "id": id, + }, + ) diff --git a/pycti/api/opencti_api_work.py b/pycti/api/opencti_api_work.py index 4ef5dde86..fc30468a5 100644 --- a/pycti/api/opencti_api_work.py +++ b/pycti/api/opencti_api_work.py @@ -128,6 +128,20 @@ def delete_work(self, work_id: str): ) return work["data"] + def delete(self, **kwargs): + id = kwargs.get("id", None) + query = """ + mutation ConnectorWorksMutation($workId: ID!) { + workEdit(id: $workId) { + delete + } + }""" + work = self.api.query( + query, + {"workId": id}, + ) + return work["data"] + def wait_for_work_to_finish(self, work_id: str): status = "" cnt = 0 diff --git a/pycti/api/opencti_api_workspace.py b/pycti/api/opencti_api_workspace.py new file mode 100644 index 000000000..a2161b900 --- /dev/null +++ b/pycti/api/opencti_api_workspace.py @@ -0,0 +1,19 @@ +class OpenCTIApiWorkspace: + """OpenCTIApiWorkspace""" + + def __init__(self, api): + self.api = api + + def delete(self, **kwargs): + id = kwargs.get("id", None) + query = """ + mutation WorkspaceDelete($id: ID!) { + workspaceDelete(id: $id) + } + """ + self.api.query( + query, + { + "id": id, + }, + ) diff --git a/pycti/entities/opencti_group.py b/pycti/entities/opencti_group.py index 3aad289dc..25b531982 100644 --- a/pycti/entities/opencti_group.py +++ b/pycti/entities/opencti_group.py @@ -315,12 +315,16 @@ def create(self, **kwargs) -> Optional[Dict]: ) return self.opencti.process_multiple_fields(result["data"]["groupAdd"]) - def delete(self, id: str): + def delete(self, **kwargs): """Delete a given group from OpenCTI :param id: ID of the group to delete. :type id: str """ + id = kwargs.get("id", None) + if id is None: + self.opencti.admin_logger.error("[opencti_user] Missing parameter: id") + return None self.opencti.admin_logger.info("Deleting group", {"id": id}) query = """ mutation GroupDelete($id: ID!) { diff --git a/pycti/entities/opencti_stix_core_object.py b/pycti/entities/opencti_stix_core_object.py index 6603856dd..b88511993 100644 --- a/pycti/entities/opencti_stix_core_object.py +++ b/pycti/entities/opencti_stix_core_object.py @@ -1912,4 +1912,4 @@ def remove_from_draft(self, **kwargs): self.opencti.query(query, {"id": id}) else: self.opencti.app_logger.error("[stix_core_object] Missing parameters: id") - return None \ No newline at end of file + return None diff --git a/pycti/utils/constants.py b/pycti/utils/constants.py index 23be24d7e..a5b7de6ff 100644 --- a/pycti/utils/constants.py +++ b/pycti/utils/constants.py @@ -50,7 +50,7 @@ class StixCyberObservableTypes(Enum): PERSONA = "Persona" @classmethod - def has_value(cls, value): + def has_value(cls, value: str) -> bool: lower_attr = list(map(lambda x: x.lower(), cls._value2member_map_)) return value.lower() in lower_attr diff --git a/pycti/utils/opencti_stix2.py b/pycti/utils/opencti_stix2.py index 8fe656f16..fb97e3e60 100644 --- a/pycti/utils/opencti_stix2.py +++ b/pycti/utils/opencti_stix2.py @@ -30,6 +30,7 @@ from pycti.utils.opencti_stix2_utils import ( OBSERVABLES_VALUE_INT, STIX_CYBER_OBSERVABLE_MAPPING, + STIX_OBJECTS, ) datefinder.ValueError = ValueError, OverflowError @@ -907,6 +908,22 @@ def get_stix_helper(self): "sighting": self.opencti.stix_sighting_relationship, } + def get_internal_helper(self): + # Import + return { + "user": self.opencti.user, + "group": self.opencti.group, + "capability": self.opencti.capability, + "role": self.opencti.role, + "settings": self.opencti.settings, + "work": self.opencti.work, + "deleteoperation": self.opencti.trash, + "draftworkspace": self.opencti.draft, + "playbook": self.opencti.playbook, + "workspace": self.opencti.workspace, + "publicdashboard": self.opencti.public_dashboard, + } + def generate_standard_id_from_stix(self, data): stix_helpers = self.get_stix_helper() helper = stix_helpers.get(data["type"]) @@ -2483,6 +2500,21 @@ def organization_unshare(self, item): item["id"], organization_ids, sharing_direct_container ) + def element_operation_delete(self, item): + # If data is stix, just use the generic stix function for deletion + if item["type"] in STIX_OBJECTS: + item["force_delete"] = item["opencti_operation"] == "delete-force" + self.opencti.stix.delete(id=item["id"]) + else: + # Element is not knowledge we need to use the right api + stix_helper = self.get_internal_helper().get(item["type"]) + if stix_helper and hasattr(stix_helper, "delete"): + stix_helper.delete(id=item["id"]) + else: + raise ValueError( + "Delete operation or no stix helper", {"type": item["type"]} + ) + def import_item( self, item, @@ -2499,15 +2531,11 @@ def import_item( item["opencti_operation"] == "delete" or item["opencti_operation"] == "delete-force" ): - delete_id = item["id"] - force_delete = item["opencti_operation"] == "delete-force" - self.opencti.stix.delete(id=delete_id, force_delete=force_delete) - elif item["opencti_operation"] == "delete-draft": - self.opencti.draft.delete(item["id"]) + self.element_operation_delete(item=item) elif item["opencti_operation"] == "revert-draft": self.opencti.stix_core_object.remove_from_draft(id=item["id"]) elif item["opencti_operation"] == "restore": - self.opencti.trash.delete_operation_restore(item["id"]) + self.opencti.trash.restore(item["id"]) elif item["opencti_operation"] == "merge": target_id = item["merge_target_id"] source_ids = item["merge_source_ids"] diff --git a/pycti/utils/opencti_stix2_splitter.py b/pycti/utils/opencti_stix2_splitter.py index bf6890df6..30521417f 100644 --- a/pycti/utils/opencti_stix2_splitter.py +++ b/pycti/utils/opencti_stix2_splitter.py @@ -10,6 +10,7 @@ ) from pycti.utils.opencti_stix2_utils import ( STIX_CYBER_OBSERVABLE_MAPPING, + SUPPORTED_INTERNAL_OBJECTS, SUPPORTED_STIX_ENTITY_OBJECTS, ) @@ -17,9 +18,9 @@ supported_types = ( SUPPORTED_STIX_ENTITY_OBJECTS # entities + + SUPPORTED_INTERNAL_OBJECTS # internals + list(STIX_CYBER_OBSERVABLE_MAPPING.keys()) # observables + ["relationship", "sighting"] # relationships - + ["deleteoperation"] # OpenCTI specific type for operation ) diff --git a/pycti/utils/opencti_stix2_utils.py b/pycti/utils/opencti_stix2_utils.py index defae5e5f..32e56e346 100644 --- a/pycti/utils/opencti_stix2_utils.py +++ b/pycti/utils/opencti_stix2_utils.py @@ -2,6 +2,21 @@ from stix2 import EqualityComparisonExpression, ObjectPath, ObservationExpression +SUPPORTED_INTERNAL_OBJECTS = [ + "user", + "group", + "capability", + "role", + "settings", + "work", + "trash", + "draftworkspace", + "playbook", + "deleteoperation", + "workspace", + "publicdashboard", +] + SUPPORTED_STIX_ENTITY_OBJECTS = [ "attack-pattern", "campaign", @@ -83,6 +98,12 @@ "persona": "Persona", } +STIX_OBJECTS = ( + SUPPORTED_STIX_ENTITY_OBJECTS # entities + + list(STIX_CYBER_OBSERVABLE_MAPPING.keys()) # observables + + ["relationship", "sighting"] # relationships +) + PATTERN_MAPPING = { "Autonomous-System": ["number"], "Directory": ["path"], From 96e16b6adf8080ec0739da4965d7c0d136d36bbd Mon Sep 17 00:00:00 2001 From: Julien Richard Date: Fri, 14 Feb 2025 22:33:38 +0100 Subject: [PATCH 09/11] [client] Adapt is_compatible compute --- pycti/utils/opencti_stix2_splitter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pycti/utils/opencti_stix2_splitter.py b/pycti/utils/opencti_stix2_splitter.py index 30521417f..dffa533be 100644 --- a/pycti/utils/opencti_stix2_splitter.py +++ b/pycti/utils/opencti_stix2_splitter.py @@ -178,11 +178,11 @@ def enlist_element( # Put in cache if self.cache_index.get(item_id) is None: # enlist only if compatible - if item["type"] == "relationship" and item.get("opencti_operation") is None: + if item["type"] == "relationship": is_compatible = ( item["source_ref"] is not None and item["target_ref"] is not None ) - elif item["type"] == "sighting" and item.get("opencti_operation") is None: + elif item["type"] == "sighting": is_compatible = ( item["sighting_of_ref"] is not None and len(item["where_sighted_refs"]) > 0 From 9cba0539d79299d4671429c02cb2ba229b9201cd Mon Sep 17 00:00:00 2001 From: Julien Richard Date: Mon, 24 Feb 2025 12:37:01 +0100 Subject: [PATCH 10/11] [client] Add ask_enrichments and change operation case --- pycti/entities/opencti_stix_core_object.py | 34 ++++++++++++++++++++-- pycti/utils/opencti_stix2.py | 6 ++-- 2 files changed, 34 insertions(+), 6 deletions(-) diff --git a/pycti/entities/opencti_stix_core_object.py b/pycti/entities/opencti_stix_core_object.py index b88511993..4c8b3d1c8 100644 --- a/pycti/entities/opencti_stix_core_object.py +++ b/pycti/entities/opencti_stix_core_object.py @@ -1784,6 +1784,34 @@ def clear_access_restriction(self, **kwargs): self.opencti.app_logger.error("[stix_core_object] Missing parameters: id") return None + """ + Ask enrichment with single connector + + :param element_id: the Stix-Core-Object id + :param connector_id the connector + :return void + """ + + def ask_enrichment(self, **kwargs): + element_id = kwargs.get("element_id", None) + connector_id = kwargs.get("connector_id", None) + query = """ + mutation StixCoreObjectEdit($id: ID!, $connectorId: ID!) { + stixCoreObjectEdit(id: $id) { + askEnrichment(connectorId: $connectorId) { + id + } + } + } + """ + self.opencti.query( + query, + { + "id": element_id, + "connectorId": connector_id, + }, + ) + """ Ask enrichment with multiple connectors @@ -1792,13 +1820,13 @@ def clear_access_restriction(self, **kwargs): :return void """ - def ask_enrichment(self, **kwargs): + def ask_enrichments(self, **kwargs): element_id = kwargs.get("element_id", None) connector_ids = kwargs.get("connector_ids", None) query = """ - mutation StixCoreObjectEdit($id: ID!, $connectorId: [ID!]!) { + mutation StixCoreObjectEdit($id: ID!, $connectorIds: [ID!]!) { stixCoreObjectEdit(id: $id) { - askEnrichment(connectorId: $connectorId) { + askEnrichments(connectorIds: $connectorIds) { id } } diff --git a/pycti/utils/opencti_stix2.py b/pycti/utils/opencti_stix2.py index fb97e3e60..21656bf94 100644 --- a/pycti/utils/opencti_stix2.py +++ b/pycti/utils/opencti_stix2.py @@ -2529,10 +2529,10 @@ def import_item( if "opencti_operation" in item: if ( item["opencti_operation"] == "delete" - or item["opencti_operation"] == "delete-force" + or item["opencti_operation"] == "delete_force" ): self.element_operation_delete(item=item) - elif item["opencti_operation"] == "revert-draft": + elif item["opencti_operation"] == "revert_draft": self.opencti.stix_core_object.remove_from_draft(id=item["id"]) elif item["opencti_operation"] == "restore": self.opencti.trash.restore(item["id"]) @@ -2558,7 +2558,7 @@ def import_item( ) elif item["opencti_operation"] == "enrichment": connector_ids = item["connector_ids"] - self.opencti.stix_core_object.ask_enrichment( + self.opencti.stix_core_object.ask_enrichments( element_id=item["id"], connector_ids=connector_ids ) else: From e2addc0187af0ae35202a02ba70ae5d15d8d6322 Mon Sep 17 00:00:00 2001 From: Julien Richard Date: Mon, 24 Feb 2025 22:48:13 +0100 Subject: [PATCH 11/11] [client] Upgrade after 6.5.3 --- pycti/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pycti/__init__.py b/pycti/__init__.py index 3fe1201ed..bb260619c 100644 --- a/pycti/__init__.py +++ b/pycti/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -__version__ = "6.5.2" +__version__ = "6.5.3" from .api.opencti_api_client import OpenCTIApiClient from .api.opencti_api_connector import OpenCTIApiConnector