From 9df892639f8eec77cead1f4e3f3d6f18e1e08db6 Mon Sep 17 00:00:00 2001 From: Julien Richard Date: Sat, 25 Jan 2025 19:40:02 +0100 Subject: [PATCH 01/13] [client] Introduce background tasks support --- pycti/api/opencti_api_client.py | 8 + pycti/api/opencti_api_draft.py | 19 ++ pycti/api/opencti_api_playbook.py | 14 ++ pycti/api/opencti_api_public_dashboard.py | 19 ++ pycti/api/opencti_api_trash.py | 41 ++++ pycti/api/opencti_api_work.py | 14 ++ pycti/api/opencti_api_workspace.py | 19 ++ pycti/entities/opencti_group.py | 6 +- pycti/entities/opencti_stix.py | 7 +- pycti/entities/opencti_stix_core_object.py | 237 +++++++++++++++++++++ pycti/utils/constants.py | 2 +- pycti/utils/opencti_stix2.py | 92 +++++++- pycti/utils/opencti_stix2_splitter.py | 2 + pycti/utils/opencti_stix2_utils.py | 21 ++ 14 files changed, 491 insertions(+), 10 deletions(-) create mode 100644 pycti/api/opencti_api_draft.py create mode 100644 pycti/api/opencti_api_public_dashboard.py create mode 100644 pycti/api/opencti_api_trash.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 081f2c14e..0c34efceb 100644 --- a/pycti/api/opencti_api_client.py +++ b/pycti/api/opencti_api_client.py @@ -11,8 +11,12 @@ from pycti import __version__ from pycti.api.opencti_api_connector import OpenCTIApiConnector from pycti.api.opencti_api_pir import OpenCTIApiPir +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 @@ -168,6 +172,10 @@ def __init__( self.session = requests.session() # Define the dependencies 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 new file mode 100644 index 000000000..9869c9619 --- /dev/null +++ b/pycti/api/opencti_api_draft.py @@ -0,0 +1,19 @@ +class OpenCTIApiDraft: + """OpenCTIApiDraft""" + + def __init__(self, api): + self.api = api + + def delete(self, **kwargs): + id = kwargs.get("id", None) + query = """ + mutation DraftWorkspaceDelete($id: ID!) { + draftWorkspaceDelete(id: $id) + } + """ + self.api.query( + query, + { + "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 new file mode 100644 index 000000000..ee90c9bdd --- /dev/null +++ b/pycti/api/opencti_api_trash.py @@ -0,0 +1,41 @@ +class OpenCTIApiTrash: + """OpenCTIApiTrash""" + + def __init__(self, api): + self.api = api + + def restore(self, operation_id: str): + query = """ + mutation DeleteOperationRestore($id: ID!) { + deleteOperationRestore(id: $id) + } + """ + self.api.query( + query, + { + "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 5d11a0fbd..193f38c05 100644 --- a/pycti/api/opencti_api_work.py +++ b/pycti/api/opencti_api_work.py @@ -131,6 +131,20 @@ def delete_work(self, work_id: str): work = self.api.query(query, {"workId": work_id}, True) 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 05ed793f7..1d966b934 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.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 a2b17e369..afba20a89 100644 --- a/pycti/entities/opencti_stix_core_object.py +++ b/pycti/entities/opencti_stix_core_object.py @@ -1680,6 +1680,220 @@ 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 + + """ + 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 + + """ + 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 { + id + } + } + } + """ + self.opencti.query( + query, + { + "id": element_id, + }, + ) + else: + 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 + + :param element_id: the Stix-Core-Object id + :param connector_ids the connectors + :return void + """ + + def ask_enrichments(self, **kwargs): + element_id = kwargs.get("element_id", None) + connector_ids = kwargs.get("connector_ids", None) + query = """ + mutation StixCoreObjectEdit($id: ID!, $connectorIds: [ID!]!) { + stixCoreObjectEdit(id: $id) { + askEnrichments(connectorIds: $connectorIds) { + id + } + } + } + """ + self.opencti.query( + query, + { + "id": element_id, + "connectorId": connector_ids, + }, + ) + + """ + 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, sharing_direct_container): + query = """ + mutation StixCoreObjectEdit($id: ID!, $organizationId: [ID!]!, $directContainerSharing: Boolean) { + stixCoreObjectEdit(id: $id) { + restrictionOrganizationAdd(organizationId: $organizationId, directContainerSharing: $directContainerSharing) { + id + } + } + } + """ + self.opencti.query( + query, + { + "id": entity_id, + "organizationId": organization_ids, + "directContainerSharing": sharing_direct_container, + }, + ) + + """ + 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, sharing_direct_container + ): + query = """ + mutation StixCoreObjectEdit($id: ID!, $organizationId: [ID!]!, $directContainerSharing: Boolean) { + stixCoreObjectEdit(id: $id) { + restrictionOrganizationDelete(organizationId: $organizationId, directContainerSharing: $directContainerSharing) { + id + } + } + } + """ + self.opencti.query( + query, + { + "id": entity_id, + "organizationId": organization_ids, + "directContainerSharing": sharing_direct_container, + }, + ) + """ Delete a Stix-Core-Object object @@ -1702,3 +1916,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 diff --git a/pycti/utils/constants.py b/pycti/utils/constants.py index 85d36e05d..6fbdadb96 100644 --- a/pycti/utils/constants.py +++ b/pycti/utils/constants.py @@ -48,7 +48,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 45483787d..4b1f89f98 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 @@ -909,6 +910,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"]) @@ -2474,11 +2491,54 @@ 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 rules_rescan(self, item): + self.opencti.stix_core_object.rules_rescan(element_id=item["id"]) + + def organization_share(self, item): + 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["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 element_operation_delete(self, item, operation): + # If data is stix, just use the generic stix function for deletion + if item["type"] in STIX_OBJECTS: + item["force_delete"] = 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 apply_opencti_operation(self, item, operation): - if operation == "delete": - delete_id = item["id"] - self.opencti.stix.delete(id=delete_id) - elif operation == "merge": + if operation == "delete" or operation == "delete_force": + self.element_operation_delete(item=item, operation=operation) + 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"]) + elif item["opencti_operation"] == "merge": target_id = item["merge_target_id"] source_ids = item["merge_source_ids"] self.opencti.stix.merge(id=target_id, object_ids=source_ids) @@ -2492,8 +2552,30 @@ def apply_opencti_operation(self, item, operation): id = item["id"] input = item["input"] self.opencti.pir.pir_unflag_element(id=id, input=input) + elif operation == "rule_apply": + self.rule_apply(item=item) + elif operation == "rule_clear": + self.rule_clear(item=item) + elif operation == "rules_rescan": + self.rules_rescan(item=item) + elif operation == "share": + self.organization_share(item=item) + elif operation == "unshare": + self.organization_unshare(item=item) + elif 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_enrichments( + element_id=item["id"], connector_ids=connector_ids + ) else: - raise ValueError("Not supported opencti_operation") + raise ValueError( + "Not supported opencti_operation", + {"operation": operation}, + ) def import_item( self, diff --git a/pycti/utils/opencti_stix2_splitter.py b/pycti/utils/opencti_stix2_splitter.py index 5cc074c79..1fe300c1f 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,6 +18,7 @@ supported_types = ( SUPPORTED_STIX_ENTITY_OBJECTS # entities + + SUPPORTED_INTERNAL_OBJECTS # internals + list(STIX_CYBER_OBSERVABLE_MAPPING.keys()) # observables + ["relationship", "sighting"] # relationships + ["pir"] 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 f58089b833b743dfa234844368175e494f6c8409 Mon Sep 17 00:00:00 2001 From: Jeremy Cloarec Date: Fri, 25 Apr 2025 16:52:06 +0200 Subject: [PATCH 02/13] [client] fix force delete args --- pycti/utils/opencti_stix2.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pycti/utils/opencti_stix2.py b/pycti/utils/opencti_stix2.py index 4b1f89f98..ca531206f 100644 --- a/pycti/utils/opencti_stix2.py +++ b/pycti/utils/opencti_stix2.py @@ -2519,8 +2519,8 @@ def organization_unshare(self, item): def element_operation_delete(self, item, operation): # If data is stix, just use the generic stix function for deletion if item["type"] in STIX_OBJECTS: - item["force_delete"] = operation == "delete-force" - self.opencti.stix.delete(id=item["id"]) + force_delete = operation == "delete-force" + self.opencti.stix.delete(id=item["id"], force_delete=force_delete) else: # Element is not knowledge we need to use the right api stix_helper = self.get_internal_helper().get(item["type"]) From 23b2da95582902a6e34ad1cb7100b554faade134 Mon Sep 17 00:00:00 2001 From: Jeremy Cloarec Date: Mon, 19 May 2025 12:08:41 +0200 Subject: [PATCH 03/13] [client] now handle operations in extensions --- pycti/utils/opencti_stix2.py | 54 ++++++++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/pycti/utils/opencti_stix2.py b/pycti/utils/opencti_stix2.py index ca531206f..cab8b0e18 100644 --- a/pycti/utils/opencti_stix2.py +++ b/pycti/utils/opencti_stix2.py @@ -2492,26 +2492,50 @@ def apply_patch(self, item): self.apply_patch_files(item) def rule_apply(self, item): - rule_id = item["opencti_rule"] + rule_id = self.opencti.get_attribute_in_extension( + "opencti_rule", item + ) + if rule_id is None: + 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"] + rule_id = self.opencti.get_attribute_in_extension( + "opencti_rule", item + ) + if rule_id is None: + 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["sharing_organization_ids"] - sharing_direct_container = item["sharing_direct_container"] + organization_ids = self.opencti.get_attribute_in_extension( + "sharing_organization_ids", item + ) + if organization_ids is None: + organization_ids = item["sharing_organization_ids"] + sharing_direct_container = self.opencti.get_attribute_in_extension( + "sharing_direct_container", item + ) + if sharing_direct_container is None: + 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["sharing_organization_ids"] - sharing_direct_container = item["sharing_direct_container"] + organization_ids = self.opencti.get_attribute_in_extension( + "sharing_organization_ids", item + ) + if organization_ids is None: + organization_ids = item["sharing_organization_ids"] + sharing_direct_container = self.opencti.get_attribute_in_extension( + "sharing_direct_container", item + ) + if sharing_direct_container is None: + sharing_direct_container = item["sharing_direct_container"] self.opencti.stix_core_object.organization_unshare( item["id"], organization_ids, sharing_direct_container ) @@ -2539,8 +2563,16 @@ def apply_opencti_operation(self, item, operation): elif item["opencti_operation"] == "restore": self.opencti.trash.restore(item["id"]) elif item["opencti_operation"] == "merge": - target_id = item["merge_target_id"] - source_ids = item["merge_source_ids"] + target_id = self.opencti.get_attribute_in_extension( + "merge_target_id", item + ) + if target_id is None: + target_id = item["merge_target_id"] + source_ids = self.opencti.get_attribute_in_extension( + "merge_source_ids", item + ) + if source_ids is None: + source_ids = item["merge_source_ids"] self.opencti.stix.merge(id=target_id, object_ids=source_ids) elif operation == "patch": self.apply_patch(item=item) @@ -2567,7 +2599,11 @@ def apply_opencti_operation(self, item, operation): element_id=item["id"] ) elif item["opencti_operation"] == "enrichment": - connector_ids = item["connector_ids"] + connector_ids = self.opencti.get_attribute_in_extension( + "connector_ids", item + ) + if connector_ids is None: + connector_ids = item["connector_ids"] self.opencti.stix_core_object.ask_enrichments( element_id=item["id"], connector_ids=connector_ids ) From 95543c3a0bf20d48a2f6a74d0d953f70da573b21 Mon Sep 17 00:00:00 2001 From: Jeremy Cloarec Date: Mon, 19 May 2025 12:10:52 +0200 Subject: [PATCH 04/13] [client] style --- pycti/utils/opencti_stix2.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/pycti/utils/opencti_stix2.py b/pycti/utils/opencti_stix2.py index cab8b0e18..0afb6b2d6 100644 --- a/pycti/utils/opencti_stix2.py +++ b/pycti/utils/opencti_stix2.py @@ -2492,17 +2492,13 @@ def apply_patch(self, item): self.apply_patch_files(item) def rule_apply(self, item): - rule_id = self.opencti.get_attribute_in_extension( - "opencti_rule", item - ) + rule_id = self.opencti.get_attribute_in_extension("opencti_rule", item) if rule_id is None: 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 = self.opencti.get_attribute_in_extension( - "opencti_rule", item - ) + rule_id = self.opencti.get_attribute_in_extension("opencti_rule", item) if rule_id is None: rule_id = item["opencti_rule"] self.opencti.stix_core_object.rule_clear(element_id=item["id"], rule_id=rule_id) @@ -2563,9 +2559,7 @@ def apply_opencti_operation(self, item, operation): elif item["opencti_operation"] == "restore": self.opencti.trash.restore(item["id"]) elif item["opencti_operation"] == "merge": - target_id = self.opencti.get_attribute_in_extension( - "merge_target_id", item - ) + target_id = self.opencti.get_attribute_in_extension("merge_target_id", item) if target_id is None: target_id = item["merge_target_id"] source_ids = self.opencti.get_attribute_in_extension( From c3cce95645bf4df5bf42f8205d0e432262cf1190 Mon Sep 17 00:00:00 2001 From: Jeremy Cloarec Date: Mon, 19 May 2025 12:20:44 +0200 Subject: [PATCH 05/13] [client] operation fix --- pycti/utils/opencti_stix2.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pycti/utils/opencti_stix2.py b/pycti/utils/opencti_stix2.py index 0afb6b2d6..4d0d3b6eb 100644 --- a/pycti/utils/opencti_stix2.py +++ b/pycti/utils/opencti_stix2.py @@ -2554,11 +2554,11 @@ def element_operation_delete(self, item, operation): def apply_opencti_operation(self, item, operation): if operation == "delete" or operation == "delete_force": self.element_operation_delete(item=item, operation=operation) - elif item["opencti_operation"] == "revert_draft": + elif operation == "revert_draft": self.opencti.stix_core_object.remove_from_draft(id=item["id"]) - elif item["opencti_operation"] == "restore": + elif operation == "restore": self.opencti.trash.restore(item["id"]) - elif item["opencti_operation"] == "merge": + elif operation == "merge": target_id = self.opencti.get_attribute_in_extension("merge_target_id", item) if target_id is None: target_id = item["merge_target_id"] @@ -2592,7 +2592,7 @@ def apply_opencti_operation(self, item, operation): self.opencti.stix_core_object.clear_access_restriction( element_id=item["id"] ) - elif item["opencti_operation"] == "enrichment": + elif operation == "enrichment": connector_ids = self.opencti.get_attribute_in_extension( "connector_ids", item ) From 6e15181409b79b9396d5137fb0347ca02a2a0d54 Mon Sep 17 00:00:00 2001 From: Jeremy Cloarec Date: Mon, 19 May 2025 14:33:33 +0200 Subject: [PATCH 06/13] [client] handle indicator field patch --- pycti/entities/opencti_indicator.py | 40 +++++++++++++++++++++++++++++ pycti/utils/opencti_stix2.py | 4 +++ 2 files changed, 44 insertions(+) diff --git a/pycti/entities/opencti_indicator.py b/pycti/entities/opencti_indicator.py index c66399143..c4355052c 100644 --- a/pycti/entities/opencti_indicator.py +++ b/pycti/entities/opencti_indicator.py @@ -301,6 +301,46 @@ def create(self, **kwargs): "name or pattern or pattern_type or x_opencti_main_observable_type" ) + +""" + Update an Indicator object field + + :param id: the Indicator id + :param input: the input of the field + """ + + +def update_field(self, **kwargs): + id = kwargs.get("id", None) + input = kwargs.get("input", None) + if id is not None and input is not None: + self.opencti.app_logger.info("Updating Indicator", {"id": id}) + query = """ + mutation IndicatorFieldPatch($id: ID!, $input: [EditInput]!) { + indicatorFieldPatch(id: $id, input: $input) { + id + standard_id + entity_type + } + } + } + """ + result = self.opencti.query( + query, + { + "id": id, + "input": input, + }, + ) + return self.opencti.process_multiple_fields( + result["data"]["indicatorFieldPatch"] + ) + else: + self.opencti.app_logger.error( + "[opencti_stix_domain_object] Missing parameters: id and input" + ) + return None + def add_stix_cyber_observable(self, **kwargs): """ Add a Stix-Cyber-Observable object to Indicator object (based-on) diff --git a/pycti/utils/opencti_stix2.py b/pycti/utils/opencti_stix2.py index 4d0d3b6eb..2eefa15e8 100644 --- a/pycti/utils/opencti_stix2.py +++ b/pycti/utils/opencti_stix2.py @@ -2485,6 +2485,10 @@ def apply_patch(self, item): self.opencti.external_reference.update_field( id=item_id, input=field_patch_without_files ) + elif item["type"] == "indicator": + self.opencti.indicator.update_field( + id=item_id, input=field_patch_without_files + ) else: self.opencti.stix_domain_object.update_field( id=item_id, input=field_patch_without_files From 403277e90b44e12fe951165bd2f09e1e509bc9ac Mon Sep 17 00:00:00 2001 From: Jeremy Cloarec Date: Mon, 19 May 2025 15:22:11 +0200 Subject: [PATCH 07/13] [client] handle indicator field patch --- pycti/entities/opencti_indicator.py | 60 ++++++++++++++--------------- 1 file changed, 29 insertions(+), 31 deletions(-) diff --git a/pycti/entities/opencti_indicator.py b/pycti/entities/opencti_indicator.py index c4355052c..f69a8f3d2 100644 --- a/pycti/entities/opencti_indicator.py +++ b/pycti/entities/opencti_indicator.py @@ -302,44 +302,42 @@ def create(self, **kwargs): ) -""" + """ Update an Indicator object field :param id: the Indicator id :param input: the input of the field """ - - -def update_field(self, **kwargs): - id = kwargs.get("id", None) - input = kwargs.get("input", None) - if id is not None and input is not None: - self.opencti.app_logger.info("Updating Indicator", {"id": id}) - query = """ - mutation IndicatorFieldPatch($id: ID!, $input: [EditInput]!) { - indicatorFieldPatch(id: $id, input: $input) { - id - standard_id - entity_type + def update_field(self, **kwargs): + id = kwargs.get("id", None) + input = kwargs.get("input", None) + if id is not None and input is not None: + self.opencti.app_logger.info("Updating Indicator", {"id": id}) + query = """ + mutation IndicatorFieldPatch($id: ID!, $input: [EditInput]!) { + indicatorFieldPatch(id: $id, input: $input) { + id + standard_id + entity_type + } } } - } - """ - result = self.opencti.query( - query, - { - "id": id, - "input": input, - }, - ) - return self.opencti.process_multiple_fields( - result["data"]["indicatorFieldPatch"] - ) - else: - self.opencti.app_logger.error( - "[opencti_stix_domain_object] Missing parameters: id and input" - ) - return None + """ + result = self.opencti.query( + query, + { + "id": id, + "input": input, + }, + ) + return self.opencti.process_multiple_fields( + result["data"]["indicatorFieldPatch"] + ) + else: + self.opencti.app_logger.error( + "[opencti_stix_domain_object] Missing parameters: id and input" + ) + return None def add_stix_cyber_observable(self, **kwargs): """ From 50c9932ef6adf05cde18b5350717e6786ef6e46c Mon Sep 17 00:00:00 2001 From: Jeremy Cloarec Date: Mon, 19 May 2025 15:26:28 +0200 Subject: [PATCH 08/13] [client] handle indicator field patch --- pycti/entities/opencti_indicator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pycti/entities/opencti_indicator.py b/pycti/entities/opencti_indicator.py index f69a8f3d2..38f8c12a0 100644 --- a/pycti/entities/opencti_indicator.py +++ b/pycti/entities/opencti_indicator.py @@ -301,13 +301,13 @@ def create(self, **kwargs): "name or pattern or pattern_type or x_opencti_main_observable_type" ) - """ Update an Indicator object field :param id: the Indicator id :param input: the input of the field """ + def update_field(self, **kwargs): id = kwargs.get("id", None) input = kwargs.get("input", None) From 68fef9ff29b85d113660ef2fd5a2b27516a27eb9 Mon Sep 17 00:00:00 2001 From: Jeremy Cloarec Date: Mon, 19 May 2025 15:44:37 +0200 Subject: [PATCH 09/13] [client] indicator update fix --- pycti/entities/opencti_indicator.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pycti/entities/opencti_indicator.py b/pycti/entities/opencti_indicator.py index 38f8c12a0..221f06967 100644 --- a/pycti/entities/opencti_indicator.py +++ b/pycti/entities/opencti_indicator.py @@ -316,10 +316,9 @@ def update_field(self, **kwargs): query = """ mutation IndicatorFieldPatch($id: ID!, $input: [EditInput]!) { indicatorFieldPatch(id: $id, input: $input) { - id - standard_id - entity_type - } + id + standard_id + entity_type } } """ From 79171b3a4b8db1cc2cadfe92341fd519a8076adb Mon Sep 17 00:00:00 2001 From: Jeremy Cloarec Date: Mon, 19 May 2025 15:50:50 +0200 Subject: [PATCH 10/13] [client] indicator update fix --- pycti/entities/opencti_indicator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pycti/entities/opencti_indicator.py b/pycti/entities/opencti_indicator.py index 221f06967..07c5353a3 100644 --- a/pycti/entities/opencti_indicator.py +++ b/pycti/entities/opencti_indicator.py @@ -314,7 +314,7 @@ def update_field(self, **kwargs): if id is not None and input is not None: self.opencti.app_logger.info("Updating Indicator", {"id": id}) query = """ - mutation IndicatorFieldPatch($id: ID!, $input: [EditInput]!) { + mutation IndicatorFieldPatch($id: ID!, $input: [EditInput!]!) { indicatorFieldPatch(id: $id, input: $input) { id standard_id From 09f9b6bde9acb750d25a35d15876c61477aa38af Mon Sep 17 00:00:00 2001 From: Jeremy Cloarec Date: Tue, 27 May 2025 17:20:32 +0200 Subject: [PATCH 11/13] [client] fix --- pycti/api/opencti_api_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pycti/api/opencti_api_client.py b/pycti/api/opencti_api_client.py index 0c34efceb..00170dcdb 100644 --- a/pycti/api/opencti_api_client.py +++ b/pycti/api/opencti_api_client.py @@ -10,8 +10,8 @@ from pycti import __version__ from pycti.api.opencti_api_connector import OpenCTIApiConnector -from pycti.api.opencti_api_pir import OpenCTIApiPir from pycti.api.opencti_api_draft import OpenCTIApiDraft +from pycti.api.opencti_api_pir import OpenCTIApiPir 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 ca04399aca3923b56bbf9efdb0d9e8fb086d9ba9 Mon Sep 17 00:00:00 2001 From: Jeremy Cloarec Date: Wed, 28 May 2025 17:28:46 +0200 Subject: [PATCH 12/13] [client] PR review --- pycti/api/opencti_api_playbook.py | 28 +++++++++++++--------- pycti/api/opencti_api_public_dashboard.py | 28 +++++++++++++--------- pycti/api/opencti_api_trash.py | 9 +++---- pycti/api/opencti_api_work.py | 5 ++++ pycti/api/opencti_api_workspace.py | 5 ++++ pycti/entities/opencti_group.py | 4 +++- pycti/entities/opencti_indicator.py | 2 +- pycti/entities/opencti_stix_core_object.py | 20 ++++++++++++---- pycti/utils/opencti_stix2.py | 2 +- 9 files changed, 69 insertions(+), 34 deletions(-) diff --git a/pycti/api/opencti_api_playbook.py b/pycti/api/opencti_api_playbook.py index 8b6c71ec5..62a24d515 100644 --- a/pycti/api/opencti_api_playbook.py +++ b/pycti/api/opencti_api_playbook.py @@ -35,14 +35,20 @@ def playbook_step_execution(self, playbook: dict, bundle: str): def delete(self, **kwargs): id = kwargs.get("id", None) - query = """ - mutation PlaybookDelete($id: ID!) { - playbookDelete(id: $id) - } - """ - self.api.query( - query, - { - "id": id, - }, - ) + if id is not None: + query = """ + mutation PlaybookDelete($id: ID!) { + playbookDelete(id: $id) + } + """ + self.api.query( + query, + { + "id": id, + }, + ) + else: + self.opencti.app_logger.error( + "[stix_playbook] Cant delete playbook, missing parameters: id" + ) + return None diff --git a/pycti/api/opencti_api_public_dashboard.py b/pycti/api/opencti_api_public_dashboard.py index 767b5379a..f52c6a3cc 100644 --- a/pycti/api/opencti_api_public_dashboard.py +++ b/pycti/api/opencti_api_public_dashboard.py @@ -6,14 +6,20 @@ def __init__(self, api): def delete(self, **kwargs): id = kwargs.get("id", None) - query = """ - mutation PublicDashboardDelete($id: ID!) { - publicDashboardDelete(id: $id) - } - """ - self.api.query( - query, - { - "id": id, - }, - ) + if id is not None: + query = """ + mutation PublicDashboardDelete($id: ID!) { + publicDashboardDelete(id: $id) + } + """ + self.api.query( + query, + { + "id": id, + }, + ) + else: + self.opencti.app_logger.error( + "[stix_public_dashboard] Cant delete public dashboard, missing parameters: id" + ) + return None diff --git a/pycti/api/opencti_api_trash.py b/pycti/api/opencti_api_trash.py index ee90c9bdd..606132433 100644 --- a/pycti/api/opencti_api_trash.py +++ b/pycti/api/opencti_api_trash.py @@ -18,16 +18,17 @@ def restore(self, operation_id: str): ) def delete(self, **kwargs): - """Delete a role given its ID + """Delete a trash item given its ID - :param id: ID for the role on the platform. + :param id: ID for the delete operation 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") + self.api.admin_logger.error( + "[opencti_trash] Cant confirm delete, missing parameter: id" + ) return None - query = """ mutation DeleteOperationConfirm($id: ID!) { deleteOperationConfirm(id: $id) { diff --git a/pycti/api/opencti_api_work.py b/pycti/api/opencti_api_work.py index 193f38c05..d9f9b9e6f 100644 --- a/pycti/api/opencti_api_work.py +++ b/pycti/api/opencti_api_work.py @@ -133,6 +133,11 @@ def delete_work(self, work_id: str): def delete(self, **kwargs): id = kwargs.get("id", None) + if id is None: + self.opencti.admin_logger.error( + "[opencti_work] Cant delete work, missing parameter: id" + ) + return None query = """ mutation ConnectorWorksMutation($workId: ID!) { workEdit(id: $workId) { diff --git a/pycti/api/opencti_api_workspace.py b/pycti/api/opencti_api_workspace.py index a2161b900..2fcee4277 100644 --- a/pycti/api/opencti_api_workspace.py +++ b/pycti/api/opencti_api_workspace.py @@ -6,6 +6,11 @@ def __init__(self, api): def delete(self, **kwargs): id = kwargs.get("id", None) + if id is None: + self.api.admin_logger.error( + "[opencti_workspace] Cant delete workspace, missing parameter: id" + ) + return None query = """ mutation WorkspaceDelete($id: ID!) { workspaceDelete(id: $id) diff --git a/pycti/entities/opencti_group.py b/pycti/entities/opencti_group.py index 1d966b934..bfbd8dbf6 100644 --- a/pycti/entities/opencti_group.py +++ b/pycti/entities/opencti_group.py @@ -323,7 +323,9 @@ def delete(self, **kwargs): """ id = kwargs.get("id", None) if id is None: - self.opencti.admin_logger.error("[opencti_user] Missing parameter: id") + self.opencti.admin_logger.error( + "[opencti_group] Cant delete group, missing parameter: id" + ) return None self.opencti.admin_logger.info("Deleting group", {"id": id}) query = """ diff --git a/pycti/entities/opencti_indicator.py b/pycti/entities/opencti_indicator.py index 07c5353a3..0f3b26392 100644 --- a/pycti/entities/opencti_indicator.py +++ b/pycti/entities/opencti_indicator.py @@ -334,7 +334,7 @@ def update_field(self, **kwargs): ) else: self.opencti.app_logger.error( - "[opencti_stix_domain_object] Missing parameters: id and input" + "[opencti_stix_domain_object] Cant update indicator field, missing parameters: id and input" ) return None diff --git a/pycti/entities/opencti_stix_core_object.py b/pycti/entities/opencti_stix_core_object.py index afba20a89..31a600650 100644 --- a/pycti/entities/opencti_stix_core_object.py +++ b/pycti/entities/opencti_stix_core_object.py @@ -1702,7 +1702,9 @@ def rule_apply(self, **kwargs): """ self.opencti.query(query, {"elementId": element_id, "ruleId": rule_id}) else: - self.opencti.app_logger.error("[stix_core_object] Missing parameters: id") + self.opencti.app_logger.error( + "[stix_core_object] Cant apply rule, missing parameters: id" + ) return None """ @@ -1727,7 +1729,9 @@ def rule_clear(self, **kwargs): """ self.opencti.query(query, {"elementId": element_id, "ruleId": rule_id}) else: - self.opencti.app_logger.error("[stix_core_object] Missing parameters: id") + self.opencti.app_logger.error( + "[stix_core_object] Cant clear rule, missing parameters: id" + ) return None """ @@ -1750,7 +1754,9 @@ def rules_rescan(self, **kwargs): """ self.opencti.query(query, {"elementId": element_id}) else: - self.opencti.app_logger.error("[stix_core_object] Missing parameters: id") + self.opencti.app_logger.error( + "[stix_core_object] Cant rescan rule, missing parameters: id" + ) return None """ @@ -1779,7 +1785,9 @@ def clear_access_restriction(self, **kwargs): }, ) else: - self.opencti.app_logger.error("[stix_core_object] Missing parameters: id") + self.opencti.app_logger.error( + "[stix_core_object] Cant clear access restriction, missing parameters: id" + ) return None """ @@ -1937,5 +1945,7 @@ def remove_from_draft(self, **kwargs): """ self.opencti.query(query, {"id": id}) else: - self.opencti.app_logger.error("[stix_core_object] Missing parameters: id") + self.opencti.app_logger.error( + "[stix_core_object] Cant remove from draft, missing parameters: id" + ) return None diff --git a/pycti/utils/opencti_stix2.py b/pycti/utils/opencti_stix2.py index 2eefa15e8..d46333420 100644 --- a/pycti/utils/opencti_stix2.py +++ b/pycti/utils/opencti_stix2.py @@ -2543,7 +2543,7 @@ def organization_unshare(self, item): def element_operation_delete(self, item, operation): # If data is stix, just use the generic stix function for deletion if item["type"] in STIX_OBJECTS: - force_delete = operation == "delete-force" + force_delete = operation == "delete_force" self.opencti.stix.delete(id=item["id"], force_delete=force_delete) else: # Element is not knowledge we need to use the right api From 62681814c00c12f6b9b40ece66318d1897992945 Mon Sep 17 00:00:00 2001 From: Jeremy Cloarec Date: Tue, 3 Jun 2025 15:42:27 +0200 Subject: [PATCH 13/13] [client] fix trash delete mutation --- pycti/api/opencti_api_trash.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pycti/api/opencti_api_trash.py b/pycti/api/opencti_api_trash.py index 606132433..eaf835f20 100644 --- a/pycti/api/opencti_api_trash.py +++ b/pycti/api/opencti_api_trash.py @@ -31,7 +31,7 @@ def delete(self, **kwargs): return None query = """ mutation DeleteOperationConfirm($id: ID!) { - deleteOperationConfirm(id: $id) { + deleteOperationConfirm(id: $id) } """ self.api.query(