{title}: {case_data[field]}
" + + tags = DEFAULT_TAGS.copy() + case_data.get("tags", []) + confidence = CONFIDENCE_MAP.get(case_data.get("severity")) + case_data["severity"] = SEVERITY_MAP.get(case_data.get("severity")) + + case_tag_fields = [ + ("caseId", "Case ID"), + ("severity", "Severity"), + ("impactStatus", "Impact Status"), + ("resolutionStatus", "Resolution Status"), + ("status", "Status"), + ("stage", "Stage"), + ("owner", "Owner"), + ] + for tag_field, title in case_tag_fields: + value = case_data.get(tag_field) + if value: + tags.append(f"{title}: {value}") + description += f"{title}: {value}
" + + # PROCESS TLP + case_tlp = case_data.get("tlp", None) + if case_tlp and TLP_PAP_MAP.get(case_tlp): + case_tlp = TLP_PAP_MAP[case_tlp] + + # PROCESS PAP + case_pap = case_data.get("pap", None) + if case_pap and TLP_PAP_MAP.get(case_pap): + tags.append(f"PAP: {TLP_PAP_MAP[case_pap]}") + + # deduplicate tags + tags = list(set(tags)) + + _id = "{{https://thehive-project.org}}report-{}".format( + str(uuid.uuid5(uuid.NAMESPACE_X500, case_data.get("id"))) + ) + + report = { + "data": { + "id": _id, + "title": f"{case_data.get('title')} - {case_data.get('caseId')}", + "description": description, + "type": "report", + }, + "meta": { + "estimated_observed_time": self.format_time( + case_data.get("updatedAt", None) + ), + "estimated_threat_start_time": self.format_time( + case_data.get("startDate", None) + ), + "tags": tags, + "tlp_color": case_tlp, + }, + "sources": [{"source_id": source_id}], + } + + if confidence: + report["data"]["confidence"] = dict(type="confidence", value=confidence) + return report + + def make_indicator(self, hive_data, source_id): + if not self.convert_eiq_observable_type(hive_data.get("dataType")): + self.error("Unsupported IOC type") + return None + + ioc_value = hive_data.get("data", None) + description = "" + tags = DEFAULT_TAGS.copy() + hive_data.get("tags", []) + + observable_type = hive_data.get("_type", None) + if observable_type is not None: + tags.append(observable_type) + description += f"Type: {observable_type}
" + + observable_id = hive_data.get("id", None) + if observable_id is not None: + tags.append("Observable ID: {}".format(observable_id)) + description += f"Observable ID: {observable_id}
" + + sighted = hive_data.get("sighted", None) + if sighted is True: + tags.append("Sighted") + description += f"Sighted: True
" + + # PROCESS TLP + tlp = hive_data.get("tlp", None) + tlp_color = TLP_PAP_MAP.get(tlp, None) if tlp else None + + # PROCESS PAP + pap = hive_data.get("pap", None) + if pap and TLP_PAP_MAP.get(pap): + tags.append(f"PAP: {TLP_PAP_MAP[pap]}") + + # deduplicate tags + tags = list(set(tags)) + + _id = "{{https://thehive-project.org}}indicator-{}".format( + str(uuid.uuid5(uuid.NAMESPACE_X500, hive_data["id"])) + ) + + indicator = { + "data": { + "id": _id, + "title": ioc_value, # use the main value as the title + "description": description, # use hive description fields combined + "type": "indicator", + "extracts": [ + { + "kind": self.convert_eiq_observable_type( + hive_data.get("dataType") + ), + "value": ioc_value, + } + ], + }, + "meta": { + "estimated_observed_time": self.format_time( + hive_data.get("updatedAt", None) + ), + "estimated_threat_start_time": self.format_time( + hive_data.get("startDate", None) + ), + "tags": tags, + "tlp_color": tlp_color, + }, + "sources": [{"source_id": source_id}], + } + return indicator + + def get_group_source_id(self): + response = requests.get( + self.eiq_host_url + "/private/groups/", + params=f"filter[name]={self.group_name}", + headers=self.headers, + ) + if response.status_code != 200: + return None + return response.json()["data"][0]["source"] + + def create_relation(self, entity_dict, source_id): + report_id = entity_dict.get("report") + indicator_id = entity_dict.get("indicator") + if not report_id or not indicator_id: + return None + + relation_id = str(uuid.uuid5(uuid.NAMESPACE_X500, f"{report_id}-{indicator_id}")) + relationship = { + "data": [ + { + "id": relation_id, + "data": { + "source": report_id, + "key": "reports", + "target": indicator_id, + }, + "sources": [source_id], + } + ] + } + + response = requests.put( + self.eiq_host_url + "/api/v2/relationships", + json=relationship, + headers=self.headers, + ) + return response + + def run(self): + try: + Responder.run(self) + + hive_data = self.get_param("data") + _type = hive_data.get("_type") + if _type not in ["case", "case_artifact"]: + self.error("Responder not supported") + # FIXME: should we return None here? + case_data = hive_data if _type == "case" else hive_data.get("case") + + source_id = self.get_group_source_id() + if not source_id: + self.error("Invalid Group name") + return + + report = self.make_report(case_data, source_id) + + indicator = None + if _type == "case_artifact": + indicator = self.make_indicator(hive_data, source_id) + if not indicator: + self.error("Unsupported IOC type") + return + + entities = self.submit_entities(report, indicator) + if not entities: + return + entity_ids = { + data["data"]["type"]: data["id"] for data in entities.get("data", []) + } + + relation_response = self.create_relation(entity_ids, source_id) + if relation_response and relation_response.status_code not in [200, 201]: + self.error( + f"While making the relationship, " + f"receiving status: {relation_response.status_code}" + ) + return + + self.report_result(entity_ids) + except Exception as ex: + self.error("Error: {}: ex: {}".format(traceback.format_exc(), ex)) + + def submit_entities(self, report: dict, indicator: dict) -> Optional[dict]: + data = [] + report and data.append(report) + indicator and data.append(indicator) + # case data contains parent case information + json_data = dict(data=data) + response = requests.put( + self.eiq_host_url + "/api/v2/entities", + json=json_data, + headers=self.headers, + ) + if response.status_code not in [200, 201]: + self.error(f"While making the call, receiving {response.status_code}") + return None + + return response.json() + + def report_result(self, entity_ids: dict) -> None: + result = {"message": "Submitted to EclecticIQ Intelligence Center"} + if entity_ids.get("report"): + result["report_platform_link"] = ( + f"{self.eiq_host_url}/entity/{entity_ids.get('report')}" + ) + + if entity_ids.get("indicator"): + result["indicator_platform_link"] = ( + f"{self.eiq_host_url}/entity/{entity_ids.get('indicator')}" + ) + self.report(result) + + def operations(self, raw): + return [ + self.build_operation("AddTagToArtifact", tag="EclecticIQ:Indicator Created") + ] + + +if __name__ == "__main__": + EclecticIQIndicator().run() diff --git a/responders/EclecticIQIndicator/requirements.txt b/responders/EclecticIQIndicator/requirements.txt new file mode 100644 index 000000000..6aabc3cfa --- /dev/null +++ b/responders/EclecticIQIndicator/requirements.txt @@ -0,0 +1,2 @@ +cortexutils +requests diff --git a/responders/FalconCustomIOC/Dockerfile b/responders/FalconCustomIOC/Dockerfile index be26deef1..0069e51d6 100644 --- a/responders/FalconCustomIOC/Dockerfile +++ b/responders/FalconCustomIOC/Dockerfile @@ -1,6 +1,6 @@ -FROM python:2 +FROM python:3 WORKDIR /worker COPY . FalconCustomIOC RUN pip install --no-cache-dir -r FalconCustomIOC/requirements.txt -ENTRYPOINT FalconCustomIOC/FalconCustomIOC.py +ENTRYPOINT FalconCustomIOC/FalconCustomIOCv2.py \ No newline at end of file diff --git a/responders/FalconCustomIOC/FalconCustomIOCv2.json b/responders/FalconCustomIOC/FalconCustomIOCv2.json new file mode 100644 index 000000000..1073f29cf --- /dev/null +++ b/responders/FalconCustomIOC/FalconCustomIOCv2.json @@ -0,0 +1,90 @@ +{ + "name": "Crowdstrike_Falcon_Custom_IOC", + "version": "2.0", + "author": "Nicolas Criton", + "url": "https://www.crowdstrike.com/blog/tech-center/consume-ioc-and-threat-feeds/", + "license": "AGPL-v3", + "description": "Submit observables to the Crowdstrike Falcon Custom IOC API", + "dataTypeList": ["thehive:alert","thehive:case_artifact"], + "command": "FalconCustomIOC/FalconCustomIOCv2.py", + "baseConfig": "FalconCustomIOCv2", + "configurationItems": [ + { + "name": "falconapi_endpoint", + "description": "CrowdStrike API endpoints: US-1 | US-2 | US-GOV-1 | EU-1", + "type": "string", + "multi": false, + "required": true + }, + { + "name": "falconapi_clientid", + "description": "Crowdstrike Falcon Client ID Oauth2 API client", + "type": "string", + "multi": false, + "required": true + }, + { + "name": "falconapi_key", + "description": "Crowdstrike Falcon Oauth2 API Key", + "type": "string", + "multi": false, + "required": true + }, + { + "name": "domain_block_expiration_days", + "description": "How many days should we block the domain IOCs sent? Default: 30", + "type": "number", + "multi": false, + "required": false, + "defaultValue": 30 + }, + { + "name": "ip_block_expiration_days", + "description": "How many days should we block the ip IOCs sent? Default: 30", + "type": "number", + "multi": false, + "required": false, + "defaultValue": 30 + }, + { + "name": "hash_block_expiration_days", + "description": "How many days should we block the hash IOCs sent? Default: 30", + "type": "number", + "multi": false, + "required": false, + "defaultValue": 30 + }, + { + "name": "action_to_take", + "description": "How the IOCs should be handled by Falcon ? Choose between 'no_action' or 'detect' -> no_action: Save the indicator for future use, but take no action / detect: Enable detections for the indicator at the selected severity (Default: detect)", + "type": "string", + "multi": false, + "required": false, + "defaultValue": "detect" + }, + { + "name": "severity_level", + "description": "Severity level when IOCs are ingested by Falcon CustomIOC: informational / low / medium / high / critical - Default: high", + "type": "string", + "multi": false, + "required": false, + "defaultValue": "high" + }, + { + "name": "tag_added_to_cs", + "description": "Tag added to the IOC in Falcon platform - Default: Cortex Incident - FalconCustomIOC", + "type": "string", + "multi": false, + "required": false, + "defaultValue": "Cortex Incident - FalconCustomIOC" + }, + { + "name": "tag_added_to_thehive", + "description": "Tag added to the IOC in TheHive platform - Default: Falcon:Custom IOC Uploaded", + "type": "string", + "multi": false, + "required": false, + "defaultValue": "Falcon:Custom IOC Uploaded" + } + ] +} \ No newline at end of file diff --git a/responders/FalconCustomIOC/FalconCustomIOCv2.py b/responders/FalconCustomIOC/FalconCustomIOCv2.py new file mode 100644 index 000000000..6653ed148 --- /dev/null +++ b/responders/FalconCustomIOC/FalconCustomIOCv2.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +import requests +import re +import json +import ipaddress + +from cortexutils.responder import Responder +from cortexutils.extractor import Extractor +from falconpy import OAuth2, IOC +from dateutil.relativedelta import relativedelta +from datetime import datetime + + +class FalconCustomIOC(Responder): + def __init__(self): + Responder.__init__(self) + self.falconapi_endpoint = self.get_param( + "config.falconapi_endpoint", None, "Falcon API Endpoint: US-1 | US-2 | US-GOV-1 | EU-1", + ) + self.falconapi_clientid = self.get_param( + "config.falconapi_clientid", None, "Falcon clientid missing" + ) + self.falconapi_key = self.get_param( + "config.falconapi_key", None, "Falcon api key missing" + ) + self.domain_block_expiration_days = self.get_param( + "config.domain_block_expiration_days", 30 + ) + self.ip_block_expiration_days = self.get_param( + "config.ip_block_expiration_days", 30 + ) + self.hash_block_expiration_days = self.get_param( + "config.hash_block_expiration_days", 30 + ) + self.action_to_take = self.get_param( + "config.action_to_take", "detect" + ) + self.severity_level = self.get_param( + "config.severity_level", "high" + ) + self.tag_added_to_cs = self.get_param( + "config.tag_added_to_cs", "Cortex Incident - FalconCustomIOC" + ) + self.tag_added_to_thehive = self.get_param( + "config.tag_added_to_thehive", "CrowdStrike:Custom IOC Uploaded" + ) + + def run(self): + try: + Responder.run(self) + ioctypes = { + "hash": "sha256", + "sha256": "sha256", + "md5": "md5", + "ip": "ipv4", + "ipv4": "ipv4", + "ip6": "ipv6", + "ipv6": "ipv6", + "domain": "domain", + "url": "domain", + } + + data_type = self.get_param("data.dataType") + if not data_type in ioctypes: + self.error("Unsupported IOC type") + return False + ioc = self.get_param("data.data", None, "No IOC provided") + + if data_type == "url": + match = re.match(r"(http:\/\/|https:\/\/)?([\w\-\.]{0,256}).*", ioc) + if match is None or match.group(2) is None: + self.error("Could not parse iocs from URL") + return False + else: + ioc = match.group(2) + data_type = Extractor().check_string(ioc) + + if data_type == "ip": + try: + ip_check = ipaddress.ip_address(ioc) + except Exception as e: + self.error(f"Could not check IP type from IOC : {e}") + return False + if isinstance(ip_check, ipaddress.IPv6Address): + data_type = "ipv6" + elif isinstance(ip_check, ipaddress.IPv4Address): + data_type = "ipv4" + else: + self.error("Could not determine IP type from IOC") + return False + + if data_type == "hash": + if len(ioc) == 32: + data_type = "md5" + elif len(ioc) == 40: + self.error("Unsupported IOC type") + return False + elif len(ioc) == 64: + data_type = "sha256" + + if data_type in ("fqdn", "domain"): + expiration_date = datetime.today() + relativedelta(days=self.domain_block_expiration_days) + elif data_type in ("ip", "ipv4", "ipv6", "ip6"): + expiration_date = datetime.today() + relativedelta(days=self.ip_block_expiration_days) + elif data_type in ("hash", "sha256", "md5"): + expiration_date = datetime.today() + relativedelta(days=self.hash_block_expiration_days) + expiration = expiration_date.strftime("%Y-%m-%dT%H:%M:%SZ") + + incident_title = self.get_param("data.case.title", None, "Can't get case title").encode("utf-8")[:128] + + auth = OAuth2( + client_id=self.falconapi_clientid, + client_secret=self.falconapi_key, + base_url=self.falconapi_endpoint + ) + + falcon_api = IOC(auth_object=auth) + response = falcon_api.indicator_create(action=self.action_to_take, + applied_globally=True, + comment="TheHive IOC incident", + description=incident_title.decode("utf-8"), + expiration=expiration, + filename="", + ignore_warnings=False, + platforms='mac,windows,linux', + severity=self.severity_level, + source="Cortex - FalconCustomIOC [" + incident_title.decode("utf-8") + "]", + tags=self.tag_added_to_cs, + type=ioctypes[data_type], + value=ioc.strip() + ) + + response_error = str(response['body']['errors']) + response_ressources = str(response['body']['resources']) + + if response['body']['errors'] is None: + self.report( + {"message": f"{ioc} successuflly submitted to Crowdstrike Falcon custom IOC api - status code: {response['status_code']}"} + ) + elif 'Duplicate type' in response_ressources: + self.error(f"Not submitted because of duplicated entry - {ioc} already found on your Falcon CustomIOC database") + return False + else: + self.error(f"Error: unable to complete action - received {response['status_code']} status code from FalconIOC API with the following message: {response_error}") + return False + + except Exception as ex: + self.error(f"Unable to send IOC to FalconCustomIOC API: {ex}") + return False + return True + + def operations(self, raw): + return [ + self.build_operation( + "AddTagToArtifact", tag=self.tag_added_to_thehive + ) + ] + +if __name__ == "__main__": + FalconCustomIOC().run() \ No newline at end of file diff --git a/responders/FalconCustomIOC/requirements.txt b/responders/FalconCustomIOC/requirements.txt index 6aabc3cfa..ff452c900 100644 --- a/responders/FalconCustomIOC/requirements.txt +++ b/responders/FalconCustomIOC/requirements.txt @@ -1,2 +1,4 @@ cortexutils -requests +crowdstrike-falconpy +datetime +python-dateutil \ No newline at end of file diff --git a/responders/JAMFProtect/JAMFProtect_IOC.py b/responders/JAMFProtect/JAMFProtect_IOC.py new file mode 100755 index 000000000..250d9ff16 --- /dev/null +++ b/responders/JAMFProtect/JAMFProtect_IOC.py @@ -0,0 +1,277 @@ +#!/usr/bin/env python3 + +from cortexutils.responder import Responder +import re +from urllib.parse import urlparse +import requests +import json + +class JAMFProtect_IOC(Responder): + def __init__(self): + Responder.__init__(self) + self.base_url = self.get_param("config.base_url") + self.client_id = self.get_param("config.client_id") + self.password = self.get_param("config.password") + self.service = self.get_param("config.service", None) + + def identify_and_extract(self, input_string): + # regular expressions for different types + patterns = { + "sha256": re.compile(r"^[a-fA-F0-9]{64}$"), + "md5": re.compile(r"^[a-fA-F0-9]{32}$"), + "sha1": re.compile(r"^[a-fA-F0-9]{40}$"), + "ipv4": re.compile(r"^(\d{1,3}\.){3}\d{1,3}$"), + "ipv6": re.compile(r"^([0-9a-fA-F]{1,4}:){7}([0-9a-fA-F]{1,4}|:)|(([0-9a-fA-F]{1,4}:){1,7}|:)(:([0-9a-fA-F]{1,4}|:)){1,7}$"), + "domain": re.compile(r"^(?!:\/\/)([a-zA-Z0-9-_]+\.)*([a-zA-Z0-9-_]{2,})(\.[a-zA-Z]{2,11})$") + } + + # check if the input_string matches any of the patterns + for key, pattern in patterns.items(): + if pattern.match(input_string): + return key, input_string + + # check if the input_string is a URL and extract the domain + try: + parsed_url = urlparse(input_string) + if parsed_url.scheme and parsed_url.netloc: + domain = parsed_url.netloc + # handle URLs with "www." + if domain.startswith("www."): + domain = domain[4:] + return "domain", domain + except Exception as e: + self.error(f"Error parsing URL: {e}") + + return None + + def get_jamf_token(self, base_url: str, client_id: str, password: str) -> str: + """ + Function to obtain a token from the Jamf Protect API. + + Parameters: + - base_url (str): The base URL of your Jamf Protect instance (e.g., "https://mycompany.protect.jamfcloud.com"). + - client_id (str): The client ID for authentication. + - password (str): The password for authentication. + + Returns: + - str: The access token if successful, raises an exception if it fails. + """ + token_url = f"{base_url}/token" + headers = {'content-type': 'application/json'} + data = { + "client_id": client_id, + "password": password + } + + try: + response = requests.post(token_url, headers=headers, data=json.dumps(data)) + response.raise_for_status() + access_token = response.json().get('access_token') + if access_token: + return access_token + else: + raise ValueError("Failed to retrieve access token.") + except requests.exceptions.RequestException as e: + raise RuntimeError(f"Failed to obtain token: {e}") + + def add_hash_to_prevention_list(self, base_url: str, token: str, list_name: str, description: str, hash_value: str, tags: list): + """ + Function to add a hash to a custom prevention list in Jamf Protect using GraphQL. + """ + graphql_url = f"{base_url}/graphql" + headers = { + "Authorization": f"{token}", + "Content-Type": "application/json" + } + + # Construct the GraphQL mutation payload + payload = { + "operationName": "createPreventList", + "variables": { + "name": list_name, + "description": description, + "type": "FILEHASH", + "list": [hash_value], + "tags": tags + }, + "query": """ + mutation createPreventList($name: String!, $tags: [String]!, $type: PREVENT_LIST_TYPE!, $list: [String]!, $description: String) { + createPreventList( + input: {name: $name, tags: $tags, type: $type, list: $list, description: $description} + ) { + ...PreventListFields + __typename + } + } + + fragment PreventListFields on PreventList { + id + name + type + count + list + created + description + __typename + } + """ + } + # Make the GraphQL request + response = requests.post(graphql_url, headers=headers, json=payload) + response.raise_for_status() + + result = response.json() + if 'errors' in result: + return f"Failed to add hash to prevention list: {result['errors']}" + else: + return f"Hash {hash_value} successfully added to prevention list {list_name}." + + def get_prevention_list_id(self, base_url: str, token: str, list_name: str) -> str: + """ + Function to get the ID of a prevention list by its name. + """ + graphql_url = f"{base_url}/graphql" + headers = { + "Authorization": f"{token}", + "Content-Type": "application/json" + } + + payload = { + "operationName": "listPreventLists", + "variables": { + "nextToken": None, + "direction": "ASC", + "field": "created", + "filter": None + }, + "query": """ + query listPreventLists($nextToken: String, $direction: OrderDirection!, $field: PreventListOrderField!, $filter: PreventListFilterInput) { + listPreventLists( + input: {next: $nextToken, order: {direction: $direction, field: $field}, pageSize: 100, filter: $filter} + ) { + items { + ...PreventListFields + __typename + } + pageInfo { + next + total + __typename + } + __typename + } + } + + fragment PreventListFields on PreventList { + id + name + type + count + list + created + description + __typename + } + """ + } + + + response = requests.post(graphql_url, headers=headers, json=payload) + response.raise_for_status() + + # check if the response contains valid json data + try: + result = response.json() + except ValueError as e: + raise RuntimeError(f"Failed to decode JSON response: {e}") + + prevention_lists = result['data']['listPreventLists']['items'] + + prevention_lists_ids = [] + # Search for the list with the specified name + for prevention_list in prevention_lists: + if prevention_list['name'] == list_name: + prevention_lists_ids.append(prevention_list['id']) + + if prevention_lists_ids == []: + raise ValueError(f"No prevention list found with name: {list_name}") + + return prevention_lists_ids + + + + def delete_prevention_list(self, base_url: str, token: str, prevent_list_ids: list): + """ + Function to delete a prevention list in Jamf Protect using GraphQL. + """ + graphql_url = f"{base_url}/graphql" + headers = { + "Authorization": f"{token}", + "Content-Type": "application/json" + } + + failed_deletions = [] + + for prevent_list_id in prevent_list_ids: + # Construct the GraphQL mutation payload + payload = { + "operationName": "deletePreventList", + "variables": { + "id": prevent_list_id + }, + "query": """ + mutation deletePreventList($id: ID!) { + deletePreventList(id: $id) { + id + __typename + } + } + """ + } + + # Make the GraphQL request + response = requests.post(graphql_url, headers=headers, json=payload) + response.raise_for_status() + + result = response.json() + if 'errors' in result: + failed_deletions.append(prevent_list_id) + + if failed_deletions: + return f"Failed to delete prevention list(s): {', '.join(failed_deletions)}" + + return f"Prevention list with ID(s) {', '.join(prevent_list_ids)} successfully deleted." + + + def run(self): + result = "" + observable_value = self.get_param("data.data", None) + ioc_type, ioc_value = self.identify_and_extract(observable_value) + if ioc_type not in ["sha256", "sha1"]: + self.error("error -- Not a hash or a valid hash : sha1 or sha256") + + case_title = self.get_param("data.case.title", None, "Can't get case title") + case_id = self.get_param("data.case.id", None, "Can't get case ID") + description = f"Pushed from TheHive - {case_title} - {case_id}" + + if self.service == "addIOC": + + token = self.get_jamf_token(self.base_url, self.client_id, self.password) + + result = self.add_hash_to_prevention_list(self.base_url,token, description, description, ioc_value, ["TheHive", f"{case_id}"]) + elif self.service == "removeIOC": + token = self.get_jamf_token(self.base_url, self.client_id, self.password) + + prevention_list_ids = self.get_prevention_list_id(self.base_url, token, description) + result = self.delete_prevention_list(self.base_url, token, prevention_list_ids) + + if 'error' in result: + self.error(result) + + self.report({"message": result}) + + + + +if __name__ == '__main__': + JAMFProtect_IOC().run() \ No newline at end of file diff --git a/responders/JAMFProtect/JAMFProtect_addHashtoPreventList.json b/responders/JAMFProtect/JAMFProtect_addHashtoPreventList.json new file mode 100644 index 000000000..162521b72 --- /dev/null +++ b/responders/JAMFProtect/JAMFProtect_addHashtoPreventList.json @@ -0,0 +1,50 @@ +{ + "name": "JAMFProtect_addHashtoPreventList", + "version": "1.0", + "author": "nusantara-self, StrangeBee", + "url": "https://github.com/TheHive-Project/Cortex-Analyzers", + "license": "AGPL-V3", + "description": "Add IOC to JAMF Protect - creates a custom prevent list for a hash", + "dataTypeList": [ + "thehive:case_artifact" + ], + "command": "JAMFProtect/JAMFProtect_IOC.py", + "baseConfig": "JAMFProtect", + "config": { + "service": "addIOC" + }, + "configurationItems": [ + { + "name": "base_url", + "description": "JAMF Protect base url", + "type": "string", + "multi": false, + "required": true, + "defaultValue": "https://mycompany.protect.jamfcloud.com" + }, + { + "name": "client_id", + "description": "JAMF Protect client ID", + "type": "string", + "multi": false, + "required": true, + "defaultValue": "" + }, + { + "name": "password", + "description": "JAMF Protect password", + "type": "string", + "multi": false, + "required": true, + "defaultValue": "" + } + ], + "registration_required": true, + "subscription_required": true, + "free_subscription": false, + "service_homepage": "https://www.jamf.com/products/jamf-protect/", + "service_logo": { + "path": "assets/jamfprotect.png", + "caption": "JAMF Protect logo" + } +} diff --git a/responders/JAMFProtect/JAMFProtect_removeHashfromPreventList.json b/responders/JAMFProtect/JAMFProtect_removeHashfromPreventList.json new file mode 100644 index 000000000..d152136c0 --- /dev/null +++ b/responders/JAMFProtect/JAMFProtect_removeHashfromPreventList.json @@ -0,0 +1,50 @@ +{ + "name": "JAMFProtect_removeHashfromPreventList", + "version": "1.0", + "author": "nusantara-self, StrangeBee", + "url": "https://github.com/TheHive-Project/Cortex-Analyzers", + "license": "AGPL-V3", + "description": "Remove IOC on JAMF Protect - removes associated custom prevent list(s) containing the hash", + "dataTypeList": [ + "thehive:case_artifact" + ], + "command": "JAMFProtect/JAMFProtect_IOC.py", + "baseConfig": "JAMFProtect", + "config": { + "service": "removeIOC" + }, + "configurationItems": [ + { + "name": "base_url", + "description": "JAMF Protect base url", + "type": "string", + "multi": false, + "required": true, + "defaultValue": "https://mycompany.protect.jamfcloud.com" + }, + { + "name": "client_id", + "description": "JAMF Protect client ID", + "type": "string", + "multi": false, + "required": true, + "defaultValue": "" + }, + { + "name": "password", + "description": "JAMF Protect password", + "type": "string", + "multi": false, + "required": true, + "defaultValue": "" + } + ], + "registration_required": true, + "subscription_required": true, + "free_subscription": false, + "service_homepage": "https://www.jamf.com/products/jamf-protect/", + "service_logo": { + "path": "assets/jamfprotect.png", + "caption": "JAMF Protect logo" + } +} diff --git a/responders/JAMFProtect/README.md b/responders/JAMFProtect/README.md new file mode 100644 index 000000000..e61dc54d6 --- /dev/null +++ b/responders/JAMFProtect/README.md @@ -0,0 +1,10 @@ +### JAMF Protect Prevent List + +This responder manages [JAMF Protect prevent lists](https://docs.jamf.com/jamf-protect/administrator-guide/Prevent_Lists.html) by adding or removing hashes as needed. + +#### Setup +- Navigate to **Administrative** > **Account** +- Create a role **PreventList-Write** with permissions **Prevent Lists: Read & Write** +- Create an API client and assign the above role +- Use these API credentials in your responders + diff --git a/responders/JAMFProtect/assets/jamfprotect.png b/responders/JAMFProtect/assets/jamfprotect.png new file mode 100644 index 000000000..b3e566f23 Binary files /dev/null and b/responders/JAMFProtect/assets/jamfprotect.png differ diff --git a/responders/JAMFProtect/requirements.txt b/responders/JAMFProtect/requirements.txt new file mode 100644 index 000000000..4a21dbf63 --- /dev/null +++ b/responders/JAMFProtect/requirements.txt @@ -0,0 +1,2 @@ +cortexutils +requests \ No newline at end of file diff --git a/responders/MSDefenderEndpoints/Dockerfile b/responders/MSDefenderEndpoints/Dockerfile index 6f153365f..6d3fed6a2 100644 --- a/responders/MSDefenderEndpoints/Dockerfile +++ b/responders/MSDefenderEndpoints/Dockerfile @@ -17,5 +17,5 @@ FROM python:3 WORKDIR /worker COPY . MSDefenderEndpoints -RUN test ! -e MSDefenderEndpoints/requirements.txt || pip install --no-cache-dir -rMSDefenderEndpoints/requirements.txt -ENTRYPOINT MSDefenderEndpoints/MSDefenderEndpoints.py \ No newline at end of file +RUN test ! -e MSDefenderEndpoints/requirements.txt || pip install --no-cache-dir -r MSDefenderEndpoints/requirements.txt +ENTRYPOINT MSDefenderEndpoints/MSDefenderEndpoints.py diff --git a/responders/MSDefenderEndpoints/MSDefenderEndpoints.py b/responders/MSDefenderEndpoints/MSDefenderEndpoints.py index 8775fd5f9..c7baaff02 100755 --- a/responders/MSDefenderEndpoints/MSDefenderEndpoints.py +++ b/responders/MSDefenderEndpoints/MSDefenderEndpoints.py @@ -7,7 +7,7 @@ import datetime class MSDefenderEndpoints(Responder): - def __init__(self): + def __init__(self): Responder.__init__(self) self.msdefenderTenantId = self.get_param('config.tenantId', None, 'TenantId missing!') self.msdefenderAppId = self.get_param('config.appId', None, 'AppId missing!') @@ -29,9 +29,9 @@ def __init__(self): } ) - def run(self): + def run(self): Responder.run(self) - url = "{}{}/oauth2/token".format( + url = "{}/{}/oauth2/token".format( self.msdefenderOAuthUri,self.msdefenderTenantId ) @@ -77,6 +77,8 @@ def getMachineId(id): if response.status_code == 200: jsonResponse = response.json() if len(response.content) > 100: + if jsonResponse["value"][0]["aadDeviceId"] is None: + return jsonResponse["value"][0]["id"] return jsonResponse["value"][0]["aadDeviceId"] else: self.error({'message': "Can't get hostname from Microsoft API"}) @@ -153,16 +155,102 @@ def unisolateMachine(machineId): except requests.exceptions.RequestException as e: self.error({'message': e}) - def pushCustomIocAlert(ipAddress): - action="Alert" + + def restrictAppExecution(machineId): + ''' + example + POST https://api.securitycenter.windows.com/api/machines/{id}/restrictCodeExecution + ''' + url = 'https://api.securitycenter.windows.com/api/machines/{}/restrictCodeExecution'.format(machineId) + body = { + 'Comment': 'Restrict code execution due to TheHive case {}'.format(self.caseId) + } + + try: + response = self.msdefenderSession.post(url=url, json=body) + if response.status_code == 201: + self.report({'message': "Restricted app execution on machine: " + self.observable }) + elif response.status_code == 400 and "ActiveRequestAlreadyExists" in response.content.decode("utf-8"): + self.report({'message': "Error restricting app execution on machine: ActiveRequestAlreadyExists"}) + else: + self.error({'message': "Can't restrict app execution"}) + except requests.exceptions.RequestException as e: + self.error({'message': e}) + + + def unrestrictAppExecution(machineId): + ''' + example + POST https://api.securitycenter.windows.com/api/machines/{id}/unrestrictCodeExecution + ''' + url = 'https://api.securitycenter.windows.com/api/machines/{}/unrestrictCodeExecution'.format(machineId) + body = { + 'Comment': '"Remove code execution restriction since machine was cleaned and validated due to TheHive case {}'.format(self.caseId) + } + + try: + response = self.msdefenderSession.post(url=url, json=body) + if response.status_code == 201: + self.report({'message': "Removed app execution restriction on machine: " + self.observable }) + elif response.status_code == 400 and "ActiveRequestAlreadyExists" in response.content.decode("utf-8"): + self.report({'message': "Error removing app execution restriction on machine: ActiveRequestAlreadyExists"}) + else: + self.error({'message': "Can't unrestrict app execution"}) + except requests.exceptions.RequestException as e: + self.error({'message': e}) + + + def startAutoInvestigation(machineId): + ''' + example + POST https://api.securitycenter.windows.com/api/machines/{id}/startInvestigation + ''' + url = 'https://api.securitycenter.windows.com/api/machines/{}/startInvestigation'.format(machineId) + + body = { + 'Comment': 'Start investigation due to TheHive case {}'.format(self.caseId) + } + + try: + response = self.msdefenderSession.post(url=url, json=body) + if response.status_code == 201: + self.report({'message': "Started Auto Investigation on : " + self.observable }) + elif response.status_code == 400 and "ActiveRequestAlreadyExists" in response.content.decode("utf-8"): + self.report({'message': "Error lauching auto investigation on machine: ActiveRequestAlreadyExists"}) + else: + self.error({'message': "Error auto investigation on machine"}) + except requests.exceptions.RequestException as e: + self.error({'message': e}) + + + def pushCustomIocAlert(observable): + + if self.observableType == 'ip': + indicatorType = 'IpAddress' + elif self.observableType == 'url': + indicatorType = 'Url' + elif self.observableType == 'domain': + indicatorType = 'DomainName' + elif self.observableType == 'hash': + if len(observable) == 32: + indicatorType = 'FileMd5' + elif len(observable) == 40: + indicatorType = 'FileSha1' + elif len(observable) == 64: + indicatorType = 'FileSha256' + else: + self.report({'message':"Observable is not a valid hash"}) + else: + self.error({'message':"Observable type must be ip, url, domain or hash"}) + url = 'https://api.securitycenter.windows.com/api/indicators' body = { - 'indicatorValue': ipAddress, - 'indicatorType': 'IpAddress', - 'action': action, - 'title': self.caseTitle, + 'indicatorValue': observable, + 'indicatorType': indicatorType, + 'action': 'Alert', + 'title': "TheHive IOC: {}".format(self.caseTitle), 'severity': 'High', - 'description': self.caseTitle, + 'description': "TheHive case: {} - caseId {}".format(self.caseTitle,self.caseId), 'recommendedActions': 'N/A' } @@ -173,13 +261,31 @@ def pushCustomIocAlert(ipAddress): except requests.exceptions.RequestException as e: self.error({'message': e}) - def pushCustomIocBlock(ipAddress): - action="AlertAndBlock" + def pushCustomIocBlock(observable): + + if self.observableType == 'ip': + indicatorType = 'IpAddress' + elif self.observableType == 'url': + indicatorType = 'Url' + elif self.observableType == 'domain': + indicatorType = 'DomainName' + elif self.observableType == 'hash': + if len(observable) == 32: + indicatorType = 'FileMd5' + elif len(observable) == 40: + indicatorType = 'FileSha1' + elif len(observable) == 64: + indicatorType = 'FileSha256' + else: + self.report({'message':"Observable is not a valid hash"}) + else: + self.error({'message':"Observable type must be ip, url, domain or hash"}) + url = 'https://api.securitycenter.windows.com/api/indicators' body = { - 'indicatorValue' : ipAddress, - 'indicatorType' : 'IpAddress', - 'action' : action, + 'indicatorValue' : observable, + 'indicatorType' : indicatorType, + 'action' : 'AlertAndBlock', 'title' : "TheHive IOC: {}".format(self.caseTitle), 'severity' : 'High', 'description' : "TheHive case: {} - caseId {}".format(self.caseTitle,self.caseId), @@ -193,13 +299,19 @@ def pushCustomIocBlock(ipAddress): except requests.exceptions.RequestException as e: self.error({'message': e}) - # print("blop") + if self.service == "isolateMachine": isolateMachine(getMachineId(self.observable)) elif self.service == "unisolateMachine": unisolateMachine(getMachineId(self.observable)) elif self.service == "runFullVirusScan": runFullVirusScan(getMachineId(self.observable)) + elif self.service == "restrictAppExecution": + restrictAppExecution(getMachineId(self.observable)) + elif self.service == "unrestrictAppExecution": + unrestrictAppExecution(getMachineId(self.observable)) + elif self.service == "startAutoInvestigation": + startAutoInvestigation(getMachineId(self.observable)) elif self.service == "pushIOCBlock": pushCustomIocBlock(self.observable) elif self.service == "pushIOCAlert": @@ -207,7 +319,7 @@ def pushCustomIocBlock(ipAddress): else: self.error({'message': "Unidentified service"}) - def operations(self, raw): + def operations(self, raw): self.build_operation('AddTagToCase', tag='MSDefenderResponder:run') if self.service == "isolateMachine": return [self.build_operation("AddTagToArtifact", tag="MsDefender:isolated")] @@ -215,6 +327,10 @@ def operations(self, raw): return [self.build_operation("AddTagToArtifact", tag="MsDefender:fullVirusScan")] elif self.service == "unisolateMachine": return [self.build_operation("AddTagToArtifact", tag="MsDefender:unIsolated")] + elif self.service == "restrictAppExecution": + return [self.build_operation("AddTagToArtifact", tag="MsDefender:restrictedAppExec")] + elif self.service == "unrestrictAppExecution": + return [self.build_operation("AddTagToArtifact", tag="MsDefender:unrestrictedAppExec")] if __name__ == '__main__': diff --git a/responders/MSDefenderEndpoints/MSDefenderEndpoints_AutoInvestigation.json b/responders/MSDefenderEndpoints/MSDefenderEndpoints_AutoInvestigation.json new file mode 100644 index 000000000..ac4ece72c --- /dev/null +++ b/responders/MSDefenderEndpoints/MSDefenderEndpoints_AutoInvestigation.json @@ -0,0 +1,61 @@ +{ + "name": "MSDefender-AutoInvestigation", + "version": "1.0", + "author": "Keijo Korte, Louis-Maximilien Dupouy", + "url": "https://github.com/TheHive-Project/Cortex-Analyzers", + "license": "AGPL-V3", + "description": "Start an automated investigation on a device", + "dataTypeList": ["thehive:case_artifact"], + "command": "MSDefenderEndpoints/MSDefenderEndpoints.py", + "baseConfig": "MSDefenderforEndpoints", + "config": { + "service": "startAutoInvestigation" + }, + "configurationItems": [ + { + "name": "tenantId", + "description": "Azure tenant ID", + "type": "string", + "multi": false, + "required": true, + "defaultValue": "abcdef12-ab12-abc12-ab12-abcdef123456" + }, + { + "name": "appId", + "description": "Azure app ID", + "type": "string", + "multi": false, + "required": true, + "defaultValue": "abcdef12-ab12-abc12-ab12-abcdef123456" + }, + { + "name": "appSecret", + "description": "Azure app secret", + "type": "string", + "multi": false, + "required": true, + "defaultValue": "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890=" + }, + { + "name": "resourceAppIdUri", + "description": "Security Center URI, usually doens't need to change", + "type": "string", + "multi": false, + "required": true, + "defaultValue": "https://api.securitycenter.windows.com" + }, + { + "name": "oAuthUri", + "description": "Azure oAuth2 authentication endpoint", + "type": "string", + "multi": false, + "required": true, + "defaultValue": "https://login.microsoftonline.com" + } + ], + "registration_required": true, + "subscription_required": true, + "free_subscription": false, + "service_homepage": "https://securitycenter.windows.com" + } + \ No newline at end of file diff --git a/responders/MSDefenderEndpoints/MSDefenderEndpoints_Isolate.json b/responders/MSDefenderEndpoints/MSDefenderEndpoints_Isolate.json index 9dcc99547..e78dac91e 100644 --- a/responders/MSDefenderEndpoints/MSDefenderEndpoints_Isolate.json +++ b/responders/MSDefenderEndpoints/MSDefenderEndpoints_Isolate.json @@ -50,7 +50,7 @@ "type": "string", "multi": false, "required": true, - "defaultValue": "https://login.windows.net/" + "defaultValue": "https://login.microsoftonline.com" } ], "registration_required": true, diff --git a/responders/MSDefenderEndpoints/MSDefenderEndpoints_PushIOCAlert.json b/responders/MSDefenderEndpoints/MSDefenderEndpoints_PushIOCAlert.json index fe9c10a2f..255fa328c 100644 --- a/responders/MSDefenderEndpoints/MSDefenderEndpoints_PushIOCAlert.json +++ b/responders/MSDefenderEndpoints/MSDefenderEndpoints_PushIOCAlert.json @@ -1,7 +1,7 @@ { "name": "MSDefender-PushIOC-Alert", - "version": "1.0", - "author": "Keijo Korte", + "version": "2.0", + "author": "Keijo Korte, Louis-Maximilien Dupouy", "url": "https://github.com/TheHive-Project/Cortex-Analyzers", "license": "AGPL-V3", "description": "Push IOC to Defender client. Alert mode", @@ -50,7 +50,7 @@ "type": "string", "multi": false, "required": true, - "defaultValue": "https://login.windows.net/" + "defaultValue": "https://login.microsoftonline.com" } ], "registration_required": true, diff --git a/responders/MSDefenderEndpoints/MSDefenderEndpoints_PushIOCBlock.json b/responders/MSDefenderEndpoints/MSDefenderEndpoints_PushIOCBlock.json index d87914e25..eb211d7cd 100644 --- a/responders/MSDefenderEndpoints/MSDefenderEndpoints_PushIOCBlock.json +++ b/responders/MSDefenderEndpoints/MSDefenderEndpoints_PushIOCBlock.json @@ -1,7 +1,7 @@ { "name": "MSDefender-PushIOC-Block", - "version": "1.0", - "author": "Keijo Korte", + "version": "2.0", + "author": "Keijo Korte, Louis-Maximilien Dupouy", "url": "https://github.com/TheHive-Project/Cortex-Analyzers", "license": "AGPL-V3", "description": "Push IOC to Defender client. Blocking mode", @@ -50,7 +50,7 @@ "type": "string", "multi": false, "required": true, - "defaultValue": "https://login.windows.net/" + "defaultValue": "https://login.microsoftonline.com" } ], "registration_required": true, diff --git a/responders/MSDefenderEndpoints/MSDefenderEndpoints_RestrictAppExecution.json b/responders/MSDefenderEndpoints/MSDefenderEndpoints_RestrictAppExecution.json new file mode 100644 index 000000000..525a80990 --- /dev/null +++ b/responders/MSDefenderEndpoints/MSDefenderEndpoints_RestrictAppExecution.json @@ -0,0 +1,61 @@ +{ + "name": "MSDefender-RestrictAppExecution", + "version": "1.0", + "author": "Keijo Korte, Louis-Maximilien Dupouy", + "url": "https://github.com/TheHive-Project/Cortex-Analyzers", + "license": "AGPL-V3", + "description": "Restrict execution of all applications on the device except a predefined set", + "dataTypeList": ["thehive:case_artifact"], + "command": "MSDefenderEndpoints/MSDefenderEndpoints.py", + "baseConfig": "MSDefenderforEndpoints", + "config": { + "service": "restrictAppExecution" + }, + "configurationItems": [ + { + "name": "tenantId", + "description": "Azure tenant ID", + "type": "string", + "multi": false, + "required": true, + "defaultValue": "abcdef12-ab12-abc12-ab12-abcdef123456" + }, + { + "name": "appId", + "description": "Azure app ID", + "type": "string", + "multi": false, + "required": true, + "defaultValue": "abcdef12-ab12-abc12-ab12-abcdef123456" + }, + { + "name": "appSecret", + "description": "Azure app secret", + "type": "string", + "multi": false, + "required": true, + "defaultValue": "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890=" + }, + { + "name": "resourceAppIdUri", + "description": "Security Center URI, usually doens't need to change", + "type": "string", + "multi": false, + "required": true, + "defaultValue": "https://api.securitycenter.windows.com" + }, + { + "name": "oAuthUri", + "description": "Azure oAuth2 authentication endpoint", + "type": "string", + "multi": false, + "required": true, + "defaultValue": "https://login.microsoftonline.com" + } + ], + "registration_required": true, + "subscription_required": true, + "free_subscription": false, + "service_homepage": "https://securitycenter.windows.com" + } + \ No newline at end of file diff --git a/responders/MSDefenderEndpoints/MSDefenderEndpoints_UnRestrictAppExecution.json b/responders/MSDefenderEndpoints/MSDefenderEndpoints_UnRestrictAppExecution.json new file mode 100644 index 000000000..7b0c20d6a --- /dev/null +++ b/responders/MSDefenderEndpoints/MSDefenderEndpoints_UnRestrictAppExecution.json @@ -0,0 +1,60 @@ +{ + "name": "MSDefender-UnRestrictAppExecution", + "version": "1.0", + "author": "Keijo Korte, Louis-Maximilien Dupouy", + "url": "https://github.com/TheHive-Project/Cortex-Analyzers", + "license": "AGPL-V3", + "description": "Enable execution of any application on the device", + "dataTypeList": ["thehive:case_artifact"], + "command": "MSDefenderEndpoints/MSDefenderEndpoints.py", + "baseConfig": "MSDefenderforEndpoints", + "config": { + "service": "unrestrictAppExecution" + }, + "configurationItems": [ + { + "name": "tenantId", + "description": "Azure tenant ID", + "type": "string", + "multi": false, + "required": true, + "defaultValue": "abcdef12-ab12-abc12-ab12-abcdef123456" + }, + { + "name": "appId", + "description": "Azure app ID", + "type": "string", + "multi": false, + "required": true, + "defaultValue": "abcdef12-ab12-abc12-ab12-abcdef123456" + }, + { + "name": "appSecret", + "description": "Azure app secret", + "type": "string", + "multi": false, + "required": true, + "defaultValue": "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890=" + }, + { + "name": "resourceAppIdUri", + "description": "Security Center URI, usually doens't need to change", + "type": "string", + "multi": false, + "required": true, + "defaultValue": "https://api.securitycenter.windows.com" + }, + { + "name": "oAuthUri", + "description": "Azure oAuth2 authentication endpoint", + "type": "string", + "multi": false, + "required": true, + "defaultValue": "https://login.microsoftonline.com" + } + ], + "registration_required": true, + "subscription_required": true, + "free_subscription": false, + "service_homepage": "https://securitycenter.windows.com" +} diff --git a/responders/MSDefenderEndpoints/MSDefenderEndpoints_Unisolate.json b/responders/MSDefenderEndpoints/MSDefenderEndpoints_Unisolate.json index 32ee5b4cd..eda10343b 100644 --- a/responders/MSDefenderEndpoints/MSDefenderEndpoints_Unisolate.json +++ b/responders/MSDefenderEndpoints/MSDefenderEndpoints_Unisolate.json @@ -50,7 +50,7 @@ "type": "string", "multi": false, "required": true, - "defaultValue": "https://login.windows.net/" + "defaultValue": "https://login.microsoftonline.com" } ], "registration_required": true, diff --git a/responders/MSDefenderEndpoints/MSDefenderEndpoints_VirusScan.json b/responders/MSDefenderEndpoints/MSDefenderEndpoints_VirusScan.json index cccbaf2f5..69a9f9645 100644 --- a/responders/MSDefenderEndpoints/MSDefenderEndpoints_VirusScan.json +++ b/responders/MSDefenderEndpoints/MSDefenderEndpoints_VirusScan.json @@ -50,7 +50,7 @@ "type": "string", "multi": false, "required": true, - "defaultValue": "https://login.windows.net/" + "defaultValue": "https://login.microsoftonline.com" } ], "registration_required": true, diff --git a/responders/MSDefenderEndpoints/README.md b/responders/MSDefenderEndpoints/README.md index cb338f07a..66394dba9 100644 --- a/responders/MSDefenderEndpoints/README.md +++ b/responders/MSDefenderEndpoints/README.md @@ -4,7 +4,10 @@ * Isolate machine * Unisolate machine +* Restrict App Execution on a machine +* Remove app restriction on a machine * Run full antivirus scan +* Run an automated scan * Push IoC to Microsoft defender * Alert * BlockAndAlert @@ -37,7 +40,7 @@ In the registration form: ##### API permission On your new application page, click API Permissions > Add permission > APIs my organization uses > type **WindowsDefenderATP** and click on WindowsDefenderATP -Choose Application permissions, select **Alert.Read.All** AND **TI.ReadWrite.All** AND **Machine.ReadAll** AND **Machine.Isolate** AND **Machine.Scan** > Click on Add permissions. +Choose Application permissions, select **Alert.Read.All** AND **TI.ReadWrite.All** AND **Machine.ReadAll** AND **Machine.Isolate** AND **Machine.Scan** AND **Machine.RestrictExecution** > Click on Add permissions. After clicking the Add Permissions button, on the next screen we need to grant consent for the permission to take effect. Press the "Grant admin consent for {your tenant name}" button. diff --git a/responders/MSEntraID/MSEntraID.py b/responders/MSEntraID/MSEntraID.py new file mode 100755 index 000000000..4b8526cf6 --- /dev/null +++ b/responders/MSEntraID/MSEntraID.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python3 +# encoding: utf-8 +# Author: Daniel Weiner @dmweiner, revised by @jahamilto +import requests +import traceback +import datetime +from cortexutils.responder import Responder + +# Initialize Azure Class +class MSEntraID(Responder): + def __init__(self): + Responder.__init__(self) + self.client_id = self.get_param('config.client_id', None, 'Microsoft Entra ID Application ID/Client ID Missing') + self.client_secret = self.get_param('config.client_secret', None, 'Microsoft Entra ID Registered Application Client Secret Missing') + self.tenant_id = self.get_param('config.tenant_id', None, 'Microsoft Entra ID Tenant ID Mising') + self.time = '' + def run(self): + Responder.run(self) + + if self.get_param('data.dataType') == 'mail': + try: + self.user = self.get_param('data.data', None, 'No UPN supplied to revoke credentials for') + if not self.user: + self.error("No user supplied") + + token_data = { + "grant_type": "client_credentials", + 'client_id': self.client_id, + 'client_secret': self.client_secret, + 'resource': 'https://graph.microsoft.com', + 'scope': 'https://graph.microsoft.com' + } + + + #Authenticate to the graph api + + redirect_uri = "https://login.microsoftonline.com/{}/oauth2/token".format(self.tenant_id) + token_r = requests.post(redirect_uri, data=token_data) + token = token_r.json().get('access_token') + + if token_r.status_code != 200: + self.error('Failure to obtain azure access token: {}'.format(token_r.content)) + + # Set headers for future requests + headers = { + 'Authorization': 'Bearer {}'.format(token) + } + + base_url = 'https://graph.microsoft.com/v1.0/' + + r = requests.post(base_url + 'users/{}/revokeSignInSessions'.format(self.user), headers=headers) + + if r.status_code != 200: + self.error('Failure to revoke access tokens of user {}: {}'.format(self.user, r.content)) + + else: + #record time of successful auth token revokation + self.time = datetime.datetime.utcnow() + + except Exception as ex: + self.error(traceback.format_exc()) + # Build report to return to Cortex + full_report = {"message": "User {} authentication tokens successfully revoked at {}".format(self.user, self.time)} + self.report(full_report) + else: + self.error('Incorrect dataType. "mail" expected.') + + +if __name__ == '__main__': + MSEntraID().run() diff --git a/responders/MSEntraID/MSEntraID_TokenRevoker.json b/responders/MSEntraID/MSEntraID_TokenRevoker.json new file mode 100644 index 000000000..32454cbfe --- /dev/null +++ b/responders/MSEntraID/MSEntraID_TokenRevoker.json @@ -0,0 +1,35 @@ +{ + "name": "MSEntraID_TokenRevoker", + "version": "1.1", + "author": "Daniel Weiner @dmweiner, revised by @jahamilto", + "url": "https://github.com/TheHive-Project/Cortex-Analyzers", + "license": "AGPL-V3", + "description": "Revoke all Microsoft Entra ID authentication session tokens for a User Principal Name.", + "dataTypeList": ["thehive:case_artifact"], + "command": "MSEntraID/MSEntraID.py", + "baseConfig": "MSEntraID", + "configurationItems": [ + {"name": "tenant_id", + "description": "Microsoft Entra ID Tenant ID", + "type": "string", + "multi": false, + "required": true + }, + {"name": "client_id", + "description": "Client ID/Application ID of Microsoft Entra ID Registered App", + "type": "string", + "multi": false, + "required": true + }, + {"name": "client_secret", + "description": "Secret for Microsoft Entra ID Registered Application", + "type": "string", + "multi": false, + "required": true + } + ], + "registration_required": true, + "subscription_required": true, + "free_subscription": false, + "service_homepage": "https://www.microsoft.com/security/business/identity-access/microsoft-entra-id" +} diff --git a/responders/MSEntraID/README.md b/responders/MSEntraID/README.md new file mode 100644 index 000000000..32b8e1db6 --- /dev/null +++ b/responders/MSEntraID/README.md @@ -0,0 +1,37 @@ +## Microsoft Entra ID Sign In Token Revoker Responder + +This responder allows you to revoke the session tokens for an Microsoft Entra ID user. Requires the UPN of the account in question, which should be entered as a "mail" observable in TheHive. + +### Config + +To enable the responder, you need three values: +1. Microsoft Entra ID Tenant ID +2. Application ID +3. Application Secret + +The first two values can be found at any time in the application's Overview page in the Microsoft Entra ID portal. The secret must be generated and then stored in a safe place, as it is only fully visible when you first make it. + +## Setup + +### Prereqs +User account with the Cloud Application Administrator role. +User account with the Global Administrator Role (most of the steps can be done with only the Cloud App Administrator role, but the final authorization for its API permissions requires GA). + +### Steps + +#### Creation +1. Navigate to the [Microsoft Entra ID Portal](https://entra.microsoft.com/) and sign in with the relevant administrator account. +2. Navigate to App Registrations, and create a new registration. +3. Provide a display name (this can be anything, and can be changed later). Click Register. + +#### Secret +4. Navigate to Certificates and Secrets. +5. Create a new client secret. Enter a relevant description and set a security-conscious expiration date. +6. Copy the Value. **This will only be fully visible for a short time, so you should immediately copy it and store it in a safe place**. + +#### API Permissions +7. Navigate to API permissions. +8. Add the Directory.ReadWrite.All and User.ReadWrite.All permissions (Microsoft Graph API, application permissions). +9. Using a GA account, select the "Grant admin consent for *TENANTNAME*" button. + +10. Place the relevant values into the config within Cortex. \ No newline at end of file diff --git a/responders/MSEntraID/requirements.txt b/responders/MSEntraID/requirements.txt new file mode 100644 index 000000000..98df81c2f --- /dev/null +++ b/responders/MSEntraID/requirements.txt @@ -0,0 +1,3 @@ +cortexutils +requests +datetime \ No newline at end of file diff --git a/responders/MailIncidentStatus/requirements.txt b/responders/MailIncidentStatus/requirements.txt index c476e0aca..a33100424 100644 --- a/responders/MailIncidentStatus/requirements.txt +++ b/responders/MailIncidentStatus/requirements.txt @@ -1,2 +1,2 @@ cortexutils -thehive4py +thehive4py~=1.8.1 diff --git a/responders/Netcraft/Dockerfile b/responders/Netcraft/Dockerfile new file mode 100644 index 000000000..2630b3e1b --- /dev/null +++ b/responders/Netcraft/Dockerfile @@ -0,0 +1,6 @@ +FROM python:3 + +WORKDIR /worker +COPY . Netcraft +RUN pip install --no-cache-dir -r Netcraft/requirements.txt +ENTRYPOINT Netcraft/Netcraft.py diff --git a/responders/Netcraft/Netcraft.py b/responders/Netcraft/Netcraft.py new file mode 100755 index 000000000..b3f4b2dcf --- /dev/null +++ b/responders/Netcraft/Netcraft.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +# encoding: utf-8 + +from cortexutils.responder import Responder +import requests + + +class NetcraftReporter(Responder): + def __init__(self): + Responder.__init__(self) + self.scheme = "https" + self.api_key = self.get_param( + 'config.api_key', None, "API-key Missing") + self.takedown_url = self.get_param( + 'config.takedown_url', None, "Takedown URL Missing") + self.observable_type = self.get_param('data.dataType', None, "Data type is empty") + self.observable_description = self.get_param('data.message', None, "Description is empty") + self.username = self.get_param( + 'config.username', None, "Takedown Username is empty") + self.password = self.get_param( + 'config.password', None, "Takedown Password is empty") + self.useUserPass = self.get_param( + 'config.useUserPass', None, "Takedown Use Username Password authentication is empty") + + def run(self): + Responder.run(self) + try: + supported_observables = ["domain", "url", "fqdn"] + if self.observable_type in supported_observables: + if self.observable_type == "domain" or self.observable_type == "fqdn": + domain = self.get_param('data.data', None, 'No artifacts available') + takedown = "{}://{}".format(self.scheme, domain) + elif self.observable_type == "url": + takedown = self.get_param('data.data') + + session = requests.Session() + session.headers.update({'User-Agent': 'Netcraft-Cortex-Responder'}) + + if self.useUserPass: + session.auth = (self.username, self.password) + else: + session.headers.update({'Authorization': 'Bearer ' + self.api_key}) + + payload = { + "attack": takedown, + "comment": "Automated takedown via Cortex" + } + response = session.post(self.takedown_url, data=payload) + + if response.status_code == 200: + self.report({'message': 'Takedown request sent to Netcraft. Message: {}'.format(response.text)}) + elif response.status_code == 401: + self.error('Failed authentication. Check API-Key. Message: {}'.format(response.text)) + else: + self.error('Failed to submit takedown request. Error code: {}. Error message: {}' + .format(response.status_code, response.text)) + else: + self.error('Incorrect dataType. "Domain", "FQDN", or "URL" expected.') + + except requests.exceptions.RequestException as e: + self.error(str(e)) + + def operations(self, raw): + return [self.build_operation('AddTagToArtifact', tag='Netcraft:takedown')] + + +if __name__ == '__main__': + NetcraftReporter().run() diff --git a/responders/Netcraft/NetcraftTakedown.json b/responders/Netcraft/NetcraftTakedown.json new file mode 100644 index 000000000..b349e34f3 --- /dev/null +++ b/responders/Netcraft/NetcraftTakedown.json @@ -0,0 +1,52 @@ +{ + "name": "Netcraft_TakedownPhishingURL", + "version": "1.0", + "author": "Keijo Korte - @korteke", + "url": "https://github.com/TheHive-Project/Cortex-Analyzers", + "license": "AGPL-V3", + "description": "Submit URL to Netcraft's Takedown API.", + "dataTypeList": ["thehive:case_artifact"], + "command": "Netcraft/Netcraft.py", + "baseConfig": "Netcraft", + "configurationItems": [ + { + "name": "api_key", + "description": "Netcraft Takedown API key", + "type": "string", + "multi": false, + "required": false + }, + { + "name": "username", + "description": "Netcraft Takedown Username", + "type": "string", + "multi": false, + "required": false + }, + { + "name": "password", + "description": "Netcraft Takedown Password", + "type": "string", + "multi": false, + "required": false + }, + { + "name": "useUserPass", + "description": "Use User and Password authentication", + "type": "boolean", + "multi": false + }, + { + "name": "takedown_url", + "description": "Netcraft Takedown URL", + "type": "string", + "multi": false, + "required": true, + "defaultValue": "https://takedown.netcraft.com/authorise.php" + } + ], + "registration_required": true, + "subscription_required": true, + "free_subscription": false, + "service_homepage": "https://www.netcraft.com/cybercrime/countermeasures/" +} diff --git a/responders/Netcraft/README.md b/responders/Netcraft/README.md new file mode 100644 index 000000000..8219db51f --- /dev/null +++ b/responders/Netcraft/README.md @@ -0,0 +1,13 @@ +### Netcraft Takedown + +This responder sends observables to [Netcraft Takedown service](https://www.netcraft.com/cybercrime/countermeasures/). + +#### Requirements +One need to request API-key from Netcraft [Contact form](https://www.netcraft.com/contact/). + +#### Configuration +- `api_key` : Netcraft Takedown API-key +- `takedown_url`: Netcraft Takedown URL (default: https://takedown.netcraft.com/authorise.php) + +#### Official documenation +Official API documentation: [Netcraft site](https://takedown.netcraft.com/help_api.php). \ No newline at end of file diff --git a/responders/Netcraft/requirements.txt b/responders/Netcraft/requirements.txt new file mode 100644 index 000000000..6aabc3cfa --- /dev/null +++ b/responders/Netcraft/requirements.txt @@ -0,0 +1,2 @@ +cortexutils +requests diff --git a/responders/PaloAltoNGFW/requirements.txt b/responders/PaloAltoNGFW/requirements.txt index a827e6c55..9868223c6 100644 --- a/responders/PaloAltoNGFW/requirements.txt +++ b/responders/PaloAltoNGFW/requirements.txt @@ -1,4 +1,4 @@ cortexutils requests pan-os-python -thehive4py \ No newline at end of file +thehive4py~=1.8.1 \ No newline at end of file diff --git a/responders/RT4/requirements.txt b/responders/RT4/requirements.txt index f47373772..52dd08a8d 100644 --- a/responders/RT4/requirements.txt +++ b/responders/RT4/requirements.txt @@ -1,4 +1,5 @@ defang jinja2 rt -requests \ No newline at end of file +requests +cortexutils diff --git a/responders/Shuffle/shuffle.py b/responders/Shuffle/shuffle.py index 63646be2b..5c8690191 100755 --- a/responders/Shuffle/shuffle.py +++ b/responders/Shuffle/shuffle.py @@ -16,7 +16,7 @@ def run(self): headers = { "Authorization": "Bearer %s" % self.api_key } - r = requests.post(parsed_url, headers=headers) + r = requests.post(parsed_url, json=self.get_data(), headers=headers) if r.status_code == 200: self.report({"Message": "Executed workflow"}) else: diff --git a/responders/Telegram/README.md b/responders/Telegram/README.md new file mode 100644 index 000000000..1552e42a5 --- /dev/null +++ b/responders/Telegram/README.md @@ -0,0 +1,15 @@ +### Telegram responder + +##### Data required for the work of the responder + +* **api_token** +How to create a telegram bot and get API token [read here](https://flowxo.com/how-to-create-a-bot-for-telegram-short-and-simple-guide-for-beginners/) + +* **chat_id** +How to get a group or channal chat ID [read here](https://stackoverflow.com/questions/32423837/telegram-bot-how-to-get-a-group-chat-id) + +* **date_format** +Make the date and time format convenient for you or use the default. About date and time code formats [here](https://www.geeksforgeeks.org/python-datetime-strptime-function/) + +* **tag** +If you want a tag to be attached to the case when executing the responder, specify its name (optional) \ No newline at end of file diff --git a/responders/Telegram/Telegram.json b/responders/Telegram/Telegram.json new file mode 100644 index 000000000..f3c49f036 --- /dev/null +++ b/responders/Telegram/Telegram.json @@ -0,0 +1,46 @@ +{ + "name": "Telegram", + "version": "1.0", + "author": "Alex Kolnik, PS Cloud Services, @ps_kz", + "url": "https://github.com/TheHive-Project/Cortex-Analyzers", + "license": "AGPL-V3", + "description": "Send a message to Telegram with information from TheHive case", + "dataTypeList": ["thehive:case"], + "command": "Telegram/telegram.py", + "baseConfig": "Telegram", + "configurationItems": [ + { + "name": "api_token", + "description": "The token is a string, like 110201543:AAHdqTcvCH1vGWJxfSeofSAs0K5PALDsaw, which is required to authorize the bot and send requests to the Bot API", + "type": "string", + "multi": false, + "required": true + }, + { + "name": "chat_id", + "description": "ID of the chat or channel to which the message will be sent", + "type": "number", + "multi": false, + "required": true + }, + { + "name": "date_format", + "description": "https://www.geeksforgeeks.org/python-datetime-strptime-function/", + "type": "string", + "multi": false, + "required": true, + "defaultValue": "%d.%m.%Y %H:%M" + }, + { + "name": "tag", + "description": "Tag name to be assigned to the case", + "type": "string", + "multi": false, + "required": false + } + ], + "registration_required": true, + "subscription_required": false, + "free_subscription": true, + "service_homepage": "https://www.telegram.org" +} diff --git a/responders/Telegram/requirements.txt b/responders/Telegram/requirements.txt new file mode 100644 index 000000000..37dfee161 --- /dev/null +++ b/responders/Telegram/requirements.txt @@ -0,0 +1 @@ +cortexutils \ No newline at end of file diff --git a/responders/Telegram/telegram.py b/responders/Telegram/telegram.py new file mode 100644 index 000000000..a92d45b09 --- /dev/null +++ b/responders/Telegram/telegram.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +# encoding: utf-8 + +import json +import requests +from datetime import datetime +from cortexutils.responder import Responder + + +class Telegram(Responder): + + def __init__(self): + Responder.__init__(self) + self.api_token = self.get_param( + "config.api_token", None, "Missing Telegram bot API token") + self.chat_id = self.get_param( + "config.chat_id", None, "Missing Telegram Chat ID") + self.date_format = self.get_param( + "config.date_format", "%d.%m.%Y %H:%M") + self.tag = self.get_param("config.tag", None) + + def run(self): + Responder.run(self) + + # converting TheHive severities to readable + severities = { + 1: 'Low', + 2: 'Medium', + 3: 'High', + 4: 'Critical' + } + + caseId = self.get_param("data.caseId") + title = self.get_param("data.title") + severity = severities[self.get_param("data.severity", 2)] + owner = self.get_param("data.owner") + description = self.get_param("data.description") + + startDate_datetime = datetime.fromtimestamp( + self.get_param("data.startDate", 0) / 1000) + startDate_formated = startDate_datetime.strftime(self.date_format) + + # markdown syntax in TheHive is different from Telegram + description = description.replace("**", "*") + description = description.replace("\n\n", "\n") + + msg_content = f'#Case{caseId}\n' + msg_content += f'*{title}*\n\n' + msg_content += f'*Severity*: {severity}\n' + msg_content += f'*Assignee*: {owner}\n' + msg_content += f'*Date*: {startDate_formated}\n\n' + msg_content += f'*Description*:\n{description}' + + msg_data = {} + msg_data['chat_id'] = self.chat_id + msg_data['text'] = msg_content + msg_data['parse_mode'] = 'markdown' + message = json.dumps(msg_data) + + hook_url = f'https://api.telegram.org/bot{self.api_token}/sendMessage' + headers = {'content-type': 'application/json', + 'Accept-Charset': 'UTF-8'} + resp_code = requests.post(hook_url, headers=headers, data=message) + + self.report({"message": f"{resp_code.text}"}) + + def operations(self, raw): + if self.tag: + return [self.build_operation("AddTagToCase", tag=self.tag)] + + +if __name__ == "__main__": + Telegram().run() diff --git a/responders/Velociraptor/requirements.txt b/responders/Velociraptor/requirements.txt index 148c0c690..7860d238b 100755 --- a/responders/Velociraptor/requirements.txt +++ b/responders/Velociraptor/requirements.txt @@ -2,4 +2,4 @@ cortexutils cryptography grpcio-tools pyvelociraptor -thehive4py \ No newline at end of file +thehive4py~=1.8.1 \ No newline at end of file diff --git a/responders/VirustotalDownloader/requirements.txt b/responders/VirustotalDownloader/requirements.txt index 8bb849d29..cfbc35b68 100644 --- a/responders/VirustotalDownloader/requirements.txt +++ b/responders/VirustotalDownloader/requirements.txt @@ -1,6 +1,6 @@ cortexutils datetime requests -thehive4py +thehive4py~=1.8.1 python-magic filetype diff --git a/thehive-templates/Capa_1_0/long.html b/thehive-templates/Capa_1_0/long.html new file mode 100644 index 000000000..d5840743f --- /dev/null +++ b/thehive-templates/Capa_1_0/long.html @@ -0,0 +1,62 @@ + +Capability | +ATT&CK ID | +ATT&CK Tactic | +ATT&CK Technique | +ATT&CK Subtechnique | +Rule Path | +Examples | +
---|---|---|---|---|---|---|
{{rule.meta.name}} | ++ + + {{rule.meta.attack[0].id}} + + + | ++ {{rule.meta.attack[0].tactic}} + | ++ {{rule.meta.attack[0].technique}} + | ++ {{rule.meta.attack[0].subtechnique || 'N/A'}} + | ++ + {{rule.meta.namespace}} + + | +{{rule.meta.examples.join(', ')}} | +
{{content.ip[protocol] | json}}- -
Source: {{content.cert.metadata.source}}
-Added at: {{content.cert.metadata.added_at}}
-Updated at: {{content.cert.metadata.updated_at}}
-Comodo Mammoth
{{content.cert.ct.comodo_mammoth.added_to_ct_at}}
Comodo Sabre
{{content.cert.ct.comodo_sabre.added_to_ct_at}}
Google Pilot
{{content.cert.ct.google_pilot.added_to_ct_at}}
Google Rocketeer
{{content.cert.ct.google_rocketeer.added_to_ct_at}}
Symantec WS CT
{{content.cert.ct.symantec_ws_ct.added_to_ct_at}}
{{cn}}
-Valid since: {{content.cert.parsed.validity.start}}
-Valid until: {{content.cert.parsed.validity.end}}
-{{content.cert | json}}- -
{{content.website[protocol] | json}}- -
{{service | json}}+
Added at: {{content.cert.added_at}}
+Modified at: {{content.cert.modified_at}}
+Validated at: {{content.cert.validated_at}}
+SHA-256: {{content.cert.fingerprint_sha256}}
+SHA-1: {{content.cert.fingerprint_sha1}}
+MD5: {{content.cert.fingerprint_md5}}
+{{dnsName}}
+Log ID: {{sct.log_id}}
+Timestamp: {{sct.timestamp | date:'medium'}}
+Version: {{sct.version}}
+{{cn}}
+Valid since: {{content.cert.parsed.validity_period.not_before | date:'medium'}}
+Valid until: {{content.cert.parsed.validity_period.not_after | date:'medium'}}
+{{content.cert | json}}+
{{service | json}}+
Aggressiveness (0 to 5) -
-Overall | -Last day | -Last week | -Last month | -
---|---|---|---|
{{content.scores.overall.aggressiveness}} | -{{content.scores.last_day.aggressiveness}} | -{{content.scores.last_week.aggressiveness}} | -{{content.scores.last_month.aggressiveness}} | -
Aggressiveness (0 to 5)
+Overall | +Last day | +Last week | +Last month | +
---|---|---|---|
{{content.scores.overall.aggressiveness}} | +{{content.scores.last_day.aggressiveness}} | +{{content.scores.last_week.aggressiveness}} | +{{content.scores.last_month.aggressiveness}} | +
SignIn ID | +Time | +Status | +IP | +App Name | +Risk | +Device ID | +Device Name | +Device OS | +Cond'l Access | +Location | +|||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
{{r.id | limitTo: 8}} | +{{r.basicDetails.signInTime}} | +{{r.basicDetails.result | limitTo: 7}} | +IPv6 | +{{r.basicDetails.ip}} | +{{r.basicDetails.appName}} | +{{r.basicDetails.riskLevel}} | +Not Available | +{{r.deviceDetails.id | limitTo: 8}} | +{{r.deviceDetails.deviceName}} | +{{r.deviceDetails.operatingSystem}} | +No | +Yes | +{{r.locationDetails.city}}, {{r.locationDetails.state}}, {{r.locationDetails.countryOrRegion}} | + +
SignIn ID | +IPv6 | +App Name | +Client App | +Resource Name | +Applied CAPs | +Device ID | +|
---|---|---|---|---|---|---|---|
{{r.id}} | +{{r.basicDetails.ip}} | +IPv4 | +{{r.basicDetails.appName}} | +{{r.basicDetails.clientApp}} | +{{r.basicDetails.resourceName}} | +{{r.appliedConditionalAccessPolicies}} | +{{r.deviceDetails.id}} | +
File Name | +{{ content["stats"]["file_name"] }} | +
File Extension | +{{ content["stats"]["file_extension"] }} | +
Total QR Code(s) | +{{ content["stats"]["total_qr_codes"] }} | +
Data Category | +{{ result.results.data_category }} | +
Data Type | +{{ result.results.data_type }} | +
Info | +{{ result.results.info }} | +
Data | +{{ result.results.data }} | +
Brute Data | +{{ result.results.brute_data }} | +
On PDF page | +{{ result.results.page }} | +