From 3f6a019e79eaa0e7aac49ade0c89ac8de15e8ad3 Mon Sep 17 00:00:00 2001 From: ValentinBouzinFiligran <162980256+ValentinBouzinFiligran@users.noreply.github.com> Date: Fri, 18 Oct 2024 11:24:02 +0200 Subject: [PATCH 1/4] [client] Add observable values in indicator properties (opencti/8312) --- pycti/entities/indicator/opencti_indicator_properties.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pycti/entities/indicator/opencti_indicator_properties.py b/pycti/entities/indicator/opencti_indicator_properties.py index c37bf0b4..8dc168f5 100644 --- a/pycti/entities/indicator/opencti_indicator_properties.py +++ b/pycti/entities/indicator/opencti_indicator_properties.py @@ -92,6 +92,10 @@ x_opencti_score x_opencti_detection x_opencti_main_observable_type + x_opencti_observable_values { + type + value + } x_mitre_platforms observables { edges { @@ -220,6 +224,10 @@ x_opencti_score x_opencti_detection x_opencti_main_observable_type + x_opencti_observable_values { + type + value + } x_mitre_platforms observables { edges { From 755ba30206e7ca5ef13b12545cb34692c346eedd Mon Sep 17 00:00:00 2001 From: Jeremy Cloarec <159018898+JeremyCloarec@users.noreply.github.com> Date: Mon, 4 Nov 2024 15:36:09 +0100 Subject: [PATCH 2/4] [client] add draft_id header handling (opencti/6577) --- pycti/api/opencti_api_client.py | 3 +++ pycti/utils/opencti_stix2.py | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/pycti/api/opencti_api_client.py b/pycti/api/opencti_api_client.py index ffdc0eb9..ce9a3d5e 100644 --- a/pycti/api/opencti_api_client.py +++ b/pycti/api/opencti_api_client.py @@ -213,6 +213,9 @@ def set_playbook_id_header(self, playbook_id): def set_event_id(self, event_id): self.request_headers["opencti-event-id"] = event_id + def set_draft_id(self, draft_id): + self.request_headers["opencti-draft-id"] = draft_id + def set_synchronized_upsert_header(self, synchronized): self.request_headers["synchronized-upsert"] = ( "true" if synchronized is True else "false" diff --git a/pycti/utils/opencti_stix2.py b/pycti/utils/opencti_stix2.py index 53ca8876..5ab35aec 100644 --- a/pycti/utils/opencti_stix2.py +++ b/pycti/utils/opencti_stix2.py @@ -2390,7 +2390,7 @@ def import_item( if item["opencti_operation"] == "delete": delete_id = item["id"] self.opencti.stix.delete(id=delete_id) - if item["opencti_operation"] == "merge": + 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) From f68138e48cec11c43e3438d56f07e6a9b1484c40 Mon Sep 17 00:00:00 2001 From: Jeremy Cloarec <159018898+JeremyCloarec@users.noreply.github.com> Date: Thu, 7 Nov 2024 10:26:25 +0100 Subject: [PATCH 3/4] [client] add ability to ingest bundle in draft (opencti/6577) --- pycti/api/opencti_api_client.py | 28 ++++++++++++++ pycti/connector/opencti_connector_helper.py | 43 +++++++++++++++++---- 2 files changed, 63 insertions(+), 8 deletions(-) diff --git a/pycti/api/opencti_api_client.py b/pycti/api/opencti_api_client.py index ce9a3d5e..a6f7e7bc 100644 --- a/pycti/api/opencti_api_client.py +++ b/pycti/api/opencti_api_client.py @@ -669,6 +669,34 @@ def upload_file(self, **kwargs): self.app_logger.error("[upload] Missing parameter: file_name") return None + def create_draft(self, **kwargs): + """create a draft in OpenCTI API + :param `**kwargs`: arguments for file name creating draft (required: `draft_name`) + :return: returns the query response for the draft creation + :rtype: id + """ + + draft_name = kwargs.get("draft_name", None) + entity_id = kwargs.get("entity_id", None) + + if draft_name is not None: + self.app_logger.info("Creating a draft.") + query = """ + mutation draftWorkspaceAdd($input: DraftWorkspaceAddInput!) { + draftWorkspaceAdd(input: $input) { + id + } + } + """ + queryResult = self.query( + query, + {"input": {"name": draft_name, "entity_id": entity_id}}, + ) + return queryResult["data"]["draftWorkspaceAdd"]["id"] + else: + self.app_logger.error("[create_draft] Missing parameter: draft_name") + return None + def upload_pending_file(self, **kwargs): """upload a file to OpenCTI API diff --git a/pycti/connector/opencti_connector_helper.py b/pycti/connector/opencti_connector_helper.py index 4ebdb36a..1c125df4 100644 --- a/pycti/connector/opencti_connector_helper.py +++ b/pycti/connector/opencti_connector_helper.py @@ -253,10 +253,18 @@ def _data_handler(self, json_data) -> None: event_data = json_data["event"] entity_id = event_data.get("entity_id") entity_type = event_data.get("entity_type") + validation_mode = event_data.get("validation_mode", "workbench") # Set the API headers - work_id = json_data["internal"]["work_id"] + internal_data = json_data["internal"] + work_id = internal_data["work_id"] + draft_id = internal_data.get("draft_id", "") self.helper.work_id = work_id + self.helper.validation_mode = validation_mode + self.helper.draft_id = draft_id + self.helper.api.set_draft_id(draft_id) + self.helper.api_impersonate.set_draft_id(draft_id) + self.helper.playbook = None self.helper.enrichment_shared_organizations = None if self.helper.connect_type == "INTERNAL_ENRICHMENT": @@ -952,6 +960,8 @@ def __init__(self, config: Dict, playbook_compatible=False) -> None: "Connector registered with ID", {"id": self.connect_id} ) self.work_id = None + self.validation_mode = "workbench" + self.draft_id = None self.playbook = None self.enrichment_shared_organizations = None self.connector_id = connector_configuration["id"] @@ -1550,6 +1560,7 @@ def send_stix2_bundle(self, bundle: str, **kwargs) -> list: """send a stix2 bundle to the API :param work_id: a valid work id + :param draft_id: a draft context to send the bundle to :param bundle: valid stix2 bundle :type bundle: :param entities_types: list of entities, defaults to None @@ -1563,6 +1574,8 @@ def send_stix2_bundle(self, bundle: str, **kwargs) -> list: :rtype: list """ work_id = kwargs.get("work_id", self.work_id) + validation_mode = kwargs.get("validation_mode", self.validation_mode) + draft_id = kwargs.get("draft_id", self.draft_id) entities_types = kwargs.get("entities_types", None) update = kwargs.get("update", False) event_version = kwargs.get("event_version", None) @@ -1627,14 +1640,23 @@ def send_stix2_bundle(self, bundle: str, **kwargs) -> list: # Upload workbench in case of pending validation if not file_name and work_id: file_name = f"{work_id}.json" + if self.connect_validate_before_import and not bypass_validation and file_name: - self.api.upload_pending_file( - file_name=file_name, - data=bundle, - mime_type="application/json", - entity_id=entity_id, - ) - return [] + if validation_mode == "workbench": + self.api.upload_pending_file( + file_name=file_name, + data=bundle, + mime_type="application/json", + entity_id=entity_id, + ) + return [] + elif validation_mode == "draft" and not draft_id: + draft_id = self.api.create_draft( + draft_name=file_name, entity_id=entity_id + ) + if not draft_id: + self.connector_logger.error("Draft couldn't be created") + return [] # If directory setup, write the bundle to the target directory if bundle_send_to_directory and bundle_send_to_directory_path is not None: @@ -1749,6 +1771,7 @@ def send_stix2_bundle(self, bundle: str, **kwargs) -> list: entities_types=entities_types, sequence=sequence, update=update, + draft_id=draft_id, ) channel.close() pika_connection.close() @@ -1774,11 +1797,14 @@ def _send_bundle(self, channel, bundle, **kwargs) -> None: :type entities_types: list, optional :param update: whether to update data in the database, defaults to False :type update: bool, optional + :param draft_id: if draft_id is set, bundle must be set in draft context + :type draft_id: """ work_id = kwargs.get("work_id", None) sequence = kwargs.get("sequence", 0) update = kwargs.get("update", False) entities_types = kwargs.get("entities_types", None) + draft_id = kwargs.get("draft_id", None) if entities_types is None: entities_types = [] @@ -1800,6 +1826,7 @@ def _send_bundle(self, channel, bundle, **kwargs) -> None: "utf-8" ), "update": update, + "draft_id": draft_id, } if work_id is not None: message["work_id"] = work_id From c7f4c65d63b726ca3b5a6621f658d6433ac5769a Mon Sep 17 00:00:00 2001 From: Julien Richard Date: Fri, 15 Nov 2024 15:43:38 +0100 Subject: [PATCH 4/4] [client] Improve id generation for STIX elements (#789) Co-authored-by: Helene Nguyen --- pycti/entities/opencti_attack_pattern.py | 8 +- pycti/entities/opencti_campaign.py | 4 + pycti/entities/opencti_case_incident.py | 4 + pycti/entities/opencti_case_rfi.py | 4 + pycti/entities/opencti_case_rft.py | 4 + pycti/entities/opencti_channel.py | 4 + pycti/entities/opencti_course_of_action.py | 7 +- pycti/entities/opencti_data_component.py | 4 + pycti/entities/opencti_data_source.py | 4 + pycti/entities/opencti_event.py | 4 + pycti/entities/opencti_external_reference.py | 6 + pycti/entities/opencti_feedback.py | 4 + pycti/entities/opencti_grouping.py | 11 +- pycti/entities/opencti_identity.py | 7 +- pycti/entities/opencti_incident.py | 4 + pycti/entities/opencti_indicator.py | 4 + pycti/entities/opencti_infrastructure.py | 4 + pycti/entities/opencti_intrusion_set.py | 4 + pycti/entities/opencti_kill_chain_phase.py | 4 + pycti/entities/opencti_language.py | 4 + pycti/entities/opencti_location.py | 26 +++- pycti/entities/opencti_malware.py | 4 + pycti/entities/opencti_malware_analysis.py | 12 +- pycti/entities/opencti_marking_definition.py | 6 + pycti/entities/opencti_narrative.py | 4 + pycti/entities/opencti_note.py | 16 ++- pycti/entities/opencti_observed_data.py | 4 + pycti/entities/opencti_opinion.py | 22 ++- pycti/entities/opencti_report.py | 4 + .../opencti_stix_core_relationship.py | 22 +++ .../opencti_stix_sighting_relationship.py | 33 ++++- pycti/entities/opencti_task.py | 7 +- pycti/entities/opencti_threat_actor.py | 13 +- pycti/entities/opencti_threat_actor_group.py | 4 + .../opencti_threat_actor_individual.py | 4 + pycti/entities/opencti_tool.py | 4 + pycti/entities/opencti_vocabulary.py | 15 +++ pycti/entities/opencti_vulnerability.py | 4 + pycti/utils/opencti_stix2.py | 125 +++++++++++------- tests/01-unit/stix/__init__.py | 0 tests/01-unit/stix/test_bundle_ids_rewrite.py | 112 ++++++++++++++++ tests/data/bundle_ids_sample.json | 89 +++++++++++++ 42 files changed, 548 insertions(+), 81 deletions(-) create mode 100644 tests/01-unit/stix/__init__.py create mode 100644 tests/01-unit/stix/test_bundle_ids_rewrite.py create mode 100644 tests/data/bundle_ids_sample.json diff --git a/pycti/entities/opencti_attack_pattern.py b/pycti/entities/opencti_attack_pattern.py index 7f549b5b..beada5a1 100644 --- a/pycti/entities/opencti_attack_pattern.py +++ b/pycti/entities/opencti_attack_pattern.py @@ -222,15 +222,19 @@ def __init__(self, opencti): @staticmethod def generate_id(name, x_mitre_id=None): - name = name.lower().strip() if x_mitre_id is not None: data = {"x_mitre_id": x_mitre_id} else: - data = {"name": name} + data = {"name": name.lower().strip()} data = canonicalize(data, utf8=False) id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) return "attack-pattern--" + id + @staticmethod + def generate_id_from_data(data): + external_id = data.get("x_mitre_id") or data.get("x_opencti_external_id") + return AttackPattern.generate_id(data.get("name"), external_id) + """ List Attack-Pattern objects diff --git a/pycti/entities/opencti_campaign.py b/pycti/entities/opencti_campaign.py index 3070a480..f3212de8 100644 --- a/pycti/entities/opencti_campaign.py +++ b/pycti/entities/opencti_campaign.py @@ -216,6 +216,10 @@ def generate_id(name): id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) return "campaign--" + id + @staticmethod + def generate_id_from_data(data): + return Campaign.generate_id(data["name"]) + """ List Campaign objects diff --git a/pycti/entities/opencti_case_incident.py b/pycti/entities/opencti_case_incident.py index e1969c22..40cc75ba 100644 --- a/pycti/entities/opencti_case_incident.py +++ b/pycti/entities/opencti_case_incident.py @@ -461,6 +461,10 @@ def generate_id(name, created): id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) return "case-incident--" + id + @staticmethod + def generate_id_from_data(data): + return CaseIncident.generate_id(data["name"], data["created"]) + """ List Case Incident objects diff --git a/pycti/entities/opencti_case_rfi.py b/pycti/entities/opencti_case_rfi.py index 62df2817..afa4b5be 100644 --- a/pycti/entities/opencti_case_rfi.py +++ b/pycti/entities/opencti_case_rfi.py @@ -457,6 +457,10 @@ def generate_id(name, created): id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) return "case-rfi--" + id + @staticmethod + def generate_id_from_data(data): + return CaseRfi.generate_id(data["name"], data["created"]) + """ List Case Rfi objects diff --git a/pycti/entities/opencti_case_rft.py b/pycti/entities/opencti_case_rft.py index 50969bf4..dddf601a 100644 --- a/pycti/entities/opencti_case_rft.py +++ b/pycti/entities/opencti_case_rft.py @@ -457,6 +457,10 @@ def generate_id(name, created): id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) return "case-rft--" + id + @staticmethod + def generate_id_from_data(data): + return CaseRft.generate_id(data["name"], data["created"]) + """ List Case Rft objects diff --git a/pycti/entities/opencti_channel.py b/pycti/entities/opencti_channel.py index dd6f8f09..9fc1b280 100644 --- a/pycti/entities/opencti_channel.py +++ b/pycti/entities/opencti_channel.py @@ -212,6 +212,10 @@ def generate_id(name): id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) return "channel--" + id + @staticmethod + def generate_id_from_data(data): + return Channel.generate_id(data["name"]) + """ List Channel objects diff --git a/pycti/entities/opencti_course_of_action.py b/pycti/entities/opencti_course_of_action.py index 43be2205..af65bbbb 100644 --- a/pycti/entities/opencti_course_of_action.py +++ b/pycti/entities/opencti_course_of_action.py @@ -196,15 +196,18 @@ def __init__(self, opencti): @staticmethod def generate_id(name, x_mitre_id=None): - name = name.lower().strip() if x_mitre_id is not None: data = {"x_mitre_id": x_mitre_id} else: - data = {"name": name} + data = {"name": name.lower().strip()} data = canonicalize(data, utf8=False) id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) return "course-of-action--" + id + @staticmethod + def generate_id_from_data(data): + return CourseOfAction.generate_id(data.get("name"), data.get("x_mitre_id")) + """ List Course-Of-Action objects diff --git a/pycti/entities/opencti_data_component.py b/pycti/entities/opencti_data_component.py index a675c5ed..c34250d6 100644 --- a/pycti/entities/opencti_data_component.py +++ b/pycti/entities/opencti_data_component.py @@ -246,6 +246,10 @@ def generate_id(name): id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) return "data-component--" + id + @staticmethod + def generate_id_from_data(data): + return DataComponent.generate_id(data["name"]) + """ List Data-Component objects diff --git a/pycti/entities/opencti_data_source.py b/pycti/entities/opencti_data_source.py index 8ef6b8a0..3935a507 100644 --- a/pycti/entities/opencti_data_source.py +++ b/pycti/entities/opencti_data_source.py @@ -204,6 +204,10 @@ def generate_id(name): id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) return "data-source--" + id + @staticmethod + def generate_id_from_data(data): + return DataSource.generate_id(data["name"]) + """ List Data-Source objects diff --git a/pycti/entities/opencti_event.py b/pycti/entities/opencti_event.py index cf5824a4..c07fcad0 100644 --- a/pycti/entities/opencti_event.py +++ b/pycti/entities/opencti_event.py @@ -216,6 +216,10 @@ def generate_id(name): id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) return "event--" + id + @staticmethod + def generate_id_from_data(data): + return Event.generate_id(data["name"]) + """ List Event objects diff --git a/pycti/entities/opencti_external_reference.py b/pycti/entities/opencti_external_reference.py index f8590313..a81f828b 100644 --- a/pycti/entities/opencti_external_reference.py +++ b/pycti/entities/opencti_external_reference.py @@ -68,6 +68,12 @@ def generate_id(url=None, source_name=None, external_id=None): id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) return "external-reference--" + id + @staticmethod + def generate_id_from_data(data): + return ExternalReference.generate_id( + data.get("url"), data.get("source_name"), data.get("external_id") + ) + """ List External-Reference objects diff --git a/pycti/entities/opencti_feedback.py b/pycti/entities/opencti_feedback.py index 9f239607..0f8925da 100644 --- a/pycti/entities/opencti_feedback.py +++ b/pycti/entities/opencti_feedback.py @@ -419,6 +419,10 @@ def generate_id(name): id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) return "feedback--" + id + @staticmethod + def generate_id_from_data(data): + return Feedback.generate_id(data["name"]) + """ List Feedback objects diff --git a/pycti/entities/opencti_grouping.py b/pycti/entities/opencti_grouping.py index 1fcd88d7..f3d0be6e 100644 --- a/pycti/entities/opencti_grouping.py +++ b/pycti/entities/opencti_grouping.py @@ -396,16 +396,23 @@ def __init__(self, opencti): """ @staticmethod - def generate_id(name, context, created): + def generate_id(name, context, created=None): name = name.lower().strip() context = context.lower().strip() if isinstance(created, datetime.datetime): created = created.isoformat() - data = {"name": name, "context": context, "created": created} + if created is None: + data = {"name": name, "context": context} + else: + data = {"name": name, "context": context, "created": created} data = canonicalize(data, utf8=False) id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) return "grouping--" + id + @staticmethod + def generate_id_from_data(data): + return Grouping.generate_id(data["name"], data["context"], data["created"]) + """ List Grouping objects diff --git a/pycti/entities/opencti_identity.py b/pycti/entities/opencti_identity.py index 4b048c47..dec283ad 100644 --- a/pycti/entities/opencti_identity.py +++ b/pycti/entities/opencti_identity.py @@ -226,12 +226,15 @@ def __init__(self, opencti): @staticmethod def generate_id(name, identity_class): - name = name.lower().strip() - data = {"name": name, "identity_class": identity_class} + data = {"name": name.lower().strip(), "identity_class": identity_class.lower()} data = canonicalize(data, utf8=False) id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) return "identity--" + id + @staticmethod + def generate_id_from_data(data): + return Identity.generate_id(data["name"], data["identity_class"]) + """ List Identity objects diff --git a/pycti/entities/opencti_incident.py b/pycti/entities/opencti_incident.py index 4f56b98b..6ada2f49 100644 --- a/pycti/entities/opencti_incident.py +++ b/pycti/entities/opencti_incident.py @@ -225,6 +225,10 @@ def generate_id(name, created): id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) return "incident--" + id + @staticmethod + def generate_id_from_data(data): + return Incident.generate_id(data["name"], data["created"]) + """ List Incident objects diff --git a/pycti/entities/opencti_indicator.py b/pycti/entities/opencti_indicator.py index a85356fa..6d209099 100644 --- a/pycti/entities/opencti_indicator.py +++ b/pycti/entities/opencti_indicator.py @@ -29,6 +29,10 @@ def generate_id(pattern): id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) return "indicator--" + id + @staticmethod + def generate_id_from_data(data): + return Indicator.generate_id(data["pattern"]) + def list(self, **kwargs): """List Indicator objects diff --git a/pycti/entities/opencti_infrastructure.py b/pycti/entities/opencti_infrastructure.py index 957333e0..d55d4574 100644 --- a/pycti/entities/opencti_infrastructure.py +++ b/pycti/entities/opencti_infrastructure.py @@ -239,6 +239,10 @@ def generate_id(name): id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) return "infrastructure--" + id + @staticmethod + def generate_id_from_data(data): + return Infrastructure.generate_id(data["name"]) + def list(self, **kwargs): """List Infrastructure objects diff --git a/pycti/entities/opencti_intrusion_set.py b/pycti/entities/opencti_intrusion_set.py index f7f4954b..8f72b2ec 100644 --- a/pycti/entities/opencti_intrusion_set.py +++ b/pycti/entities/opencti_intrusion_set.py @@ -222,6 +222,10 @@ def generate_id(name): id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) return "intrusion-set--" + id + @staticmethod + def generate_id_from_data(data): + return IntrusionSet.generate_id(data["name"]) + """ List Intrusion-Set objects diff --git a/pycti/entities/opencti_kill_chain_phase.py b/pycti/entities/opencti_kill_chain_phase.py index ce91a11f..093dd372 100644 --- a/pycti/entities/opencti_kill_chain_phase.py +++ b/pycti/entities/opencti_kill_chain_phase.py @@ -28,6 +28,10 @@ def generate_id(phase_name, kill_chain_name): phase_name=phase_name, kill_chain_name=kill_chain_name ) + @staticmethod + def generate_id_from_data(data): + return KillChainPhase.generate_id(data["phase_name"], data["kill_chain_name"]) + """ List Kill-Chain-Phase objects diff --git a/pycti/entities/opencti_language.py b/pycti/entities/opencti_language.py index fd39b656..2790dbe6 100644 --- a/pycti/entities/opencti_language.py +++ b/pycti/entities/opencti_language.py @@ -224,6 +224,10 @@ def generate_id(name): id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) return "language--" + id + @staticmethod + def generate_id_from_data(data): + return Language.generate_id(data["name"]) + """ List Language objects diff --git a/pycti/entities/opencti_location.py b/pycti/entities/opencti_location.py index 10b1d870..12a921e6 100644 --- a/pycti/entities/opencti_location.py +++ b/pycti/entities/opencti_location.py @@ -210,15 +210,33 @@ def __init__(self, opencti): @staticmethod def generate_id(name, x_opencti_location_type, latitude=None, longitude=None): - name = name.lower().strip() - if x_opencti_location_type == "position": - data = {"name": name, "latitude": latitude, "longitude": longitude} + if x_opencti_location_type == "Position": + if latitude is not None and longitude is None: + data = {"latitude": latitude} + elif latitude is None and longitude is not None: + data = {"longitude": longitude} + elif latitude is not None and longitude is not None: + data = {"latitude": latitude, "longitude": longitude} + else: + data = {"name": name.lower().strip()} else: - data = {"name": name, "x_opencti_location_type": x_opencti_location_type} + data = { + "name": name.lower().strip(), + "x_opencti_location_type": x_opencti_location_type, + } data = canonicalize(data, utf8=False) id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) return "location--" + id + @staticmethod + def generate_id_from_data(data): + return Location.generate_id( + data.get("name"), + data.get("x_opencti_location_type"), + data.get("latitude"), + data.get("longitude"), + ) + """ List Location objects diff --git a/pycti/entities/opencti_malware.py b/pycti/entities/opencti_malware.py index d1d89bd5..615c2fc2 100644 --- a/pycti/entities/opencti_malware.py +++ b/pycti/entities/opencti_malware.py @@ -250,6 +250,10 @@ def generate_id(name): id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) return "malware--" + id + @staticmethod + def generate_id_from_data(data): + return Malware.generate_id(data["name"]) + """ List Malware objects diff --git a/pycti/entities/opencti_malware_analysis.py b/pycti/entities/opencti_malware_analysis.py index d2a02ddc..2c44e7ab 100644 --- a/pycti/entities/opencti_malware_analysis.py +++ b/pycti/entities/opencti_malware_analysis.py @@ -219,13 +219,21 @@ def __init__(self, opencti): """ @staticmethod - def generate_id(result_name): + def generate_id(result_name, product=None, submitted=None): result_name = result_name.lower().strip() - data = {"result_name": result_name} + data = {"result_name": result_name, "product": product} + if submitted is not None: + data = {**data, "submitted": submitted} data = canonicalize(data, utf8=False) id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) return "malware-analysis--" + id + @staticmethod + def generate_id_from_data(data): + return MalwareAnalysis.generate_id( + data["result_name"], data["product"], data.get("submitted") + ) + """ List Malware analysis objects diff --git a/pycti/entities/opencti_marking_definition.py b/pycti/entities/opencti_marking_definition.py index 571e9fd4..cf01d5ca 100644 --- a/pycti/entities/opencti_marking_definition.py +++ b/pycti/entities/opencti_marking_definition.py @@ -31,6 +31,12 @@ def generate_id(definition, definition_type): id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) return "marking-definition--" + id + @staticmethod + def generate_id_from_data(data): + return MarkingDefinition.generate_id( + data["definition"], data["definition_type"] + ) + """ List Marking-Definition objects diff --git a/pycti/entities/opencti_narrative.py b/pycti/entities/opencti_narrative.py index eb03b18f..32477cd3 100644 --- a/pycti/entities/opencti_narrative.py +++ b/pycti/entities/opencti_narrative.py @@ -202,6 +202,10 @@ def generate_id(name): id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) return "narrative--" + id + @staticmethod + def generate_id_from_data(data): + return Narrative.generate_id(data["name"]) + """ List Narrative objects diff --git a/pycti/entities/opencti_note.py b/pycti/entities/opencti_note.py index 0ef8ba78..7430a31c 100644 --- a/pycti/entities/opencti_note.py +++ b/pycti/entities/opencti_note.py @@ -436,14 +436,22 @@ def __init__(self, opencti): @staticmethod def generate_id(created, content): - content = content.lower().strip() - if isinstance(created, datetime.datetime): - created = created.isoformat() - data = {"content": content, "created": created} + if content is None: + raise ValueError("content is required") + if created is not None: + if isinstance(created, datetime.datetime): + created = created.isoformat() + data = {"content": content, "created": created} + else: + data = {"content": content} data = canonicalize(data, utf8=False) id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) return "note--" + id + @staticmethod + def generate_id_from_data(data): + return Note.generate_id(data.get("created"), data["content"]) + """ List Note objects diff --git a/pycti/entities/opencti_observed_data.py b/pycti/entities/opencti_observed_data.py index a258f2b2..dc1293a9 100644 --- a/pycti/entities/opencti_observed_data.py +++ b/pycti/entities/opencti_observed_data.py @@ -439,6 +439,10 @@ def generate_id(object_ids): id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) return "observed-data--" + id + @staticmethod + def generate_id_from_data(data): + return ObservedData.generate_id(data["object_refs"]) + """ List ObservedData objects diff --git a/pycti/entities/opencti_opinion.py b/pycti/entities/opencti_opinion.py index 7442d948..6b435c7e 100644 --- a/pycti/entities/opencti_opinion.py +++ b/pycti/entities/opencti_opinion.py @@ -1,8 +1,10 @@ # coding: utf-8 - +import datetime import json import uuid +from stix2.canonicalization.Canonicalize import canonicalize + class Opinion: def __init__(self, opencti): @@ -211,8 +213,22 @@ def __init__(self, opencti): """ @staticmethod - def generate_id(): - return "opinion--" + str(uuid.uuid4()) + def generate_id(created, opinion): + if opinion is None: + raise ValueError("opinion is required") + if created is not None: + if isinstance(created, datetime.datetime): + created = created.isoformat() + data = {"opinion": opinion, "created": created} + else: + data = {"opinion": opinion} + data = canonicalize(data, utf8=False) + id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) + return "opinion--" + id + + @staticmethod + def generate_id_from_data(data): + return Opinion.generate_id(data.get("created"), data["opinion"]) """ List Opinion objects diff --git a/pycti/entities/opencti_report.py b/pycti/entities/opencti_report.py index 2dd5cf56..2ab74fc2 100644 --- a/pycti/entities/opencti_report.py +++ b/pycti/entities/opencti_report.py @@ -479,6 +479,10 @@ def generate_fixed_fake_id(name, published=None): id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) return "report--" + id + @staticmethod + def generate_id_from_data(data): + return Report.generate_id(data["name"], data["published"]) + """ List Report objects diff --git a/pycti/entities/opencti_stix_core_relationship.py b/pycti/entities/opencti_stix_core_relationship.py index b3f72921..2d56eb21 100644 --- a/pycti/entities/opencti_stix_core_relationship.py +++ b/pycti/entities/opencti_stix_core_relationship.py @@ -336,6 +336,7 @@ def generate_id( start_time = start_time.isoformat() if isinstance(stop_time, datetime.datetime): stop_time = stop_time.isoformat() + if start_time is not None and stop_time is not None: data = { "relationship_type": relationship_type, @@ -376,6 +377,16 @@ def generate_id( :return List of stix_core_relationship objects """ + @staticmethod + def generate_id_from_data(data): + return StixCoreRelationship.generate_id( + data["relationship_type"], + data["source_ref"], + data["target_ref"], + data.get("start_time"), + data.get("stop_time"), + ) + def list(self, **kwargs): from_or_to_id = kwargs.get("fromOrToId", None) element_with_target_types = kwargs.get("elementWithTargetTypes", None) @@ -599,6 +610,7 @@ def create(self, **kwargs): kill_chain_phases = kwargs.get("killChainPhases", None) granted_refs = kwargs.get("objectOrganization", None) x_opencti_workflow_id = kwargs.get("x_opencti_workflow_id", None) + x_opencti_stix_ids = kwargs.get("x_opencti_stix_ids", None) update = kwargs.get("update", False) self.opencti.app_logger.info( @@ -642,6 +654,7 @@ def create(self, **kwargs): "externalReferences": external_references, "killChainPhases": kill_chain_phases, "x_opencti_workflow_id": x_opencti_workflow_id, + "x_opencti_stix_ids": x_opencti_stix_ids, "update": update, } }, @@ -1132,6 +1145,10 @@ def import_from_stix2(self, **kwargs): default_date = kwargs.get("defaultDate", False) if stix_relation is not None: # Search in extensions + if "x_opencti_stix_ids" not in stix_relation: + stix_relation["x_opencti_stix_ids"] = ( + self.opencti.get_attribute_in_extension("stix_ids", stix_relation) + ) if "x_opencti_granted_refs" not in stix_relation: stix_relation["x_opencti_granted_refs"] = ( self.opencti.get_attribute_in_extension( @@ -1213,6 +1230,11 @@ def import_from_stix2(self, **kwargs): if "x_opencti_workflow_id" in stix_relation else None ), + x_opencti_stix_ids=( + stix_relation["x_opencti_stix_ids"] + if "x_opencti_stix_ids" in stix_relation + else None + ), update=update, ) else: diff --git a/pycti/entities/opencti_stix_sighting_relationship.py b/pycti/entities/opencti_stix_sighting_relationship.py index babbf6d9..3a44e4b2 100644 --- a/pycti/entities/opencti_stix_sighting_relationship.py +++ b/pycti/entities/opencti_stix_sighting_relationship.py @@ -262,7 +262,12 @@ def __init__(self, opencti): """ @staticmethod - def generate_id(source_ref, target_ref, first_seen=None, last_seen=None): + def generate_id( + sighting_of_ref, + where_sighted_refs, + first_seen=None, + last_seen=None, + ): if isinstance(first_seen, datetime.datetime): first_seen = first_seen.isoformat() if isinstance(last_seen, datetime.datetime): @@ -270,20 +275,38 @@ def generate_id(source_ref, target_ref, first_seen=None, last_seen=None): if first_seen is not None and last_seen is not None: data = { - "source_ref": source_ref, - "target_ref": target_ref, + "type": "sighting", + "sighting_of_ref": sighting_of_ref, + "where_sighted_refs": where_sighted_refs, "first_seen": first_seen, "last_seen": last_seen, } + elif first_seen is not None: + data = { + "type": "sighting", + "sighting_of_ref": sighting_of_ref, + "where_sighted_refs": where_sighted_refs, + "first_seen": first_seen, + } else: data = { - "source_ref": source_ref, - "target_ref": target_ref, + "type": "sighting", + "sighting_of_ref": sighting_of_ref, + "where_sighted_refs": where_sighted_refs, } data = canonicalize(data, utf8=False) id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) return "sighting--" + id + @staticmethod + def generate_id_from_data(data): + return StixSightingRelationship.generate_id( + data["sighting_of_ref"], + data["where_sighted_refs"], + data.get("first_seen"), + data.get("last_seen"), + ) + """ List stix_sightings objects diff --git a/pycti/entities/opencti_task.py b/pycti/entities/opencti_task.py index dab6f63a..bdda9cea 100644 --- a/pycti/entities/opencti_task.py +++ b/pycti/entities/opencti_task.py @@ -226,14 +226,17 @@ def __init__(self, opencti): @staticmethod def generate_id(name, created): - name = name.lower().strip() if isinstance(created, datetime.datetime): created = created.isoformat() - data = {"name": name, "created": created} + data = {"name": name.lower().strip(), "created": created} data = canonicalize(data, utf8=False) id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) return "task--" + id + @staticmethod + def generate_id_from_data(data): + return Task.generate_id(data["name"], data["created"]) + """ List Task objects diff --git a/pycti/entities/opencti_threat_actor.py b/pycti/entities/opencti_threat_actor.py index 7f8d0efe..3e4002a4 100644 --- a/pycti/entities/opencti_threat_actor.py +++ b/pycti/entities/opencti_threat_actor.py @@ -142,13 +142,20 @@ def __init__(self, opencti): """ @staticmethod - def generate_id(name): - name = name.lower().strip() - data = {"name": name} + def generate_id(name, opencti_type): + data = {"name": name.lower().strip(), "opencti_type": opencti_type} data = canonicalize(data, utf8=False) id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) return "threat-actor--" + id + def generate_id_from_data(self, data): + data_type = "Threat-Actor-Group" + if "x_opencti_type" in data: + data_type = data["x_opencti_type"] + elif self.opencti.get_attribute_in_extension("type", data) is not None: + data_type = self.opencti.get_attribute_in_extension("type", data) + return ThreatActor.generate_id(data["name"], data_type) + def list(self, **kwargs) -> dict: """List Threat-Actor objects diff --git a/pycti/entities/opencti_threat_actor_group.py b/pycti/entities/opencti_threat_actor_group.py index b1e56496..af9d23c2 100644 --- a/pycti/entities/opencti_threat_actor_group.py +++ b/pycti/entities/opencti_threat_actor_group.py @@ -145,6 +145,10 @@ def generate_id(name): id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) return "threat-actor--" + id + @staticmethod + def generate_id_from_data(data): + return ThreatActorGroup.generate_id(data["name"]) + def list(self, **kwargs) -> dict: """List Threat-Actor-Group objects diff --git a/pycti/entities/opencti_threat_actor_individual.py b/pycti/entities/opencti_threat_actor_individual.py index 9fa17afc..ff1590d2 100644 --- a/pycti/entities/opencti_threat_actor_individual.py +++ b/pycti/entities/opencti_threat_actor_individual.py @@ -145,6 +145,10 @@ def generate_id(name): id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) return "threat-actor--" + id + @staticmethod + def generate_id_from_data(data): + return ThreatActorIndividual.generate_id(data["name"]) + def list(self, **kwargs) -> dict: """List Threat-Actor-Individual objects diff --git a/pycti/entities/opencti_tool.py b/pycti/entities/opencti_tool.py index 73ae9467..8609f5db 100644 --- a/pycti/entities/opencti_tool.py +++ b/pycti/entities/opencti_tool.py @@ -134,6 +134,10 @@ def generate_id(name): id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) return "tool--" + id + @staticmethod + def generate_id_from_data(data): + return Tool.generate_id(data["name"]) + """ List Tool objects diff --git a/pycti/entities/opencti_vocabulary.py b/pycti/entities/opencti_vocabulary.py index a7b2eda8..c18710ba 100644 --- a/pycti/entities/opencti_vocabulary.py +++ b/pycti/entities/opencti_vocabulary.py @@ -1,4 +1,7 @@ import json +import uuid + +from stix2.canonicalization.Canonicalize import canonicalize class Vocabulary: @@ -15,6 +18,18 @@ def __init__(self, opencti): } """ + @staticmethod + def generate_id(name, category): + name = name.lower().strip() + data = {"name": name, "category": category} + data = canonicalize(data, utf8=False) + id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) + return "vocabulary--" + id + + @staticmethod + def generate_id_from_data(data): + return Vocabulary.generate_id(data["name"], data["category"]) + def list(self, **kwargs): filters = kwargs.get("filters", None) self.opencti.app_logger.info( diff --git a/pycti/entities/opencti_vulnerability.py b/pycti/entities/opencti_vulnerability.py index a6133cf3..70b31aca 100644 --- a/pycti/entities/opencti_vulnerability.py +++ b/pycti/entities/opencti_vulnerability.py @@ -131,6 +131,10 @@ def generate_id(name): id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) return "vulnerability--" + id + @staticmethod + def generate_id_from_data(data): + return Vulnerability.generate_id(data["name"]) + """ List Vulnerability objects diff --git a/pycti/utils/opencti_stix2.py b/pycti/utils/opencti_stix2.py index 126803b8..d8bea790 100644 --- a/pycti/utils/opencti_stix2.py +++ b/pycti/utils/opencti_stix2.py @@ -176,7 +176,10 @@ def pick_aliases(self, stix_object: Dict) -> Optional[List]: return None def import_bundle_from_file( - self, file_path: str, update: bool = False, types: List = None + self, + file_path: str, + update: bool = False, + types: List = None, ) -> Optional[List]: """import a stix2 bundle from a file @@ -194,7 +197,7 @@ def import_bundle_from_file( return None with open(os.path.join(file_path), encoding="utf-8") as file: data = json.load(file) - return self.import_bundle(data, update, types) + return self.import_bundle(data, update, types, None) def import_bundle_from_json( self, @@ -852,6 +855,59 @@ def get_reader(self, entity_type: str): # endregion + def get_stix_helper(self): + # Import + return { + # entities + "attack-pattern": self.opencti.attack_pattern, + "campaign": self.opencti.campaign, + "note": self.opencti.note, + "observed-data": self.opencti.observed_data, + "opinion": self.opencti.opinion, + "report": self.opencti.report, + "course-of-action": self.opencti.course_of_action, + "identity": self.opencti.identity, + "infrastructure": self.opencti.infrastructure, + "intrusion-set": self.opencti.intrusion_set, + "location": self.opencti.location, + "malware": self.opencti.malware, + "threat-actor": self.opencti.threat_actor, + "tool": self.opencti.tool, + "vulnerability": self.opencti.vulnerability, + "incident": self.opencti.incident, + "marking-definition": self.opencti.marking_definition, + "case-rfi": self.opencti.case_rfi, + "x-opencti-case-rfi": self.opencti.case_rfi, + "case-rft": self.opencti.case_rft, + "x-opencti-case-rft": self.opencti.case_rft, + "case-incident": self.opencti.case_incident, + "x-opencti-case-incident": self.opencti.case_incident, + "feedback": self.opencti.feedback, + "x-opencti-feedback": self.opencti.feedback, + "channel": self.opencti.channel, + "data-component": self.opencti.data_component, + "x-mitre-data-component": self.opencti.data_component, + "data-source": self.opencti.data_source, + "x-mitre-data-source": self.opencti.data_source, + "event": self.opencti.event, + "grouping": self.opencti.grouping, + "indicator": self.opencti.indicator, + "language": self.opencti.language, + "malware-analysis": self.opencti.malware_analysis, + "narrative": self.opencti.narrative, + "task": self.opencti.task, + "x-opencti-task": self.opencti.task, + "vocabulary": self.opencti.vocabulary, + # relationships + "relationship": self.opencti.stix_core_relationship, + "sighting": self.opencti.stix_sighting_relationship, + } + + def generate_standard_id_from_stix(self, data): + stix_helpers = self.get_stix_helper() + helper = stix_helpers.get(data["type"]) + return helper.generate_id_from_data(data) + # region import def import_object( self, stix_object: Dict, update: bool = False, types: List = None @@ -898,53 +954,16 @@ def import_object( "sample_ids": sample_refs_ids, } - # Import - importer = { - "marking-definition": self.opencti.marking_definition.import_from_stix2, - "attack-pattern": self.opencti.attack_pattern.import_from_stix2, - "campaign": self.opencti.campaign.import_from_stix2, - "channel": self.opencti.channel.import_from_stix2, - "event": self.opencti.event.import_from_stix2, - "note": self.opencti.note.import_from_stix2, - "observed-data": self.opencti.observed_data.import_from_stix2, - "opinion": self.opencti.opinion.import_from_stix2, - "report": self.opencti.report.import_from_stix2, - "grouping": self.opencti.grouping.import_from_stix2, - "case-rfi": self.opencti.case_rfi.import_from_stix2, - "x-opencti-case-rfi": self.opencti.case_rfi.import_from_stix2, - "case-rft": self.opencti.case_rft.import_from_stix2, - "x-opencti-case-rft": self.opencti.case_rft.import_from_stix2, - "task": self.opencti.task.import_from_stix2, - "x-opencti-task": self.opencti.task.import_from_stix2, - "case-incident": self.opencti.case_incident.import_from_stix2, - "x-opencti-case-incident": self.opencti.case_incident.import_from_stix2, - "feedback": self.opencti.feedback.import_from_stix2, - "x-opencti-feedback": self.opencti.feedback.import_from_stix2, - "course-of-action": self.opencti.course_of_action.import_from_stix2, - "data-component": self.opencti.data_component.import_from_stix2, - "x-mitre-data-component": self.opencti.data_component.import_from_stix2, - "data-source": self.opencti.data_source.import_from_stix2, - "x-mitre-data-source": self.opencti.data_source.import_from_stix2, - "identity": self.opencti.identity.import_from_stix2, - "indicator": self.opencti.indicator.import_from_stix2, - "infrastructure": self.opencti.infrastructure.import_from_stix2, - "intrusion-set": self.opencti.intrusion_set.import_from_stix2, - "location": self.opencti.location.import_from_stix2, - "malware": self.opencti.malware.import_from_stix2, - "malware-analysis": self.opencti.malware_analysis.import_from_stix2, - "threat-actor": self.opencti.threat_actor.import_from_stix2, - "tool": self.opencti.tool.import_from_stix2, - "narrative": self.opencti.narrative.import_from_stix2, - "vulnerability": self.opencti.vulnerability.import_from_stix2, - "incident": self.opencti.incident.import_from_stix2, - } - do_import = importer.get( - stix_object["type"], - lambda **kwargs: self.unknown_type(stix_object), - ) - stix_object_results = do_import( - stixObject=stix_object, extras=extras, update=update - ) + stix_helper = self.get_stix_helper().get(stix_object["type"]) + if stix_helper: + stix_object_results = stix_helper.import_from_stix2( + stixObject=stix_object, extras=extras, update=update + ) + else: + stix_object_results = None + self.opencti.app_logger.error( + "Unknown object type, doing nothing...", {"type": stix_object["type"]} + ) if stix_object_results is None: return None @@ -1373,6 +1392,11 @@ def import_sighting( if "x_opencti_workflow_id" in stix_sighting else None ), + x_opencti_stix_ids=( + stix_sighting["x_opencti_stix_ids"] + if "x_opencti_stix_ids" in stix_sighting + else None + ), update=update, ignore_dates=( stix_sighting["x_opencti_ignore_dates"] @@ -2403,7 +2427,7 @@ def import_item( # region Resolve the to to_ids = [] if "x_opencti_where_sighted_refs" in item: - for where_sighted_ref in item["_opencti_where_sighted_refs"]: + for where_sighted_ref in item["x_opencti_where_sighted_refs"]: to_ids.append(where_sighted_ref) elif "where_sighted_refs" in item: for where_sighted_ref in item["where_sighted_refs"]: @@ -2623,6 +2647,7 @@ def import_bundle( if "x_opencti_event_version" in stix_bundle else None ) + stix2_splitter = OpenCTIStix2Splitter() _, bundles = stix2_splitter.split_bundle_with_expectations( stix_bundle, False, event_version diff --git a/tests/01-unit/stix/__init__.py b/tests/01-unit/stix/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tests/01-unit/stix/test_bundle_ids_rewrite.py b/tests/01-unit/stix/test_bundle_ids_rewrite.py new file mode 100644 index 00000000..16143cec --- /dev/null +++ b/tests/01-unit/stix/test_bundle_ids_rewrite.py @@ -0,0 +1,112 @@ +import json + +from pycti import OpenCTIApiClient, OpenCTIStix2 + + +def get_cti_helper(): + client = OpenCTIApiClient( + "http://fake:4000", "fake", ssl_verify=False, perform_health_check=False + ) + return OpenCTIStix2(client) + + +def load_test_file(): + with open("tests/data/bundle_ids_sample.json", "r") as content_file: + content = content_file.read() + bundle_data = json.loads(content) + return bundle_data + + +# !! WARNING !!, this need to be changed along with 01-unit/domain/identifier-test.js +# fmt: off +def test_ids_generation(): + gen_id = get_cti_helper().generate_standard_id_from_stix + # attack-pattern + assert gen_id({"type": "attack-pattern", "name": "attack"}) =='attack-pattern--25f21617-8de8-5d5e-8cd4-b7e88547ba76' + assert gen_id({"type": "attack-pattern", "name": "attack", "x_opencti_external_id": 'MITREID'}) == 'attack-pattern--b74cfee2-7b14-585e-862f-fea45e802da9' + assert gen_id({"type": "attack-pattern", "name": "Spear phishing messages with malicious links", "x_mitre_id": 'T1368'}) == 'attack-pattern--a01046cc-192f-5d52-8e75-6e447fae3890' + assert gen_id({"type": "attack-pattern", "x_mitre_id": "MITREID"}) == 'attack-pattern--b74cfee2-7b14-585e-862f-fea45e802da9' + assert gen_id({"type": "attack-pattern", "name": "Evil Pattern!", "description": "Test Attack Pattern!"}) == 'attack-pattern--23a5b210-f675-5936-ae14-21327e9798e2' + # campaign + assert gen_id({"type": "campaign", "name": "attack"}) == 'campaign--25f21617-8de8-5d5e-8cd4-b7e88547ba76' + # note + assert gen_id({"type": "note", "content": "My note content!"}) == "note--2b4ab5af-2307-58e1-8862-a6a269aae798" + assert gen_id({"type": "note", "content": "My note content!", "created": "2022-11-25T19:00:05.000Z"}) == "note--10861e5c-049e-54f6-9736-81c106e39a0b" + # observed-data + assert gen_id({"type": "observed-data", "object_refs": ["id"]}) == "observed-data--4765c523-81bc-54c8-b1af-ee81d961dad1" + # opinion + assert gen_id({"type": "opinion", "opinion": "Good"}) == "opinion--0aef8829-207e-508b-b1f1-9da07f3379cb" + assert gen_id({"type": "opinion", "opinion": "Good", "created": "2022-11-25T19:00:05.000Z"}) == "opinion--941dbd61-c6b1-5290-b63f-19a38983d7f7" + # report + assert gen_id({"type": "report", "name": "Report", "published": "2022-11-25T19:00:05.000Z"}) == "report--761c6602-975f-5e5e-b220-7a2d41f33ce4" + # course-of-action + assert gen_id({"type": "course-of-action", "x_mitre_id": "MITREID"}) == "course-of-action--b74cfee2-7b14-585e-862f-fea45e802da9" + assert gen_id({"type": "course-of-action", "x_mitre_id": "MITREID", "name": "Name"}) == "course-of-action--b74cfee2-7b14-585e-862f-fea45e802da9" + assert gen_id({"type": "course-of-action", "name": "Name"}) == "course-of-action--e6e2ee8d-e54d-50cd-b77c-df8c8eea7726" + # identity + assert gen_id({"type": "identity", "name": "julien", "identity_class": "Individual"}) == "identity--d969b177-497f-598d-8428-b128c8f5f819" + assert gen_id({"type": "identity", "name": "julien", "identity_class": "Sector"}) == "identity--14ffa2a4-e16a-522a-937a-784c0ac1fab0" + assert gen_id({"type": "identity", "name": "julien", "identity_class": "System"}) == "identity--8af97482-121d-53f7-a533-9c48f06b5a38" + assert gen_id({"type": "identity", "name": "organization", "identity_class": "individual"}) == "identity--00f7eb8c-6af2-5ed5-9ede-ede4c623de3b" + # infrastructure + assert gen_id({"type": "infrastructure", "name": "infra"}) == "infrastructure--8a20116f-5a41-5508-ae4b-c293ac67c527" + # intrusion-set + assert gen_id({"type": "intrusion-set", "name": "intrusion"}) == "intrusion-set--30757026-c4bd-574d-ae52-8d8503b4818e" + # location + assert gen_id({"type": "location", "name": "Lyon", "x_opencti_location_type": "City"}) == "location--da430873-42c8-57ca-b08b-a797558c6cbd" + assert gen_id({"type": "location", "latitude": 5.12, "name": "Position1", "x_opencti_location_type": "Position"}) == "location--56b3fc50-5091-5f2e-bd19-7b40ee3881e4" + assert gen_id({"type": "location", "longitude": 5.12, "name": 'Position2', "x_opencti_location_type": "Position"}) == "location--dd2cf94c-1d58-58a1-b21f-0ede4059aaf0" + assert gen_id({"type": "location", "latitude": 5.12, "longitude": 5.12, "x_opencti_location_type": "Position"}) == "location--57acef55-747a-55ef-9c49-06ca85f8d749" + assert gen_id({"type": "location", "name": 'Position3', "x_opencti_location_type": "Position"}) == "location--a4152781-8721-5d44-ae2d-e492665bc35b" + # malware + assert gen_id({"type": "malware", "name": "malware"}) == "malware--92ddf766-b27c-5159-8f46-27002bba2f04" + # threat-actor-group + assert gen_id({"type": "threat-actor", "name": "CARD04"}) == "threat-actor--6d458783-df3b-5398-8e30-282655ad7b94" + assert gen_id({"type": "threat-actor", "name": "CARD04", "x_opencti_type": "Threat-Actor-Group"}) == "threat-actor--6d458783-df3b-5398-8e30-282655ad7b94" + # tool + assert gen_id({"type": "tool", "name": "my-tool"}) == "tool--41cd21d0-f50e-5e3d-83fc-447e0def97b7" + # vulnerability + assert gen_id({"type": "vulnerability", "name": "vulnerability"}) == "vulnerability--2c690168-aec3-57f1-8295-adf53f4dc3da" + # incident + assert gen_id({"type": "incident", "name": "incident", "created": "2022-11-25T19:00:05.000Z"}) == "incident--0e117c15-0a94-5ad3-b090-0395613f5b29" + # case-incident + assert gen_id({"type": "case-incident", "name": "case", "created": "2022-11-25T19:00:05.000Z"}) == "case-incident--4838a141-bd19-542c-85d9-cce0382645b5" + # case-rfi + assert gen_id({"type": "case-rfi", "name": "case", "created": "2022-11-25T19:00:05.000Z"}) == "case-rfi--4838a141-bd19-542c-85d9-cce0382645b5" + # case-rft + assert gen_id({"type": "case-rft", "name": "case", "created": "2022-11-25T19:00:05.000Z"}) == "case-rft--4838a141-bd19-542c-85d9-cce0382645b5" + # feedback, not supported yet + # assert gen_id("case-feedback", {"name": "case", "created": "2022-11-25T19:00:05.000Z"}) == "feedback--4838a141-bd19-542c-85d9-cce0382645b5" + # channel + assert gen_id({"type": "channel", "name": "channel"}) == "channel--4936cdd5-6b6a-5c92-a756-cae1f09dcd80" + # data-component + assert gen_id({"type": "data-component", "name": "data-component"}) == "data-component--32fdc52a-b4c5-5268-af2f-cdf820271f0b" + # data-source + assert gen_id({"type": "data-source", "name": "data-source"}) == "data-source--f0925972-35e1-5172-9161-4d7180908339" + # grouping + assert gen_id({"type": "grouping", "name": "grouping", "context": "context", "created": "2022-11-25T19:00:05.000Z"}) == "grouping--7c3e3534-9c09-568a-9485-377054b4c588" + # language + assert gen_id({"type": "language", "name": "fr"}) == "language--0ef28873-9d49-5cdb-a53a-eb7613391ee9" + # malware-analysis + assert gen_id({"type": "malware-analysis", "product": "linux", "result_name": "result"}) == "malware-analysis--3d501241-a4a5-574d-a503-301a6426f8c1" + assert gen_id({"type": "malware-analysis", "product": "linux", "result_name": "result", "submitted": "2022-11-25T19:00:05.000Z"}) == "malware-analysis--d7ffe68a-0d5f-5fea-a375-3338ba4ea13c" + # narrative + assert gen_id({"type": "narrative", "name": "narrative"}) == "narrative--804a7e40-d39c-59b6-9e3f-1ba1bc92b739" + # task + assert gen_id({"type": "task", "name": "case", "created": "2022-11-25T19:00:05.000Z"}) == "task--4838a141-bd19-542c-85d9-cce0382645b5" + # Threat-actor-individual + assert gen_id({"type": "threat-actor", "name": "CARD04", "x_opencti_type": "Threat-Actor-Individual"}) == "threat-actor--af15b6ae-a3dd-54d3-8fa0-3adfe0391d01" + # vocabulary + assert gen_id({"type": "vocabulary", "name": "facebook", "category": "account_type_ov"}) == "vocabulary--85ae7185-ff6f-509b-a011-3069921614aa" + # relationship + base_relationship = {"type": "relationship", "relationship_type": "based-on", "source_ref": "from_id", "target_ref": "to_id"} + assert gen_id(base_relationship) == "relationship--0b11fa67-da01-5d34-9864-67d4d71c3740" + assert gen_id({**base_relationship, "start_time": "2022-11-25T19:00:05.000Z"}) == "relationship--c5e1e2ce-14d6-535b-911d-267e92119e01" + assert gen_id({**base_relationship, "start_time": "2022-11-25T19:00:05.000Z", "stop_time": "2022-11-26T19:00:05.000Z"}) == "relationship--a7778a7d-a743-5193-9912-89f88f9ed0b4" + assert gen_id({"type": "relationship", 'relationship_type': 'uses', 'source_ref': 'malware--21c45dbe-54ec-5bb7-b8cd-9f27cc518714', 'start_time': '2020-02-29T22:30:00.000Z', 'stop_time': '2020-02-29T22:30:00.000Z', 'target_ref': 'attack-pattern--fd8179dd-1632-5ec8-8b93-d2ae121e05a4'}) == 'relationship--67f5f01f-6b15-5154-ae31-019a75fedcff' + # sighting + base_sighting = {"type": "sighting", "sighting_of_ref": "from_id", "where_sighted_refs": ["to_id"]} + assert gen_id(base_sighting) == 'sighting--161901df-21bb-527a-b96b-354119279fe2' + assert gen_id({**base_sighting, "first_seen": "2022-11-25T19:00:05.000Z"}) == "sighting--3c59ceea-8e41-5adb-a257-d070d19e6d2b" + assert gen_id({**base_sighting, "first_seen": "2022-11-25T19:00:05.000Z", "last_seen": "2022-11-26T19:00:05.000Z"}) == "sighting--b4d307b6-d22c-5f22-b530-876c298493da" +# fmt: on diff --git a/tests/data/bundle_ids_sample.json b/tests/data/bundle_ids_sample.json new file mode 100644 index 00000000..2e4af49e --- /dev/null +++ b/tests/data/bundle_ids_sample.json @@ -0,0 +1,89 @@ +{ + "type": "bundle", + "id": "bundle--8c939929-688f-4a72-badb-3dd1bd6af0fa", + "objects": [ + { + "id": "malware--d650c5b9-4b43-5781-8576-ea52bd6c7ce5", + "spec_version": "2.1", + "revoked": false, + "confidence": 100, + "created": "2024-03-13T09:56:18.259Z", + "modified": "2024-03-13T09:56:18.259Z", + "name": "BasicMalware", + "is_family": false, + "x_opencti_id": "75f2a512-fcc6-4cbc-a2ef-52ca9c57df46", + "x_opencti_type": "Malware", + "type": "malware" + }, + { + "id": "identity--7b82b010-b1c0-4dae-981f-7756374a17da", + "type": "identity", + "spec_version": "2.1", + "name": "ANSSI", + "identity_class": "organization", + "labels": ["identity"], + "created": "2020-02-23T23:40:53.575Z", + "modified": "2020-02-27T08:45:39.351Z", + "x_opencti_organization_type": "CSIRT" + }, + { + "id": "marking-definition--78ca4366-f5b8-4764-83f7-34ce38198e27", + "type": "marking-definition", + "spec_version": "2.1", + "definition_type": "TLP", + "definition": { + "TLP": "TLP:TEST" + }, + "created": "2020-02-25T09:02:29.040Z", + "modified": "2020-02-25T09:02:29.040Z", + "created_by_ref": "marking-definition--78ca4366-f5b8-4764-83f7-34ce38198e27" + }, + { + "id": "report--a445d22a-db0c-4b5d-9ec8-e9ad0b6dbdd7", + "type": "report", + "spec_version": "2.1", + "name": "A demo report for testing purposes", + "labels": ["report"], + "description": "Report for testing purposes (random data).", + "published": "2020-03-01T14:02:48.111Z", + "created": "2020-03-01T14:02:55.327Z", + "modified": "2020-03-01T14:09:48.078Z", + "report_types": ["threat-report"], + "x_opencti_report_status": 2, + "confidence": 3, + "created_by_ref": "identity--7b82b010-b1c0-4dae-981f-7756374a17da", + "object_marking_refs": ["marking-definition--78ca4366-f5b8-4764-83f7-34ce38198e27"], + "object_refs": [ + "observed-data--7d258c31-9a26-4543-aecb-2abc5ed366be", + "malware--d650c5b9-4b43-5781-8576-ea52bd6c7ce5" + ] + }, + { + "id": "relationship--ba52fced-422a-4bee-816a-85aa21c9eacc", + "type": "relationship", + "spec_version": "2.1", + "relationship_type": "related-to", + "source_ref": "malware--d650c5b9-4b43-5781-8576-ea52bd6c7ce5", + "target_ref": "report--a445d22a-db0c-4b5d-9ec8-e9ad0b6dbdd7", + "created": "2020-03-01T14:07:14.316Z", + "modified": "2020-03-01T14:07:14.316Z", + "start_time": "1900-01-01T00:00:00.000Z", + "stop_time": "1900-01-01T00:00:00.000Z", + "created_by_ref": "identity--7b82b010-b1c0-4dae-981f-7756374a17da", + "object_marking_refs": ["marking-definition--78ca4366-f5b8-4764-83f7-34ce38198e27"] + }, + { + "type": "sighting", + "spec_version": "2.1", + "id": "sighting--ee20065d-2555-424f-ad9e-0f8428623c75", + "created": "2016-08-06T20:08:31.000Z", + "modified": "2016-09-06T20:08:31.000Z", + "sighting_of_ref": "malware--d650c5b9-4b43-5781-8576-ea52bd6c7ce5", + "where_sighted_refs": ["identity--7b82b010-b1c0-4dae-981f-7756374a17da"], + "first_seen": "2016-08-06T20:08:31.000Z", + "last_seen": "2016-08-07T20:08:31.000Z", + "count": 12, + "x_opencti_negative": true + } + ] +}