From e7678ffc203db8a6be8181f806131e0522bb6c12 Mon Sep 17 00:00:00 2001 From: Julien Richard Date: Tue, 28 May 2024 18:22:50 +0200 Subject: [PATCH] [client] Limit stix_ids explosion by rewriting the standard_id in client python (#659) --- pycti/connector/opencti_connector_helper.py | 13 ++ pycti/entities/opencti_attack_pattern.py | 7 +- 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 | 4 + 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 + 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 | 150 ++++++++++++------ tests/01-unit/stix/__init__.py | 0 tests/01-unit/stix/test_bundle_ids_rewrite.py | 136 ++++++++++++++++ tests/data/bundle_ids_sample.json | 76 +++++++++ 41 files changed, 544 insertions(+), 71 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/connector/opencti_connector_helper.py b/pycti/connector/opencti_connector_helper.py index 0af8c9a1..e2106e03 100644 --- a/pycti/connector/opencti_connector_helper.py +++ b/pycti/connector/opencti_connector_helper.py @@ -732,6 +732,13 @@ def __init__(self, config: Dict, playbook_compatible=False) -> None: False, True, ) + self.keep_original_id = get_config_variable( + "CONNECTOR_KEEP_ORIGINAL_ID", + ["connector", "keep_original_id"], + config, + False, + False, + ) self.bundle_send_to_directory = get_config_variable( "CONNECTOR_SEND_TO_DIRECTORY", ["connector", "send_to_directory"], @@ -1103,6 +1110,7 @@ def send_stix2_bundle(self, bundle: str, **kwargs) -> list: entity_id = kwargs.get("entity_id", None) file_name = kwargs.get("file_name", None) bundle_send_to_queue = kwargs.get("send_to_queue", self.bundle_send_to_queue) + keep_original_id = kwargs.get("keep_original_id", self.keep_original_id) bundle_send_to_directory = kwargs.get( "send_to_directory", self.bundle_send_to_directory ) @@ -1113,6 +1121,11 @@ def send_stix2_bundle(self, bundle: str, **kwargs) -> list: "send_to_directory_retention", self.bundle_send_to_directory_retention ) + # Bundle ids must be rewritten + bundle = self.api.stix2.prepare_bundle_ids( + bundle=bundle, use_json=True, keep_original_id=keep_original_id + ) + # In case of enrichment ingestion, ensure the sharing if needed if self.enrichment_shared_organizations is not None: # Every element of the bundle must be enriched with the same organizations diff --git a/pycti/entities/opencti_attack_pattern.py b/pycti/entities/opencti_attack_pattern.py index 7363a84f..09ce757b 100644 --- a/pycti/entities/opencti_attack_pattern.py +++ b/pycti/entities/opencti_attack_pattern.py @@ -222,15 +222,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 "attack-pattern--" + id + @staticmethod + def generate_id_from_data(data): + return AttackPattern.generate_id(data.get("name"), data.get("x_mitre_id")) + """ List Attack-Pattern objects diff --git a/pycti/entities/opencti_campaign.py b/pycti/entities/opencti_campaign.py index fea3f10e..6bfc9535 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 feaa15d7..101d4229 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 705cd019..3cb4057c 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 af273b1a..44e90ce0 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 8caeeb21..7be347ea 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 6ee5c5b4..a5c5cf16 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 905fdf65..cdfb557f 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 eaf93956..09fb3cb7 100644 --- a/pycti/entities/opencti_grouping.py +++ b/pycti/entities/opencti_grouping.py @@ -403,6 +403,10 @@ def generate_id(name, context): 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"]) + """ List Grouping objects diff --git a/pycti/entities/opencti_identity.py b/pycti/entities/opencti_identity.py index a9fdeb0d..2062fb7b 100644 --- a/pycti/entities/opencti_identity.py +++ b/pycti/entities/opencti_identity.py @@ -216,12 +216,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 20e9a7c9..6cc4a0c3 100644 --- a/pycti/entities/opencti_indicator.py +++ b/pycti/entities/opencti_indicator.py @@ -278,6 +278,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 ab90d3bb..6bfac1d3 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 adab77d9..a5e16a8e 100644 --- a/pycti/entities/opencti_kill_chain_phase.py +++ b/pycti/entities/opencti_kill_chain_phase.py @@ -30,6 +30,10 @@ def generate_id(phase_name, kill_chain_name): id = str(uuid.uuid5(uuid.UUID("00abedb4-aa42-466c-9c01-fed23315a9b7"), data)) return "kill-chain-phase--" + id + @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 dcee99ae..ae277087 100644 --- a/pycti/entities/opencti_location.py +++ b/pycti/entities/opencti_location.py @@ -200,15 +200,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 39391785..c91e171c 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..5740f9f8 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, submitted): 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 69ef1419..5153e23e 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 790c8c1a..43fe3c68 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 04a77472..c03f57de 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 366efc08..cae3d833 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_task.py b/pycti/entities/opencti_task.py index 81858056..3ba7ee3b 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 7ad47d47..35bf8a38 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 ef6377cc..b97a50c2 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 24b9e5e7..857b6df4 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 48a57673..4721fff5 100644 --- a/pycti/entities/opencti_vulnerability.py +++ b/pycti/entities/opencti_vulnerability.py @@ -128,6 +128,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 6831f7f2..a61c6459 100644 --- a/pycti/utils/opencti_stix2.py +++ b/pycti/utils/opencti_stix2.py @@ -853,6 +853,50 @@ def get_reader(self, entity_type: str): # endregion + def get_stix_helper(self): + # Import + return { + "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, + } + # region import def import_object( self, stix_object: Dict, update: bool = False, types: List = None @@ -899,53 +943,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 @@ -2393,6 +2400,49 @@ def export_selected( return bundle + def prepare_bundle_ids(self, bundle, use_json=True, keep_original_id=False): + if use_json: + try: + bundle_data = json.loads(bundle) + except: + raise Exception("File data is not a valid JSON") + else: + bundle_data = bundle + cache_ids = {} + # First iteration to cache all entity ids + stix_helpers = self.get_stix_helper() + for item in bundle_data["objects"]: + if item["type"] != "relationship" and item["type"] != "sighting": + helper = stix_helpers.get(item["type"]) + if hasattr(helper, "generate_id_from_data"): + standard_id = helper.generate_id_from_data(item) + cache_ids[item["id"]] = standard_id + # Second iteration to replace and remap + for item in bundle_data["objects"]: + # For entities, try to replace the main id + # Keep the current one if needed + if item["type"] != "relationship" and item["type"] != "sighting": + if cache_ids.get(item["id"]): + original_id = item["id"] + item["id"] = cache_ids[original_id] + if keep_original_id: + item["x_opencti_stix_ids"] = item.get( + "x_opencti_stix_ids", [] + ) + [original_id] + # For all elements, replace all refs (source_ref, object_refs, ...) + ref_keys = list( + filter(lambda i: i.endswith("_ref") or i.endswith("_refs"), item.keys()) + ) + for ref_key in ref_keys: + if ref_key.endswith("_refs"): + item[ref_key] = list( + map(lambda id_ref: cache_ids.get(id_ref, id_ref), item[ref_key]) + ) + else: + item[ref_key] = cache_ids.get(item[ref_key], item[ref_key]) + + return json.dumps(bundle_data) if use_json else bundle_data + def import_item( self, item, @@ -2633,6 +2683,12 @@ def import_bundle( if "x_opencti_event_version" in stix_bundle else None ) + + # Bundle ids must be rewritten + stix_bundle = self.prepare_bundle_ids( + bundle=stix_bundle, use_json=False, keep_original_id=False + ) + stix2_splitter = OpenCTIStix2Splitter() try: bundles = stix2_splitter.split_bundle(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..df6cb338 --- /dev/null +++ b/tests/01-unit/stix/test_bundle_ids_rewrite.py @@ -0,0 +1,136 @@ +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("../../data/bundle_ids_sample.json", "r") as content_file: + content = content_file.read() + bundle_data = json.loads(content) + return bundle_data + + +def gen_id(data_type, data): + helper = get_cti_helper() + stix_helpers = helper.get_stix_helper() + helper = stix_helpers.get(data_type) + return helper.generate_id_from_data(data) + + +# fmt: off +def test_ids_generation(): + # attack-pattern + assert gen_id("attack-pattern", {"name": "attack"}) =='attack-pattern--25f21617-8de8-5d5e-8cd4-b7e88547ba76' + assert gen_id("attack-pattern", {"name": "attack", "x_mitre_id": 'MITREID'}) == 'attack-pattern--b74cfee2-7b14-585e-862f-fea45e802da9' + assert gen_id("attack-pattern", {"x_mitre_id": "MITREID"}) == 'attack-pattern--b74cfee2-7b14-585e-862f-fea45e802da9' + # campaign + assert gen_id("campaign", {"name": "attack"}) == 'campaign--25f21617-8de8-5d5e-8cd4-b7e88547ba76' + # note + assert gen_id("note", {"content": "My note content!"}) == "note--2b4ab5af-2307-58e1-8862-a6a269aae798" + assert gen_id("note", {"content": "My note content!", "created": "2022-11-25T19:00:05.000Z"}) == "note--10861e5c-049e-54f6-9736-81c106e39a0b" + # observed-data + assert gen_id("observed-data", {"object_refs": ["id"]}) == "observed-data--4765c523-81bc-54c8-b1af-ee81d961dad1" + # opinion + assert gen_id("opinion", {"opinion": "Good"}) == "opinion--0aef8829-207e-508b-b1f1-9da07f3379cb" + assert gen_id("opinion", {"opinion": "Good", "created": "2022-11-25T19:00:05.000Z"}) == "opinion--941dbd61-c6b1-5290-b63f-19a38983d7f7" + # report + assert gen_id("report", {"name": "Report", "published": "2022-11-25T19:00:05.000Z"}) == "report--761c6602-975f-5e5e-b220-7a2d41f33ce4" + # course-of-action + assert gen_id("course-of-action", {"x_mitre_id": "MITREID"}) == "course-of-action--b74cfee2-7b14-585e-862f-fea45e802da9" + assert gen_id("course-of-action", {"x_mitre_id": "MITREID", "name": "Name"}) == "course-of-action--b74cfee2-7b14-585e-862f-fea45e802da9" + assert gen_id("course-of-action", {"name": "Name"}) == "course-of-action--e6e2ee8d-e54d-50cd-b77c-df8c8eea7726" + # identity + assert gen_id("identity", {"name": "julien", "identity_class": "Individual"}) == "identity--d969b177-497f-598d-8428-b128c8f5f819" + assert gen_id("identity", {"name": "julien", "identity_class": "Sector"}) == "identity--14ffa2a4-e16a-522a-937a-784c0ac1fab0" + assert gen_id("identity", {"name": "julien", "identity_class": "System"}) == "identity--8af97482-121d-53f7-a533-9c48f06b5a38" + assert gen_id("identity", {"name": "organization", "identity_class": "individual"}) == "identity--00f7eb8c-6af2-5ed5-9ede-ede4c623de3b" + # infrastructure + assert gen_id("infrastructure", {"name": "infra"}) == "infrastructure--8a20116f-5a41-5508-ae4b-c293ac67c527" + # intrusion-set + assert gen_id("intrusion-set", {"name": "intrusion"}) == "intrusion-set--30757026-c4bd-574d-ae52-8d8503b4818e" + # location + assert gen_id("location", {"name": "Lyon", "x_opencti_location_type": "City"}) == "location--da430873-42c8-57ca-b08b-a797558c6cbd" + assert gen_id("location", {"latitude": 5.12, "name": "Position1", "x_opencti_location_type": "Position"}) == "location--56b3fc50-5091-5f2e-bd19-7b40ee3881e4" + assert gen_id("location", {"longitude": 5.12, "name": 'Position2', "x_opencti_location_type": "Position"}) == "location--dd2cf94c-1d58-58a1-b21f-0ede4059aaf0" + assert gen_id("location", {"latitude": 5.12, "longitude": 5.12, "x_opencti_location_type": "Position"}) == "location--57acef55-747a-55ef-9c49-06ca85f8d749" + assert gen_id("location", {"name": 'Position3', "x_opencti_location_type": "Position"}) == "location--a4152781-8721-5d44-ae2d-e492665bc35b" + # malware + assert gen_id("malware", {"name": "malware"}) == "malware--92ddf766-b27c-5159-8f46-27002bba2f04" + # threat-actor-group + assert gen_id("threat-actor", {"name": "CARD04"}) == "threat-actor--6d458783-df3b-5398-8e30-282655ad7b94" + assert gen_id("threat-actor", {"name": "CARD04", "x_opencti_type": "Threat-Actor-Group"}) == "threat-actor--6d458783-df3b-5398-8e30-282655ad7b94" + # tool + assert gen_id("tool", {"name": "my-tool"}) == "tool--41cd21d0-f50e-5e3d-83fc-447e0def97b7" + # vulnerability + assert gen_id("vulnerability", {"name": "vulnerability"}) == "vulnerability--2c690168-aec3-57f1-8295-adf53f4dc3da" + # incident + assert gen_id("incident", {"name": "incident", "created": "2022-11-25T19:00:05.000Z"}) == "incident--0e117c15-0a94-5ad3-b090-0395613f5b29" + # case-incident + assert gen_id("case-incident", {"name": "case", "created": "2022-11-25T19:00:05.000Z"}) == "case-incident--4838a141-bd19-542c-85d9-cce0382645b5" + # case-rfi + assert gen_id("case-rfi", {"name": "case", "created": "2022-11-25T19:00:05.000Z"}) == "case-rfi--4838a141-bd19-542c-85d9-cce0382645b5" + # case-rft + assert gen_id("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("channel", {"name": "channel"}) == "channel--4936cdd5-6b6a-5c92-a756-cae1f09dcd80" + # data-component + assert gen_id("data-component", {"name": "data-component"}) == "data-component--32fdc52a-b4c5-5268-af2f-cdf820271f0b" + # data-source + assert gen_id("data-source", {"name": "data-source"}) == "data-source--f0925972-35e1-5172-9161-4d7180908339" + # grouping + assert gen_id("grouping", {"name": "grouping", "context": "context"}) == "grouping--8462bd42-4cad-54ae-a261-efc1a762d83d" + # language + assert gen_id("language", {"name": "fr"}) == "language--0ef28873-9d49-5cdb-a53a-eb7613391ee9" + # malware-analysis + assert gen_id("malware-analysis", {"product": "linux", "result_name": "result"}) == "malware-analysis--3d501241-a4a5-574d-a503-301a6426f8c1" + assert gen_id("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("narrative", {"name": "narrative"}) == "narrative--804a7e40-d39c-59b6-9e3f-1ba1bc92b739" + # task + assert gen_id("task", {"name": "case", "created": "2022-11-25T19:00:05.000Z"}) == "task--4838a141-bd19-542c-85d9-cce0382645b5" + # Threat-actor-individual + assert gen_id("threat-actor", {"name": "CARD04", "x_opencti_type": "Threat-Actor-Individual"}) == "threat-actor--af15b6ae-a3dd-54d3-8fa0-3adfe0391d01" + # vocabulary + assert gen_id("vocabulary", {"name": "facebook", "category": "account_type_ov"}) == "vocabulary--85ae7185-ff6f-509b-a011-3069921614aa" +# fmt: on + + +def test_prepare_bundle_ids_keep_original(): + helper = get_cti_helper() + bundle_data = load_test_file() + malware_source = bundle_data["objects"][0] + assert malware_source["id"] == "malware--d650c5b9-4b43-5781-8576-ea52bd6c7ce5" + assert malware_source.get("x_opencti_stix_ids") is None + prepared_bundle = helper.prepare_bundle_ids( + bundle=bundle_data, use_json=False, keep_original_id=True + ) + print(json.dumps(prepared_bundle)) + malware_target = prepared_bundle["objects"][0] + assert malware_target["id"] == "malware--d650c5b9-4b43-5781-8576-ea52bd6c7ce0" + assert malware_target.get("x_opencti_stix_ids") == [ + "malware--d650c5b9-4b43-5781-8576-ea52bd6c7ce5" + ] + + +def test_prepare_bundle_ids(): + helper = get_cti_helper() + bundle_data = load_test_file() + malware_source = bundle_data["objects"][0] + assert malware_source["id"] == "malware--d650c5b9-4b43-5781-8576-ea52bd6c7ce5" + assert malware_source.get("x_opencti_stix_ids") is None + prepared_bundle = helper.prepare_bundle_ids( + bundle=bundle_data, use_json=False, keep_original_id=False + ) + print(json.dumps(prepared_bundle)) + malware_target = prepared_bundle["objects"][0] + assert malware_target["id"] == "malware--d650c5b9-4b43-5781-8576-ea52bd6c7ce0" + assert malware_target.get("x_opencti_stix_ids") is None diff --git a/tests/data/bundle_ids_sample.json b/tests/data/bundle_ids_sample.json new file mode 100644 index 00000000..7f26d27d --- /dev/null +++ b/tests/data/bundle_ids_sample.json @@ -0,0 +1,76 @@ +{ + "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-85aa21c9eaca", + "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"] + } + ] +}