From dee7530fed20ee3c98f9f4eba10c18f04812db45 Mon Sep 17 00:00:00 2001 From: Michael Auger Date: Fri, 24 Jan 2020 18:36:46 -0500 Subject: [PATCH 1/2] Cisco AMP for Endpoints Responder Initial Release of the Cisco AMP for Endpoints Responder --- responders/AMPforEndpoints/AMPforEndpoints.py | 240 ++++++++++++++++++ .../AMPforEndpoints_IsolationStart.json | 44 ++++ .../AMPforEndpoints_IsolationStop.json | 37 +++ .../AMPforEndpoints_MoveGUID.json | 44 ++++ .../AMPforEndpoints_SCDAdd.json | 44 ++++ .../AMPforEndpoints_SCDRemove.json | 44 ++++ responders/AMPforEndpoints/requirements.txt | 2 + 7 files changed, 455 insertions(+) create mode 100644 responders/AMPforEndpoints/AMPforEndpoints.py create mode 100644 responders/AMPforEndpoints/AMPforEndpoints_IsolationStart.json create mode 100644 responders/AMPforEndpoints/AMPforEndpoints_IsolationStop.json create mode 100644 responders/AMPforEndpoints/AMPforEndpoints_MoveGUID.json create mode 100644 responders/AMPforEndpoints/AMPforEndpoints_SCDAdd.json create mode 100644 responders/AMPforEndpoints/AMPforEndpoints_SCDRemove.json create mode 100644 responders/AMPforEndpoints/requirements.txt diff --git a/responders/AMPforEndpoints/AMPforEndpoints.py b/responders/AMPforEndpoints/AMPforEndpoints.py new file mode 100644 index 000000000..06fdc3c1e --- /dev/null +++ b/responders/AMPforEndpoints/AMPforEndpoints.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python +# encoding: utf-8 + +import re +import json +import requests +from cortexutils.responder import Responder + + +class AMPforEndpoints(Responder): + def __init__(self): + Responder.__init__(self) + self.service = self.get_param("config.service", None, "Service Missing") + self.amp_cloud = self.get_param("config.amp_cloud", None, "AMP HOST Missing") + self.client_id = self.get_param("config.client_id", None, "Client ID Missing") + self.api_key = self.get_param("config.api_key", None, "API Key Missing") + + if self.service in ("scdadd", "scdremove"): + self.scd_guid = self.get_param( + "config.scd_guid", None, "Simple Custom Detectoin GUID Missing" + ) + if self.service in ("moveguid"): + self.group_guid = self.get_param( + "config.group_guid", None, "Group GUID Missing" + ) + if self.service in ("isolationstart"): + self.unlock_code = self.get_param("config.unlock_code", None) + + self.amp_session = requests.Session() + self.amp_session.auth = (self.client_id, self.api_key) + self.amp_session.headers.update( + { + "User-Agent": "AMPforEndpoints-Cortex-Responder", + "Content-Type": "application/json", + "Accept": "application/json", + "Accept-Encoding": "gzip, deflate", + } + ) + + def run(self): + def parse_amp_error(error_response): + """Parse AMP for Endponts error response + Return the human readable error message + """ + try: + response = error_response.json() + errors = response.get("errors", []) + details = errors[0].get("details", []) + error = details[0] + return error + except IndexError: + return "Something went wrong! Recieved status code: {}".format( + error_response.status_code + ) + + def validate_guid(guid): + """Validate the provided GUIDs is the correct format + """ + expression = r"^[A-Fa-f0-9]{8}\-[A-Fa-f0-9]{4}\-[A-Fa-f0-9]{4}\-[A-Fa-f0-9]{4}\-[A-Fa-f0-9]{12}$" + return bool(re.match(expression, guid)) + + def scd_add(amp_cloud, scd_guid, sha256, caseId, title): + """Add a SHA256 to a Simple Custom Detection List + """ + url = "https://{}/v1/file_lists/{}/files/{}".format( + amp_cloud, scd_guid, sha256 + ) + + body = { + "description": "The Hive Case ID: {} Case Title: {}".format( + caseId, title + ) + } + body = json.dumps(body) + response = self.amp_session.post(url, data=body) + + if response.status_code == 201: + self.report({"message": "SHA256 Added to SCD"}) + if response.status_code == 409: + self.error("SHA256 already on SCD list") + else: + self.error("Failed to add to blacklist.") + + def scd_remove(amp_cloud, scd_guid, sha256): + """Remove a SHA256 from a Simple Custom Detection List + """ + url = "https://{}/v1/file_lists/{}/files/{}".format( + amp_cloud, scd_guid, sha256 + ) + + response = self.amp_session.delete(url) + + if response.status_code == 200: + self.report({"message": "SHA256 removed from SCD"}) + else: + error = parse_amp_error(response) + self.error(error) + + def move_guid(amp_cloud, group_guid, connector_guid): + """Move a connector GUID to a new group + """ + url = "https://{}/v1/computers/{}".format(amp_cloud, connector_guid) + + body = {"group_guid": group_guid} + body = json.dumps(body) + + response = self.amp_session.patch(url, data=body) + response_json = response.json() + data = response_json.get("data", {}) + hostname = data.get("hostname", "for uknown hostname") + + if response.status_code == 202: + self.report({"message": "Connector {} moved".format(hostname)}) + else: + error = parse_amp_error(response) + self.error(error) + + def isolation_start(amp_cloud, connector_guid, unlock_code): + """Send request to start host isolation for a connector + """ + url = "https://{}/v1/computers/{}/isolation".format( + amp_cloud, connector_guid + ) + + if unlock_code: + body = {"unlock_code": unlock_code} + body = json.dumps(body) + response = self.amp_session.put(url, data=body) + else: + response = self.amp_session.put(url) + + if response.status_code == 200: + self.report( + { + "message": "Request sent to start isolation for connector: {}".format( + connector_guid + ) + } + ) + else: + error = parse_amp_error(response) + self.error(error) + + def isolation_stop(amp_cloud, connector_guid): + """Send request to stop host isolation for a connector + """ + url = "https://{}/v1/computers/{}/isolation".format( + amp_cloud, connector_guid + ) + + response = self.amp_session.delete(url) + + if response.status_code == 200: + self.report( + { + "message": "Request sent to stop isolation for connector: {}".format( + connector_guid + ) + } + ) + else: + error = parse_amp_error(response) + self.error(error) + + Responder.run(self) + + dataType = self.get_param("data.dataType") + + if dataType == "hash" and self.service in ("scdadd", "scdremove"): + sha256 = self.get_param("data.data", None) + caseId = self.get_param("data.case.caseId", None, "caseId is missing") + title = self.get_param("data.case.title", None, "title is missing").encode( + "utf-8" + ) + + # Valide a valid SHA256 was provided + if not re.match(r"^[A-Fa-f0-9]{64}$", sha256): + self.error("{} is not a SHA256".format(sha256)) + + # Add SHA256 to Simple Custom Detection list + if self.service == "scdadd": + scd_add(self.amp_cloud, self.scd_guid, sha256, caseId, title) + + # Remove SHA256 from Simple Custom Detection list + if self.service == "scdremove": + scd_remove(self.amp_cloud, self.scd_guid, sha256) + + if dataType == "other" and self.service in ( + "moveguid", + "isolationstart", + "isolationstop", + ): + connector_guid = self.get_param("data.data", None) + + # Validate the connector GUID is the right format + if not validate_guid(connector_guid): + self.error( + "{} is not a valid AMP connector GUID".format(connector_guid) + ) + + # Validate the Group GUID is the right format + if self.service in ("moveguid") and not validate_guid(self.group_guid): + self.error("{} is not a valid AMP Group GUID".format(self.group_guid)) + + # Move the connector GUID to a new group + if self.service in ("moveguid"): + move_guid(self.amp_cloud, self.group_guid, connector_guid) + + # Start host isolation + if self.service in ("isolationstart"): + # Check if the unlock_code is less than 24 characters + if len(self.unlock_code) > 24: + self.error( + "Validation failed: Unlock Code is invalid, Unlock Code is too long. (Maximum 24 characters)" + ) + # Check if the unlock_code contains spaces + if bool(" " in self.unlock_code): + self.error( + "Validation failed: Unlock Code is invalid, Unlock Code cannot contain spaces" + ) + isolation_start(self.amp_cloud, connector_guid, self.unlock_code) + + # Stop host isolation + if self.service in ("isolationstop"): + isolation_stop(self.amp_cloud, connector_guid) + + # Return an error for all other datatypes + self.error( + "Incorrect dataType received '{}' as '{}'".format( + self.get_param("data.data", None), dataType + ) + ) + + def operations(self, raw): + if self.service == "scdadd": + return [self.build_operation("AddTagToArtifact", tag="AMP:blocked")] + + +if __name__ == "__main__": + AMPforEndpoints().run() diff --git a/responders/AMPforEndpoints/AMPforEndpoints_IsolationStart.json b/responders/AMPforEndpoints/AMPforEndpoints_IsolationStart.json new file mode 100644 index 000000000..3ca62de9b --- /dev/null +++ b/responders/AMPforEndpoints/AMPforEndpoints_IsolationStart.json @@ -0,0 +1,44 @@ +{ + "name": "AMPforEndpoints_IsolationStart", + "version": "1.0", + "author": "Cisco Security", + "url": "https://github.com/CiscoSecurity", + "license": "MIT", + "description": "Start host isolation for an AMP for Endpoints connector", + "dataTypeList": ["thehive:case_artifact"], + "command": "AMPforEndpoints/AMPforEndpoints.py", + "baseConfig": "AMPforEndpoints", + "config": { + "service": "isolationstart" + }, + "configurationItems": [ + { + "name": "amp_cloud", + "description": "FQDN of the AMP for Endpoints cloud to interact with", + "type": "string", + "multi": false, + "required": true + }, + { + "name": "client_id", + "description": "Client ID for AMP for Endpoints", + "type": "string", + "multi": false, + "required": true + }, + { + "name": "api_key", + "description": "API Key for AMP for Endpoints", + "type": "string", + "multi": false, + "required": true + }, + { + "name": "unlock_code", + "description": "Custom unlock code used to stop isolation from the endpoint (Maximum 24 characters)", + "type": "string", + "multi": false, + "required": false + } + ] +} diff --git a/responders/AMPforEndpoints/AMPforEndpoints_IsolationStop.json b/responders/AMPforEndpoints/AMPforEndpoints_IsolationStop.json new file mode 100644 index 000000000..3c94f6e2c --- /dev/null +++ b/responders/AMPforEndpoints/AMPforEndpoints_IsolationStop.json @@ -0,0 +1,37 @@ +{ + "name": "AMPforEndpoints_IsolationStop", + "version": "1.0", + "author": "Cisco Security", + "url": "https://github.com/CiscoSecurity", + "license": "MIT", + "description": "Stop host isolation for an AMP for Endpoints connector", + "dataTypeList": ["thehive:case_artifact"], + "command": "AMPforEndpoints/AMPforEndpoints.py", + "baseConfig": "AMPforEndpoints", + "config": { + "service": "isolationstop" + }, + "configurationItems": [ + { + "name": "amp_cloud", + "description": "FQDN of the AMP for Endpoints cloud to interact with", + "type": "string", + "multi": false, + "required": true + }, + { + "name": "client_id", + "description": "Client ID for AMP for Endpoints", + "type": "string", + "multi": false, + "required": true + }, + { + "name": "api_key", + "description": "API Key for AMP for Endpoints", + "type": "string", + "multi": false, + "required": true + } + ] +} diff --git a/responders/AMPforEndpoints/AMPforEndpoints_MoveGUID.json b/responders/AMPforEndpoints/AMPforEndpoints_MoveGUID.json new file mode 100644 index 000000000..6c1e94b99 --- /dev/null +++ b/responders/AMPforEndpoints/AMPforEndpoints_MoveGUID.json @@ -0,0 +1,44 @@ +{ + "name": "AMPforEndpoints_MoveGUID", + "version": "1.0", + "author": "Cisco Security", + "url": "https://github.com/CiscoSecurity", + "license": "MIT", + "description": "Move an AMP for Endpoints connector GUID to a different Group", + "dataTypeList": ["thehive:case_artifact"], + "command": "AMPforEndpoints/AMPforEndpoints.py", + "baseConfig": "AMPforEndpoints", + "config": { + "service": "moveguid" + }, + "configurationItems": [ + { + "name": "amp_cloud", + "description": "FQDN of the AMP for Endpoints cloud to interact with", + "type": "string", + "multi": false, + "required": true + }, + { + "name": "client_id", + "description": "Client ID for AMP for Endpoints", + "type": "string", + "multi": false, + "required": true + }, + { + "name": "api_key", + "description": "API Key for AMP for Endpoints", + "type": "string", + "multi": false, + "required": true + }, + { + "name": "group_guid", + "description": "AMP for Endpoints Group GUID for the group connectors will be moved to", + "type": "string", + "multi": false, + "required": true + } + ] +} diff --git a/responders/AMPforEndpoints/AMPforEndpoints_SCDAdd.json b/responders/AMPforEndpoints/AMPforEndpoints_SCDAdd.json new file mode 100644 index 000000000..9a011f0fd --- /dev/null +++ b/responders/AMPforEndpoints/AMPforEndpoints_SCDAdd.json @@ -0,0 +1,44 @@ +{ + "name": "AMPforEndpoints_SCDAdd", + "version": "1.0", + "author": "Cisco Security", + "url": "https://github.com/CiscoSecurity", + "license": "MIT", + "description": "Add a SHA256 to an AMP for Endpoints Simple Custom Detection list", + "dataTypeList": ["thehive:case_artifact"], + "command": "AMPforEndpoints/AMPforEndpoints.py", + "baseConfig": "AMPforEndpoints", + "config": { + "service": "scdadd" + }, + "configurationItems": [ + { + "name": "amp_cloud", + "description": "FQDN of the AMP for Endpoints cloud to interact with", + "type": "string", + "multi": false, + "required": true + }, + { + "name": "client_id", + "description": "Client ID for AMP for Endpoints", + "type": "string", + "multi": false, + "required": true + }, + { + "name": "api_key", + "description": "API Key for AMP for Endpoints", + "type": "string", + "multi": false, + "required": true + }, + { + "name": "scd_guid", + "description": "AMP for Endpoints Simple Custom Detection GUID", + "type": "string", + "multi": false, + "required": true + } + ] +} diff --git a/responders/AMPforEndpoints/AMPforEndpoints_SCDRemove.json b/responders/AMPforEndpoints/AMPforEndpoints_SCDRemove.json new file mode 100644 index 000000000..5d08bf687 --- /dev/null +++ b/responders/AMPforEndpoints/AMPforEndpoints_SCDRemove.json @@ -0,0 +1,44 @@ +{ + "name": "AMPforEndpoints_SCDRemove", + "version": "1.0", + "author": "Cisco Security", + "url": "https://github.com/CiscoSecurity", + "license": "MIT", + "description": "Remove a SHA256 to an AMP for Endpoints Simple Custom Detection list", + "dataTypeList": ["thehive:case_artifact"], + "command": "AMPforEndpoints/AMPforEndpoints.py", + "baseConfig": "AMPforEndpoints", + "config": { + "service": "scdremove" + }, + "configurationItems": [ + { + "name": "amp_cloud", + "description": "FQDN of the AMP for Endpoints cloud to interact with", + "type": "string", + "multi": false, + "required": true + }, + { + "name": "client_id", + "description": "Client ID for AMP for Endpoints", + "type": "string", + "multi": false, + "required": true + }, + { + "name": "api_key", + "description": "API Key for AMP for Endpoints", + "type": "string", + "multi": false, + "required": true + }, + { + "name": "scd_guid", + "description": "AMP for Endpoints Simple Custom Detection GUID", + "type": "string", + "multi": false, + "required": true + } + ] +} diff --git a/responders/AMPforEndpoints/requirements.txt b/responders/AMPforEndpoints/requirements.txt new file mode 100644 index 000000000..2cac23c03 --- /dev/null +++ b/responders/AMPforEndpoints/requirements.txt @@ -0,0 +1,2 @@ +requests +cortexutils From c844fa60eb343dcba4c6fce2fc3824a3845a3ca9 Mon Sep 17 00:00:00 2001 From: Michael Auger Date: Sun, 26 Jan 2020 05:22:55 -0500 Subject: [PATCH 2/2] Fix unlock code validation When no unlock code was provided the validation would still execute resulting in an exception --- responders/AMPforEndpoints/AMPforEndpoints.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/responders/AMPforEndpoints/AMPforEndpoints.py b/responders/AMPforEndpoints/AMPforEndpoints.py index 06fdc3c1e..ba4fbcf00 100644 --- a/responders/AMPforEndpoints/AMPforEndpoints.py +++ b/responders/AMPforEndpoints/AMPforEndpoints.py @@ -209,12 +209,12 @@ def isolation_stop(amp_cloud, connector_guid): # Start host isolation if self.service in ("isolationstart"): # Check if the unlock_code is less than 24 characters - if len(self.unlock_code) > 24: + if self.unlock_code and self.unlock_code and len(self.unlock_code) > 24: self.error( "Validation failed: Unlock Code is invalid, Unlock Code is too long. (Maximum 24 characters)" ) # Check if the unlock_code contains spaces - if bool(" " in self.unlock_code): + if self.unlock_code and bool(" " in self.unlock_code): self.error( "Validation failed: Unlock Code is invalid, Unlock Code cannot contain spaces" )