mirror of
https://github.com/splunk/security_content
synced 2026-06-08 17:32:49 +00:00
398 lines
17 KiB
Python
398 lines
17 KiB
Python
import sys
|
|
import json
|
|
import splunk
|
|
import splunklib.client
|
|
import splunklib.results
|
|
from splunklib.searchcommands import dispatch, GeneratingCommand, Configuration, Option
|
|
from splunklib.searchcommands.validators import Boolean
|
|
import splunk.mining.dcutils
|
|
import time
|
|
from datetime import datetime, timedelta
|
|
import re
|
|
|
|
|
|
@Configuration(streaming=True, local=True)
|
|
class DetectCommand(GeneratingCommand):
|
|
logger = splunk.mining.dcutils.getLogger()
|
|
story = Option(require=True)
|
|
|
|
# global variables
|
|
detection_searches_to_run = []
|
|
investigative_searches_to_run = []
|
|
support_searches_to_run = []
|
|
story_results = {}
|
|
collection_names = []
|
|
COLLECTION_NAME = "mp_detect_new"
|
|
DETECT_INDEX = "detect_index"
|
|
collection_results = {}
|
|
|
|
def _support_searches(self, content):
|
|
support_data = {}
|
|
support_data['search_name'] = content['action.escu.full_search_name']
|
|
support_data['search_description'] = content['description']
|
|
support_data['search'] = content['search']
|
|
self.support_searches_to_run.append(support_data)
|
|
# self.logger.info("detect.py - prepping to run support search: {0}".format(support_data['search_name']))
|
|
return self.support_searches_to_run
|
|
|
|
def _investigative_searches(self, content):
|
|
investigative_data = {}
|
|
investigative_data['search_name'] = content['action.escu.full_search_name']
|
|
investigative_data['search_description'] = content['description']
|
|
investigative_data['search'] = content['search']
|
|
investigative_data['fields_required'] = content['action.escu.fields_required']
|
|
self.investigative_searches_to_run.append(investigative_data)
|
|
return self.investigative_searches_to_run
|
|
|
|
def _detection_searches(self, content):
|
|
detection_data = {}
|
|
detection_data['search_name'] = content['action.escu.full_search_name']
|
|
detection_data['search_description'] = content['description']
|
|
detection_data['search'] = content['search']
|
|
detection_data['entities'] = content['action.escu.entities']
|
|
detection_data['mappings'] = json.loads(content['action.escu.mappings'])
|
|
self.detection_searches_to_run.append(detection_data)
|
|
#self.logger.info("detect.py - prepping to run detection search: {0}".format(detection_data['search_name']))
|
|
return self.detection_searches_to_run
|
|
|
|
def _run_support(self, support_searches_to_run, service, earliest_time, latest_time):
|
|
# Run all Support searches
|
|
support_search_name = []
|
|
|
|
for search in support_searches_to_run:
|
|
# setup service job
|
|
latest_support_time = earliest_time
|
|
|
|
earliest_utc = datetime.utcfromtimestamp(earliest_time).strftime('%Y-%m-%d %H:%M:%S.%f')
|
|
support_earliest_time = datetime.strptime(earliest_utc, '%Y-%m-%d %H:%M:%S.%f') - timedelta(days=30)
|
|
support_earliest_time = support_earliest_time.strftime('%s')
|
|
kwargs = {"exec_mode": "normal", "earliest_time": support_earliest_time, "latest_time": latest_support_time}
|
|
spl = search['search']
|
|
if spl[0] != "|":
|
|
spl = "| search %s" % spl
|
|
#self.logger.info("detect.py - running support search: {0}".format(search['search_name']))
|
|
job = service.jobs.create(spl, **kwargs)
|
|
if job['isFailed'] == True:
|
|
self.logger.info("detect.py - NO support search: {0}".format(spl))
|
|
while True:
|
|
job.refresh()
|
|
if job['isDone'] == "1":
|
|
self.logger.info("detect.py - completed support search: {0}".format(search['search_name']))
|
|
break
|
|
# append each completed support search
|
|
support_search_name.append(search['search_name'])
|
|
|
|
return support_search_name
|
|
|
|
def _process_entities(self, result, search, entity_results):
|
|
for key, value in result.items():
|
|
|
|
# Convert search['entities'] to a list
|
|
entities = str(search['entities']).strip('][').replace('"', '').split(', ')
|
|
|
|
# if the key exists lets append to it
|
|
|
|
if key in entity_results:
|
|
|
|
# if the result back is a list and its name is a entity lets store each value
|
|
if type(value) == list and key in entities:
|
|
for i in value:
|
|
# if we haven't stored this value lets add it
|
|
if i not in entity_results[key]:
|
|
entity_results[key].append(i)
|
|
|
|
# if the result is a string and is name is a entity of the detection lets store it
|
|
|
|
if type(value) == str and key in entities and value not in entity_results[key]:
|
|
entity_results[key].append(value)
|
|
|
|
else:
|
|
# its the first time we see this entity lets create a list its values
|
|
entity_results[key] = []
|
|
if type(value) == list and key in entities:
|
|
for i in value:
|
|
# if we haven't stored this value lets add it
|
|
if i not in entity_results[key]:
|
|
entity_results[key].append(i)
|
|
|
|
if type(value) == str and key in entities and value not in entity_results[key]:
|
|
entity_results[key].append(value)
|
|
|
|
# lets build an entity object from the results and a list to store them in
|
|
entity = {}
|
|
for entity_name, entity_value in entity_results.items():
|
|
|
|
# first check that the entity result is not empty
|
|
if entity_value:
|
|
if entity_name in entity.keys():
|
|
# self.logger.info( "detect.py - ENTITY name {0} EXISTS | ENTITY value {1} | SEARCH: {2}".format(entity_name,entity_value,search['search_name']))
|
|
for v in entity_value:
|
|
entity[entity_name]['count'] += 1
|
|
entity[entity_name]['entity_results'].append(v)
|
|
else:
|
|
# self.logger.info("detect.py - ENTITY name {0} DOES NOT EXISTS | ENTITY value {1} | SEARCH: {2}".format(entity_name,entity_value,search['search_name']))
|
|
entity[entity_name] = {}
|
|
for v in entity_value:
|
|
if 'count' in entity[entity_name].keys():
|
|
entity[entity_name]['count'] += 1
|
|
else:
|
|
entity[entity_name]['count'] = 1
|
|
if 'entity_results' in entity[entity_name].keys():
|
|
entity[entity_name]['entity_results'].append(v)
|
|
else:
|
|
entity[entity_name]['entity_results'] = []
|
|
entity[entity_name]['entity_results'].append(v)
|
|
return entity
|
|
|
|
# Store Data in an index, not using this function
|
|
def _store_index(self, service):
|
|
|
|
indexes = service.indexes
|
|
index = []
|
|
for i in indexes:
|
|
index.append(i.name)
|
|
# Create an index if it doesnt exist
|
|
if self.DETECT_INDEX not in index:
|
|
detect_index = service.indexes.create(self.DETECT_INDEX)
|
|
detect_index = service.indexes[self.DETECT_INDEX]
|
|
mysocket = detect_index.attach()
|
|
mysocket.send(json.dumps(self.collection_results))
|
|
mysocket.close()
|
|
|
|
self.logger.info("detect.py - DONE -----")
|
|
|
|
def _store_collections(self, collection):
|
|
|
|
self.collection_results['story'] = self.story
|
|
self.collection_results['detections'] = self.story_results['detections']
|
|
self.collection_results['executed_by'] = self.story_results['executed_by']
|
|
collection.data.insert(json.dumps(self.collection_results))
|
|
|
|
def _get_username(self, service):
|
|
search = '| rest /services/authentication/current-context/context | fields + username'
|
|
results = service.jobs.oneshot(search)
|
|
username_results = splunklib.results.ResultsReader(results)
|
|
username = next(iter(username_results))['username']
|
|
self.story_results['executed_by'] = username
|
|
|
|
def _process_job_results(self, job, job_results, search, support_search_name):
|
|
|
|
# if there are results lets process them
|
|
if job['resultCount'] > "0":
|
|
# place to store results and entity results
|
|
detection_results = []
|
|
|
|
entities = []
|
|
entity_results = dict()
|
|
# process results
|
|
|
|
epoch = []
|
|
for result in job_results:
|
|
for k,v in result.items():
|
|
if k == "firstTime":
|
|
first = int(time.mktime(time.strptime(v, '%m/%d/%Y %H:%M:%S')))
|
|
epoch.append(first)
|
|
|
|
if k == "lastTime":
|
|
last = int(time.mktime(time.strptime(v, '%m/%d/%Y %H:%M:%S')))
|
|
epoch.append(last)
|
|
# add store detection results
|
|
detection_results.append(dict(result))
|
|
|
|
# lets process entity results now
|
|
entity = self._process_entities(result, search, entity_results)
|
|
|
|
entities.append(entity)
|
|
|
|
#self.logger.info("detect.py - PROCESSED ENTITY {0} | SEARCH: {1}".format(entity, search['search_name']))
|
|
if epoch:
|
|
first_detection_time = min(epoch)
|
|
first_detection_time = datetime.utcfromtimestamp(first_detection_time).strftime('%Y-%m-%d %H:%M:%S')
|
|
|
|
last_detection_time= max(epoch)
|
|
last_detection_time = datetime.utcfromtimestamp(last_detection_time).strftime('%Y-%m-%d %H:%M:%S')
|
|
else:
|
|
first_detection_time = ""
|
|
last_detection_time = ""
|
|
|
|
detection = {}
|
|
detection['first_detection_time'] = first_detection_time
|
|
detection['last_detection_time'] = last_detection_time
|
|
detection['detection_result_count'] = job['resultCount']
|
|
detection['detection_search_name'] = search['search_name']
|
|
detection['mappings'] = search['mappings']
|
|
detection['detection_results'] = detection_results
|
|
detection['support_search_name'] = support_search_name
|
|
detection['entities'] = entities
|
|
self.story_results['detections'].append(detection)
|
|
|
|
# Wondering if we want to tell the users that a particular detection search ran successfully and produced no results.
|
|
if job['resultCount'] == "0":
|
|
detection = {}
|
|
detection['first_detection_time'] = "null"
|
|
detection['last_detection_time'] = "null"
|
|
detection['detection_result_count'] = job['resultCount']
|
|
detection['detection_search_name'] = search['search_name']
|
|
detection['mappings'] = search['mappings']
|
|
detection['detection_results'] = "null"
|
|
detection['support_search_name'] = support_search_name
|
|
detection['entities'] = "null"
|
|
self.story_results['detections'].append(detection)
|
|
|
|
|
|
|
|
def _run_detections(self, detection_searches_to_run, service, earliest_time, latest_time, support_search_name):
|
|
# create an array to store our detections in
|
|
self.story_results['detections'] = []
|
|
|
|
# run detection searches
|
|
for search in detection_searches_to_run:
|
|
|
|
# set parameters for search
|
|
kwargs = {"exec_mode": "normal", "earliest_time": earliest_time, "latest_time": latest_time}
|
|
spl = search['search']
|
|
#self.logger.info("detect.py - running detection search: {0}".format(search['search_name']))
|
|
|
|
# add pipe if is missing
|
|
if spl[0] != "|":
|
|
spl = "| search %s" % spl
|
|
|
|
# dispatch job
|
|
job = service.jobs.create(spl, **kwargs)
|
|
|
|
# we sleep for 2 seconds to not DOS Splunk with submitting searches
|
|
time.sleep(2)
|
|
|
|
|
|
# check for results, if done we process them
|
|
while True:
|
|
job.refresh()
|
|
if job['isFailed'] == "1":
|
|
self.logger.info("detect.py - Failed detection search: {0}".format(spl))
|
|
#raise Exception('detect.py - The search: {0} failed to execute'.format(search['search_name']))
|
|
break
|
|
|
|
if job['isDone'] == "1":
|
|
self.logger.info("detect.py - Finished Detection search: {0}".format(search['search_name']))
|
|
# process raw results with reader
|
|
job_results = splunklib.results.ResultsReader(job.results())
|
|
|
|
# process job results into detection objects extract the necessary keys
|
|
self._process_job_results(job, job_results, search, support_search_name)
|
|
break
|
|
|
|
|
|
|
|
def generate(self):
|
|
|
|
# connect to splunk and start execution
|
|
port = splunk.getDefault('port')
|
|
service = splunklib.client.connect(token=self._metadata.searchinfo.session_key, port=port, owner="nobody",app="DA-ESS-ContentUpdate")
|
|
self.logger.info("detect.pytime - starting run story - {0} ".format(self.story))
|
|
|
|
# get story name
|
|
self.story_results['story'] = self.story
|
|
|
|
# get username
|
|
self._get_username(service)
|
|
|
|
# get time window
|
|
if hasattr(self.search_results_info, 'search_et') and hasattr(self.search_results_info, 'search_lt'):
|
|
earliest_time = self.search_results_info.search_et
|
|
latest_time = self.search_results_info.search_lt
|
|
|
|
# get saved_searches
|
|
savedsearches = service.saved_searches
|
|
|
|
# create collection if it does not exists otherwise wipe it
|
|
if self.COLLECTION_NAME in service.kvstore:
|
|
service.kvstore.delete(self.COLLECTION_NAME)
|
|
service.kvstore.create(self.COLLECTION_NAME)
|
|
|
|
collection = service.kvstore[self.COLLECTION_NAME]
|
|
|
|
detection_searches_to_run = []
|
|
support_searches_to_run = []
|
|
|
|
# get all savedsearches content
|
|
for savedsearch in savedsearches:
|
|
content = savedsearch.content
|
|
|
|
# check we are on the right story
|
|
if 'action.escu.analytic_story' in content:
|
|
stories = str(content['action.escu.analytic_story']).strip('][').replace('"', '').split(', ')
|
|
for s in stories:
|
|
|
|
if s == self.story:
|
|
|
|
# if it has a support search grab it otherwise replace its value with a message
|
|
# THIS CAN BE REMOVED AFTER BASELINE MODULE IS CONSTRUCTED
|
|
if content['action.escu.search_type'] == 'support':
|
|
support_searches_to_run = self._support_searches(content)
|
|
|
|
|
|
# if it has detection searches grab it
|
|
if content['action.escu.search_type'] == 'detection':
|
|
detection_searches_to_run = self._detection_searches(content)
|
|
|
|
# if detection to run is empty we likely got a incorrect story name
|
|
if len(detection_searches_to_run) < 1:
|
|
self.logger.error("detect.py - No detection searches in this story")
|
|
|
|
raise Exception(
|
|
'no detections found for story: {0} .. try a correct story name or check spelling'.format(self.story))
|
|
if support_searches_to_run:
|
|
support_search_name = self._run_support(support_searches_to_run, service,earliest_time,latest_time)
|
|
else:
|
|
support_search_name = ["No Support or Baseline search in this Analytic Story"]
|
|
|
|
|
|
# now lets run all the detection searches and process their results into story_results['detections']
|
|
self._run_detections(detection_searches_to_run, service, earliest_time, latest_time, support_search_name)
|
|
self.logger.info("detect.py - detection searches run and processed -")
|
|
|
|
|
|
# lets store results in a KVStore to run investigate.py on that store
|
|
self._store_collections(collection)
|
|
self.logger.info("detect.py - detection results added to KVSTore - {0} ".format(self.COLLECTION_NAME))
|
|
self.logger.info("detect.py - COMPLETED")
|
|
|
|
# Index the results
|
|
#self._store_index(service)
|
|
|
|
|
|
# Yield individual results after processing them to preserve field names
|
|
|
|
for result in self.story_results['detections']:
|
|
result['executed_by'] = self.story_results['executed_by']
|
|
result['story'] = self.story_results['story']
|
|
|
|
yield {
|
|
|
|
'_time': time.time(),
|
|
'_raw': result,
|
|
'sourcetype': "_json",
|
|
'story': result['story'],
|
|
'executed_by': result['executed_by'],
|
|
'detection_result_count': result['detection_result_count'],
|
|
'detection_search_name': result['detection_search_name'],
|
|
'first_detection_time': result['first_detection_time'],
|
|
'last_detection_time': result['last_detection_time'],
|
|
'support_search_name': result['support_search_name'],
|
|
'entities': result['entities'],
|
|
'mappings': result['mappings'],
|
|
'detection_results' : result['detection_results']
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(self):
|
|
super(DetectCommand, self).__init__()
|
|
|
|
|
|
dispatch(DetectCommand, sys.argv, sys.stdin, sys.stdout, __name__) |