def _dt_utils_delete_rows_function(self, event, *args, **kwargs): """Function: Function that deletes rows from a Data Table""" log = logging.getLogger(__name__) try: # Instansiate new Resilient API object res_client = self.rest_client() workflow_id = event.message.get('workflow_instance', {}).get('workflow_instance_id') inputs = { "incident_id": get_function_input(kwargs, "incident_id"), # number (required) "dt_utils_datatable_api_name": get_function_input(kwargs, "dt_utils_datatable_api_name"), # text (required) "dt_utils_rows_ids": get_function_input(kwargs, "dt_utils_rows_ids", optional=True), # text (optional) "dt_utils_search_column": get_function_input(kwargs, "dt_utils_search_column", optional=True), # text (optional) "dt_utils_search_value": get_function_input(kwargs, "dt_utils_search_value", optional=True), # text (optional) } log.info("incident_id: {0}".format(inputs["incident_id"])) log.info("dt_utils_datatable_api_name: {0}".format(inputs["dt_utils_datatable_api_name"])) log.info("dt_utils_rows_ids: {0}".format(inputs["dt_utils_rows_ids"])) log.info("dt_utils_search_column: {0}".format(inputs["dt_utils_search_column"])) log.info(u"dt_utils_search_value: {0}".format(inputs["dt_utils_search_value"])) # Ensure correct search inputs are defined correctly valid_search_inputs = validate_search_inputs(rows_ids=inputs["dt_utils_rows_ids"], search_column=inputs["dt_utils_search_column"], search_value=inputs["dt_utils_search_value"]) if not valid_search_inputs["valid"]: raise ValueError(valid_search_inputs["msg"]) # Create payload dict with inputs payload = FunctionPayload(inputs) # Instantiate a new RESDatatable datatable = RESDatatable(res_client, payload.inputs["incident_id"], payload.inputs["dt_utils_datatable_api_name"]) # get datatable row_id if function used on a datatable row_id = datatable.get_row_id_from_workflow(workflow_id) row_id and log.debug("Current row_id: %s", row_id) # Get the data table data datatable.get_data() deleted_rows = datatable.delete_rows(payload.inputs["dt_utils_rows_ids"], payload.inputs["dt_utils_search_column"], payload.inputs["dt_utils_search_value"], row_id, workflow_id) if not deleted_rows: yield StatusMessage("No row(s) found.") payload.success = False elif "error" in deleted_rows: yield StatusMessage(u"Row(s) not deleted. Error: {0}".format(deleted_rows["error"])) payload.success = False raise FunctionError("Failed to delete a row.") else: yield StatusMessage("Row(s) {0} in {1} deleted.".format(deleted_rows, datatable.api_name)) payload.rows_ids = deleted_rows payload.success = True results = payload.as_dict() log.info("Complete") # Produce a FunctionResult with the results yield FunctionResult(results) except Exception: yield FunctionError()
def _isitphishing_url_function(self, event, *args, **kwargs): """Function: isitphishing_url This function takes URL string as a required parameter. It queries the Vade Secure isiphishing API to analyze the URL to determine if it is PHISHING or SPAM. The results contain the result of the query in 'contents' and the URL input parameter to the function. """ try: rp = ResultPayload(CONFIG_DATA_SECTION, **kwargs) # Get the function parameters: isitphishing_url = kwargs.get("isitphishing_url") # text log = logging.getLogger(__name__) log.info("isitphishing_url: %s", isitphishing_url) # Form the URL for API request. API_URL = u"{0}/url".format(self.options["isitphishing_api_url"]) # Get the license key to access the API endpoint. auth_token = get_license_key(self.options["isitphishing_name"], self.options["isitphishing_license"]) # Build the header and the data payload. headers = { "Authorization": u'Bearer {}'.format(auth_token), "Content-type": "application/json", } payload = { "url": isitphishing_url, "force": False, "smart": True, "timeout": 8000 } yield StatusMessage( "Query isitPhishing.org endpoint for status of URL {0}.". format(isitphishing_url)) # Make URL request rc = RequestsCommon(self.opts, self.options) results_analysis = rc.execute_call("post", API_URL, payload, log=log, headers=headers) results = rp.done(True, results_analysis) # Send back the results and the input parameter. results = { "content": results_analysis, "inputs": { "URL": isitphishing_url } } # Produce a FunctionResult with the results yield FunctionResult(results) except Exception as err: yield FunctionError(err)
def _fn_calendar_invite_function(self, event, *args, **kwargs): """Function: A function to invite people to a meeting via a calendar invite""" try: # Get the calendar meeting information input calendar_invite_datetime = kwargs.get( "calendar_invite_datetime") # datetime picker calendar_invite_subject = kwargs.get( "calendar_invite_subject") # text calendar_invite_description = kwargs.get( "calendar_invite_description") # text area calendar_invite_extra_email_addr = kwargs.get( "calendar_invite_extra_email_addr") # text area incident_id = kwargs.get("calendar_invite_incident_id") # number log = logging.getLogger(__name__) log.info(u"calendar_invite_datetime: %s", calendar_invite_datetime) log.info(u"calendar_invite_subject: %s", calendar_invite_subject) log.info(u"calendar_invite_description: %s", calendar_invite_description) log.info(u"calendar_invite_extra_email_addr %s", calendar_invite_extra_email_addr) # Email sender information host = self.email_host port = int(self.email_port) nickname = self.email_nickname e_login = self.email_username e_password = self.email_password now_utc = datetime.datetime.utcnow() meeting_time_utc = datetime.datetime.utcfromtimestamp( calendar_invite_datetime / 1000) if now_utc > meeting_time_utc: log.error("Calendar date and time for meeting is in the past.") raise ValueError( "Calendar date and time for meeting is in the past.") # Get email addresses of the members and owner of the incident. client = self.rest_client() attendees = get_email_addresses(client, log, incident_id, calendar_invite_extra_email_addr) yield StatusMessage("Sending Emails to {}".format(attendees)) # Build the email message string to be sent. sender = u"{} <{}>".format(nickname, e_login) email_message_string = build_email_message( calendar_invite_datetime, calendar_invite_subject, calendar_invite_description, nickname, e_login, sender, attendees) yield StatusMessage("Connecting to Mail Server") # Connect to SMTP server and send the message. send_email(host, port, sender, e_login, e_password, attendees, email_message_string) yield StatusMessage("Send Mail - Complete") results = { "recipient": attendees, "sender": sender, "subject": calendar_invite_subject, "description": calendar_invite_description } # Produce a FunctionResult with the results yield FunctionResult(results) except Exception as e: yield FunctionError()
def _cb_retrieve_process_list_function(self, event, *args, **kwargs): results = {} results["was_successful"] = False results["hostname"] = None lock_acquired = False try: # Get the function parameters: incident_id = kwargs.get("incident_id") # number hostname = kwargs.get("hostname") # text log = logging.getLogger(__name__) # Establish logging days_later_timeout_length = datetime.datetime.now( ) + datetime.timedelta( days=DAYS_UNTIL_TIMEOUT) # Max duration length before aborting hostname = hostname.upper( )[:15] # CB limits hostname to 15 characters sensor = cb.select(Sensor).where( 'hostname:' + hostname) # Query CB for the hostname's sensor timeouts = 0 # Number of timeouts that have occurred if len(sensor) <= 0: # Host does not have CB agent, abort yield StatusMessage( "[FATAL ERROR] CB could not find hostname: " + str(hostname)) yield FunctionResult(results) return sensor = sensor[0] # Get the sensor object from the query results["hostname"] = str(hostname).upper() while timeouts <= MAX_TIMEOUTS: # Max timeouts before aborting try: now = datetime.datetime.now() # Check if the sensor is queued to restart, wait up to 90 seconds before checking online status three_minutes_passed = datetime.datetime.now( ) + datetime.timedelta(minutes=3) while (sensor.restart_queued is True) and (three_minutes_passed >= now): time.sleep(3) # Give the CPU a break, it works hard! now = datetime.datetime.now() sensor = (cb.select(Sensor).where('hostname:' + hostname) )[0] # Retrieve the latest sensor vitals # Check online status if sensor.status != "Online": yield StatusMessage('[WARNING] Hostname: ' + str(hostname) + ' is offline. Will attempt for ' + str(DAYS_UNTIL_TIMEOUT) + ' days...') # Check lock status if os.path.exists( '/home/integrations/.resilient/cb_host_locks/{}.lock' .format(hostname)): yield StatusMessage( '[WARNING] A running action has a lock on ' + str(hostname) + '. Will attempt for ' + str(DAYS_UNTIL_TIMEOUT) + ' days...') # Wait for offline and locked hosts for days_later_timeout_length while (sensor.status != "Online" or os.path.exists( '/home/integrations/.resilient/cb_host_locks/{}.lock' .format(hostname))) and (days_later_timeout_length >= now): time.sleep(3) # Give the CPU a break, it works hard! now = datetime.datetime.now() sensor = (cb.select(Sensor).where('hostname:' + hostname) )[0] # Retrieve the latest sensor vitals # Abort after DAYS_UNTIL_TIMEOUT if sensor.status != "Online" or os.path.exists( '/home/integrations/.resilient/cb_host_locks/{}.lock' .format(hostname)): yield StatusMessage('[FATAL ERROR] Hostname: ' + str(hostname) + ' is still offline!') yield FunctionResult(results) return # Check if the sensor is queued to restart, wait up to 90 seconds before continuing three_minutes_passed = datetime.datetime.now( ) + datetime.timedelta(minutes=3) while (sensor.restart_queued is True) and ( three_minutes_passed >= now ): # If the sensor is queued to restart, wait up to 90 seconds time.sleep(3) # Give the CPU a break, it works hard! now = datetime.datetime.now() sensor = (cb.select(Sensor).where('hostname:' + hostname) )[0] # Retrieve the latest sensor vitals # Verify the incident still exists and is reachable, if not abort try: incident = self.rest_client().get( '/incidents/{0}?text_content_output_format=always_text&handle_format=names' .format(str(incident_id))) except Exception as err: if err.message and "not found" in err.message.lower(): log.info('[FATAL ERROR] Incident ID ' + str(incident_id) + ' no longer exists.') log.info('[FAILURE] Fatal error caused exit!') else: log.info( '[FATAL ERROR] Incident ID ' + str(incident_id) + ' could not be reached, Resilient instance may be down.' ) log.info('[FAILURE] Fatal error caused exit!') return # Acquire host lock try: f = os.fdopen( os.open( '/home/integrations/.resilient/cb_host_locks/{}.lock' .format(hostname), os.O_CREAT | os.O_WRONLY | os.O_EXCL), 'w') f.close() lock_acquired = True except OSError: continue # Establish a session to the host sensor yield StatusMessage( '[INFO] Establishing session to CB Sensor #' + str(sensor.id) + ' (' + sensor.hostname + ')') session = cb.live_response.request_session(sensor.id) yield StatusMessage('[SUCCESS] Connected on Session #' + str(session.session_id) + ' to CB Sensor #' + str(sensor.id) + ' (' + sensor.hostname + ')') process_list = session.list_processes( ) # Retrieve process information list with tempfile.NamedTemporaryFile( delete=False ) as temp_file: # Create temporary temp_file for the CSV file try: with UnicodeWriter(temp_file.name) as csv_writer: csv_writer.writerow([ 'Process Name:', 'Path:', 'Command Line:', 'Username:'******'PID:', 'SID:', 'Parent PID:', 'Created Time:', 'Process CB URL:' ]) for process in process_list: process_exe = process['path'].rsplit( '\\')[-1] process_path = process['path'] process_commandline = process[ 'command_line'] process_username = process['username'] process_pid = str(process['pid']) process_sid = str(process['sid']) process_parent_pid = str(process['parent']) process_created_time = datetime.datetime.fromtimestamp( process['create_time']).strftime( '%Y-%m-%d %H:%M:%S') process_cb_url = str( cb.url + r'/#/analyze/' + process['proc_guid'] + r'/' + str(process['create_time']) + '?cb.legacy_5x_mode=false') csv_writer.writerow([ process_exe, process_path, process_commandline, process_username, process_pid, process_sid, process_parent_pid, process_created_time, process_cb_url ]) yield StatusMessage( '[SUCCESS] Retrieved process data file from Sensor!' ) self.rest_client().post_attachment( '/incidents/{0}/attachments'.format( incident_id), temp_file.name, '{0}-running_processes.csv'.format( sensor.hostname) ) # Post temp_file to incident yield StatusMessage( '[SUCCESS] Posted CSV data file to the incident as an attachment!' ) finally: os.unlink( temp_file.name) # Delete temporary temp_file except TimeoutError: # Catch TimeoutError and handle timeouts = timeouts + 1 if timeouts <= MAX_TIMEOUTS: yield StatusMessage( '[ERROR] TimeoutError was encountered. Reattempting... (' + str(timeouts) + '/' + str(MAX_TIMEOUTS) + ')') try: session.close() except: pass sensor = (cb.select(Sensor).where('hostname:' + hostname) )[0] # Retrieve the latest sensor vitals sensor.restart_sensor( ) # Restarting the sensor may avoid a timeout from occurring again time.sleep(30) # Sleep to apply sensor restart sensor = (cb.select(Sensor).where('hostname:' + hostname) )[0] # Retrieve the latest sensor vitals else: yield StatusMessage( '[FATAL ERROR] TimeoutError was encountered. The maximum number of retries was reached. Aborting!' ) yield StatusMessage( '[FAILURE] Fatal error caused exit!') continue except ( ApiError, ProtocolError, NewConnectionError, ConnectTimeoutError, MaxRetryError ) as err: # Catch urllib3 connection exceptions and handle if 'ApiError' in str( type(err).__name__ ) and 'network connection error' not in str(err): raise # Only handle ApiError involving network connection error timeouts = timeouts + 1 if timeouts <= MAX_TIMEOUTS: yield StatusMessage( '[ERROR] Carbon Black was unreachable. Reattempting in 30 minutes... (' + str(timeouts) + '/' + str(MAX_TIMEOUTS) + ')') time.sleep( 1800 ) # Sleep for 30 minutes, backup service may have been running. else: yield StatusMessage( '[FATAL ERROR] ' + str(type(err).__name__) + ' was encountered. The maximum number of retries was reached. Aborting!' ) yield StatusMessage( '[FAILURE] Fatal error caused exit!') continue except Exception as err: # Catch all other exceptions and abort yield StatusMessage('[FATAL ERROR] Encountered: ' + str(err)) yield StatusMessage('[FAILURE] Fatal error caused exit!') else: results["was_successful"] = True try: session.close() except: pass yield StatusMessage( '[INFO] Session has been closed to CB Sensor #' + str(sensor.id) + '(' + sensor.hostname + ')') break # Release the host lock if acquired if lock_acquired is True: os.remove( '/home/integrations/.resilient/cb_host_locks/{}.lock'. format(hostname)) # Produce a FunctionResult with the results yield FunctionResult(results) except Exception: yield FunctionError()
def _ldap_md_utilities_search_function(self, event, *args, **kwargs): """Function: Resilient Function to do a search or query against an LDAP server.""" ############################################## def replace_ldap_param(ldap_param_value=None, ldap_search_filter=""): re_pattern = "%ldap_param%" # if "ldap_param" in ldap_search_filter: if re.search(re_pattern, ldap_search_filter) is not None: if ldap_param_value is None: raise ValueError("The LDAP Search Filter '{0}' contains the key '%ldap_param%' but no value has been given for ldap_search_param.".format(ldap_search_filter)) else: # Insert escaped param value in filter, need to escape any backslashes X 2 for regex. ldap_search_filter = re.sub(re_pattern, ldap_param_value.replace('\\', '\\\\'), ldap_search_filter) return ldap_search_filter ############################################## try: # Get the wf_instance_id of the workflow this Function was called in wf_instance_id = event.message["workflow_instance"]["workflow_instance_id"] yield StatusMessage("Starting 'ldap_md_utilities_search' running in workflow '{0}'".format(wf_instance_id)) # Get the function parameters: ldap_md_domain_name = kwargs.get("ldap_md_domain_name") # text ldap_md_search_attributes = kwargs.get("ldap_md_search_attributes") # text ldap_md_search_param = kwargs.get("ldap_md_search_param") # text ldap_md_search_base = kwargs.get("ldap_md_search_base") # text ldap_md_search_filter = self.get_textarea_param(kwargs.get("ldap_md_search_filter")) # textarea log = logging.getLogger(__name__) log.info("ldap_md_domain_name: %s", ldap_md_domain_name) log.info("ldap_md_search_attributes: %s", ldap_md_search_attributes) log.info("ldap_md_search_param: %s", ldap_md_search_param) log.info("ldap_md_search_base: %s", ldap_md_search_base) log.info("ldap_md_search_filter: %s", ldap_md_search_filter) yield StatusMessage("Function Inputs OK") # Instansiate helper (which gets appconfigs from file) helper = LDAPUtilitiesHelper(self.options, ldap_md_domain_name) log.info("[app.config] -ldap_server: %s", helper.LDAP_SERVER) #log.info("[app.config] -ldap_pas: %s", helper.LDAP_PAS) log.info("[app.config] -ldap_user_dn: %s", helper.LDAP_USER_DN) yield StatusMessage("Appconfig Settings OK") ############################################## # If search_attributes is not specified, request that ALL_ATTRIBUTES for the DN be returned if ldap_md_search_attributes is None: ldap_md_search_attributes = ALL_ATTRIBUTES else: ldap_md_search_attributes = [str(attr) for attr in ldap_md_search_attributes.split(',')] if ldap_md_search_param is not None: # Escape special chars from the search_param ldap_md_search_param = escape_filter_chars(ldap_md_search_param) ldap_md_search_filter = replace_ldap_param(ldap_md_search_param, ldap_md_search_filter) # Instansiate LDAP Server and Connection conn = helper.get_ldap_connection() try: # Bind to the connection conn.bind() except Exception as err: raise ValueError("Cannot connect to LDAP Server. Ensure credentials are correct\n Error: {0}".format(err)) try: # Inform user yield StatusMessage("Connected to {0}".format("Active Directory" if helper.LDAP_IS_ACTIVE_DIRECTORY else "LDAP Server")) res = None entries = [] success = False yield StatusMessage("Attempting to Search") res = conn.search( search_base=ldap_md_search_base, search_filter=ldap_md_search_filter, attributes=ldap_md_search_attributes) if res and len(conn.entries) > 0: entries = json.loads(conn.response_to_json())["entries"] log.info("Result contains %s entries", len(entries)) # Each entry has 'dn' and dict of 'attributes'. Move attributes to the top level for easier processing. for entry in entries: entry.update(entry.pop("attributes", None)) yield StatusMessage("{0} entries found".format(len(entries))) success = True else: yield StatusMessage("No entries found") success = False except LDAPSocketOpenError: success = False raise ValueError("Invalid Search Base", ldap_md_search_base) except Exception as err: success = False raise ValueError("Could not Search the LDAP Server. Ensure 'ldap_search_base' is valid", err) finally: # Unbind connection conn.unbind() ############################################## results = { "success": success, "domain_name": ldap_md_domain_name, "entries": entries } yield StatusMessage("Finished 'ldap_md_utilities_search' that was running in workflow '{0}'".format(wf_instance_id)) # Produce a FunctionResult with the results yield FunctionResult(results) except Exception: yield FunctionError()
def _base64_to_artifact_function(self, event, *args, **kwargs): """Function: """ try: log = logging.getLogger(__name__) # Get the function parameters: # artifact_file_type: # "Email Attachment", "Malware Sample", "Log File", "X509 Certificate File", "Other File", etc. base64content = kwargs.get("base64content") # text incident_id = kwargs.get("incident_id") # number artifact_file_type = self.get_select_param( kwargs.get("artifact_file_type")) or "Malware Sample" file_name = kwargs.get("file_name") # text content_type = kwargs.get("content_type") # text description = self.get_textarea_param( kwargs.get("description")) # textarea content_type = content_type \ or mimetypes.guess_type(file_name or "")[0] \ or "application/octet-stream" log.info("incident_id: %s", incident_id) log.info("artifact_file_type: %s", artifact_file_type) log.info("file_name: %s", file_name) log.info("content_type: %s", content_type) log.info("description: %s", description) # To post a file artifact, we need to specify the type by id # So, look up the id of the type name client = self.rest_client() artifact_type_id = 16 # Other File (as default) for value in client.cached_get( "/types/artifact/fields/type")["values"]: if artifact_file_type == value["label"]: artifact_type_id = value["value"] break yield StatusMessage("Writing artifact...") with tempfile.NamedTemporaryFile(delete=False) as temp_file: try: temp_file.write(base64.b64encode(s_to_b(base64content))) temp_file.close() # Create a new artifact client = self.rest_client() artifact_uri = "/incidents/{}/artifacts/files".format( incident_id) new_artifact = client.post_artifact_file( artifact_uri, artifact_type_id, temp_file.name, description=description, value=file_name, mimetype=content_type) finally: os.unlink(temp_file.name) # Produce a FunctionResult with the return value if isinstance(new_artifact, list): new_artifact = new_artifact[0] log.info(json.dumps(new_artifact)) yield FunctionResult(new_artifact) except Exception: yield FunctionError()
def _exchange_find_emails_function(self, event, *args, **kwargs): """Function: """ try: # Get the function parameters: exchange_email = kwargs.get("exchange_email") # text exchange_folder_path = kwargs.get("exchange_folder_path") # text exchange_email_ids = kwargs.get("exchange_email_ids") # text exchange_sender = kwargs.get("exchange_sender") # text exchange_message_subject = kwargs.get( "exchange_message_subject") # text exchange_message_body = kwargs.get("exchange_message_body") # text exchange_start_date = kwargs.get( "exchange_start_date") # datepicker exchange_end_date = kwargs.get("exchange_end_date") # datepicker exchange_has_attachments = kwargs.get( "exchange_has_attachments") # boolean exchange_order_by_recency = kwargs.get( "exchange_order_by_recency") # boolean exchange_num_emails = kwargs.get("exchange_num_emails") # int exchange_search_subfolders = kwargs.get( "exchange_search_subfolders") # boolean log = logging.getLogger(__name__) if exchange_folder_path is None: exchange_folder_path = self.options.get('default_folder_path') log.info( 'No folder path was specified, using value from config file' ) log.info("exchange_email: %s" % exchange_email) log.info("exchange_folder_path: %s" % exchange_folder_path) log.info("exchange_email_ids: %s" % exchange_email_ids) log.info("exchange_sender: %s" % exchange_sender) log.info("exchange_message_subject: %s" % exchange_message_subject) log.info("exchange_message_body: %s" % exchange_message_body) log.info("exchange_start_date: %s" % exchange_start_date) log.info("exchange_end_date: %s" % exchange_end_date) log.info("exchange_has_attachments: %s" % exchange_has_attachments) log.info("exchange_order_by_recency: %s" % exchange_order_by_recency) log.info("exchange_num_emails: %s" % exchange_num_emails) log.info("exchange_search_subfolders: %s" % exchange_search_subfolders) # Initialize utils utils = exchange_utils(self.options, self.opts) # Find emails yield StatusMessage("Finding emails") emails = utils.get_emails( exchange_email, exchange_folder_path, exchange_email_ids, exchange_sender, exchange_message_subject, exchange_message_body, exchange_start_date, exchange_end_date, exchange_has_attachments, exchange_order_by_recency, exchange_num_emails, exchange_search_subfolders) yield StatusMessage("Done finding emails, %d emails found" % emails.count()) # Populate results with query data results = utils.create_email_function_results(emails) # Produce a FunctionResult with the results yield FunctionResult(results) except Exception: yield FunctionError()
def _exchange_online_write_message_as_attachment_function( self, event, *args, **kwargs): """Function: This function will get the mime content of an Exchange Online message and write it as an attachment.""" try: # Initialize the results payload rp = ResultPayload(CONFIG_DATA_SECTION, **kwargs) # Validate fields validate_fields( ['incident_id', 'exo_email_address', 'exo_messages_id'], kwargs) # Get the function parameters: incident_id = kwargs.get("incident_id") # number task_id = kwargs.get("task_id") # number email_address = kwargs.get("exo_email_address") # text message_id = kwargs.get("exo_messages_id") # text attachment_name = kwargs.get("exo_attachment_name") # text LOG.info(u"incident_id: %s", incident_id) LOG.info(u"task_id: %s", task_id) LOG.info(u"exo_email_address: %s", email_address) LOG.info(u"exo_messages_id: %s", message_id) LOG.info(u"exo_attachment_name: %s", attachment_name) yield StatusMessage( u"Starting to get message mime for email address: {}".format( email_address)) # Get the MS Graph helper class MS_graph_helper = MSGraphHelper( self.options.get("microsoft_graph_token_url"), self.options.get("microsoft_graph_url"), self.options.get("tenant_id"), self.options.get("client_id"), self.options.get("client_secret"), self.options.get("max_messages"), self.options.get("max_users"), RequestsCommon(self.opts, self.options).get_proxies()) # Call MS Graph API to get the user profile response = MS_graph_helper.get_message_mime( email_address, message_id) datastream = BytesIO(response.content) if attachment_name is None: attachment_name = u"message-{}-{}.eml".format( email_address, message_id) LOG.info(u"attachment_name: %s", attachment_name) # Get the rest client so we can add the attachment to the incident. rest_client = self.rest_client() # Write the file as attachement: failures will raise an exception write_file_attachment(rest_client, attachment_name, datastream, incident_id, task_id) results_data = {"attachment_name": attachment_name} results = rp.done(True, results_data) yield StatusMessage( u"Returning results for get message mime for email address: {0}\n attachment name: {1}" .format(email_address, attachment_name)) # Produce a FunctionResult with the results yield FunctionResult(results) except Exception as err: LOG.error(err) yield FunctionError(err)
def _cb_refresh_av_signatures(self, event, *args, **kwargs): results = {} results["was_successful"] = False results["hostname"] = None lock_acquired = False try: # Get the function parameters: incident_id = kwargs.get("incident_id") # number hostname = kwargs.get("hostname") # text log = logging.getLogger(__name__) # Establish logging days_later_timeout_length = datetime.datetime.now( ) + datetime.timedelta( days=DAYS_UNTIL_TIMEOUT) # Max duration length before aborting hostname = hostname.upper( )[:15] # CB limits hostname to 15 characters sensor = cb.select(Sensor).where( 'hostname:' + hostname) # Query CB for the hostname's sensor timeouts = 0 # Number of timeouts that have occurred if len(sensor) <= 0: # Host does not have CB agent, abort yield StatusMessage( "[FATAL ERROR] CB could not find hostname: " + str(hostname)) yield FunctionResult(results) return sensor = sensor[0] # Get the sensor object from the query results["hostname"] = str(hostname).upper() while timeouts <= MAX_TIMEOUTS: # Max timeouts before aborting try: now = datetime.datetime.now() # Check if the sensor is queued to restart, wait up to 90 seconds before checking online status three_minutes_passed = datetime.datetime.now( ) + datetime.timedelta(minutes=3) while (sensor.restart_queued is True) and (three_minutes_passed >= now): time.sleep(3) # Give the CPU a break, it works hard! now = datetime.datetime.now() sensor = (cb.select(Sensor).where('hostname:' + hostname) )[0] # Retrieve the latest sensor vitals # Check online status if sensor.status != "Online": yield StatusMessage('[WARNING] Hostname: ' + str(hostname) + ' is offline. Will attempt for ' + str(DAYS_UNTIL_TIMEOUT) + ' days...') # Check lock status if os.path.exists( '/home/integrations/.resilient/cb_host_locks/{}.lock' .format(hostname)): yield StatusMessage( '[WARNING] A running action has a lock on ' + str(hostname) + '. Will attempt for ' + str(DAYS_UNTIL_TIMEOUT) + ' days...') # Wait for offline and locked hosts for days_later_timeout_length while (sensor.status != "Online" or os.path.exists( '/home/integrations/.resilient/cb_host_locks/{}.lock' .format(hostname))) and (days_later_timeout_length >= now): time.sleep(3) # Give the CPU a break, it works hard! now = datetime.datetime.now() sensor = (cb.select(Sensor).where('hostname:' + hostname) )[0] # Retrieve the latest sensor vitals # Abort after DAYS_UNTIL_TIMEOUT if sensor.status != "Online" or os.path.exists( '/home/integrations/.resilient/cb_host_locks/{}.lock' .format(hostname)): yield StatusMessage('[FATAL ERROR] Hostname: ' + str(hostname) + ' is still offline!') yield FunctionResult(results) return # Check if the sensor is queued to restart, wait up to 90 seconds before continuing three_minutes_passed = datetime.datetime.now( ) + datetime.timedelta(minutes=3) while (sensor.restart_queued is True) and ( three_minutes_passed >= now ): # If the sensor is queued to restart, wait up to 90 seconds time.sleep(3) # Give the CPU a break, it works hard! now = datetime.datetime.now() sensor = (cb.select(Sensor).where('hostname:' + hostname) )[0] # Retrieve the latest sensor vitals # Verify the incident still exists and is reachable, if not abort try: incident = self.rest_client().get( '/incidents/{0}?text_content_output_format=always_text&handle_format=names' .format(str(incident_id))) except Exception as err: if err.message and "not found" in err.message.lower(): log.info('[FATAL ERROR] Incident ID ' + str(incident_id) + ' no longer exists.') log.info('[FAILURE] Fatal error caused exit!') else: log.info( '[FATAL ERROR] Incident ID ' + str(incident_id) + ' could not be reached, Resilient instance may be down.' ) log.info('[FAILURE] Fatal error caused exit!') return # Acquire host lock try: f = os.fdopen( os.open( '/home/integrations/.resilient/cb_host_locks/{}.lock' .format(hostname), os.O_CREAT | os.O_WRONLY | os.O_EXCL), 'w') f.close() lock_acquired = True except OSError: continue # Establish a session to the host sensor yield StatusMessage( '[INFO] Establishing session to CB Sensor #' + str(sensor.id) + ' (' + sensor.hostname + ')') session = cb.live_response.request_session(sensor.id) yield StatusMessage('[SUCCESS] Connected on Session #' + str(session.session_id) + ' to CB Sensor #' + str(sensor.id) + ' (' + sensor.hostname + ')') av_paths = [] try: session.list_directory( r'C:\Program Files\Microsoft Security Client\mpcmdrun.exe' ) av_paths.append( r'C:\Program Files\Microsoft Security Client\mpcmdrun.exe' ) except TimeoutError: raise except Exception: pass try: session.list_directory( r'C:\Program Files\Windows Defender\mpcmdrun.exe') av_paths.append( r'C:\Program Files\Windows Defender\mpcmdrun.exe') except TimeoutError: raise except Exception: pass if len( av_paths ) > 1: # If both Defender and Microsoft Security Client were found for process in session.list_processes( ): # Use whichever version is running on the endpoint if ('c:\program files\microsoft security client' in process['path'].lower()): av_paths.remove( 'C:\Program Files\Windows Defender\mpcmdrun.exe' ) break elif ('c:\program files\windows defender' in process['path'].lower()): av_paths.remove( 'C:\Program Files\Microsoft Security Client\mpcmdrun.exe' ) break for detected_av_path in av_paths: o = session.create_process( detected_av_path + ' -RemoveDefinitions -All', True, None, None, 300) yield StatusMessage( '[SUCCESS] Signature removal command sent to ' + detected_av_path) log.info('[INFO] Output was: \n' + str(o)) results["remove_definitions_output"] = str(o) o = session.create_process( detected_av_path + ' -SignatureUpdate', True, None, None, 300) log.info( '[SUCCESS] Signature update command sent to ' + detected_av_path) yield StatusMessage( '[SUCCESS] Signature update command sent to ' + detected_av_path) log.info('[INFO] Output was: \n' + str(o)) results["signature_update_output"] = str(o) if len(av_paths) == 0: yield StatusMessage( '[ERROR] Neither Windows Defender nor Microsoft Security Client were detected.' ) yield StatusMessage( '[FAILURE] Signatures were neither cleaned nor updated!' ) results["was_successful"] = False try: session.close() except: pass break except TimeoutError: # Catch TimeoutError and handle timeouts = timeouts + 1 if timeouts <= MAX_TIMEOUTS: yield StatusMessage( '[ERROR] TimeoutError was encountered. Reattempting... (' + str(timeouts) + '/' + str(MAX_TIMEOUTS) + ')') try: session.close() except: pass sensor = (cb.select(Sensor).where('hostname:' + hostname) )[0] # Retrieve the latest sensor vitals sensor.restart_sensor( ) # Restarting the sensor may avoid a timeout from occurring again time.sleep(30) # Sleep to apply sensor restart sensor = (cb.select(Sensor).where('hostname:' + hostname) )[0] # Retrieve the latest sensor vitals else: yield StatusMessage( '[FATAL ERROR] TimeoutError was encountered. The maximum number of retries was reached. Aborting!' ) yield StatusMessage( '[FAILURE] Fatal error caused exit!') continue except ( ApiError, ProtocolError, NewConnectionError, ConnectTimeoutError, MaxRetryError ) as err: # Catch urllib3 connection exceptions and handle if 'ApiError' in str( type(err).__name__ ) and 'network connection error' not in str(err): raise # Only handle ApiError involving network connection error timeouts = timeouts + 1 if timeouts <= MAX_TIMEOUTS: yield StatusMessage( '[ERROR] Carbon Black was unreachable. Reattempting in 30 minutes... (' + str(timeouts) + '/' + str(MAX_TIMEOUTS) + ')') time.sleep( 1800 ) # Sleep for 30 minutes, backup service may have been running. else: yield StatusMessage( '[FATAL ERROR] ' + str(type(err).__name__) + ' was encountered. The maximum number of retries was reached. Aborting!' ) yield StatusMessage( '[FAILURE] Fatal error caused exit!') continue except Exception as err: # Catch all other exceptions and abort yield StatusMessage('[FATAL ERROR] Encountered: ' + str(err)) yield StatusMessage('[FAILURE] Fatal error caused exit!') else: results["was_successful"] = True try: session.close() except: pass yield StatusMessage( '[INFO] Session has been closed to CB Sensor #' + str(sensor.id) + '(' + sensor.hostname + ')') break # Release the host lock if acquired if lock_acquired is True: os.remove( '/home/integrations/.resilient/cb_host_locks/{}.lock'. format(hostname)) # Produce a FunctionResult with the results yield FunctionResult(results) except Exception: yield FunctionError()
def _ldap_md_utilities_add_to_groups_function(self, event, *args, **kwargs): """Function: A function that allows adding multiple users to multiple groups""" try: # Get the wf_instance_id of the workflow this Function was called in wf_instance_id = event.message["workflow_instance"][ "workflow_instance_id"] yield StatusMessage( "Starting 'ldap_md_utilities_add_to_groups' running in workflow '{0}'" .format(wf_instance_id)) # Get the function parameters: ldap_md_domain_name = kwargs.get("ldap_md_domain_name") # text ldap_md_multiple_user_dn = kwargs.get( "ldap_md_multiple_user_dn") # text ldap_md_multiple_group_dn = kwargs.get( "ldap_md_multiple_group_dn") # text log = logging.getLogger(__name__) log.info("ldap_md_domain_name: %s", ldap_md_domain_name) log.info("ldap_md_multiple_user_dn: %s", ldap_md_multiple_user_dn) log.info("ldap_md_multiple_group_dn: %s", ldap_md_multiple_group_dn) yield StatusMessage("Function Inputs OK") # Instansiate helper (which gets appconfigs from file) helper = LDAPUtilitiesHelper(self.options, ldap_md_domain_name) log.info("[app.config] -ldap_server: %s", helper.LDAP_SERVER) log.info("[app.config] -ldap_user_dn: %s", helper.LDAP_USER_DN) yield StatusMessage("Appconfig Settings OK") ############################################## if not helper.LDAP_IS_ACTIVE_DIRECTORY: raise FunctionError( "This function only supports an Active Directory connection. Make sure ldap_is_active_directory is set to True in the app.config file" ) try: # Try converting input to an array ldap_md_multiple_user_dn = literal_eval( ldap_md_multiple_user_dn) ldap_md_multiple_group_dn = literal_eval( ldap_md_multiple_group_dn) except Exception: raise ValueError( """ldap_md_multiple_user_dn and ldap_md_multiple_group_dn must be a string repersenation of an array e.g. "['dn=Accounts Group,dc=example,dc=com', 'dn=IT Group,dc=example,dc=com']" """ ) # Instansiate LDAP Server and Connection c = helper.get_ldap_connection() try: # Bind to the connection c.bind() except Exception as err: raise ValueError( "Cannot connect to LDAP Server. Ensure credentials are correct\n Error: {0}" .format(err)) # Inform user msg = "Connected to {0}".format("Active Directory") yield StatusMessage(msg) res = False try: yield StatusMessage("Attempting to add user(s) to group(s)") # perform the removeMermbersFromGroups operation res = ad_add_members_to_groups(c, ldap_md_multiple_user_dn, ldap_md_multiple_group_dn, True) # Test: res = 'ad_add_members_to_groups(c, ' + str(ldap_md_multiple_user_dn) + ', ' + str(ldap_md_multiple_group_dn) + ', True)' except Exception: raise ValueError("Ensure all user and group DNs exist") finally: # Unbind connection c.unbind() ############################################## results = { "success": res, "domain_name": ldap_md_domain_name, "users_dn": ldap_md_multiple_user_dn, "groups_dn": ldap_md_multiple_group_dn } yield StatusMessage( "Finished 'ldap_md_utilities_add_to_groups' that was running in workflow '{0}'" .format(wf_instance_id)) # Produce a FunctionResult with the results yield FunctionResult(results) except Exception: yield FunctionError()
def _cb_retrieve_usb_history_function(self, event, *args, **kwargs): results = {} results["was_successful"] = False results["hostname"] = None lock_acquired = False try: # Get the function parameters: incident_id = kwargs.get("incident_id") # number hostname = kwargs.get("hostname") # text log = logging.getLogger(__name__) # Establish logging days_later_timeout_length = datetime.datetime.now( ) + datetime.timedelta( days=DAYS_UNTIL_TIMEOUT) # Max duration length before aborting hostname = hostname.upper( )[:15] # CB limits hostname to 15 characters sensor = cb.select(Sensor).where( 'hostname:' + hostname) # Query CB for the hostname's sensor timeouts = 0 # Number of timeouts that have occurred if len(sensor) <= 0: # Host does not have CB agent, abort yield StatusMessage( "[FATAL ERROR] CB could not find hostname: " + str(hostname)) yield FunctionResult(results) return sensor = sensor[0] # Get the sensor object from the query results["hostname"] = str(hostname).upper() while timeouts <= MAX_TIMEOUTS: # Max timeouts before aborting try: now = datetime.datetime.now() # Check if the sensor is queued to restart, wait up to 90 seconds before checking online status three_minutes_passed = datetime.datetime.now( ) + datetime.timedelta(minutes=3) while (sensor.restart_queued is True) and (three_minutes_passed >= now): time.sleep(3) # Give the CPU a break, it works hard! now = datetime.datetime.now() sensor = (cb.select(Sensor).where('hostname:' + hostname) )[0] # Retrieve the latest sensor vitals # Check online status if sensor.status != "Online": yield StatusMessage('[WARNING] Hostname: ' + str(hostname) + ' is offline. Will attempt for ' + str(DAYS_UNTIL_TIMEOUT) + ' days...') # Check lock status if os.path.exists( '/home/integrations/.resilient/cb_host_locks/{}.lock' .format(hostname)): yield StatusMessage( '[WARNING] A running action has a lock on ' + str(hostname) + '. Will attempt for ' + str(DAYS_UNTIL_TIMEOUT) + ' days...') # Wait for offline and locked hosts for days_later_timeout_length while (sensor.status != "Online" or os.path.exists( '/home/integrations/.resilient/cb_host_locks/{}.lock' .format(hostname))) and (days_later_timeout_length >= now): time.sleep(3) # Give the CPU a break, it works hard! now = datetime.datetime.now() sensor = (cb.select(Sensor).where('hostname:' + hostname) )[0] # Retrieve the latest sensor vitals # Abort after DAYS_UNTIL_TIMEOUT if sensor.status != "Online" or os.path.exists( '/home/integrations/.resilient/cb_host_locks/{}.lock' .format(hostname)): yield StatusMessage('[FATAL ERROR] Hostname: ' + str(hostname) + ' is still offline!') yield FunctionResult(results) return # Check if the sensor is queued to restart, wait up to 90 seconds before continuing three_minutes_passed = datetime.datetime.now( ) + datetime.timedelta(minutes=3) while (sensor.restart_queued is True) and ( three_minutes_passed >= now ): # If the sensor is queued to restart, wait up to 90 seconds time.sleep(3) # Give the CPU a break, it works hard! now = datetime.datetime.now() sensor = (cb.select(Sensor).where('hostname:' + hostname) )[0] # Retrieve the latest sensor vitals # Verify the incident still exists and is reachable, if not abort try: incident = self.rest_client().get( '/incidents/{0}?text_content_output_format=always_text&handle_format=names' .format(str(incident_id))) except Exception as err: if err.message and "not found" in err.message.lower(): log.info('[FATAL ERROR] Incident ID ' + str(incident_id) + ' no longer exists.') log.info('[FAILURE] Fatal error caused exit!') else: log.info( '[FATAL ERROR] Incident ID ' + str(incident_id) + ' could not be reached, Resilient instance may be down.' ) log.info('[FAILURE] Fatal error caused exit!') return # Acquire host lock try: f = os.fdopen( os.open( '/home/integrations/.resilient/cb_host_locks/{}.lock' .format(hostname), os.O_CREAT | os.O_WRONLY | os.O_EXCL), 'w') f.close() lock_acquired = True except OSError: continue # Establish a session to the host sensor yield StatusMessage( '[INFO] Establishing session to CB Sensor #' + str(sensor.id) + ' (' + sensor.hostname + ')') session = cb.live_response.request_session(sensor.id) yield StatusMessage('[SUCCESS] Connected on Session #' + str(session.session_id) + ' to CB Sensor #' + str(sensor.id) + ' (' + sensor.hostname + ')') try: session.create_directory( 'C:\Windows\CarbonBlack\Reports') except TimeoutError: raise except Exception: pass # Existed already try: session.create_directory( r'C:\Windows\CarbonBlack\Tools') except TimeoutError: raise except Exception: pass # Existed already try: session.delete_file( r'C:\Windows\CarbonBlack\Tools\USBD.exe') except TimeoutError: raise except Exception: pass # Didn't exist already try: session.delete_file( r'C:\Windows\CarbonBlack\Tools\DLV.exe') except TimeoutError: raise except Exception: pass # Didn't exist already session.put_file(open(PATH_TO_UTILITY_1, 'rb'), r'C:\Windows\CarbonBlack\Tools\USBD.exe' ) # Place the utility on the endpoint session.put_file(open(PATH_TO_UTILITY_2, 'rb'), r'C:\Windows\CarbonBlack\Tools\DLV.exe' ) # Place the utility on the endpoint session.create_process( r'C:\Windows\CarbonBlack\Tools\USBD.exe /shtml "C:\Windows\CarbonBlack\Reports\usb-dump1.html" /sort "Last Plug/Unplug Date"', True) # Execute the utility yield StatusMessage( '[SUCCESS] Executed USBD.exe on Sensor!') session.create_process( r'C:\Windows\CarbonBlack\Tools\DLV.exe /shtml "C:\Windows\CarbonBlack\Reports\usb-dump2.html" /sort "Drive Letter"', True) # Execute the utility yield StatusMessage( '[SUCCESS] Executed DLV.exe on Sensor!') with tempfile.NamedTemporaryFile( delete=False ) as temp_file: # Create temporary temp_file for HTML file try: temp_file.write( session.get_file( r'C:\Windows\CarbonBlack\Reports\usb-dump1.html' ) ) # Write the HTML file from the endpoint to temp_file temp_file.close() yield StatusMessage( '[SUCCESS] Retrieved HTML data file from Sensor!' ) self.rest_client().post_attachment( '/incidents/{0}/attachments'.format( incident_id), temp_file.name, '{0}-USB_drives.html'.format(sensor.hostname) ) # Post temp_file to incident yield StatusMessage( '[SUCCESS] Posted HTML data file to the incident as an attachment!' ) finally: os.unlink( temp_file.name) # Delete temporary temp_file with tempfile.NamedTemporaryFile( delete=False ) as temp_file: # Create temporary temp_file for HTML file try: temp_file.write( session.get_file( r'C:\Windows\CarbonBlack\Reports\usb-dump2.html' ) ) # Write the HTML file from the endpoint to temp_file temp_file.close() yield StatusMessage( '[SUCCESS] Retrieved HTML data file from Sensor!' ) self.rest_client().post_attachment( '/incidents/{0}/attachments'.format( incident_id), temp_file.name, '{0}-drive_letters.html'.format( sensor.hostname) ) # Post temp_file to incident yield StatusMessage( '[SUCCESS] Posted HTML data file to the incident as an attachment!' ) finally: os.unlink( temp_file.name) # Delete temporary temp_file session.delete_file( r'C:\Windows\CarbonBlack\Tools\USBD.exe') session.delete_file( r'C:\Windows\CarbonBlack\Tools\DLV.exe') session.delete_file( r'C:\Windows\CarbonBlack\Reports\usb-dump1.html') session.delete_file( r'C:\Windows\CarbonBlack\Reports\usb-dump2.html') except TimeoutError: # Catch TimeoutError and handle timeouts = timeouts + 1 if timeouts <= MAX_TIMEOUTS: yield StatusMessage( '[ERROR] TimeoutError was encountered. Reattempting... (' + str(timeouts) + '/' + str(MAX_TIMEOUTS) + ')') try: session.close() except: pass sensor = (cb.select(Sensor).where('hostname:' + hostname) )[0] # Retrieve the latest sensor vitals sensor.restart_sensor( ) # Restarting the sensor may avoid a timeout from occurring again time.sleep(30) # Sleep to apply sensor restart sensor = (cb.select(Sensor).where('hostname:' + hostname) )[0] # Retrieve the latest sensor vitals else: yield StatusMessage( '[FATAL ERROR] TimeoutError was encountered. The maximum number of retries was reached. Aborting!' ) yield StatusMessage( '[FAILURE] Fatal error caused exit!') continue except ( ApiError, ProtocolError, NewConnectionError, ConnectTimeoutError, MaxRetryError ) as err: # Catch urllib3 connection exceptions and handle if 'ApiError' in str( type(err).__name__ ) and 'network connection error' not in str(err): raise # Only handle ApiError involving network connection error timeouts = timeouts + 1 if timeouts <= MAX_TIMEOUTS: yield StatusMessage( '[ERROR] Carbon Black was unreachable. Reattempting in 30 minutes... (' + str(timeouts) + '/' + str(MAX_TIMEOUTS) + ')') time.sleep( 1800 ) # Sleep for 30 minutes, backup service may have been running. else: yield StatusMessage( '[FATAL ERROR] ' + str(type(err).__name__) + ' was encountered. The maximum number of retries was reached. Aborting!' ) yield StatusMessage( '[FAILURE] Fatal error caused exit!') continue except Exception as err: # Catch all other exceptions and abort yield StatusMessage('[FATAL ERROR] Encountered: ' + str(err)) yield StatusMessage('[FAILURE] Fatal error caused exit!') else: results["was_successful"] = True try: session.close() except: pass yield StatusMessage( '[INFO] Session has been closed to CB Sensor #' + str(sensor.id) + '(' + sensor.hostname + ')') break # Release the host lock if acquired if lock_acquired is True: os.remove( '/home/integrations/.resilient/cb_host_locks/{}.lock'. format(hostname)) # Produce a FunctionResult with the results yield FunctionResult(results) except Exception: yield FunctionError()
def _cb_retrieve_prefetch_files_function(self, event, *args, **kwargs): results = {} results["was_successful"] = False results["hostname"] = None lock_acquired = False try: # Get the function parameters: incident_id = kwargs.get("incident_id") # number hostname = kwargs.get("hostname") # text log = logging.getLogger(__name__) # Establish logging days_later_timeout_length = datetime.datetime.now( ) + datetime.timedelta( days=DAYS_UNTIL_TIMEOUT) # Max duration length before aborting hostname = hostname.upper( )[:15] # CB limits hostname to 15 characters lock_file = '/home/integrations/.resilient/cb_host_locks/{}.lock'.format( hostname) sensor = cb.select(Sensor).where( 'hostname:' + hostname) # Query CB for the hostname's sensor timeouts = 0 # Number of timeouts that have occurred if len(sensor) <= 0: # Host does not have CB agent, abort yield StatusMessage( "[FATAL ERROR] CB could not find hostname: " + str(hostname)) yield StatusMessage('[FAILURE] Fatal error caused exit!') yield FunctionResult(results) return sensor = sensor[0] # Get the sensor object from the query results["hostname"] = str(hostname).upper() while timeouts <= MAX_TIMEOUTS: # Max timeouts before aborting try: now = datetime.datetime.now() # Check if the sensor is queued to restart, wait up to 90 seconds before checking online status three_minutes_passed = datetime.datetime.now( ) + datetime.timedelta(minutes=3) while (sensor.restart_queued is True) and (three_minutes_passed >= now): time.sleep(3) # Give the CPU a break, it works hard! now = datetime.datetime.now() sensor = (cb.select(Sensor).where('hostname:' + hostname) )[0] # Retrieve the latest sensor vitals # Check online status if sensor.status != "Online": yield StatusMessage('[WARNING] Hostname: ' + str(hostname) + ' is offline. Will attempt for ' + str(DAYS_UNTIL_TIMEOUT) + ' days...') # Check lock status if os.path.exists(lock_file) and lock_acquired is False: yield StatusMessage( '[WARNING] A running action has a lock on ' + str(hostname) + '. Will attempt for ' + str(DAYS_UNTIL_TIMEOUT) + ' days...') # Wait for offline and locked hosts for days_later_timeout_length while (sensor.status != "Online" or (os.path.exists(lock_file) and lock_acquired is False)) and (days_later_timeout_length >= now): time.sleep(3) # Give the CPU a break, it works hard! now = datetime.datetime.now() sensor = (cb.select(Sensor).where('hostname:' + hostname) )[0] # Retrieve the latest sensor vitals # Abort after DAYS_UNTIL_TIMEOUT if sensor.status != "Online" or (os.path.exists(lock_file) and lock_acquired is False): yield StatusMessage('[FATAL ERROR] Hostname: ' + str(hostname) + ' is still offline!') yield StatusMessage( '[FAILURE] Fatal error caused exit!') break # Check if the sensor is queued to restart, wait up to 90 seconds before continuing three_minutes_passed = datetime.datetime.now( ) + datetime.timedelta(minutes=3) while (sensor.restart_queued is True) and ( three_minutes_passed >= now ): # If the sensor is queued to restart, wait up to 90 seconds time.sleep(3) # Give the CPU a break, it works hard! now = datetime.datetime.now() sensor = (cb.select(Sensor).where('hostname:' + hostname) )[0] # Retrieve the latest sensor vitals # Verify the incident still exists and is reachable, if not abort try: incident = self.rest_client().get( '/incidents/{0}?text_content_output_format=always_text&handle_format=names' .format(str(incident_id))) except Exception as err: if err.message and "not found" in err.message.lower(): log.info('[FATAL ERROR] Incident ID ' + str(incident_id) + ' no longer exists.') log.info('[FAILURE] Fatal error caused exit!') else: log.info( '[FATAL ERROR] Incident ID ' + str(incident_id) + ' could not be reached, Resilient instance may be down.' ) log.info('[FAILURE] Fatal error caused exit!') break # Acquire host lock if lock_acquired is False: try: f = os.fdopen( os.open(lock_file, os.O_CREAT | os.O_WRONLY | os.O_EXCL), 'w') f.close() lock_acquired = True except OSError: continue # Establish a session to the host sensor yield StatusMessage( '[INFO] Establishing session to CB Sensor #' + str(sensor.id) + ' (' + sensor.hostname + ')') session = cb.live_response.request_session(sensor.id) yield StatusMessage('[SUCCESS] Connected on Session #' + str(session.session_id) + ' to CB Sensor #' + str(sensor.id) + ' (' + sensor.hostname + ')') files_to_retrieve = [ ] # Stores log file path located for retrieval try: session.create_directory( r'C:\Windows\CarbonBlack\Reports') except TimeoutError: raise except Exception: pass # Existed already # Collect the prefetch files list via PowerShell into a CSV, store the CSV in a tempfile, and post it to the incident as an attachment powershell_cmd = ( r"gci -path C:\Windows\Prefetch\*.pf -ea 0 | select Name,LastAccessTime,CreationTime | sort LastAccessTime | " r"ConvertTo-csv -NoTypeInformation | out-file C:\Windows\CarbonBlack\Reports\prefetch.csv -Encoding ascii" ) session.create_process( r'powershell.exe -ExecutionPolicy Bypass -Command "' + powershell_cmd + '" ', True, None, None, 300, True) with tempfile.NamedTemporaryFile( delete=False ) as temp_file: # Create temporary temp_file for the CSV file try: temp_file.write( session.get_file( r'C:\Windows\CarbonBlack\Reports\prefetch.csv' ) ) # Write the CSV file from the endpoint to temp_file temp_file.close() yield StatusMessage( '[SUCCESS] Retrieved CSV data file from Sensor!' ) self.rest_client().post_attachment( '/incidents/{0}/attachments'.format( incident_id), temp_file.name, '{0}-prefetch.csv'.format(sensor.hostname) ) # Post temp_file to incident yield StatusMessage( '[SUCCESS] Posted a CSV data file to the incident as an attachment!' ) finally: os.unlink( temp_file.name) # Delete temporary temp_file session.delete_file( r'C:\Windows\CarbonBlack\Reports\prefetch.csv') prefetch_directory = session.list_directory( r'C:\Windows\Prefetch\\') for file in prefetch_directory: # For each file in the Prefetch directory if file['filename'].endswith('.pf'): files_to_retrieve.append( r'C:\Windows\Prefetch\{}'.format( file['filename']) ) # Store the file path into files_to_retrieve with tempfile.NamedTemporaryFile( delete=False ) as temp_zip: # Create temporary temp_zip for creating zip_file try: with zipfile.ZipFile( temp_zip, 'w' ) as zip_file: # Establish zip_file from temporary temp_zip for packaging prefetch files into for each_file in files_to_retrieve: # For each located log file with tempfile.NamedTemporaryFile( delete=False ) as temp_file: # Create temp_file for log try: temp_file.write( session.get_file(each_file) ) # Write the log to temp_file temp_file.close() zip_file.write( temp_file.name, each_file.replace( 'C:\\Windows\\Prefetch\\', '').replace('\\', os.sep), compress_type=zipfile. ZIP_DEFLATED ) # Write temp_file into zip_file finally: os.unlink( temp_file.name ) # Delete temporary temp_file if os.stat( temp_zip.name).st_size <= MAX_UPLOAD_SIZE: self.rest_client().post_attachment( '/incidents/{0}/attachments'.format( incident_id), temp_zip.name, '{0}-prefetch_files.zip'.format( sensor.hostname) ) # Post temp_zip to incident yield StatusMessage( '[SUCCESS] Posted a ZIP file of the prefetch files to the incident as an attachment!' ) else: if not os.path.exists( os.path.normpath( '/mnt/cyber-sec-forensics/Resilient/{0}' .format(incident_id))): os.makedirs( '/mnt/cyber-sec-forensics/Resilient/{0}' .format(incident_id)) shutil.copyfile( temp_zip.name, '/mnt/cyber-sec-forensics/Resilient/{0}/{1}-prefetch_files-{2}.zip' .format(incident_id, sensor.hostname, str(int(time.time()))) ) # Post temp_zip to network share yield StatusMessage( '[SUCCESS] Posted a ZIP file of the prefetch files to the forensics network share!' ) finally: os.unlink( temp_zip.name) # Delete temporary temp_file except TimeoutError: # Catch TimeoutError and handle timeouts = timeouts + 1 if timeouts <= MAX_TIMEOUTS: yield StatusMessage( '[ERROR] TimeoutError was encountered. Reattempting... (' + str(timeouts) + '/' + str(MAX_TIMEOUTS) + ')') try: session.close() except: pass sensor = (cb.select(Sensor).where('hostname:' + hostname) )[0] # Retrieve the latest sensor vitals sensor.restart_sensor( ) # Restarting the sensor may avoid a timeout from occurring again time.sleep(30) # Sleep to apply sensor restart sensor = (cb.select(Sensor).where('hostname:' + hostname) )[0] # Retrieve the latest sensor vitals else: yield StatusMessage( '[FATAL ERROR] TimeoutError was encountered. The maximum number of retries was reached. Aborting!' ) yield StatusMessage( '[FAILURE] Fatal error caused exit!') continue except ( ApiError, ProtocolError, NewConnectionError, ConnectTimeoutError, MaxRetryError ) as err: # Catch urllib3 connection exceptions and handle if 'ApiError' in str( type(err).__name__ ) and 'network connection error' not in str(err): raise # Only handle ApiError involving network connection error timeouts = timeouts + 1 if timeouts <= MAX_TIMEOUTS: yield StatusMessage( '[ERROR] Carbon Black was unreachable. Reattempting in 30 minutes... (' + str(timeouts) + '/' + str(MAX_TIMEOUTS) + ')') time.sleep( 1800 ) # Sleep for 30 minutes, backup service may have been running. else: yield StatusMessage( '[FATAL ERROR] ' + str(type(err).__name__) + ' was encountered. The maximum number of retries was reached. Aborting!' ) yield StatusMessage( '[FAILURE] Fatal error caused exit!') continue except Exception as err: # Catch all other exceptions and abort yield StatusMessage('[FATAL ERROR] Encountered: ' + str(err)) yield StatusMessage('[FAILURE] Fatal error caused exit!') else: results["was_successful"] = True try: session.close() except: pass yield StatusMessage( '[INFO] Session has been closed to CB Sensor #' + str(sensor.id) + '(' + sensor.hostname + ')') break # Release the host lock if acquired if lock_acquired is True: os.remove(lock_file) # Produce a FunctionResult with the results yield FunctionResult(results) except Exception: yield FunctionError()
def _cb_retrieve_windows_av_events_function(self, event, *args, **kwargs): results = {} results["was_successful"] = False results["hostname"] = None lock_acquired = False try: # Get the function parameters: incident_id = kwargs.get("incident_id") # number hostname = kwargs.get("hostname") # text log = logging.getLogger(__name__) # Establish logging days_later_timeout_length = datetime.datetime.now( ) + datetime.timedelta( days=DAYS_UNTIL_TIMEOUT) # Max duration length before aborting hostname = hostname.upper( )[:15] # CB limits hostname to 15 characters sensor = cb.select(Sensor).where( 'hostname:' + hostname) # Query CB for the hostname's sensor timeouts = 0 # Number of timeouts that have occurred if len(sensor) <= 0: # Host does not have CB agent, abort yield StatusMessage( "[FATAL ERROR] CB could not find hostname: " + str(hostname)) yield FunctionResult(results) return sensor = sensor[0] # Get the sensor object from the query results["hostname"] = str(hostname).upper() while timeouts <= MAX_TIMEOUTS: # Max timeouts before aborting try: now = datetime.datetime.now() # Check if the sensor is queued to restart, wait up to 90 seconds before checking online status three_minutes_passed = datetime.datetime.now( ) + datetime.timedelta(minutes=3) while (sensor.restart_queued is True) and (three_minutes_passed >= now): time.sleep(3) # Give the CPU a break, it works hard! now = datetime.datetime.now() sensor = (cb.select(Sensor).where('hostname:' + hostname) )[0] # Retrieve the latest sensor vitals # Check online status if sensor.status != "Online": yield StatusMessage('[WARNING] Hostname: ' + str(hostname) + ' is offline. Will attempt for ' + str(DAYS_UNTIL_TIMEOUT) + ' days...') # Check lock status if os.path.exists( '/home/integrations/.resilient/cb_host_locks/{}.lock' .format(hostname)): yield StatusMessage( '[WARNING] A running action has a lock on ' + str(hostname) + '. Will attempt for ' + str(DAYS_UNTIL_TIMEOUT) + ' days...') # Wait for offline and locked hosts for days_later_timeout_length while (sensor.status != "Online" or os.path.exists( '/home/integrations/.resilient/cb_host_locks/{}.lock' .format(hostname))) and (days_later_timeout_length >= now): time.sleep(3) # Give the CPU a break, it works hard! now = datetime.datetime.now() sensor = (cb.select(Sensor).where('hostname:' + hostname) )[0] # Retrieve the latest sensor vitals # Abort after DAYS_UNTIL_TIMEOUT if sensor.status != "Online" or os.path.exists( '/home/integrations/.resilient/cb_host_locks/{}.lock' .format(hostname)): yield StatusMessage('[FATAL ERROR] Hostname: ' + str(hostname) + ' is still offline!') yield FunctionResult(results) return # Check if the sensor is queued to restart, wait up to 90 seconds before continuing three_minutes_passed = datetime.datetime.now( ) + datetime.timedelta(minutes=3) while (sensor.restart_queued is True) and ( three_minutes_passed >= now ): # If the sensor is queued to restart, wait up to 90 seconds time.sleep(3) # Give the CPU a break, it works hard! now = datetime.datetime.now() sensor = (cb.select(Sensor).where('hostname:' + hostname) )[0] # Retrieve the latest sensor vitals # Verify the incident still exists and is reachable, if not abort try: incident = self.rest_client().get( '/incidents/{0}?text_content_output_format=always_text&handle_format=names' .format(str(incident_id))) except Exception as err: if err.message and "not found" in err.message.lower(): log.info('[FATAL ERROR] Incident ID ' + str(incident_id) + ' no longer exists.') log.info('[FAILURE] Fatal error caused exit!') else: log.info( '[FATAL ERROR] Incident ID ' + str(incident_id) + ' could not be reached, Resilient instance may be down.' ) log.info('[FAILURE] Fatal error caused exit!') return # Acquire host lock try: f = os.fdopen( os.open( '/home/integrations/.resilient/cb_host_locks/{}.lock' .format(hostname), os.O_CREAT | os.O_WRONLY | os.O_EXCL), 'w') f.close() lock_acquired = True except OSError: continue # Establish a session to the host sensor yield StatusMessage( '[INFO] Establishing session to CB Sensor #' + str(sensor.id) + ' (' + sensor.hostname + ')') session = cb.live_response.request_session(sensor.id) yield StatusMessage('[SUCCESS] Connected on Session #' + str(session.session_id) + ' to CB Sensor #' + str(sensor.id) + ' (' + sensor.hostname + ')') try: session.create_directory( 'C:\Windows\CarbonBlack\Reports') except TimeoutError: raise except Exception: pass # Existed already session.create_process( r'''cmd.exe /c wevtutil qe "System" /rd:True /q:"*[System[Provider[@Name='Microsoft Antimalware']]]" /f:Text > C:\Windows\CarbonBlack\Reports\Antimalware_Events.txt''', True, None, None, 300, True) # Execute the utility yield StatusMessage( '[SUCCESS] Queried all Microsoft Antimalware events on Sensor!' ) session.create_process( r'''cmd.exe /c wevtutil qe "Microsoft-Windows-Windows Defender/Operational" /rd:True /f:Text > C:\Windows\CarbonBlack\Reports\Defender_Events.txt''', True, None, None, 300, True) # Execute the utility yield StatusMessage( '[SUCCESS] Queried all Windows Defender events on Sensor!' ) with tempfile.NamedTemporaryFile( delete=False ) as temp_file: # Create temporary temp_file for TXT file try: file_size = session.list_directory( r'C:\Windows\CarbonBlack\Reports\Antimalware_Events.txt' )[0]['size'] # File size in bytes custom_timeout = int( (file_size / TRANSFER_RATE) + 120 ) # The expected timeout duration + 120 seconds for good measure temp_file.write( session.get_file( r'C:\Windows\CarbonBlack\Reports\Antimalware_Events.txt', timeout=custom_timeout) ) # Write the HTML file from the endpoint to temp_file temp_file.close() yield StatusMessage( '[SUCCESS] Retrieved Microsoft Antimalware events data file from Sensor!' ) if os.stat(temp_file.name).st_size == 0: yield StatusMessage( '[SUCCESS] Microsoft Antimalware events data file is empty. Skipping...' ) # If file is empty, don't send else: self.rest_client().post_attachment( '/incidents/{0}/attachments'.format( incident_id), temp_file.name, '{0}-Antimalware_Events.txt'.format( sensor.hostname) ) # Post temp_file to incident yield StatusMessage( '[SUCCESS] Posted Microsoft Antimalware events data file to the incident as an attachment!' ) finally: os.unlink( temp_file.name) # Delete temporary temp_file with tempfile.NamedTemporaryFile( delete=False ) as temp_file: # Create temporary temp_file for TXT file try: file_size = session.list_directory( r'C:\Windows\CarbonBlack\Reports\Defender_Events.txt' )[0]['size'] # File size in bytes custom_timeout = int( (file_size / TRANSFER_RATE) + 120 ) # The expected timeout duration + 120 seconds for good measure temp_file.write( session.get_file( r'C:\Windows\CarbonBlack\Reports\Defender_Events.txt', timeout=custom_timeout) ) # Write the HTML file from the endpoint to temp_file temp_file.close() yield StatusMessage( '[SUCCESS] Retrieved Windows Defender events data file from Sensor!' ) if os.stat(temp_file.name).st_size == 0: yield StatusMessage( '[SUCCESS] Windows Defender events data file is empty. Skipping...' ) # If file is empty, don't send elif os.stat( temp_file.name).st_size <= MAX_UPLOAD_SIZE: self.rest_client().post_attachment( '/incidents/{0}/attachments'.format( incident_id), temp_file.name, '{0}-Defender_Events.txt'.format( sensor.hostname) ) # Post temp_file to incident yield StatusMessage( '[SUCCESS] Posted Windows Defender events data file to the incident as an attachment!' ) else: if not os.path.exists( os.path.normpath( '/mnt/cyber-sec-forensics/Resilient/{0}' .format(incident_id))): os.makedirs( '/mnt/cyber-sec-forensics/Resilient/{0}' .format(incident_id)) shutil.copyfile( temp_file.name, '/mnt/cyber-sec-forensics/Resilient/{0}/{1}-Defender_Events-{2}.txt' .format(incident_id, sensor.hostname, str(int(time.time()))) ) # Post temp_file to network share yield StatusMessage( '[SUCCESS] Posted Windows Defender events data file to the forensics network share!' ) finally: os.unlink( temp_file.name) # Delete temporary temp_file session.delete_file( r'C:\Windows\CarbonBlack\Reports\Antimalware_Events.txt' ) session.delete_file( r'C:\Windows\CarbonBlack\Reports\Defender_Events.txt') except TimeoutError: # Catch TimeoutError and handle timeouts = timeouts + 1 if timeouts <= MAX_TIMEOUTS: yield StatusMessage( '[ERROR] TimeoutError was encountered. Reattempting... (' + str(timeouts) + '/' + str(MAX_TIMEOUTS) + ')') try: session.close() except: pass sensor = (cb.select(Sensor).where('hostname:' + hostname) )[0] # Retrieve the latest sensor vitals sensor.restart_sensor( ) # Restarting the sensor may avoid a timeout from occurring again time.sleep(30) # Sleep to apply sensor restart sensor = (cb.select(Sensor).where('hostname:' + hostname) )[0] # Retrieve the latest sensor vitals else: yield StatusMessage( '[FATAL ERROR] TimeoutError was encountered. The maximum number of retries was reached. Aborting!' ) yield StatusMessage( '[FAILURE] Fatal error caused exit!') continue except ( ApiError, ProtocolError, NewConnectionError, ConnectTimeoutError, MaxRetryError ) as err: # Catch urllib3 connection exceptions and handle if 'ApiError' in str( type(err).__name__ ) and 'network connection error' not in str(err): raise # Only handle ApiError involving network connection error timeouts = timeouts + 1 if timeouts <= MAX_TIMEOUTS: yield StatusMessage( '[ERROR] Carbon Black was unreachable. Reattempting in 30 minutes... (' + str(timeouts) + '/' + str(MAX_TIMEOUTS) + ')') time.sleep( 1800 ) # Sleep for 30 minutes, backup service may have been running. else: yield StatusMessage( '[FATAL ERROR] ' + str(type(err).__name__) + ' was encountered. The maximum number of retries was reached. Aborting!' ) yield StatusMessage( '[FAILURE] Fatal error caused exit!') continue except Exception as err: # Catch all other exceptions and abort yield StatusMessage('[FATAL ERROR] Encountered: ' + str(err)) yield StatusMessage('[FAILURE] Fatal error caused exit!') else: results["was_successful"] = True try: session.close() except: pass yield StatusMessage( '[INFO] Session has been closed to CB Sensor #' + str(sensor.id) + '(' + sensor.hostname + ')') break # Release the host lock if acquired if lock_acquired is True: os.remove( '/home/integrations/.resilient/cb_host_locks/{}.lock'. format(hostname)) # Produce a FunctionResult with the results yield FunctionResult(results) except Exception: yield FunctionError()
def _umbrella_dns_rr_hist_function(self, event, *args, **kwargs): """Function: Resilient Function : Cisco Umbrella Investigate for DNS RR History for a IP, Type and Domain Name""" try: # Get the function parameters: umbinv_resource = kwargs.get("umbinv_resource") # text umbinv_dns_type = self.get_select_param( kwargs.get("umbinv_dns_type") ) # select, values: "A", "NS", "MX", "TXT", "CNAME" incident_id = kwargs.get("incident_id") # number artifact_type = kwargs.get("artifact_type") # text log = logging.getLogger(__name__) log.info("umbinv_resource: %s", umbinv_resource) log.info("umbinv_dns_type: %s", umbinv_dns_type) log.info("incident_id: %s", incident_id) log.info("artifact_type: %s", artifact_type) if is_none(umbinv_resource): raise ValueError( "Required parameter 'umbinv_resource' not set") if is_none(umbinv_dns_type): raise ValueError( "Required parameter 'umbinv_dns_type' not set") if is_none(incident_id): raise ValueError("Required parameter 'incident_id' not set") if is_none(artifact_type): raise ValueError("Required parameter 'artifact_type' not set") yield StatusMessage("Starting...") res = None res_type = None process_result = {} func_name = event.name params = { "resource": umbinv_resource.strip(), "dns_type": umbinv_dns_type, "incident_id": incident_id, "artifact_type": artifact_type } validate_params(params) process_params(params, process_result) if "_res" not in process_result or "_res_type" not in process_result: raise ValueError( "Parameter 'umbinv_resource' was not processed correctly") else: res = process_result.pop("_res") res_type = process_result.pop("_res_type") if res_type != "domain_name" and res_type != "ip_address": raise ValueError( "Parameter 'umbinv_resource' was an incorrect type '{}', should be a 'domain name', " "or an 'ip address'.".format(res_type)) api_token = self.options.get("api_token") base_url = self.options.get("base_url") rinv = ResilientInv(api_token, base_url) yield StatusMessage("Running Cisco Investigate query...") rtn = rinv.rr_history(res, query_type=umbinv_dns_type) query_execution_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') if ("rrs" in rtn and len(rtn["rrs"]) == 0) \ or ("rrs_tf" in rtn and len(rtn["rrs_tf"]) == 0): log.debug(json.dumps(rtn)) yield StatusMessage( "No Results returned for resource '{}' with query type '{}'." .format(res, umbinv_dns_type)) results = {} elif ("rrs" in rtn and len(rtn["rrs"]) > int(self.options.get("results_limit", "200"))) \ or ("rrs_tf" in rtn and len(rtn["rrs_tf"]) > int(self.options.get("results_limit", "200"))): att_report = create_attachment(self, func_name, res, params, rtn, query_execution_time) # Add in "query_execution_time" and "ip_address" to result to facilitate post-processing. results = { "over_limit": True, "resource_name": res, "att_name": att_report["name"], "query_execution_time": query_execution_time } yield StatusMessage( "Returning 'dns_rr_history' results for resource '{0}' as attachment: {1} ." .format(res, att_report["name"])) else: # Add in "query_execution_time" and "ip_address" to result to facilitate post-processing. results = { "dns_rr_history": json.loads(json.dumps(rtn)), "resource_name": res, "query_execution_time": query_execution_time } yield StatusMessage( "Returning 'dns_rr_history' results for resource '{}'.". format(res)) yield StatusMessage("Done...") log.debug(json.dumps(results)) # Produce a FunctionResult with the results yield FunctionResult(results) except Exception: log.exception("Exception in Resilient Function.") yield FunctionError()
def _icdx_find_events_function(self, event, *args, **kwargs): """Function: Takes a number of parameters in a search request and attempts to gather events from the ICDx Platform. Returns a response containing a list of events or a response with a 204 status code when no results are found.""" try: rc = ResultPayload(ICDX_SECTION, **kwargs) # Get the function parameters: icdx_search_request = self.get_textarea_param( kwargs.get("icdx_search_request")) # textarea log = logging.getLogger(__name__) log.info("icdx_search_request: %s", icdx_search_request) if icdx_search_request is None: raise FunctionError( "Encountered error: icdx_search_request may not be None") helper = ICDXHelper(self.options) yield StatusMessage( "Attempting to gather config and setup the AMQP Client") try: # Initialise the AmqpFacade, pass in config values amqp_client = AmqpFacade( host=helper.get_config_option("icdx_amqp_host"), port=helper.get_config_option("icdx_amqp_port", True), virtual_host=helper.get_config_option("icdx_amqp_vhost"), username=helper.get_config_option("icdx_amqp_username"), amqp_password=helper.get_config_option( "icdx_amqp_password")) yield StatusMessage( "Config options gathered and AMQP client setup.") except Exception as e: yield StatusMessage( "Encountered error while initialising the AMQP Client; Reason (if any): {0}" .format(str(e))) raise FunctionError() # Prepare request payload try: search_payload = json.loads(icdx_search_request) # Set the hard limit for displaying in the UI """ if hard limit is not set, it defaults to 100 if hard limit is set and exceeds 100 that is the new limit if hard limit is set and does not exceed 100, 100 is the limit Max limit appears to be 500 """ search_payload['hard_limit'] = max([ helper.safe_cast(val=i, to_type=int) for i in [helper.get_config_option("icdx_search_limit", True), 100] if i is not None ]) # Payload is now a dict -- read 'limit' and replace with app.config value if exceeds it if search_payload['limit'] > search_payload['hard_limit']: search_payload['limit'] = search_payload['hard_limit'] # All okay, request has valid JSON and an ID yield StatusMessage( "Request payload validated. Sending request with a where attribute of {0} and a filter attribute of {1}" .format( search_payload.get('where', None) or '"No Where Condition"', search_payload.get('filter', None) or '"No Filter Condition"')) except Exception: raise FunctionError( "Problem parsing the JSON for search request") # Make the call to ICDx and get a handle on any results search_result, status = amqp_client.call( json.dumps(search_payload)) yield StatusMessage( "ICDX call complete with status: {}".format(status)) result_set, num_of_results = None, 0 if not search_result: log.info("Received an empty result set for search query.") else: log.info("Received response. Parsing for information") # Get results dict and number of results if status == 200: res = json.loads(search_result) result_set, num_of_results = res["result"], len( res["result"]) elif status == 400: yield StatusMessage( "Received a 400 (Bad Request). Check the formatting of your ICDx Query" ) execution_time = int(time.time() * 1000) success = False if status != 200 else True results = rc.done(success=success, content={ "result_set": result_set, "num_of_results": num_of_results, "execution_time": execution_time }) results.update({ "result_set": result_set, "num_of_results": num_of_results, "execution_time": execution_time, "success": success }) yield StatusMessage( "Finishing. Received results: {}. Number of results: {}". format(results["success"], num_of_results)) # Produce a FunctionResult with the results yield FunctionResult(results) except Exception: yield FunctionError()
def _ldap_utilities_remove_from_groups_function(self, event, *args, **kwargs): """Function: A function that allows you to remove multiple from multiple groups""" log = logging.getLogger(__name__) try: yield StatusMessage("Starting ldap_utilities_remove_from_groups") # Instansiate helper (which gets appconfigs from file) helper = LDAPUtilitiesHelper(self.options) yield StatusMessage("Appconfig Settings OK") # Get function inputs input_ldap_multiple_user_dn_asString = helper.get_function_input( kwargs, "ldap_multiple_user_dn" ) # text (required) [string repersentation of an array] input_ldap_multiple_group_dn_asString = helper.get_function_input( kwargs, "ldap_multiple_group_dn" ) # text (required) [string repersentation of an array] yield StatusMessage("Function Inputs OK") if not helper.LDAP_IS_ACTIVE_DIRECTORY: raise FunctionError( "This function only supports an Active Directory connection. Make sure ldap_is_active_directory is set to True in the app.config file" ) try: # Try converting input to an array input_ldap_multiple_user_dn = literal_eval( input_ldap_multiple_user_dn_asString) input_ldap_multiple_group_dn = literal_eval( input_ldap_multiple_group_dn_asString) except Exception: raise ValueError( """input_ldap_multiple_user_dn and input_ldap_multiple_group_dn must be a string repersenation of an array e.g. "['dn=Accounts Group,dc=example,dc=com', 'dn=IT Group,dc=example,dc=com']" """ ) # Instansiate LDAP Server and Connection c = helper.get_ldap_connection() try: # Bind to the connection c.bind() except: raise ValueError( "Cannot connect to LDAP Server. Ensure credentials are correct" ) # Inform user msg = "Connected to {0}".format("Active Directory") yield StatusMessage(msg) res = False users_dn = [] try: yield StatusMessage( "Attempting to remove user(s) from group(s)") # perform the removeMermbersFromGroups operation res = ad_remove_members_from_groups( c, input_ldap_multiple_user_dn, input_ldap_multiple_group_dn, True) # Return list of users that were removed, and ignore users that do not exist, not valid, or not member of group if res and "changes" in c.request: users_dn = c.request["changes"][0]["attribute"]["value"] except Exception: raise ValueError("Ensure all group DNs exist") finally: # Unbind connection c.unbind() results = { "success": res, "users_dn": users_dn if len(users_dn) > 0 else None, "groups_dn": input_ldap_multiple_group_dn } log.info("Completed") # Produce a FunctionResult with the results yield FunctionResult(results) except Exception: yield FunctionError()
def _umbrella_classifiers_function(self, event, *args, **kwargs): """Function: Resilient Function : Cisco Umbrella Investigate for Classifiers.""" try: # Get the function parameters: umbinv_domain = kwargs.get("umbinv_domain") # text log = logging.getLogger(__name__) log.info("umbinv_domain: %s", umbinv_domain) if is_none(umbinv_domain): raise ValueError("Required parameter 'umbinv_domain' not set.") yield StatusMessage("Starting...") domain = None process_result = {} params = {"domain": umbinv_domain.strip()} validate_params(params) process_params(params, process_result) if "_domain" not in process_result: raise ValueError( "Parameter 'umbinv_domain' was not processed correctly") else: domain = process_result.pop("_domain") api_token = self.options.get("api_token") base_url = self.options.get("base_url") rinv = ResilientInv(api_token, base_url, proxies=self.proxies) yield StatusMessage("Running Cisco Investigate query...") classifiers_res = True info_res = True # Run against 'classifiers' endpoint rtn_classifiers = rinv.classifiers_classifiers(domain) # Run against 'info' endpoint rtn_info = rinv.classifiers_info(domain) query_execution_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') if ("securityCategories" in rtn_classifiers and not rtn_classifiers["securityCategories"]) and \ ("attacks" in rtn_classifiers and not rtn_classifiers["attacks"]) and \ ("threatTypes" in rtn_classifiers and not rtn_classifiers["threatTypes"]): classifiers_res = False if "firstQueried" in rtn_info and rtn_info["firstQueried"] is None: info_res = False else: # Make 'firstQueried' more readable fq = rtn_info["firstQueried"] try: secs = int(fq) / 1000 fq_readable = datetime.fromtimestamp(secs).strftime( '%Y-%m-%d %H:%M:%S') rtn_info["first_queried_converted"] = fq_readable except ValueError: yield FunctionError( 'timestamp value incorrectly specified') if not classifiers_res and not info_res: log.debug(json.dumps(rtn_classifiers)) log.debug(json.dumps(rtn_info)) results = {} yield StatusMessage( "No Results returned for domain '{}'.".format(domain)) else: # Add "query_execution_time" and "domain_name" to result to facilitate post-processing. results = { "classifiers_classifiers": json.loads(json.dumps(rtn_classifiers)), "classifiers_info": json.loads(json.dumps(rtn_info)), "domain_name": domain, "query_execution_time": query_execution_time } yield StatusMessage( "Returning 'classifiers and info' results for domain '{}'." .format(domain)) yield StatusMessage("done...") log.debug(json.dumps(results)) # Produce a FunctionResult with the results yield FunctionResult(results) except Exception: log.exception("Exception in Resilient Function.") yield FunctionError()
def _fn_odbc_query_function(self, event, *args, **kwargs): """Resilient Function: A function that makes ODBC queries Using prepared SQL statements, where parameters are passed to the database separately, protecting against SQL injection attacks. Inputs: Inputs: sql_query: a SQL query with set parameters using a question mark as a place holder, SQL statements SELECT, INSERT, UPDATE and DELETE are supported sql_condition_value1: value for the question mark - condition value 1 sql_condition_value2: value for the question mark - condition value 2 sql_condition_value3: value for the question mark - condition value 3 """ odbc_connection = None try: # Get the function parameters: if "sql_query" not in kwargs or kwargs.get("sql_query") == '': LOG.error(u"Required field sql_query is missing or empty") raise ValueError("Required field sql_query is missing or empty") sql_query = self.get_textarea_param(kwargs.get("sql_query")) # textarea LOG.info(u"sql_query: %s", sql_query) # ------------------------------------------------------ # When adding more condition input fields to the function, you need to load them here # and pass the new variable/s to the function_utils.prepare_sql_parameters(). # ------------------------------------------------------ sql_condition_value1 = kwargs.get("sql_condition_value1") # text sql_condition_value2 = kwargs.get("sql_condition_value2") # text sql_condition_value3 = kwargs.get("sql_condition_value3") # text LOG.info(u"sql_condition_value1: %s", sql_condition_value1) LOG.info(u"sql_condition_value2: %s", sql_condition_value2) LOG.info(u"sql_condition_value3: %s", sql_condition_value3) sql_params = function_utils.prepare_sql_parameters(sql_condition_value1, sql_condition_value2, sql_condition_value3) # Read configuration settings: if "sql_connection_string" in self.options: sql_connection_string = self.options["sql_connection_string"] else: LOG.error(u"Mandatory config setting 'sql_connection_string' not set.") raise ValueError("Mandatory config setting 'sql_connection_string' not set.") sql_restricted_sql_statements = self.options["sql_restricted_sql_statements"] \ if "sql_restricted_sql_statements" in self.options else None sql_autocommit = function_utils.str_to_bool(self.options["sql_autocommit"]) \ if "sql_autocommit" in self.options else False sql_query_timeout = int(self.options["sql_query_timeout"]) \ if "sql_query_timeout" in self.options else None sql_database_type = self.options["sql_database_type"].lower() \ if "sql_database_type" in self.options else None sql_number_of_records_returned = int(self.options["sql_number_of_records_returned"]) \ if "sql_number_of_records_returned" in self.options else None yield StatusMessage("Starting...") yield StatusMessage("Validating...") function_utils.validate_data(sql_restricted_sql_statements, sql_query) yield StatusMessage("Opening ODBC connection...") odbc_connection = odbc_utils.OdbcConnection(sql_connection_string, sql_autocommit, sql_query_timeout) odbc_connection.configure_unicode_settings(sql_database_type) odbc_connection.create_cursor() yield StatusMessage("Executing an ODBC query...") # Check what SQL statement is executed, get the first word in sql_query sql_statement = function_utils.get_type_sql_statement(sql_query) if sql_statement == 'select': LOG.debug(u"Query: %s. Params: %s. Fetching %s records.", sql_query, sql_params, sql_number_of_records_returned) rows = odbc_connection.execute_select_statement(sql_query, sql_params, sql_number_of_records_returned) results = function_utils.prepare_results(odbc_connection.get_cursor_description(), rows) LOG.info(json.dumps(str(results))) if results.get("entries") is None: yield StatusMessage("No query results returned...") else: yield StatusMessage("Result contains {} entries...".format(len(results.get("entries")))) elif sql_statement == 'update' or sql_statement == 'delete' \ or sql_statement == 'insert': LOG.debug(u"Query: %s. Params: %s.", sql_query, sql_params) # Return row count and set results to empty list row_count = odbc_connection.execute_odbc_query(sql_query, sql_params) results = function_utils.prepare_results(None, None) LOG.info(u"%s rows processed", row_count) yield StatusMessage("{} rows processed".format(row_count)) else: LOG.error(u"SQL statement '%s' is not supported", sql_statement) raise ValueError("SQL statement '{}' is not supported".format(sql_statement)) yield StatusMessage("Done...") yield FunctionResult(results) except Exception as err: LOG.error(str(err)) raise FunctionError(str(err)) # Commit changes and tear down connection finally: yield StatusMessage("Closing ODBC connection...") if odbc_connection: odbc_connection.close_connections()
def _cb_force_reboot_with_message_function(self, event, *args, **kwargs): results = {} results["was_successful"] = False results["hostname"] = None lock_acquired = False try: # Get the function parameters: incident_id = kwargs.get("incident_id") # number hostname = kwargs.get("hostname") # text minutes = kwargs.get("minutes") # number custom_message = kwargs.get("custom_message") # text log = logging.getLogger(__name__) # Establish logging days_later_timeout_length = datetime.datetime.now( ) + datetime.timedelta( days=DAYS_UNTIL_TIMEOUT) # Max duration length before aborting hostname = hostname.upper( )[:15] # CB limits hostname to 15 characters lock_file = '/home/integrations/.resilient/cb_host_locks/{}.lock'.format( hostname) sensor = cb.select(Sensor).where( 'hostname:' + hostname) # Query CB for the hostname's sensor if minutes is None: minutes = 5 # Default to a 5 minutes if custom_message is None: custom_message = 'System restarting for cyber security reasons.' timeouts = 0 # Number of timeouts that have occurred if len(sensor) <= 0: # Host does not have CB agent, abort yield StatusMessage( "[FATAL ERROR] CB could not find hostname: " + str(hostname)) yield StatusMessage('[FAILURE] Fatal error caused exit!') yield FunctionResult(results) return sensor = sensor[0] # Get the sensor object from the query results["hostname"] = str(hostname).upper() while timeouts <= MAX_TIMEOUTS: # Max timeouts before aborting try: now = datetime.datetime.now() # Check if the sensor is queued to restart, wait up to 90 seconds before checking online status three_minutes_passed = datetime.datetime.now( ) + datetime.timedelta(minutes=3) while (sensor.restart_queued is True) and (three_minutes_passed >= now): time.sleep(3) # Give the CPU a break, it works hard! now = datetime.datetime.now() sensor = (cb.select(Sensor).where('hostname:' + hostname) )[0] # Retrieve the latest sensor vitals # Check online status if sensor.status != "Online": yield StatusMessage('[WARNING] Hostname: ' + str(hostname) + ' is offline. Will attempt for ' + str(DAYS_UNTIL_TIMEOUT) + ' days...') # Check lock status if os.path.exists(lock_file) and lock_acquired is False: yield StatusMessage( '[WARNING] A running action has a lock on ' + str(hostname) + '. Will attempt for ' + str(DAYS_UNTIL_TIMEOUT) + ' days...') # Wait for offline and locked hosts for days_later_timeout_length while (sensor.status != "Online" or (os.path.exists(lock_file) and lock_acquired is False)) and (days_later_timeout_length >= now): time.sleep(3) # Give the CPU a break, it works hard! now = datetime.datetime.now() sensor = (cb.select(Sensor).where('hostname:' + hostname) )[0] # Retrieve the latest sensor vitals # Abort after DAYS_UNTIL_TIMEOUT if sensor.status != "Online" or (os.path.exists(lock_file) and lock_acquired is False): yield StatusMessage('[FATAL ERROR] Hostname: ' + str(hostname) + ' is still offline!') yield StatusMessage( '[FAILURE] Fatal error caused exit!') break # Check if the sensor is queued to restart, wait up to 90 seconds before continuing three_minutes_passed = datetime.datetime.now( ) + datetime.timedelta(minutes=3) while (sensor.restart_queued is True) and ( three_minutes_passed >= now ): # If the sensor is queued to restart, wait up to 90 seconds time.sleep(3) # Give the CPU a break, it works hard! now = datetime.datetime.now() sensor = (cb.select(Sensor).where('hostname:' + hostname) )[0] # Retrieve the latest sensor vitals # Verify the incident still exists and is reachable, if not abort try: incident = self.rest_client().get( '/incidents/{0}?text_content_output_format=always_text&handle_format=names' .format(str(incident_id))) except Exception as err: if err.message and "not found" in err.message.lower(): log.info('[FATAL ERROR] Incident ID ' + str(incident_id) + ' no longer exists.') log.info('[FAILURE] Fatal error caused exit!') else: log.info( '[FATAL ERROR] Incident ID ' + str(incident_id) + ' could not be reached, Resilient instance may be down.' ) log.info('[FAILURE] Fatal error caused exit!') break # Acquire host lock if lock_acquired is False: try: f = os.fdopen( os.open(lock_file, os.O_CREAT | os.O_WRONLY | os.O_EXCL), 'w') f.close() lock_acquired = True except OSError: continue # Establish a session to the host sensor yield StatusMessage( '[INFO] Establishing session to CB Sensor #' + str(sensor.id) + ' (' + sensor.hostname + ')') session = cb.live_response.request_session(sensor.id) yield StatusMessage('[SUCCESS] Connected on Session #' + str(session.session_id) + ' to CB Sensor #' + str(sensor.id) + ' (' + sensor.hostname + ')') session.create_process( 'shutdown -r -f -t ' + str(minutes * 60) + ' -d p:5:19 -c "' + str(custom_message) + ' Restart will occur in ' + str(minutes) + ' minutes. Contact CTS Security and Compliance at x3199 option 5 with any questions."', True, None, None, 300, True) yield StatusMessage("[SUCCESS] Reboot has been scheduled!") except TimeoutError: # Catch TimeoutError and handle timeouts = timeouts + 1 if timeouts <= MAX_TIMEOUTS: yield StatusMessage( '[ERROR] TimeoutError was encountered. Reattempting... (' + str(timeouts) + '/' + str(MAX_TIMEOUTS) + ')') try: session.close() except: pass sensor = (cb.select(Sensor).where('hostname:' + hostname) )[0] # Retrieve the latest sensor vitals sensor.restart_sensor( ) # Restarting the sensor may avoid a timeout from occurring again time.sleep(30) # Sleep to apply sensor restart sensor = (cb.select(Sensor).where('hostname:' + hostname) )[0] # Retrieve the latest sensor vitals else: yield StatusMessage( '[FATAL ERROR] TimeoutError was encountered. The maximum number of retries was reached. Aborting!' ) yield StatusMessage( '[FAILURE] Fatal error caused exit!') continue except ( ApiError, ProtocolError, NewConnectionError, ConnectTimeoutError, MaxRetryError ) as err: # Catch urllib3 connection exceptions and handle if 'ApiError' in str( type(err).__name__ ) and 'network connection error' not in str(err): raise # Only handle ApiError involving network connection error timeouts = timeouts + 1 if timeouts <= MAX_TIMEOUTS: yield StatusMessage( '[ERROR] Carbon Black was unreachable. Reattempting in 30 minutes... (' + str(timeouts) + '/' + str(MAX_TIMEOUTS) + ')') time.sleep( 1800 ) # Sleep for 30 minutes, backup service may have been running. else: yield StatusMessage( '[FATAL ERROR] ' + str(type(err).__name__) + ' was encountered. The maximum number of retries was reached. Aborting!' ) yield StatusMessage( '[FAILURE] Fatal error caused exit!') continue except Exception as err: # Catch all other exceptions and abort yield StatusMessage('[FATAL ERROR] Encountered: ' + str(err)) yield StatusMessage('[FAILURE] Fatal error caused exit!') else: results["was_successful"] = True try: session.close() except: pass yield StatusMessage( '[INFO] Session has been closed to CB Sensor #' + str(sensor.id) + '(' + sensor.hostname + ')') break # Release the host lock if acquired if lock_acquired is True: os.remove(lock_file) # Produce a FunctionResult with the results yield FunctionResult(results) except Exception: yield FunctionError()
def _qradar_search_function(self, event, *args, **kwargs): """Function: Search QRadar""" try: required_fields = ["qradar_query", "qradar_query_all_results"] validate_fields(required_fields, kwargs) # Get the function parameters: qradar_query = self.get_textarea_param(kwargs.get("qradar_query")) # textarea qradar_query_param1 = kwargs.get("qradar_query_param1") # text qradar_query_param2 = kwargs.get("qradar_query_param2") # text qradar_query_param3 = kwargs.get("qradar_query_param3") # text qradar_query_param4 = kwargs.get("qradar_query_param4") # text qradar_query_param5 = kwargs.get("qradar_query_param5") # text qradar_query_range_start = kwargs.get("qradar_query_range_start") # number qradar_query_range_end = kwargs.get("qradar_query_range_end") # number qradar_query_all_results = False if "Yes" in kwargs.get("qradar_query_all_results")["name"]: qradar_query_all_results = True LOG.info("qradar_query: %s", qradar_query) LOG.info("qradar_query_param1: %s", qradar_query_param1) LOG.info("qradar_query_param2: %s", qradar_query_param2) LOG.info("qradar_query_param3: %s", qradar_query_param3) LOG.info("qradar_query_param4: %s", qradar_query_param4) LOG.info("qradar_query_param5: %s", qradar_query_param5) LOG.info("qradar_query_range_start: %s", qradar_query_range_start) LOG.info("qradar_query_range_end: %s", qradar_query_range_end) LOG.info("qradar_query_all_results: %s", qradar_query_all_results) qradar_verify_cert = True if "verify_cert" in self.options and self.options["verify_cert"].lower() == "false": qradar_verify_cert = False timeout = None try: if "search_timeout" in self.options: timeout = float(self.options["search_timeout"]) except Exception: LOG.debug("Failed to read search_timeout: {}".format(self.options["search_timeout"])) LOG.debug("Connection to {} using {}".format(self.options["host"], self.options.get("username") or "service token")) query_string = function_utils.make_query_string(qradar_query, [qradar_query_param1, qradar_query_param2, qradar_query_param3, qradar_query_param4, qradar_query_param5]) LOG.info("Running query: " + query_string) yield StatusMessage("starting...") qradar_client = QRadarClient(host=self.options["host"], username=self.options.get("username", None), password=self.options.get("qradarpassword", None), token=self.options.get("qradartoken", None), cafile=qradar_verify_cert, opts=self.opts, function_opts=self.options) result = qradar_client.ariel_search(query_string, qradar_query_all_results, range_start=qradar_query_range_start, range_end=qradar_query_range_end, timeout=timeout) yield StatusMessage("done...") yield FunctionResult(result) except Exception as e: LOG.error(str(e)) yield FunctionError()
def _qradar_advisor_offense_analysis_function(self, event, *args, **kwargs): """Function: This function performs two tasks: 1. call the QRadar Advisor REST API to retrieve insights for a given QRadar offense 2. call the QRadar Advisor REST API to perform analysis on the QRadar offense The input is qradar_offense_id in the input. The reply from QRadar Advisor analysis is in stix format. This function then 1. extract the observables from the stix objects 2. generate a html representation for the stix The return to Resilient server includes the above two, together with the raw replies for offense insights and offense analysis.""" try: # Get the function parameters: qradar_offense_id = kwargs.get("qradar_offense_id") # text qradar_advisor_result_stage = self.get_select_param( kwargs.get("qradar_advisor_result_stage") ) # select, values: "stage1", "stage2", "stage3" qradar_analysis_restart_if_existed = kwargs.get( "qradar_analysis_restart_if_existed") # boolean log = logging.getLogger(__name__) log.info("qradar_offense_id: %s", qradar_offense_id) log.info("qradar_advisor_result_stage: %s", qradar_advisor_result_stage) log.info("qradar_analysis_restart_if_existed: %s", qradar_analysis_restart_if_existed) qradar_verify_cert = True if "verify_cert" in self.options and self.options[ "verify_cert"] == "false": qradar_verify_cert = False yield StatusMessage("starting...") if qradar_analysis_restart_if_existed: # User wants restart a new analysis. Warn him/her it could take some time yield StatusMessage( "Restarting a new analysis. It could take up to 15 minutes..." ) offense_analysis_timeout = int( self.options.get("offense_analysis_timeout", 1200)) offense_analysis_period = int( self.options.get("offense_analysis_period", 5)) log.debug("Using timeout: {}".format( str(offense_analysis_timeout))) log.debug("Using period: {}".format(str(offense_analysis_period))) client = QRadarAdvisorClient( qradar_host=self.options["qradar_host"], qradar_token=self.options["qradar_advisor_token"], advisor_app_id=self.options["qradar_advisor_app_id"], cafile=qradar_verify_cert, log=log) stix_json = client.offense_analysis( offense_id=qradar_offense_id, restart_if_existed=qradar_analysis_restart_if_existed, return_stage=qradar_advisor_result_stage, timeout=offense_analysis_timeout, period=offense_analysis_period) # # extract list of observables from this stix bundle # observables = stix_utils.get_observables(stix_json=stix_json, log=log) # # generate a folder-tree like structure in html for this stix bundle # html_str = stix_tree.get_html(stix_json, log) # # get the insights for this offense # insights = client.offense_insights(offense_id=qradar_offense_id) yield StatusMessage("done...") yield StatusMessage("Returning {} observables".format( str(len(observables)))) results = { "observables": observables, "note": html_str, "insights": insights, # Return the raw insights dict "stix": stix_json # Return the raw stix2 dict } # Produce a FunctionResult with the results yield FunctionResult(results) except Exception as e: log.error(e.message) yield FunctionError(e.message)
def _cb_remove_system_isolation_function(self, event, *args, **kwargs): results = {} results["was_successful"] = False results["hostname"] = None try: # Get the function parameters: incident_id = kwargs.get("incident_id") # number hostname = kwargs.get("hostname") # text log = logging.getLogger(__name__) hostname = hostname.upper( )[:15] # CB limits hostname to 15 characters sensor = cb.select(Sensor).where( 'hostname:' + hostname) # Query CB for the hostname's sensor if len(sensor) <= 0: # Host does not have CB agent, abort yield StatusMessage( "[FATAL ERROR] CB could not find hostname: " + str(hostname)) yield StatusMessage('[FAILURE] Fatal error caused exit!') yield FunctionResult(results) return sensor = sensor[0] # Get the sensor object from the query results["hostname"] = str(hostname).upper() try: if sensor.group.id in protected_sensor_group_ids: yield StatusMessage( '[FAILURE] Hostname ' + str(hostname) + ' is in a protected group, isolation removal not allowed via Resilient!' ) yield FunctionResult(results) return elif sensor.network_isolation_enabled is False: yield StatusMessage('[FAILURE] Hostname ' + str(hostname) + ' is not isolated!') yield FunctionResult(results) return else: yield StatusMessage( '[INFO] Attempting to remove isolation from hostname ' + str(hostname) + '...') is_successful = sensor.unisolate( ) # Isolate the sensor, wait indefinitely until the sensor acknowledges the isolation. Store the 'True' return value to is_successful except Exception as err: # Catch all exceptions and abort yield StatusMessage('[FATAL ERROR] Encountered: ' + str(err)) yield StatusMessage('[FAILURE] Fatal error caused exit!') if is_successful is True: results["was_successful"] = True yield StatusMessage('[SUCCESS] The CB agent on hostname ' + str(hostname) + ' acknowledged the isolation removal.') else: # This can only occur if there was an exception yield StatusMessage('[FAILURE] Hostname ' + str(hostname) + ' was NOT removed from isolation!') # Produce a FunctionResult with the results yield FunctionResult(results) except Exception: yield FunctionError()
def _attachment_zip_extract_function(self, event, *args, **kwargs): """Function: Extract a file from a zipfile attachment, producing a base64 string.""" try: log = logging.getLogger(__name__) # Get the function parameters: incident_id = kwargs.get("incident_id") # number task_id = kwargs.get("task_id") # number attachment_id = kwargs.get("attachment_id") # number file_path = kwargs.get("file_path") # text zipfile_password = kwargs.get("zipfile_password") # text if incident_id is None and task_id is None: raise FunctionError( "Error: incident_id or task_id must be specified.") if attachment_id is None: raise FunctionError("Error: attachment_id must be specified.") if file_path is None: raise FunctionError("Error: file_path must be specified.") log.info("incident_id: %s", incident_id) log.info("task_id: %s", task_id) log.info("attachment_id: %s", attachment_id) log.info("file_path: %s", file_path) yield StatusMessage("Reading attachment...") if task_id: metadata_uri = "/tasks/{}/attachments/{}".format( task_id, attachment_id) data_uri = "/tasks/{}/attachments/{}/contents".format( task_id, attachment_id) else: metadata_uri = "/incidents/{}/attachments/{}".format( incident_id, attachment_id) data_uri = "/incidents/{}/attachments/{}/contents".format( incident_id, attachment_id) client = self.rest_client() metadata = client.get(metadata_uri) data = client.get_content(data_uri) results = {} with tempfile.NamedTemporaryFile(delete=False) as temp_file: try: temp_file.write(data) temp_file.close() # Examine with zip zfile = zipfile.ZipFile(temp_file.name, "r") # Read the metadata, since it may be useful zinfo = zfile.getinfo(file_path) # Don't include zinfo.extra since it's not a string results["info"] = { "filename": zinfo.filename, "date_time": epoch_millis(zinfo.date_time), "compress_type": zinfo.compress_type, "comment": zinfo.comment.decode("utf-8") if isinstance( zinfo.comment, bytes) else zinfo.comment, "create_system": zinfo.create_system, "create_version": zinfo.create_version, "extract_version": zinfo.extract_version, "flag_bits": zinfo.flag_bits, "volume": zinfo.volume, "internal_attr": zinfo.internal_attr, "external_attr": zinfo.external_attr, "header_offset": zinfo.header_offset, "CRC": zinfo.CRC, "compress_size": zinfo.compress_size, "file_size": zinfo.file_size } # Extract the file we want b64data = base64.b64encode( zfile.read(file_path, zipfile_password)) results["content"] = b64data.decode("utf-8") if isinstance( b64data, bytes) else b64data, except (KeyError, zipfile.LargeZipFile, zipfile.BadZipfile) as exc: # results["error"] = str(exc) # To help debug, list the contents log.info(zfile.namelist()) raise finally: os.unlink(temp_file.name) # Produce a FunctionResult with the return value yield FunctionResult(results) except Exception: yield FunctionError()
def _umbrella_timeline_function(self, event, *args, **kwargs): """Function: Resilient Function : Cisco Umbrella Investigate Investigate for Timeline.""" try: # Get the function parameters: umbinv_resource = kwargs.get("umbinv_resource") # text log = logging.getLogger(__name__) log.info("resource: %s", umbinv_resource) if is_none(umbinv_resource): raise ValueError( "Required parameter 'umbinv_resource' not set") yield StatusMessage("Starting...") res = None res_type = None process_result = {} params = {"resource": umbinv_resource.strip()} validate_params(params) process_params(params, process_result) if "_res" not in process_result or "_res_type" not in process_result: raise ValueError( "Parameter 'umbinv_resource' was not processed correctly") else: res = process_result.pop("_res") res_type = process_result.pop("_res_type") if res_type != "domain_name" and res_type != "ip_address" and res_type != "url": raise ValueError( "Parameter 'umbinv_resource' was an incorrect type '{}', should be a 'domain name', " "an 'ip address' or a 'url'.".format(res_type)) api_token = self.options.get("api_token") base_url = self.options.get("base_url") rinv = ResilientInv(api_token, base_url, proxies=self.proxies) yield StatusMessage("Running Cisco Investigate query...") rtn = rinv.timeline(res) query_execution_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S') if len(rtn) == 0: log.debug(json.dumps(rtn)) yield StatusMessage( "No Results returned for resource '{}'.".format(res)) results = {} else: # Make timestamp more readable for x in range(len(rtn)): try: secs = int(rtn[x]['timestamp']) / 1000 ts_readable = datetime.fromtimestamp(secs).strftime( '%Y-%m-%d %H:%M:%S') rtn[x]['timestamp_converted'] = ts_readable except ValueError: yield FunctionError( 'timestamp value incorrectly specified') # Add "query_execution_time" to result to facilitate post-processing. results = { "timeline": json.loads(json.dumps(rtn)), "resource_name": res, "query_execution_time": query_execution_time } yield StatusMessage( "Returning 'thread_grid_samples' results for resource '{}'." .format(res)) yield StatusMessage("done...") log.debug(json.dumps(results)) # Produce a FunctionResult with the results yield FunctionResult(results) except Exception: log.exception("Exception in Resilient Function.") yield FunctionError()
def _pulsedive_query_id_function(self, event, *args, **kwargs): """Function: Query Pulsedive for information on an indicator ID, threat ID, or feed ID. This function gets input values from the action rule activity fields and sends them to Pulsedive's Query endpoint Results: a summary will be written to an incident note and full details will be written to an incident attachment. """ try: log = logging.getLogger(__name__) log.info("config params: %s", self.options) # get query type: Indicator, Threat, or Feed pulsedive_id = kwargs.get("pulsedive_id") pulsedive_query_type = self.get_select_param(kwargs.get("pulsedive_query_type")) pulsedive_id_report_type = self.get_select_param(kwargs.get( "pulsedive_id_report_type")) # === get url parameters based on report type mapping = self._get_mapping(pulsedive_query_type, **kwargs) # eliminate empty/null vars pulsedive_data = {} for k, v in mapping.items(): if v is not None and v != "": pulsedive_data[k] = v # add key pulsedive_data["key"] = self.options["pulsedive_api_key"] log.info("%s parameters: %s", pulsedive_query_type, pulsedive_data) # === set incident parameters incident_id = kwargs.get("incident_id") # integer if kwargs.get("attachment_name") is None: attachment_name = u"pulsedive_{}_id{}_{}.txt".format( pulsedive_query_type, pulsedive_id, pulsedive_id_report_type) else: attachment_name = kwargs.get("attachment_name").replace(u" ", u"_") log.info("function params: pulsedive id = %s, type = %s, report = %s,\ incident='%s', attachment='%s'", pulsedive_id, pulsedive_query_type, pulsedive_id_report_type, incident_id, attachment_name) yield StatusMessage("starting...") # form the url request api_url = "{}/info.php?".format(self.options["pulsedive_api_url"]) # === make the api call rp = ResultPayload(CONFIG_SECTION, **kwargs) rc = RequestsCommon(self.opts, self.options) # initialize resp = rc.execute_call_v2("get", url=api_url, params=pulsedive_data ) # === Get the rest client so we can add the attachment to the incident client = self.rest_client() # prepare datastream to output to attachment if pulsedive_data["pretty"] == "Yes": # Pulsedive returns pp format if requested. Convert to bytestream for file handling. datastream = BytesIO(resp.content) else: # Convert dict to string first, then convert to bytestream for file handling. ds = resp.text datastream = BytesIO(ds.encode("utf-8")) # Write the file as attachment: failures will raise an exception write_file_attachment(client, attachment_name, datastream=datastream, incident_id=incident_id, task_id=None) # === prepare results resp_json = rp.done(True, resp.json()) results = { "resp_json": resp_json, "att_name": attachment_name } # Produce a FunctionResult with the results yield FunctionResult(results) yield StatusMessage("done...") except Exception as err: yield FunctionError(err)
def _fn_tor_function(self, event, *args, **kwargs): """Function: This TOR function searches for the given IP Address or host names in TOR exit node Network by using RESTful API""" try: # Get the function parameters: tor_search_data = kwargs.get("tor_search_data") # text log = logging.getLogger(__name__) log.info("tor_search_data: %s", tor_search_data) yield StatusMessage("starting...") __params_data = { 'flag': self.options.get('flag'), 'fields': self.options.get('data_fields'), 'search': tor_search_data } __params_data = "&".join("%s=%s" % (k, v) for k, v in __params_data.items()) __response_data = requests.get(self.options.get('base_url'), params=__params_data) __MATCH_FLAG = False if __response_data.status_code == 200: __response_data_json_obj = __response_data.json() __relays_data_list = __response_data_json_obj.get('relays') search_field_list = self.options.get('data_fields').split(',') if not __relays_data_list: log.info("Given Search Artifact is Not Matched...!") __MATCH_FLAG = False else: for relay_data in __relays_data_list: for field in search_field_list: data = relay_data.get(field) if data is not None: if isinstance(data, list): for element in data: if element.find(tor_search_data) != -1: log.info( 'Given Search Artifact matched..!' ) __MATCH_FLAG = True else: if data.find(tor_search_data) != -1: log.info( 'Given Search Artifact matched..!') __MATCH_FLAG = True else: __MATCH_FLAG = False log.info(__response_data.text) if __MATCH_FLAG: results = { 'status': 'success', 'value': True, 'data': __response_data.text } else: log.info("Given Search Artifact is Not Matched...!") results = { 'status': 'failed', 'value': False, 'data': __response_data.text } yield StatusMessage("done...") # Produce a FunctionResult with the results yield FunctionResult(results) except Exception as e: yield FunctionError(e)
def _mcafee_publish_to_dxl_function(self, event, *args, **kwargs): """Function: A function which takes 3 inputs: mcafee_topic_name: String of the topic name. ie: /mcafee/service/epo/remote/epo1. mcafee_dxl_payload: The text of the payload to publish to the topic. mcafee_return_request: Specify whether or not to wait for and return the response. The function will publish the provided payload to the provided topic. Indicate whether acknowledgment response should be returned.""" try: yield StatusMessage("Starting...") # Get the function parameters: mcafee_topic_name = kwargs.get("mcafee_topic_name") # text if not mcafee_topic_name: yield FunctionError("mcafee_topic_name is required") mcafee_dxl_payload = kwargs.get("mcafee_dxl_payload") # text if not mcafee_dxl_payload: yield FunctionError("mcafee_dxl_payload is required") mcafee_publish_method = self.get_select_param( kwargs.get("mcafee_publish_method") ) # select, values: "Event", "Service" if not mcafee_publish_method: yield FunctionError("mcafee_publish_method is required") mcafee_wait_for_response = self.get_select_param( kwargs.get( "mcafee_wait_for_response")) # select, values: "Yes", "No" log.info("mcafee_topic_name: %s", mcafee_topic_name) log.info("mcafee_dxl_payload: %s", mcafee_dxl_payload) log.info("mcafee_publish_method: %s", mcafee_publish_method) log.info("mcafee_wait_for_response: %s", mcafee_wait_for_response) response = None # Publish Event if mcafee_publish_method == "Event": event = Event(mcafee_topic_name) event.payload = mcafee_dxl_payload yield StatusMessage("Publishing Event...") self.client.send_event(event) # Invoke Service else: req = Request(mcafee_topic_name) req.payload = mcafee_dxl_payload yield StatusMessage("Invoking Service...") if mcafee_wait_for_response == "No": self.client.async_request(req) else: response = Response( self.client.sync_request(req, timeout=300)) yield StatusMessage("Done...") r = { "mcafee_topic_name": mcafee_topic_name, "mcafee_dxl_payload": mcafee_dxl_payload, "mcafee_publish_method": mcafee_publish_method, "mcafee_wait_for_response": mcafee_wait_for_response } # Initialize the results payload rp = ResultPayload(PACKAGE_NAME, **kwargs) # Return response from publishing to topic if response is not None: # Convert response object to dict response_dict = vars(response) # Convert dict bytes fields to string for python 3 response_dict_str = convert(response_dict) r["response"] = response_dict_str results = rp.done(True, r) # Include for backward compatibility results["mcafee_topic_name"] = mcafee_topic_name results["mcafee_dxl_payload"] = mcafee_dxl_payload results["mcafee_publish_method"] = mcafee_publish_method results["mcafee_wait_for_response"] = mcafee_wait_for_response yield FunctionResult(results) except Exception as e: yield FunctionError(e)
def _qradar_search_function(self, event, *args, **kwargs): """Function: Search QRadar""" try: # Get the function parameters: qradar_query = self.get_textarea_param( kwargs.get("qradar_query")) # textarea qradar_query_param1 = kwargs.get("qradar_query_param1") # text qradar_query_param2 = kwargs.get("qradar_query_param2") # text qradar_query_param3 = kwargs.get("qradar_query_param3") # text qradar_query_param4 = kwargs.get("qradar_query_param4") # text qradar_query_param5 = kwargs.get("qradar_query_param5") # text qradar_query_range_start = kwargs.get( "qradar_query_range_start") # number qradar_query_range_end = kwargs.get( "qradar_query_range_end") # number log = logging.getLogger(__name__) log.info("qradar_query: %s", qradar_query) log.info("qradar_query_param1: %s", qradar_query_param1) log.info("qradar_query_param2: %s", qradar_query_param2) log.info("qradar_query_param3: %s", qradar_query_param3) log.info("qradar_query_param4: %s", qradar_query_param4) log.info("qradar_query_param5: %s", qradar_query_param5) log.info("qradar_query_range_start: %s", qradar_query_range_start) log.info("qradar_query_range_end: %s", qradar_query_range_end) qradar_verify_cert = True if "verify_cert" in self.options and self.options[ "verify_cert"] == "false": qradar_verify_cert = False timeout = None try: if "search_timeout" in self.options: timeout = float(self.options["search_timeout"]) except: log.debug("Failed to read search_timeout: {}".format( self.options["search_timeout"])) log.debug("Connection to {} using {}".format( self.options["host"], self.options["username"])) query_string = function_utils.make_query_string( qradar_query, [ qradar_query_param1, qradar_query_param2, qradar_query_param3, qradar_query_param4, qradar_query_param5 ]) log.info("Running query: " + query_string) yield StatusMessage("starting...") qradar_client = QRadarClient( host=self.options["host"], username=self.options.get("username", None), password=self.options.get("qradarpassword", None), token=self.options.get("qradartoken", None), cafile=qradar_verify_cert) result = qradar_client.ariel_search( query_string, range_start=qradar_query_range_start, range_end=qradar_query_range_end, timeout=timeout) yield StatusMessage("done...") yield FunctionResult(result) except Exception as e: log.error(str(e)) yield FunctionError()
def _mcafee_tie_set_file_reputation_function(self, event, *args, **kwargs): """Function: Set a file's reputation. This works on MD5, SH1 and SHA256 hashes.""" try: yield StatusMessage("Starting") # Get the function parameters: mcafee_tie_hash_type = kwargs.get("mcafee_tie_hash_type") # text mcafee_tie_comment = kwargs.get("mcafee_tie_comment") # text mcafee_tie_reputation_type = self.get_select_param( kwargs.get("mcafee_tie_reputation_type") ) # select, values: "Enterprise", "External" mcafee_tie_trust_level = self.get_select_param( kwargs.get("mcafee_tie_trust_level") ) # select, values: "KNOWN_TRUSTED_INSTALLED", "KNOWN_TRUSTED", "MOST_LIKELY_TRUSTED", "MIGHT_BE_TRUSTED", "UNKNOWN", "MIGHT_BE_MALICIOUS", "MOST_LIKELY_MALICIOUS", "KNOWN_MALICIOUS", "NOT SET" mcafee_tie_hash = kwargs.get("mcafee_tie_hash") # text mcafee_tie_filename = kwargs.get("mcafee_tie_filename") # text log = logging.getLogger(__name__) log.info("mcafee_tie_reputation_type: %s", mcafee_tie_reputation_type) log.info("mcafee_tie_hash_type: %s", mcafee_tie_hash_type) log.info("mcafee_tie_comment: %s", mcafee_tie_comment) log.info("mcafee_tie_trust_level: %s", mcafee_tie_trust_level) log.info("mcafee_tie_hash: %s", mcafee_tie_hash) log.info("mcafee_tie_filename: %s", mcafee_tie_filename) validate_fields([ "mcafee_tie_reputation_type", "mcafee_tie_hash_type", "mcafee_tie_hash", "mcafee_tie_trust_level" ], kwargs) result_payload = ResultPayload(PACKAGE_NAME, **kwargs) tie_client = get_tie_client(self.client) hash_type = get_mcafee_hash_type(mcafee_tie_hash_type) trust_level = get_trust_level_value(mcafee_tie_trust_level) hash_payload = {hash_type: mcafee_tie_hash} LOG.debug("Trust Level: %s, payload: %s", trust_level, hash_payload) # set the call back routine to catch changes or errors result_callback = MyReputationChangeCallback() tie_client.add_file_reputation_change_callback(result_callback) if mcafee_tie_reputation_type == "Enterprise": # Set the Enterprise reputation for notepad.exe to Known Trusted tie_client.set_file_reputation(trust_level, hash_payload, comment=mcafee_tie_comment, filename=mcafee_tie_filename) elif mcafee_tie_reputation_type == "External": # Set the Enterprise reputation for notepad.exe to Known Trusted tie_client.set_external_file_reputation( trust_level, hash_payload, comment=mcafee_tie_comment, filename=mcafee_tie_filename) else: raise ValueError("Unknown reputation type: %s", mcafee_tie_reputation_type) # wait for result or a timeout wait_iter = 4 sleep_time = 5 # sleep while waiting for a call back. Sleep time progesses: 5, 10, 15, ... while wait_iter > 0 and not result_callback.result: time.sleep(sleep_time) sleep_time += 5 wait_iter -= 1 yield StatusMessage("Finished" if result_callback.result else "No result received. Check McAfee console") results = result_payload.done( True if result_callback.result else False, result_callback.result) # Produce a FunctionResult with the results yield FunctionResult(results) except Exception: yield FunctionError()
def _function_cve_search_function(self, event, *args, **kwargs): """Function: A Function to Search Common Vulnerability Exposures Data from https://cve.circl.lu Data Base.""" try: # Get the function parameters: cve_id = kwargs.get("cve_id") # text if cve_id: cve_id = cve_id.strip() cve_vendor = kwargs.get("cve_vendor") # text if cve_vendor: cve_vendor = cve_vendor.strip() cve_product = kwargs.get("cve_product") # text if cve_product: cve_product = cve_product.strip() cve_published_date_from = kwargs.get( "cve_published_date_from") # datepicker cve_published_date_to = kwargs.get( "cve_published_date_to") # datepicker # Getting Max Result Display Flag from app.config MAX_RESULTS_RETURN = int(self.options.get('max_results_display')) # Variables to Store Parsed CVE API Data _result_search_data = dict() log = logging.getLogger(__name__) log.info("cve_id: %s", cve_id) log.info("cve_vendor: %s", cve_vendor) log.info("cve_product: %s", cve_product) log.info("cve_published_date_from: %s", cve_published_date_from) log.info("cve_published_date_to: %s", cve_published_date_to) yield StatusMessage( "Searching the CVE Database. ID:{}, Vendor:{}, Product:{}". format(cve_id, cve_vendor, cve_product)) if cve_id is None and ((cve_vendor is None and cve_product) or (cve_vendor and cve_product is None)): raise ValueError( "Specify both cve_vendor and cve_product or cve-id") if cve_id and (cve_product or cve_vendor): raise ValueError( "Specify both cve_vendor and cve_product or cve-id") if cve_id: _browse_data = self._get_specific_cve_data(cve_id=cve_id) elif cve_vendor and cve_product: _browse_data = self._search_cve_api(vendor_name=cve_vendor, product=cve_product) else: _browse_data = self._last_30_cves() # Defining a key in result dictionary to store parsed data _result_search_data['content'] = [] # Type Of the rest api call like browse,search,specific cve,last, db info api_call_type = _browse_data['api_call'] # Rest Api Response Data _browse_data_content = _browse_data['content'] if _browse_data_content: if api_call_type == 'search': _result_search_data['api_call'] = 'search' search_data_list = self._parse_search_results( _browse_data_content, cve_published_date_from, cve_published_date_to, MAX_RESULTS_RETURN) _result_search_data['content'].extend(search_data_list) elif api_call_type == 'cve': _result_search_data['api_call'] = 'cve' cve_data_list = self._parse_cve_results( _browse_data_content) _result_search_data['content'].extend(cve_data_list) elif api_call_type == 'last': _result_search_data['api_call'] = 'last' last_data_list = self._parse_last_cve_results( _browse_data_content, MAX_RESULTS_RETURN) _result_search_data['content'].extend(last_data_list) log.debug("The Data Received from CVE DB : {}".format( _result_search_data)) yield StatusMessage("Successfully searched the CVE Database.") # Produce a FunctionResult with the results yield FunctionResult(_result_search_data) except Exception as er: yield FunctionError(er)