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 diff --git a/pycti/api/opencti_api_client.py b/pycti/api/opencti_api_client.py index 5826aef4a..266135716 100644 --- a/pycti/api/opencti_api_client.py +++ b/pycti/api/opencti_api_client.py @@ -10,8 +10,12 @@ 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_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 @@ -151,6 +155,10 @@ def __init__( # 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 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.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 4846cc16b..4c8b3d1c8 100644 --- a/pycti/entities/opencti_stix_core_object.py +++ b/pycti/entities/opencti_stix_core_object.py @@ -1682,6 +1682,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 @@ -1704,3 +1918,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 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 2eeb6fcfa..21656bf94 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"]) @@ -2458,6 +2475,46 @@ 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): + # 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, @@ -2470,17 +2527,45 @@ def import_item( try: self.opencti.set_retry_number(processing_count) if "opencti_operation" in item: - if item["opencti_operation"] == "delete": - delete_id = item["id"] - self.opencti.stix.delete(id=delete_id) + if ( + item["opencti_operation"] == "delete" + or item["opencti_operation"] == "delete_force" + ): + 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.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) 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) + 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) + 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_enrichments( + element_id=item["id"], connector_ids=connector_ids + ) 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) diff --git a/pycti/utils/opencti_stix2_splitter.py b/pycti/utils/opencti_stix2_splitter.py index 10a655908..dffa533be 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 ) 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"],