import demistomock as demisto # noqa: F401 from CommonServerPython import * # noqa: F401 """Orion API Integration for Cortex XSOAR (aka Demisto) API Documentation: """ """ IMPORTS """ import json from time import sleep from typing import Any, cast """ CONSTANTS """ INTEGRATION_NAME = "Orion" COMMAND_PREFIX = "omw" INTEGRATION_ENTRY_CONTEXT = "Orion" class Client(BaseClient): def __init__(self, base_url: str, insecure: bool, password: str, proxy: bool, feedReliability: str): self.reliability = DBotScoreReliability.get_dbot_score_reliability_from_str(feedReliability) self.force = False self.base_url = base_url if password is None: raise Exception("Please enter your Orion Malware API key.") super().__init__(f"{self.base_url}orion/api/v4.0/", verify=not insecure, proxy=proxy, headers={"apikey": password}) def analyze_hash(self, hash_data: str) -> dict: results: dict[str, Any] = {} try: report_id = self.get_report_id_from_hash(hash_data) if report_id == "": results["error"] = f"No reports for this Hash: {hash_data}" return results except Exception as e: results["error"] = f"Error in getting report_id from hash: {str(e)}" return results try: results["report"] = self.get_report_lite(report_id) results["report_url"] = self.base_url + "report/file/" + report_id except Exception as e: results["error"] = f"Error in report: {str(e)}" return results def analyze_file(self, filepath: str, filename: str): """""" results: dict[str, Any] = {} with open(filepath, "rb") as f: data = f.read() form_data = {"json": json.dumps({"filename": filename, "client_version": 1, "visibility": "group", "force": self.force})} files_data = {"data": (None, data)} try: res = self.submit_task(form_data, files_data) if "task" in res: task_id = res["task"]["$oid"] else: results["error"] = json.dumps(res) return results except Exception as e: results["error"] = f"Error in upload: {str(e)}" return results try: while self.status_task(task_id): sleep(2) report_id = self.get_report_id(task_id) except Exception as e: results["error"] = f"Error in task status: {str(e)}" return results try: results["report"] = self.get_report_lite(report_id) results["report_url"] = self.base_url + "report/file/" + report_id except Exception as e: results["error"] = f"Error in report: {str(e)}" return results def omw_status(self) -> str: res = self._http_request("GET", "status", ok_codes=(401, 200)) if res == {}: return "ok" else: return json.dumps(res) def status_task(self, task_id: str) -> bool: res = self._http_request("GET", f"tasks/id={task_id}", ok_codes=(200, 404)) return res["task"]["status"] in [0, 1] def get_report_id(self, task_id: str) -> str: res = self._http_request("GET", f"tasks/id={task_id}", ok_codes=(200, 404)) return res["task"]["report_id"] def get_report_lite(self, report_id: str) -> dict: res = self._http_request("GET", f"filereports/{report_id}/overview", ok_codes=(200, 404)) return res def get_report_id_from_hash(self, hash_data: str) -> str: res = self._http_request("GET", f"tasks/{hash_data}", ok_codes=(200, 404)) if res and len(res["tasks"]) > 0: return res["tasks"][0]["report_id"] return "" def submit_task(self, json_data: dict, files_data: dict) -> dict: res = self._http_request("POST", "tasks", data=json_data, files=files_data) return res class OrionFile(Common.File): """Orion File Indicator.""" def __init__( self, dbot_score, count_orion_vendors_which_flagged_malicious=None, orion_vendors_which_flagged_malicious=None, orion_detection_names=None, **kwargs, ): super().__init__(dbot_score, **kwargs) self.count_orion_vendors_which_flagged_malicious = count_orion_vendors_which_flagged_malicious self.orion_vendors_which_flagged_malicious = orion_vendors_which_flagged_malicious self.orion_detection_names = orion_detection_names def to_context(self): context = super().to_context() file_context = context[super().CONTEXT_PATH] file_context["Orion"] = {} # create Orion Engine Detection Names dans Indicator fields # create Orion Engine Detections dans Indicator fields # create Orion Engine Vendors dans Indicator fields # Mapper les indicator fields dans l'indicator type File avec les champs File.Orion.x if self.count_orion_vendors_which_flagged_malicious is not None: file_context["Orion"]["EngineDetections"] = self.count_orion_vendors_which_flagged_malicious if self.orion_vendors_which_flagged_malicious is not None: file_context["Orion"]["EngineVendors"] = self.orion_vendors_which_flagged_malicious if self.orion_detection_names is not None: file_context["Orion"]["EngineDetectionNames"] = self.orion_detection_names if not file_context["Orion"]: file_context.pop("Orion", None) return context def raise_if_hash_not_valid(file_hash: str): """Raises an error if file_hash is not valid Args: file_hash: file hash Raises: ValueError: if hash is not of type SHA-256, SHA-1 or MD5self.base_url Examples: >>> raise_if_hash_not_valid('not a hash') Traceback (most recent call last): ... ValueError: Hash "not a hash" is not of type SHA-256, SHA-1 or MD5 >>> raise_if_hash_not_valid('7e641f6b9706d860baf09fe418b6cc87') """ if get_hash_type(file_hash) not in ("sha256", "sha1", "md5"): raise ValueError(f'Hash "{file_hash}" is not of type SHA-256, SHA-1 or MD5') def get_file_context(entry_id: str) -> dict: """Gets a File object from context. Args: entry_id: The entry ID of the file Returns: File object contains Name, Hashes and more information """ context = demisto.dt(demisto.context(), f'File(val.EntryID === "{entry_id}")') if not context: return {} if isinstance(context, list): return context[0] return context def detect_dbot_type_from_network(address: str): dbot_type = None try: # export ip and hostnames if not address.startswith("<"): if is_ip_valid(address): dbot_type = "ip" elif "." in address: dbot_type = "domain" else: dbot_type = "hostname" except Exception: pass return dbot_type def _get_file_indicator(client: Client, file_hash: str, orion_report: dict): identification: dict[str, Any] = orion_report.get("identification", {}) level = orion_report["risk"]["level"] score = 0 match level: case "Safe": score = 1 case "Low": score = 1 case "Medium": score = 2 case "High": score = 3 case "Severe": score = 3 orion_vendors_which_flagged_malicious = {x["engine_name"]: x["threat_name"] for x in orion_report.get("threat_analysis", [])} return OrionFile( dbot_score=Common.DBotScore( file_hash, DBotScoreType.FILE, integration_name=INTEGRATION_NAME, score=score, malicious_description="test", # modify reliability=client.reliability, ), count_orion_vendors_which_flagged_malicious=len(orion_vendors_which_flagged_malicious), orion_vendors_which_flagged_malicious=list(orion_vendors_which_flagged_malicious.keys()), orion_detection_names=list(orion_vendors_which_flagged_malicious.values()), name=(identification or {}).get("filename"), size=(identification or {}).get("size"), sha1=(identification or {}).get("sha1"), sha256=(identification or {}).get("sha256"), file_type=(identification or {}).get("type"), md5=(identification or {}).get("md5"), ) def build_hash_output( client: Client, file_hash: str, raw_response: dict, ) -> CommandResults: orion_report = raw_response["report"]["filereport"] orion_report["report_url"] = raw_response["report_url"] file_indicator = _get_file_indicator(client, file_hash, orion_report) indicator_relationships = [] for net in orion_report["networks"]: try: if "address" in net: dbot_type = detect_dbot_type_from_network(net["address"]) if dbot_type == "ip": relationship = EntityRelationship( entity_a=file_hash, entity_a_type=FeedIndicatorType.File, entity_b=net["address"], entity_b_type=FeedIndicatorType.IP, name=EntityRelationship.Relationships.USES, source_reliability=client.reliability, brand=INTEGRATION_NAME, ) elif dbot_type == "domain": relationship = EntityRelationship( entity_a=file_hash, entity_a_type=FeedIndicatorType.File, entity_b=net["address"], entity_b_type=FeedIndicatorType.Domain, name=EntityRelationship.Relationships.USES, source_reliability=client.reliability, brand=INTEGRATION_NAME, ) elif dbot_type == "hostname": relationship = EntityRelationship( entity_a=file_hash, entity_a_type=FeedIndicatorType.File, entity_b=net["address"], entity_b_type=FeedIndicatorType.Host, name=EntityRelationship.Relationships.USES, source_reliability=client.reliability, brand=INTEGRATION_NAME, ) else: pass indicator_relationships.append(relationship) except Exception as e: demisto.debug(f"{e}") demisto.debug(f'Error creating network relationships: {file_hash}, {net["address"]}') for ttp in orion_report["matched_mitre_attacks"]: try: relationship = EntityRelationship( entity_a=file_hash, entity_a_type=FeedIndicatorType.File, entity_b=ttp["id"], entity_b_type=ThreatIntel.ObjectsNames.ATTACK_PATTERN, name=EntityRelationship.Relationships.USES, source_reliability=client.reliability, brand=INTEGRATION_NAME, ) indicator_relationships.append(relationship) except Exception as e: demisto.debug(f"{e}") demisto.debug(f'Error creating ttp relationships: {ttp["id"]}') demisto.debug("returning command result") command_results = CommandResults( outputs_prefix=f"{INTEGRATION_ENTRY_CONTEXT}.File", outputs_key_field="tasks_tree.report_id", indicator=file_indicator, outputs=orion_report, relationships=indicator_relationships, ) return command_results def check_module(client: Client) -> str: """ 1 API Call """ return client.omw_status() def upload_file(client: Client, args: dict) -> list[CommandResults]: """ 1 API Call """ entry_ids = argToList(args.get("entryID")) if len(entry_ids) > 1: raise DemistoException("You can supply only one entry ID.") results: list[CommandResults] = [] execution_metrics = ExecutionMetrics() for entry_id in entry_ids: try: file_obj = demisto.getFilePath(entry_id) file_path = file_obj["path"] file_name = file_obj["name"] raw_response = client.analyze_file(file_path, file_name) error = raw_response.get("error", {}) if error != {}: execution_metrics.quota_error += 1 results.append(CommandResults(readable_output=error)) continue # build Command results from analyze_hash. less complex than VT else: results.append( build_hash_output(client, raw_response["report"]["filereport"]["identification"]["md5"], raw_response) ) execution_metrics.success += 1 except Exception as exc: err = f"Could not process {entry_id=}.\n{str(exc)}" demisto.debug(err) demisto.results({"Type": entryTypes["error"], "ContentsFormat": formats["text"], "Contents": err}) if execution_metrics.is_supported(): _metric_results = execution_metrics.metrics metric_results = cast(CommandResults, _metric_results) results.append(metric_results) return results def file_scan_command(client: Client, args: dict) -> list[CommandResults]: """ 1 API Call """ return upload_file(client, args) def hash_scan_command(client: Client, args: dict) -> list[CommandResults]: """ 1 API Call """ hashes = argToList(args.get("file_hash")) results: list[CommandResults] = [] execution_metrics = ExecutionMetrics() for hash in hashes: hash = hash.lower() raise_if_hash_not_valid(hash) try: raw_response = client.analyze_hash(hash) error = raw_response.get("error", {}) if error != {}: execution_metrics.quota_error += 1 results.append(CommandResults(readable_output=error)) continue # build Command results from analyze_hash. less complex than VT else: results.append(build_hash_output(client, hash, raw_response)) execution_metrics.success += 1 except Exception as exc: # If anything happens, just keep going demisto.debug(f'Could not process hash: "{hash}"\n {str(exc)}') execution_metrics.general_error += 1 results.append(CommandResults(readable_output=f'Could not process hash: "{hash}"\n {str(exc)}')) continue if execution_metrics.is_supported(): _metric_results = execution_metrics.metrics metric_results = cast(CommandResults, _metric_results) results.append(metric_results) return results def main(): params = demisto.params() args = demisto.args() command = demisto.command() results: Union[CommandResults, str, list[CommandResults]] handle_proxy() feedReliability = params.get("feedReliability") base_url = params.get("base_url") insecure = argToBoolean(params.get("insecure", False)) password = params.get("api_key", {}).get("password", None) proxy = argToBoolean(params.get("proxy", False)) try: client = Client(base_url=base_url, insecure=insecure, password=password, proxy=proxy, feedReliability=feedReliability) demisto.debug(f"Command called {command}") if command == "test-module": results = check_module(client) elif command == "hash-scan": results = hash_scan_command(client, args) elif command == "file-scan": results = file_scan_command(client, args) else: raise NotImplementedError(f"Command {command} not implemented") return_results(results) except Exception as e: return_error(f"Failed to execute {command} command. Error: {str(e)}") if __name__ in ("builtins", "__builtin__", "__main__"): main()