diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 5f3e558c43..d61ff868b3 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,12 +1,12 @@ # PR Template for new Detections For Authors: -1. Make sure that CI/CD [detection-testing and build-and-validate](https://github.com/splunk/security_content/actions) jobs passed ✔️. +- [ ] Make sure that CI/CD [detection-testing and build-and-validate](https://github.com/splunk/security_content/actions) jobs passed ✔️. For Reviewers: - [ ] Verify CI/CD jobs have passed without errors. - [ ] Validate SPL logic. - [ ] Validate tags, description, and how to implement. -- [ ] Validate name patches `__` +- [ ] Validate name matches `__` - [ ] Verify references match analytic. - [ ] Is there an Atomic Test? \ No newline at end of file diff --git a/.github/workflows/detection-testing.yml b/.github/workflows/detection-testing.yml index 0146f06aed..98ee2d736a 100644 --- a/.github/workflows/detection-testing.yml +++ b/.github/workflows/detection-testing.yml @@ -163,7 +163,7 @@ jobs: cd bin/automated_detection_testing/ci/detection_testing_batch source .venv/bin/activate - python3 detection_testing_execution.py run -c prior_config/${{ matrix.manifest_filename}} --splunkbase_username ${{ secrets.SPLUNKBASE_TESTING_USERNAME }} --splunkbase_password ${{ secrets.SPLUNKBASE_TESTING_KEY }} + python3 detection_testing_execution.py run -c prior_config/${{ matrix.manifest_filename}} - name: Upload Test Results Files diff --git a/bin/automated_detection_testing/ci/detection_testing_batch/detection_testing_execution.py b/bin/automated_detection_testing/ci/detection_testing_batch/detection_testing_execution.py index ae4ae4ef2b..267f6af863 100644 --- a/bin/automated_detection_testing/ci/detection_testing_batch/detection_testing_execution.py +++ b/bin/automated_detection_testing/ci/detection_testing_batch/detection_testing_execution.py @@ -1,6 +1,7 @@ import argparse import copy import csv +from ctypes.wintypes import tagRECT import json import os import queue @@ -19,6 +20,8 @@ from tempfile import mkdtemp from timeit import default_timer as timer from typing import Union from urllib.parse import urlparse +import signal + import docker import requests @@ -30,7 +33,7 @@ import modules.new_arguments2 from modules import (container_manager, new_arguments2, testing_service, validate_args) from modules.github_service import GithubService -from modules.validate_args import validate, validate_and_write +from modules.validate_args import validate, validate_and_write, ES_APP_NAME SPLUNK_CONTAINER_APPS_DIR = "/opt/splunk/etc/apps" index_file_local_path = "indexes.conf.tar" @@ -45,27 +48,60 @@ datamodel_file_container_path = os.path.join( authorizations_file_local_path = "authorize.conf.tar" authorizations_file_container_path = "/opt/splunk/etc/system/local" +CONTAINER_APP_DIRECTORY = "apps" MAX_RECOMMENDED_CONTAINERS_BEFORE_WARNING = 2 -def download_file_from_http(url:str, target:str)->None: - #Will just overwrite an existing file + + + +def download_file_from_http(url:str, destination_file:str, overwrite_file:bool=False)->None: + if os.path.exists(destination_file) and overwrite_file is False: + print(f"[{destination_file}] already exists...using cached version") + return + print(f"downloading to [{destination_file}]") file_to_download = requests.get(url, stream=True) - with open(target, "wb") as output: + with open(destination_file, "wb") as output: for piece in file_to_download.iter_content(chunk_size=(1024*1024)): output.write(piece) -def copy_local_apps_to_directory(apps: dict[str, dict], target_directory) -> None: - for key, item in apps.items(): +def copy_local_apps_to_directory(apps: dict[str, dict], splunkbase_username:tuple[str,None] = None, splunkbase_password:tuple[str,None] = None, mock:bool = False, target_directory:str = "apps") -> str: + if mock is True: + target_directory = os.path.join("prior_config", target_directory) + # Remove the apps directory or the prior config directory. If it's just an apps directory, then we don't want + #to remove that. + shutil.rmtree(target_directory, ignore_errors=True) + try: + # Make sure the directory exists. If it already did, that's okay. Don't delete anything from it + # We want to re-use previously downloaded apps + os.makedirs(target_directory, exist_ok = True) + + except Exception as e: + raise(Exception(f"Some error occured when trying to make the {target_directory}: [{str(e)}]")) + + + for key, item in apps.items(): + + # These apps are URLs that will be passed. The apps will be downloaded and installed by the container + # # Get the file from an http source + splunkbase_info = True if ('app_number' in item and item['app_number'] is not None and + 'app_version' in item and item['app_version'] is not None) else False + splunkbase_creds = True if (splunkbase_username is not None and + splunkbase_password is not None) else False + can_download_from_splunkbase = splunkbase_info and splunkbase_creds + + + #local apps can either have a local_path or an http_path if 'local_path' in item: source_path = os.path.abspath(os.path.expanduser(item['local_path'])) base_name = os.path.basename(source_path) dest_path = os.path.join(target_directory, base_name) try: + print(f"copying {os.path.relpath(source_path)} to {os.path.relpath(dest_path)}") shutil.copy(source_path, dest_path) item['local_path'] = dest_path except shutil.SameFileError as e: @@ -77,25 +113,34 @@ def copy_local_apps_to_directory(apps: dict[str, dict], target_directory) -> Non source_path, dest_path, str(e)), file=sys.stderr) sys.exit(1) - # These apps are URLs that will be passed. The apps will be downloaded and installed by the container - # # Get the file from an http source - # elif 'http_path' in item: - # http_path = item['http_path'] - # try: - # url_parse_obj = urlparse(http_path) - # path_after_host = url_parse_obj[2].rstrip('/') #removes / at the end, if applicable - # base_name = path_after_host.rpartition('/')[-1] #just get the file name - # dest_path = os.path.join(target_directory, base_name) #write the whole path - # download_file_from_http(http_path, dest_path) - # #we need to updat the local path because this is used to copy it into the container later - # item['local_path'] = dest_path - # except Exception as e: - # print("Error trying to download %s @ %s: [%s]. This app is required.\n\tQuitting..."%(key, http_path, str(e)),file=sys.stderr) - # sys.exit(1) - # else: - # print("Error - trying to install a local app that does not have 'local_path' or 'http_path'.\n\tQuitting...") - # sys.exit(1) + + elif can_download_from_splunkbase is True: + #Don't do anything, this will be downloaded from splunkbase + pass + elif splunkbase_info is True and splunkbase_creds is False and mock is True: + #Don't need to do anything, when this actually runs the apps will be downloaded from Splunkbase + #There is another opportunity to provide the creds then + pass + elif 'http_path' in item and can_download_from_splunkbase is False: + http_path = item['http_path'] + try: + url_parse_obj = urlparse(http_path) + path_after_host = url_parse_obj[2].rstrip('/') #removes / at the end, if applicable + base_name = path_after_host.rpartition('/')[-1] #just get the file name + dest_path = os.path.join(target_directory, base_name) #write the whole path + download_file_from_http(http_path, dest_path) + #we need to update the local path because this is used to copy it into the container later + item['local_path'] = dest_path + #Remove the HTTP Path, we will use the local_path instead + except Exception as e: + print("Error trying to download %s @ %s: [%s]. This app is required.\n\tQuitting..."%(key, http_path, str(e)),file=sys.stderr) + sys.exit(1) + elif splunkbase_info is False: + print(f"Error - trying to install an app [{key}] that does not have 'local_path', 'http_path', " + "or 'app_version' and 'app_number' for installing from Splunkbase.\n\tQuitting...") + sys.exit(1) + return target_directory def ensure_security_content(branch: str, commit_hash: Union[str,None], pr_number: Union[int, None], persist_security_content: bool) -> tuple[GithubService, bool]: @@ -183,7 +228,7 @@ def generate_escu_app(persist_security_content: bool = False) -> str: else: os.mkdir("slim_packaging") - os.mkdir("apps") + try: SPLUNK_PACKAGING_TOOLKIT_URL = "https://download.splunk.com/misc/packaging-toolkit/splunk-packaging-toolkit-0.9.0.tar.gz" SPLUNK_PACKAGING_TOOLKIT_FILENAME = 'splunk-packaging-toolkit-latest.tar.gz' @@ -216,7 +261,7 @@ def generate_escu_app(persist_security_content: bool = False) -> str: ret = subprocess.run("; ".join(commands), shell=True, capture_output=True) - if ret.returncode != 0: + if ret.returncode != 0: print("Command List:\n%s" % (commands)) print("Error generating new ESCU Package.\n\tQuitting and dumping error...\n[%s]" % ( ret.stderr.decode('utf-8')), file=sys.stderr) @@ -226,27 +271,10 @@ def generate_escu_app(persist_security_content: bool = False) -> str: return output_file_path_from_root + def finish_mock(settings: dict, detections: list[str], output_file_template: str = "prior_config/config_tests_%d.json")->bool: num_containers = settings['num_containers'] - try: - # Remove the prior config directory if it exists. If not, continue - shutil.rmtree("prior_config", ignore_errors=True) - - # We want to make the prior_config directory and the prior_config/apps directory - os.makedirs("prior_config/apps") - except FileExistsError as e: - print("Directory priorconfig/apps exists, but we just deleted it!\n\tQuitting...", file=sys.stderr) - return False - except Exception as e: - print("Some error occured when trying to make the configs folder: [%s]\n\tQuitting..." % ( - str(e)), file=sys.stderr) - return False - - # Copy the apps to the appropriate local. This will also update - # the app paths in settings['local_apps'] - copy_local_apps_to_directory(settings['local_apps'], "prior_config/apps") - for output_file_index in range(0, num_containers): fname = output_file_template % (output_file_index) @@ -285,9 +313,8 @@ def finish_mock(settings: dict, detections: list[str], output_file_template: str # Make sure that it still validates after all of the changes try: - with open(fname, 'w') as cfg: - validated_settings, b = validate_and_write( - mock_settings, cfg) + with open(fname, 'w') as outfile: + validated_settings, b = validate_and_write(configuration=mock_settings, output_file = outfile, strip_credentials=True) if validated_settings is None: print( "There was an error validating the updated mock settings.\n\tQuitting...", file=sys.stderr) @@ -331,23 +358,40 @@ def main(args: list[str]): credentials_needed = False credential_error = False - if len(settings['splunkbase_apps']) > 0: - credentials_needed = True - if settings['splunkbase_username'] == None and credentials_needed: - print("Error - you have listed apps to download from Splunkbase but have "\ - "not provided --splunkbase_username via the command line or config file.",file=sys.stderr) - credential_error = True + + if settings['splunkbase_username'] == None or settings['splunkbase_password'] == None: + + missing_credentials = [] + if settings['splunkbase_username'] == None: + missing_credentials.append("--splunkbase_username") + if settings['splunkbase_password'] == None: + missing_credentials.append("--splunkbase_password") + + missing_credentials_string = '\n\t'.join(missing_credentials) + + splunkbase_only_apps = [] + for app,content in settings['apps'].items(): + if 'local_path' not in content and 'http_path' not in content: + splunkbase_only_apps.append(app) + if len(splunkbase_only_apps) != 0: + print(f"Error - you have attempted to install the following apps: {splunkbase_only_apps}, " + "but you have not provided a local_path or an http_path in the config file. Normally, " + "we would download these from Splunkbase, but the following credentials are " + f"missing:\n\t{missing_credentials_string}\n Please provide them on the command line " + "or in the config file.\n\tQuitting...") + sys.exit(1) + + print(f"You have listed apps to install but have "\ + f"not provided\n\t{missing_credentials_string} \nvia the command line or config file. " + f"We will download these files from S3 rather than Splunkbase.") + else: + + print(f"You have listed apps to install and provided Splunkbase credentials. "\ + f"These apps will be downloaded and installed from Splunkbase!") - if settings['splunkbase_password'] == None and credentials_needed: - print("Error - you have listed apps to download from Splunkbase but have "\ - "not provided --splunkbase_password via the command line or config file.",file=sys.stderr) - credential_error = True - if credential_error: - print("Please supply the required credentials to continue.\n\tQuitting...",file=sys.stderr) - sys.exit(1) FULL_DOCKER_HUB_CONTAINER_NAME = "splunk/splunk:%s" % settings['container_tag'] @@ -405,42 +449,36 @@ def main(args: list[str]): print("***This run will test [%d] detections!***"%(len(all_test_files))) - #Set up the directory that will be used to store the local apps/apps we build - local_volume_absolute_path = os.path.abspath( - os.path.join(os.getcwd(), "apps")) - try: - # remove the directory first - shutil.rmtree(local_volume_absolute_path, ignore_errors=True) - os.mkdir(local_volume_absolute_path) - except FileExistsError as e: - # Directory already exists, do nothing - pass - except Exception as e: - print("Error creating the apps folder [%s]: [%s]\n\tQuitting..." - % (local_volume_absolute_path, str(e)), file=sys.stderr) - sys.exit(1) - #Add the info about the mount - mounts = [{"local_path": local_volume_absolute_path, - "container_path": "/tmp/apps", "type": "bind", "read_only": True}] + # Check to see if we want to install ESCU and whether it was preeviously generated and we should use that file - if 'SPLUNK_ES_CONTENT_UPDATE' in settings['local_apps'] and settings['local_apps']['SPLUNK_ES_CONTENT_UPDATE']['local_path'] is not None: + if ES_APP_NAME in settings['apps'] and settings['apps'][ES_APP_NAME]['local_path'] is not None: # Using a pregenerated ESCU, no need to build it pass - elif 'SPLUNK_ES_CONTENT_UPDATE' not in settings['local_apps']: - print("%s was not found in %s. We assume this is an error and shut down.\n\t" - "Quitting..." % ('SPLUNK_ES_CONTENT_UPDATE', "settings['local_apps']"), file=sys.stderr) + + elif ES_APP_NAME not in settings['apps']: + print(f"{ES_APP_NAME} was not found in {settings['apps'].keys()}. We assume this is an error and shut down.\n\t" + "Quitting...", file=sys.stderr) sys.exit(1) else: # Generate the ESCU package from this branch. source_path = generate_escu_app(settings['persist_security_content']) - settings['local_apps']['SPLUNK_ES_CONTENT_UPDATE']['local_path'] = source_path + settings['apps']['SPLUNK_ES_CONTENT_UPDATE']['local_path'] = source_path # Copy all the apps, to include ESCU (whether pregenerated or just generated) - copy_local_apps_to_directory( - settings['local_apps'], local_volume_absolute_path) + try: + relative_app_path = copy_local_apps_to_directory(settings['apps'], + splunkbase_username = settings['splunkbase_username'], + splunkbase_password = settings['splunkbase_password'], + mock=settings['mock'], target_directory = CONTAINER_APP_DIRECTORY) + + mounts = [{"local_path": os.path.abspath(relative_app_path), + "container_path": "/tmp/apps", "type": "bind", "read_only": True}] + except Exception as e: + print(f"Error occurred when copying apps to app folder: [{str(e)}]\n\tQuitting...", file=sys.stderr) + sys.exit(1) # If this is a mock run, finish it now @@ -467,13 +505,45 @@ def main(args: list[str]): + def shutdown_signal_handler_setup(sig, frame): + + print(f"Signal {sig} received... stopping all [{settings['num_containers']}] containers and shutting down...") + shutdown_client = docker.client.from_env() + errorCount = 0 + for container_number in range(settings['num_containers']): + container_name = settings['local_base_container_name']%container_number + print(f"Shutting down {container_name}...", file=sys.stderr, end='') + sys.stdout.flush() + try: + container = shutdown_client.containers.get(container_name) + #Note that stopping does not remove any of the volumes or logs, + #so stopping can be useful if we want to debug any container failure + container.stop(timeout=10) + print("done", file=sys.stderr) + except Exception as e: + print(f"Error trying to shut down {container_name}. It may have already shut down. Stop it youself with 'docker containter stop {container_name}", sys.stderr) + errorCount += 1 + if errorCount == 0: + print("All containers shut down successfully", file=sys.stderr) + else: + print(f"{errorCount} containers may still be running. Find out what is running with:\n\t'docker container ls'\nand shut them down with\n\t'docker container stop CONTAINER_NAME' ", file=sys.stderr) + + print("Quitting...",file=sys.stderr) + #We must use os._exit(1) because sys.exit(1) actually generates an exception which can be caught! And then we don't Quit! + os._exit(1) + + + + + #Setup requires a different teardown handler than during execution + signal.signal(signal.SIGINT, shutdown_signal_handler_setup) + try: cm = container_manager.ContainerManager(all_test_files, FULL_DOCKER_HUB_CONTAINER_NAME, settings['local_base_container_name'], settings['num_containers'], - settings['local_apps'], - settings['splunkbase_apps'], + settings['apps'], settings['branch'], settings['commit_hash'], reproduce_test_config, @@ -492,6 +562,15 @@ def main(args: list[str]): print("Error - unrecoverable error trying to set up the containers: [%s].\n\tQuitting..."%(str(e)),file=sys.stderr) sys.exit(1) + def shutdown_signal_handler_execution(sig, frame): + #Set that a container has failed which will gracefully stop the other containers. + #This way we get our full cleanup routine, too! + print("Got a signal to shut down. Shutting down all containers, please wait...", file=sys.stderr) + cm.synchronization_object.containerFailure() + + #Update the signal handler + + signal.signal(signal.SIGINT, shutdown_signal_handler_execution) try: result = cm.run_test() except Exception as e: @@ -508,7 +587,10 @@ def main(args: list[str]): sys.exit(0) else: print("Test Execution Failed - review the logs for more details") - sys.exit(1) + #Because one or more of the threads could be stuck in a certain setup loop, like + #trying to copy files to a containers (which igonores errors), we must os._exit + #instead of sys.exit + os._exit(1) if __name__ == "__main__": diff --git a/bin/automated_detection_testing/ci/detection_testing_batch/modules/container_manager.py b/bin/automated_detection_testing/ci/detection_testing_batch/modules/container_manager.py index 6b25b19b7f..975bf5d9e6 100644 --- a/bin/automated_detection_testing/ci/detection_testing_batch/modules/container_manager.py +++ b/bin/automated_detection_testing/ci/detection_testing_batch/modules/container_manager.py @@ -1,4 +1,5 @@ from collections import OrderedDict +from tabnanny import check import docker import datetime import docker.types @@ -25,8 +26,7 @@ class ContainerManager: full_docker_hub_name: str, container_name_template: str, num_containers: int, - local_apps: OrderedDict, - splunkbase_apps:OrderedDict, + apps: OrderedDict, branch:str, commit_hash:str, summarization_reproduce_failure_config:dict, @@ -43,12 +43,15 @@ class ContainerManager: interactive:bool=False ): + #Used to determine whether or not we should wait for container threads to finish when summarizing + self.all_tests_completed = False + self.synchronization_object = test_driver.TestDriver( test_list, num_containers, summarization_reproduce_failure_config) self.mounts = self.create_mounts(mounts) - self.local_apps = local_apps - self.splunkbase_apps = splunkbase_apps + self.apps = apps + if container_password is None: self.container_password = self.get_random_password() @@ -93,21 +96,29 @@ class ContainerManager: self.baseline['TEST_FINISH_TIME'] = "TO BE UPDATED" self.baseline['TEST_DURATION'] = "TO BE UPDATED" - for key in self.local_apps: - self.baseline[key] = self.local_apps[key] + for key in self.apps: + self.baseline[key] = self.apps[key] - for key in self.splunkbase_apps: - self.baseline[key] = self.splunkbase_apps[key] + def run_test(self)->bool: - self.run_containers() self.run_status_thread() - for container in self.containers: - container.thread.join() - print(container.get_container_summary()) - print("Waiting for next summary thread printout to finish...") + self.run_containers() self.summary_thread.join() + + + + + for container in self.containers: + if self.all_tests_completed == True: + container.thread.join() + elif self.all_tests_completed == False: + #For some reason, we stopped early. So don't wait on the child threads to finish. Don't join, + #these threads may be stuck in their setup loops. Continue on. + pass + + print(container.get_container_summary()) print("All containers completed testing!") @@ -170,8 +181,7 @@ class ContainerManager: self.synchronization_object, full_docker_hub_name, container_name, - self.local_apps, - self.splunkbase_apps, + self.apps, web_port_tuple, management_port_tuple, self.container_password, @@ -219,16 +229,19 @@ class ContainerManager: password = "".join(password_list) return password - def queue_status_thread(self, status_interval:int=60)->None: + def queue_status_thread(self, status_interval:int=60, num_steps:int=10)->None: while True: - if self.synchronization_object.checkContainerFailure(): - print("One of the containers has shut down prematurely and generated an exception. Shut down the rest of the containers.") - for container in self.containers: - container.stopContainer() - print("All containers stopped") - - return None + #This for loop lets us run the summarize print less often, but check for failure more often + for chunk in range(0, status_interval, int(status_interval/num_steps)): + if self.synchronization_object.checkContainerFailure(): + print("One of the containers has shut down prematurely or the test was halted. Ensuring all containers are stopped.") + for container in self.containers: + container.stopContainer() + print("All containers stopped") + self.all_tests_completed = False + return None + time.sleep(status_interval/num_steps) at_least_one_container_has_started_running_tests = False for container in self.containers: @@ -237,9 +250,9 @@ class ContainerManager: break if self.synchronization_object.summarize(testing_currently_active = at_least_one_container_has_started_running_tests) == False: #There are no more tests to run, so we can return from this thread - + self.all_tests_completed = True return None - time.sleep(status_interval) + def setup_image(self, reuse_images: bool, container_name: str) -> None: client = docker.client.from_env() diff --git a/bin/automated_detection_testing/ci/detection_testing_batch/modules/splunk_container.py b/bin/automated_detection_testing/ci/detection_testing_batch/modules/splunk_container.py index fea6d69bad..1c2c3c7f28 100644 --- a/bin/automated_detection_testing/ci/detection_testing_batch/modules/splunk_container.py +++ b/bin/automated_detection_testing/ci/detection_testing_batch/modules/splunk_container.py @@ -23,15 +23,14 @@ SPLUNKBASE_URL = "https://splunkbase.splunk.com/app/%d/release/%s/download" SPLUNK_START_ARGS = "--accept-license" #Give ten minutes to start - this is probably enough time -MAX_CONTAINER_START_TIME_SECONDS = 60*10 +MAX_CONTAINER_START_TIME_SECONDS = 60*20 class SplunkContainer: def __init__( self, synchronization_object: test_driver.TestDriver, full_docker_hub_path, container_name: str, - local_apps: OrderedDict, - splunkbase_apps: OrderedDict, + apps: OrderedDict, web_port_tuple: tuple[str, int], management_port_tuple: tuple[str, int], container_password: str, @@ -49,15 +48,15 @@ class SplunkContainer: self.client = docker.client.from_env() self.full_docker_hub_path = full_docker_hub_path self.container_password = container_password - self.local_apps = local_apps - self.splunkbase_apps = splunkbase_apps + + self.apps = apps self.files_to_copy_to_container = files_to_copy_to_container self.splunk_ip = splunk_ip self.container_name = container_name self.mounts = mounts self.environment = self.make_environment( - local_apps, splunkbase_apps, container_password, splunkbase_username, splunkbase_password + apps, container_password, splunkbase_username, splunkbase_password ) self.ports = self.make_ports(web_port_tuple, management_port_tuple) self.web_port = web_port_tuple[1] @@ -72,50 +71,56 @@ class SplunkContainer: self.num_tests_completed = 0 + def prepare_apps_path( self, - local_apps: OrderedDict, - splunkbase_apps: OrderedDict, + apps: OrderedDict, splunkbase_username: Union[str, None] = None, splunkbase_password: Union[str, None] = None, ) -> tuple[str, bool]: apps_to_install = [] + + #We don't require credentials unless we install at least one splunkbase app require_credentials = False - for app_name, app_info in self.local_apps.items(): - - if 'local_path' in app_info: + #If the username and password are supplied, then we will use splunkbase... + #assuming that the app_name and app_number are supplied. Note that if a + #local_path is supplied, then it should override this option! + if splunkbase_username is not None and splunkbase_password is not None: + use_splunkbase = True + else: + use_splunkbase = False + + for app_name, app_info in self.apps.items(): + if use_splunkbase is True and 'local_path' not in app_info: + target = SPLUNKBASE_URL % (app_info["app_number"], app_info["app_version"]) + apps_to_install.append(target) + #We will require credentials since we are installing at least one splunkbase app + require_credentials = True + #Some paths may have a local_path and an HTTP path defined. Default to the local_path first, + #mostly because we may have copied it before into the cache to speed up start time. + elif 'local_path' in app_info: app_file_name = os.path.basename(app_info['local_path']) app_file_container_path = os.path.join("/tmp/apps", app_file_name) - apps_to_install.append(app_file_container_path) + apps_to_install.append(app_file_container_path) elif 'http_path' in app_info: apps_to_install.append(app_info['http_path']) + else: - print("Error, the app %s: %s has no http_path or local_path.\n\tQuitting..."%(app_name,app_info), file=sys.stderr) + if use_splunkbase is True: + print("Error, the app %s: %s could not be installed from Splunkbase because " + "--splunkbase_username and.or --splunkbase_password were not provided." + "\n\tQuitting..."%(app_name,app_info), file=sys.stderr) + else: + print("Error, the app %s: %s has no http_path or local_path.\n\tQuitting..."%(app_name,app_info), file=sys.stderr) sys.exit(1) - for app_name, app_info in self.splunkbase_apps.items(): - - if splunkbase_username is None or splunkbase_password is None: - raise Exception( - "Error: Requested app from Splunkbase but Splunkbase username and/or password were not supplied." - ) - target = SPLUNKBASE_URL % ( - app_info["app_number"], app_info["app_version"]) - apps_to_install.append(target) - require_credentials = True - # elif app["location"] == "local": - # apps_to_install.append(app["container_path"]) - #for printing out all the app paths we will install - #for num, name in zip(range(len(apps_to_install)),apps_to_install): - # print("%d: %s"%(num,name)) return ",".join(apps_to_install), require_credentials def make_environment( self, - local_apps: OrderedDict, - splunkbase_apps: OrderedDict, + apps: OrderedDict, container_password: str, splunkbase_username: Union[str, None] = None, splunkbase_password: Union[str, None] = None, @@ -124,7 +129,7 @@ class SplunkContainer: env["SPLUNK_START_ARGS"] = SPLUNK_START_ARGS env["SPLUNK_PASSWORD"] = container_password splunk_apps_url, require_credentials = self.prepare_apps_path( - local_apps, splunkbase_apps, splunkbase_username, splunkbase_password + apps, splunkbase_username, splunkbase_password ) if require_credentials: @@ -314,10 +319,36 @@ class SplunkContainer: time.sleep(seconds_between_attempts) - @wrapt_timeout_decorator.timeout(MAX_CONTAINER_START_TIME_SECONDS, timeout_exception=RuntimeError) + #@wrapt_timeout_decorator.timeout(MAX_CONTAINER_START_TIME_SECONDS, timeout_exception=RuntimeError) def setup_container(self): - + self.container.start() + + + # def shutdown_signal_handler(sig, frame): + # shutdown_client = docker.client.from_env() + # errorCount = 0 + + # print(f"Shutting down {self.container_name}...", file=sys.stderr) + # try: + # container = shutdown_client.containers.get(self.container_name) + # #Note that stopping does not remove any of the volumes or logs, + # #so stopping can be useful if we want to debug any container failure + # container.stop(timeout=10) + # print(f"{self.container_name} shut down successfully", file=sys.stderr) + # except Exception as e: + # print(f"Error trying to shut down {self.container_name}. It may have already shut down. Stop it youself with 'docker containter stop {self.container_name}", sys.stderr) + + + # #We must use os._exit(1) because sys.exit(1) actually generates an exception which can be caught! And then we don't Quit! + # import os + # os._exit(1) + + + + # import signal + # signal.signal(signal.SIGINT, shutdown_signal_handler) + # By default, first copy the index file then the datamodel file for file_description, file_dict in self.files_to_copy_to_container.items(): self.extract_tar_file_to_container( @@ -419,7 +450,7 @@ class SplunkContainer: % (detection_to_test, str(e)) ) - traceback.print_exc() + #traceback.print_exc() #import pdb #pdb.set_trace() # Fill in all the "Empty" fields with default values. Otherwise, we will not be able to diff --git a/bin/automated_detection_testing/ci/detection_testing_batch/modules/test_driver.py b/bin/automated_detection_testing/ci/detection_testing_batch/modules/test_driver.py index a55dc9edb2..3bbbbcb6e1 100644 --- a/bin/automated_detection_testing/ci/detection_testing_batch/modules/test_driver.py +++ b/bin/automated_detection_testing/ci/detection_testing_batch/modules/test_driver.py @@ -235,7 +235,7 @@ class TestDriver: if self.checkContainerFailure(): - print("One or more containers crashed, so testing did not complete successfully. We wrote out all the results that we could") + print("One or more containers crashed or the test was HALTED early, so testing did not complete successfully. We wrote out all the results that we could") return False else: return success @@ -269,16 +269,7 @@ class TestDriver: def summarize(self,testing_currently_active:bool=False)->bool: - - if self.checkContainerFailure() == True: - print("Error running containers... shutting down", file=sys.stderr) - return False - - - - self.lock.acquire() - try: #Get a summary of some system stats diff --git a/bin/automated_detection_testing/ci/detection_testing_batch/modules/validate_args.py b/bin/automated_detection_testing/ci/detection_testing_batch/modules/validate_args.py index a7398dd0d0..bb74a7ee92 100644 --- a/bin/automated_detection_testing/ci/detection_testing_batch/modules/validate_args.py +++ b/bin/automated_detection_testing/ci/detection_testing_batch/modules/validate_args.py @@ -8,7 +8,7 @@ from typing import Union # If we want, we can easily add a description field to any of the objects here! - +ES_APP_NAME = "SPLUNK_ES_CONTENT_UPDATE" setup_schema = { "type": "object", "properties": { @@ -50,7 +50,7 @@ setup_schema = { }, - "local_apps": { + "apps": { "type": "object", "additionalProperties": False, "patternProperties": { @@ -59,131 +59,101 @@ setup_schema = { "additionalProperties": False, "properties": { "app_number": { - "type": [ - "integer", - "null" - ] + "type": ["integer","null"] }, "app_version": { - "type": [ - "string", - "null" - ] + "type": ["string","null"] }, "local_path": { - "type": [ - "string", - "null" - ] + "type": ["string","null"] }, "http_path": { - "type": [ - "string" - ] + "type": ["string", "null"] } }, - "oneOf": [ + "anyOf": [ {"required": ["local_path"]}, - {"required": ["http_path"]} + {"required": ["http_path"] }, + {"required": ["app_number", "app_version"] }, ] } }, "default": { - "SPLUNK_ES_CONTENT_UPDATE": { + ES_APP_NAME : { "app_number": 3449, "app_version": None, "local_path": None }, #The default apps below were taken from the attack_range loadout: https://github.com/splunk/attack_range/blob/develop/attack_range.conf.template - "SPLUNK_WINDOWS_TA": { - "app_number": 0, - "app_version": None, - "http_path": "https://attack-range-appbinaries.s3-us-west-2.amazonaws.com/splunk-add-on-for-microsoft-windows_812.tgz" + "SPLUNK_ADD_ON_FOR_MICROSOFT_WINDOWS": { + "app_number": 742, + "app_version": "8.4.0", + "http_path": "https://attack-range-appbinaries.s3.us-west-2.amazonaws.com/Latest/splunk-add-on-for-microsoft-windows_840.tgz" }, - "SPLUNK_ADD_ON_FOR_SYSMON_OLD": { - "app_number": 1, - "app_version": None, - "http_path": "https://attack-range-appbinaries.s3-us-west-2.amazonaws.com/splunk-add-on-for-microsoft-sysmon_1062.tgz" + "ADD_ON_FOR_LINUX_SYSMON": { + "app_number": 6176, + "app_version": "1.0.4", + "http_path": "https://attack-range-appbinaries.s3.us-west-2.amazonaws.com/Latest/add-on-for-linux-sysmon_104.tgz" }, - "SPLUNK_SYSMON_LINUX_TA_PATCHED": { - "app_number": 2, - "app_version": None, - "http_path": "https://attack-range-appbinaries.s3-us-west-2.amazonaws.com/add-on-for-linux-sysmon_103_PATCHED.tgz" + "SPLUNK_ADD_ON_FOR_SYSMON": { + "app_number": 5709, + "app_version": "2.0.0", + "http_path": "https://attack-range-appbinaries.s3.us-west-2.amazonaws.com/Latest/splunk-add-on-for-sysmon_200.tgz" }, - "SPLUNK_CIM_APP": { - "app_number": 3, - "app_version": None, - "http_path": "https://attack-range-appbinaries.s3-us-west-2.amazonaws.com/splunk-common-information-model-cim_4200.tgz" + "SPLUNK_COMMON_INFORMATION_MODEL": { + "app_number": 1621, + "app_version": "5.0.0", + "http_path": "https://attack-range-appbinaries.s3.us-west-2.amazonaws.com/Latest/splunk-common-information-model-cim_500.tgz" }, - "SPLUNK_AWS_TA": { - "app_number": 4, - "app_version": None, - "http_path": "https://attack-range-appbinaries.s3-us-west-2.amazonaws.com/splunk-add-on-for-amazon-web-services_510.tgz" + "PYTHON_FOR_SCIENTIFIC_COMPUTING_FOR_LINUX_64_BIT": { + "app_number": 2882, + "app_version": "3.0.2", + "http_path": "https://attack-range-appbinaries.s3.us-west-2.amazonaws.com/Latest/python-for-scientific-computing-for-linux-64-bit_302.tgz" }, - "SPLUNK_PYTHON_APP": { - "app_number": 5, - "app_version": None, - "http_path": "https://attack-range-appbinaries.s3-us-west-2.amazonaws.com/python-for-scientific-computing-for-linux-64-bit_202.tgz" + "SPLUNK_MACHINE_LEARNING_TOOLKIT": { + "app_number": 2890, + "app_version": "5.3.1", + "http_path": "https://attack-range-appbinaries.s3.us-west-2.amazonaws.com/Latest/splunk-machine-learning-toolkit_531.tgz" }, - "SPLUNK_ASX_APP": { - "app_number": 17, - "app_version": None, - "http_path": "https://attack-range-appbinaries.s3-us-west-2.amazonaws.com/Splunk_ASX-latest.tar.gz" - }, - "SPLUNK_MLTK_APP": { - "app_number": 6, - "app_version": None, - "http_path": "https://attack-range-appbinaries.s3-us-west-2.amazonaws.com/splunk-machine-learning-toolkit_521.tgz" - }, - "SPLUNK_STREAM_APP": { - "app_number": 7, - "app_version": None, - "http_path": "https://attack-range-appbinaries.s3-us-west-2.amazonaws.com/splunk-app-for-stream_730.tgz" + "SPLUNK_APP_FOR_STREAM": { + "app_number": 1809, + "app_version": "8.0.1", + "http_path": "https://attack-range-appbinaries.s3.us-west-2.amazonaws.com/Latest/splunk-app-for-stream_801.tgz" }, "SPLUNK_ADD_ON_FOR_STREAM_WIRE_DATA": { - "app_number": 8, - "app_version": None, - "http_path": "https://attack-range-appbinaries.s3-us-west-2.amazonaws.com/splunk-add-on-for-stream-wire-data_730.tgz" + "app_number": 5234, + "app_version": "8.0.1", + "http_path": "https://attack-range-appbinaries.s3.us-west-2.amazonaws.com/Latest/splunk-add-on-for-stream-wire-data_801.tgz" }, "SPLUNK_ADD_ON_FOR_STREAM_FORWARDERS": { - "app_number": 9, - "app_version": None, - "http_path": "https://attack-range-appbinaries.s3-us-west-2.amazonaws.com/splunk-add-on-for-stream-forwarders_730.tgz" - }, - "SPLUNK_SECURITY_ESSENTIALS": { - "app_number": 10, - "app_version": None, - "http_path": "https://attack-range-appbinaries.s3-us-west-2.amazonaws.com/splunk-security-essentials_333.tgz" - }, - "SPLUNK_ADD_ON_FOR_ZEEK_AKA_BRO": { - "app_number": 11, - "app_version": None, - "http_path": "https://attack-range-appbinaries.s3-us-west-2.amazonaws.com/splunk-add-on-for-zeek-aka-bro_400.tgz" + "app_number": 5238, + "app_version": "8.0.1", + "http_path": "https://attack-range-appbinaries.s3.us-west-2.amazonaws.com/Latest/splunk-add-on-for-stream-forwarders_801.tgz" }, "SPLUNK_ADD_ON_FOR_AMAZON_KINESIS_FIREHOSE": { - "app_number": 12, - "app_version": None, - "http_path": "https://attack-range-appbinaries.s3-us-west-2.amazonaws.com/splunk-add-on-for-amazon-kinesis-firehose_131r7d1d093.tgz" + "app_number": 3719, + "app_version": "1.3.2", + "http_path": "https://attack-range-appbinaries.s3.us-west-2.amazonaws.com/Latest/splunk-add-on-for-amazon-kinesis-firehose_132.tgz" }, "SPLUNK_ADD_ON_FOR_MICROSOFT_OFFICE_365": { - "app_number": 13, - "app_version": None, - "http_path": "https://attack-range-appbinaries.s3-us-west-2.amazonaws.com/splunk-add-on-for-microsoft-office-365_202.tgz" + "app_number": 4055, + "app_version": "2.2.0", + "http_path": "https://attack-range-appbinaries.s3.us-west-2.amazonaws.com/Latest/splunk-add-on-for-microsoft-office-365_220.tgz" }, - "SPLUNK_LINUX_TA": { - "app_number": 14, - "app_version": None, - "http_path": "https://attack-range-appbinaries.s3-us-west-2.amazonaws.com/splunk-add-on-for-unix-and-linux_820.tgz" + "SPLUNK_ADD_ON_FOR_UNIX_AND_LINUX": { + "app_number": 833, + "app_version": "8.4.0", + "http_path": "https://attack-range-appbinaries.s3.us-west-2.amazonaws.com/Latest/splunk-add-on-for-unix-and-linux_840.tgz" }, - "SPLUNK_NGINX_TA": { - "app_number": 15, - "app_version": None, - "http_path": "https://attack-range-appbinaries.s3-us-west-2.amazonaws.com/splunk-add-on-for-nginx_310.tgz" + "SPLUNK_ADD_ON_FOR_NGINX": { + "app_number": 3258, + "app_version": "3.1.0", + "http_path": "https://attack-range-appbinaries.s3.us-west-2.amazonaws.com/Latest/splunk-add-on-for-nginx_310.tgz" }, "SPLUNK_TA_FOR_ZEEK": { - "app_number": 16, - "app_version": None, - "http_path": "https://attack-range-appbinaries.s3-us-west-2.amazonaws.com/ta-for-zeek_105.tgz" + "app_number": 5466, + "app_version": "1.0.5", + "http_path": "https://attack-range-appbinaries.s3.us-west-2.amazonaws.com/Latest/ta-for-zeek_105.tgz" }, } @@ -226,100 +196,6 @@ setup_schema = { }, - - - "splunkbase_apps": { - "type": "object", - "patternProperties": { - "^.*$": { - "type": "object", - "additionalProperties": False, - "properties": { - "app_number": { - "type": "integer" - }, - "app_version": { - "type": "string" - } - } - } - }, - - "default": { - "SPLUNK_ADD_ON_FOR_SYSMON": { - "app_number": 5709, - "app_version": "1.0.1" - } - }, - - # "default": { - # "ADD-ON_FOR_LINUX_SYSMON": { - # "app_number": 6176, - # "app_version": "1.0.3" - # }, - # "SPLUNK_ADD_ON_FOR_AMAZON_WEB_SERVICES": { - # "app_number": 1876, - # "app_version": "5.2.1" - # }, - - # "SPLUNK_ADD_ON_FOR_MICROSOFT_OFFICE_365": - # { - # "app_number": 4055, - # "app_version": "2.2.0" - # }, - - # "SPLUNK_ADD_ON_FOR_AMAZON_KINESIS_FIREHOSE": { - # "app_number": 3719, - # "app_version": "1.3.2" - # }, - - # "SPLUNK_ANALYTIC_STORY_EXECUTION_APP": { - # "app_number": 4971, - # "app_version": "2.0.3" - # }, - - # "PYTHON_FOR_SCIENTIC_COMPUTING_LINUX_64_BIT": { - # "app_number": 2882, - # "app_version": "3.0.1" - # }, - - # "SPLUNK_MACHINE_LEARNING_TOOLKIT": { - # "app_number": 2890, - # "app_version": "5.3.0" - # }, - - # "SPLUNK_APP_FOR_STREAM": { - # "app_number": 1809, - # "app_version": "8.0.1" - # }, - # "SPLUNK_ADD_ON_FOR_STREAM_WIRE_DATA": { - # "app_number": 5234, - # "app_version": "8.0.1" - # }, - # "SPLUNK_ADD_ON_FOR_STREAM_FORWARDERS": { - # "app_number": 5238, - # "app_version": "8.0.1" - # }, - # "SPLUNK_ADD_ON_FOR_ZEEK_AKA_BRO": { - # "app_number": 1617, - # "app_version": "4.0.0" - # }, - # "SPLUNK_ADD_ON_FOR_UNIX_AND_LINUX": { - # "app_number": 833, - # "app_version": "8.4.0" - # }, - # "SPLUNK_ADD_ON_FOR_SYSMON": { - # "app_number": 5709, - # "app_version": "1.0.1" - # }, - # # According to https://docs.splunk.com/Documentation/ES/6.6.2/Install/Datamodels, these are included in ES. Don't install separately. - # "SPLUNK_COMMON_INFORMATION_MODEL": { - # "app_number": 1621, - # "app_version": "5.0.0" - # } - # } - }, - "splunkbase_username": { "type": ["string", "null"], "default": None diff --git a/bin/automated_detection_testing/ci/detection_testing_batch/test_config_github_actions.json b/bin/automated_detection_testing/ci/detection_testing_batch/test_config_github_actions.json index 70ba5473a2..647e4f808a 100644 --- a/bin/automated_detection_testing/ci/detection_testing_batch/test_config_github_actions.json +++ b/bin/automated_detection_testing/ci/detection_testing_batch/test_config_github_actions.json @@ -1,113 +1,93 @@ { + "apps": { + "ADD_ON_FOR_LINUX_SYSMON": { + "app_number": 6176, + "app_version": "1.0.4", + "http_path": "https://attack-range-appbinaries.s3.us-west-2.amazonaws.com/Latest/add-on-for-linux-sysmon_104.tgz" + }, + "PYTHON_FOR_SCIENTIFIC_COMPUTING_FOR_LINUX_64_BIT": { + "app_number": 2882, + "app_version": "3.0.2", + "http_path": "https://attack-range-appbinaries.s3.us-west-2.amazonaws.com/Latest/python-for-scientific-computing-for-linux-64-bit_302.tgz" + }, + "SPLUNK_ADD_ON_FOR_AMAZON_KINESIS_FIREHOSE": { + "app_number": 3719, + "app_version": "1.3.2", + "http_path": "https://attack-range-appbinaries.s3.us-west-2.amazonaws.com/Latest/splunk-add-on-for-amazon-kinesis-firehose_132.tgz" + }, + "SPLUNK_ADD_ON_FOR_MICROSOFT_OFFICE_365": { + "app_number": 4055, + "app_version": "2.2.0", + "http_path": "https://attack-range-appbinaries.s3.us-west-2.amazonaws.com/Latest/splunk-add-on-for-microsoft-office-365_220.tgz" + }, + "SPLUNK_ADD_ON_FOR_MICROSOFT_WINDOWS": { + "app_number": 742, + "app_version": "8.4.0", + "http_path": "https://attack-range-appbinaries.s3.us-west-2.amazonaws.com/Latest/splunk-add-on-for-microsoft-windows_840.tgz" + }, + "SPLUNK_ADD_ON_FOR_NGINX": { + "app_number": 3258, + "app_version": "3.1.0", + "http_path": "https://attack-range-appbinaries.s3.us-west-2.amazonaws.com/Latest/splunk-add-on-for-nginx_310.tgz" + }, + "SPLUNK_ADD_ON_FOR_STREAM_FORWARDERS": { + "app_number": 5238, + "app_version": "8.0.1", + "http_path": "https://attack-range-appbinaries.s3.us-west-2.amazonaws.com/Latest/splunk-add-on-for-stream-forwarders_801.tgz" + }, + "SPLUNK_ADD_ON_FOR_STREAM_WIRE_DATA": { + "app_number": 5234, + "app_version": "8.0.1", + "http_path": "https://attack-range-appbinaries.s3.us-west-2.amazonaws.com/Latest/splunk-add-on-for-stream-wire-data_801.tgz" + }, + "SPLUNK_ADD_ON_FOR_SYSMON": { + "app_number": 5709, + "app_version": "2.0.0", + "http_path": "https://attack-range-appbinaries.s3.us-west-2.amazonaws.com/Latest/splunk-add-on-for-sysmon_200.tgz" + }, + "SPLUNK_ADD_ON_FOR_UNIX_AND_LINUX": { + "app_number": 833, + "app_version": "8.4.0", + "http_path": "https://attack-range-appbinaries.s3.us-west-2.amazonaws.com/Latest/splunk-add-on-for-unix-and-linux_840.tgz" + }, + "SPLUNK_APP_FOR_STREAM": { + "app_number": 1809, + "app_version": "8.0.1", + "http_path": "https://attack-range-appbinaries.s3.us-west-2.amazonaws.com/Latest/splunk-app-for-stream_801.tgz" + }, + "SPLUNK_COMMON_INFORMATION_MODEL": { + "app_number": 1621, + "app_version": "5.0.0", + "http_path": "https://attack-range-appbinaries.s3.us-west-2.amazonaws.com/Latest/splunk-common-information-model-cim_500.tgz" + }, + "SPLUNK_ES_CONTENT_UPDATE": { + "app_number": 3449, + "app_version": null, + "local_path": null + }, + "SPLUNK_MACHINE_LEARNING_TOOLKIT": { + "app_number": 2890, + "app_version": "5.3.1", + "http_path": "https://attack-range-appbinaries.s3.us-west-2.amazonaws.com/Latest/splunk-machine-learning-toolkit_531.tgz" + }, + "SPLUNK_TA_FOR_ZEEK": { + "app_number": 5466, + "app_version": "1.0.5", + "http_path": "https://attack-range-appbinaries.s3.us-west-2.amazonaws.com/Latest/ta-for-zeek_105.tgz" + } + }, "branch": "BRANCH_DOES_NOT_EXIST_USE_CLI_ARGUMENT", "commit_hash": null, "container_tag": "latest", "detections_file": null, "detections_list": null, "folders": [ - "endpoint", - "cloud", - "network", - "web" + "endpoint", + "cloud", + "network", + "web" ], "interactive": false, - "local_apps": { - "SPLUNK_ADD_ON_FOR_AMAZON_KINESIS_FIREHOSE": { - "app_number": 12, - "app_version": null, - "http_path": "https://attack-range-appbinaries.s3-us-west-2.amazonaws.com/splunk-add-on-for-amazon-kinesis-firehose_131r7d1d093.tgz" - }, - "SPLUNK_ADD_ON_FOR_MICROSOFT_OFFICE_365": { - "app_number": 13, - "app_version": null, - "http_path": "https://attack-range-appbinaries.s3-us-west-2.amazonaws.com/splunk-add-on-for-microsoft-office-365_202.tgz" - }, - "SPLUNK_ADD_ON_FOR_STREAM_FORWARDERS": { - "app_number": 9, - "app_version": null, - "http_path": "https://attack-range-appbinaries.s3-us-west-2.amazonaws.com/splunk-add-on-for-stream-forwarders_730.tgz" - }, - "SPLUNK_ADD_ON_FOR_STREAM_WIRE_DATA": { - "app_number": 8, - "app_version": null, - "http_path": "https://attack-range-appbinaries.s3-us-west-2.amazonaws.com/splunk-add-on-for-stream-wire-data_730.tgz" - }, - "SPLUNK_ADD_ON_FOR_SYSMON_OLD": { - "app_number": 1, - "app_version": null, - "http_path": "https://attack-range-appbinaries.s3-us-west-2.amazonaws.com/splunk-add-on-for-microsoft-sysmon_1062.tgz" - }, - "SPLUNK_ADD_ON_FOR_ZEEK_AKA_BRO": { - "app_number": 11, - "app_version": null, - "http_path": "https://attack-range-appbinaries.s3-us-west-2.amazonaws.com/splunk-add-on-for-zeek-aka-bro_400.tgz" - }, - "SPLUNK_ASX_APP": { - "app_number": 17, - "app_version": null, - "http_path": "https://attack-range-appbinaries.s3-us-west-2.amazonaws.com/Splunk_ASX-latest.tar.gz" - }, - "SPLUNK_AWS_TA": { - "app_number": 4, - "app_version": null, - "http_path": "https://attack-range-appbinaries.s3-us-west-2.amazonaws.com/splunk-add-on-for-amazon-web-services_510.tgz" - }, - "SPLUNK_CIM_APP": { - "app_number": 3, - "app_version": null, - "http_path": "https://attack-range-appbinaries.s3-us-west-2.amazonaws.com/splunk-common-information-model-cim_4200.tgz" - }, - "SPLUNK_ES_CONTENT_UPDATE": { - "app_number": 3449, - "app_version": null, - "local_path": null - }, - "SPLUNK_LINUX_TA": { - "app_number": 14, - "app_version": null, - "http_path": "https://attack-range-appbinaries.s3-us-west-2.amazonaws.com/splunk-add-on-for-unix-and-linux_820.tgz" - }, - "SPLUNK_MLTK_APP": { - "app_number": 6, - "app_version": null, - "http_path": "https://attack-range-appbinaries.s3.us-west-2.amazonaws.com/splunk-machine-learning-toolkit_531.tgz" - }, - "SPLUNK_NGINX_TA": { - "app_number": 15, - "app_version": null, - "http_path": "https://attack-range-appbinaries.s3-us-west-2.amazonaws.com/splunk-add-on-for-nginx_310.tgz" - }, - "SPLUNK_PYTHON_APP": { - "app_number": 5, - "app_version": null, - "http_path": "https://attack-range-appbinaries.s3.us-west-2.amazonaws.com/python-for-scientific-computing-for-linux-64-bit_302.tgz" - }, - "SPLUNK_SECURITY_ESSENTIALS": { - "app_number": 10, - "app_version": null, - "http_path": "https://attack-range-appbinaries.s3-us-west-2.amazonaws.com/splunk-security-essentials_333.tgz" - }, - "SPLUNK_STREAM_APP": { - "app_number": 7, - "app_version": null, - "http_path": "https://attack-range-appbinaries.s3-us-west-2.amazonaws.com/splunk-app-for-stream_730.tgz" - }, - "SPLUNK_SYSMON_LINUX_TA_PATCHED": { - "app_number": 2, - "app_version": null, - "http_path": "https://attack-range-appbinaries.s3-us-west-2.amazonaws.com/add-on-for-linux-sysmon_103_PATCHED.tgz" - }, - "SPLUNK_TA_FOR_ZEEK": { - "app_number": 16, - "app_version": null, - "http_path": "https://attack-range-appbinaries.s3-us-west-2.amazonaws.com/ta-for-zeek_105.tgz" - }, - "SPLUNK_WINDOWS_TA": { - "app_number": 0, - "app_version": null, - "http_path": "https://attack-range-appbinaries.s3-us-west-2.amazonaws.com/splunk-add-on-for-microsoft-windows_812.tgz" - } - }, "local_base_container_name": "splunk_test_%d", "mock": false, "mode": "changes", @@ -119,17 +99,11 @@ "show_splunk_app_password": false, "splunk_app_password": null, "splunk_container_apps_directory": "/opt/splunk/etc/apps", - "splunkbase_apps": { - "SPLUNK_ADD_ON_FOR_SYSMON": { - "app_number": 5709, - "app_version": "1.0.1" - } - }, "splunkbase_password": null, "splunkbase_username": null, "types": [ - "Anomaly", - "Hunting", - "TTP" + "Anomaly", + "Hunting", + "TTP" ] -} +} \ No newline at end of file diff --git a/bin/doc_gen.py b/bin/doc_gen.py index 83c71a4cdb..282436d5b5 100644 --- a/bin/doc_gen.py +++ b/bin/doc_gen.py @@ -1,3 +1,5 @@ +from threading import Thread +from functools import cache import glob import yaml import argparse @@ -12,6 +14,7 @@ from stix2 import Filter from pycvesearch import CVESearch CVESSEARCH_API_URL = 'https://cve.circl.lu' +CVESSEARCH_API_TIMEOUT = 10 def load_objects(REPO_PATH, TYPE): @@ -94,6 +97,16 @@ def parse_and_add_lookups(search_string, lookups): return lookup_objects +#This function hits an API, which can be slow, especially if the API +#or network connection is slow. This has sometimes caused issues +#in our CI/CD taking a very long time. Here, we memoize/cache +#the calls to this function - there will be many duplicates which +#should all return the same results. This will gives us a significant +#speedup. +#Hackish way of keeping the original functionality and allowing it +#to easily return a value when in a thread... making an optional +#default argument with a type that is mutable! +@cache def get_cve_enrichment_new(cve_id): cve = CVESearch(CVESSEARCH_API_URL) result = cve.id(cve_id) @@ -103,6 +116,15 @@ def get_cve_enrichment_new(cve_id): cve_enriched['summary'] = result['summary'] return cve_enriched +#helper function to easily return a value from a thread that is +#running a memoized/@cached function +def get_cve_enrichment_new_wrapper(cve_id,mutable_list): + mutable_list.append(get_cve_enrichment_new(cve_id)) + + + + + def get_all_techniques(projects_path): path_cti = path.join(projects_path,'cti/enterprise-attack') fs = FileSystemSource(path_cti) @@ -327,8 +349,17 @@ def generate_doc_detections(REPO_PATH, OUTPUT_DIR, TEMPLATE_PATH, attack, messag cves = [] if 'cve' in detection_yaml['tags']: for cve_id in detection_yaml['tags']['cve']: - cve = get_cve_enrichment_new(cve_id) - cves.append(cve) + mutable_list = [] + try: + cve_thread = Thread(target=get_cve_enrichment_new_wrapper, args=(cve_id, mutable_list)) + cve_thread.start() + cve_thread.join(timeout=CVESSEARCH_API_TIMEOUT) + if cve_thread.is_alive(): + raise(Exception(f"Timed out getting CVE Enrichment from {CVESSEARCH_API_URL} after {CVESSEARCH_API_TIMEOUT} seconds.")) + cves.append(mutable_list[0]) + except Exception as e: + print(f"Error - {str(e)}\nQuitting...",file=sys.stderr) + sys.exit(1) detection_yaml['cve'] = cves # enrich with macros diff --git a/detections/cloud/o365_excessive_authentication_failures_alert.yml b/detections/cloud/o365_excessive_authentication_failures_alert.yml index 73c75b0e8c..65dece8106 100644 --- a/detections/cloud/o365_excessive_authentication_failures_alert.yml +++ b/detections/cloud/o365_excessive_authentication_failures_alert.yml @@ -1,17 +1,19 @@ name: O365 Excessive Authentication Failures Alert id: d441364c-349c-453b-b55f-12eccab67cf9 -version: 1 -date: '2020-12-16' +version: 2 +date: '2022-02-18' author: Rod Soto, Splunk type: Anomaly datamodel: [] description: This search detects when an excessive number of authentication failures occur this search also includes attempts against MFA prompt codes -search: '`o365_management_activity` Workload=AzureActiveDirectory UserAuthenticationMethod=* - status=Failed | stats count earliest(_time) as firstTime latest(_time) values(UserAuthenticationMethod) - AS UserAuthenticationMethod values(UserAgent) AS UserAgent values(status) AS status - values(src_ip) AS src_ip by user | where count > 10 |`security_content_ctime(firstTime)` - |`security_content_ctime(lastTime)` | `o365_excessive_authentication_failures_alert_filter`' +search: '`o365_management_activity` Workload=AzureActiveDirectory UserAuthenticationMethod=* status=failure + | stats count earliest(_time) as firstTime latest(_time) values(UserAuthenticationMethod) AS UserAuthenticationMethod + values(UserAgent) AS UserAgent values(status) AS status values(src_ip) AS src_ip by user + | where count > 10 + | `security_content_ctime(firstTime)` + | `security_content_ctime(lastTime)` + | `o365_excessive_authentication_failures_alert_filter`' how_to_implement: You must install splunk Microsoft Office 365 add-on. This search works with o365:management:activity known_false_positives: The threshold for alert is above 10 attempts and this should diff --git a/detections/endpoint/detect_regasm_with_network_connection.yml b/detections/endpoint/detect_regasm_with_network_connection.yml index bbcab20120..dc693a7c50 100644 --- a/detections/endpoint/detect_regasm_with_network_connection.yml +++ b/detections/endpoint/detect_regasm_with_network_connection.yml @@ -1,7 +1,7 @@ name: Detect Regasm with Network Connection id: 07921114-6db4-4e2e-ae58-3ea8a52ae93f -version: 1 -date: '2021-02-16' +version: 2 +date: '2022-02-18' author: Michael Haag, Splunk type: TTP datamodel: [] @@ -15,10 +15,12 @@ description: The following analytic identifies regasm.exe with a network connect and review accordingly. Review the reputation of the remote IP or domain and block as needed. regsvcs.exe and regasm.exe are natively found in C:\Windows\Microsoft.NET\Framework\v*\regasm|regsvcs.exe and C:\Windows\Microsoft.NET\Framework64\v*\regasm|regsvcs.exe. -search: '`sysmon` EventID=3 dest_ip!=10.0.0.0/12 dest_ip!=172.16.0.0/12 dest_ip!=192.168.0.0/16 - process_name=regasm.exe | rename Computer as dest | stats count min(_time) as firstTime - max(_time) as lastTime by dest, User, process_name, src_ip, dest_host, dest_ip | - `security_content_ctime(firstTime)` | `security_content_ctime(lastTime)` | `detect_regasm_with_network_connection_filter`' +search: '`sysmon` EventID=3 dest_ip!=10.0.0.0/12 dest_ip!=172.16.0.0/12 dest_ip!=192.168.0.0/16 process_name=regasm.exe + | rename Computer as dest + | stats count min(_time) as firstTime max(_time) as lastTime by dest, user, process_name, src_ip, dest_ip + | `security_content_ctime(firstTime)` + | `security_content_ctime(lastTime)` + | `detect_regasm_with_network_connection_filter`' how_to_implement: To successfully implement this search, you need to be ingesting logs with the process name, parent process, and command-line executions from your endpoints. If you are using Sysmon, you must have at least version 6.0.4 of the @@ -59,7 +61,7 @@ tags: type: User role: - Victim - - name: Computer + - name: dest type: Hostname role: - Victim @@ -77,7 +79,7 @@ tags: - dest_ip - process_name - Computer - - User + - user - src_ip - dest_host - dest_ip diff --git a/detections/endpoint/detect_regsvcs_with_network_connection.yml b/detections/endpoint/detect_regsvcs_with_network_connection.yml index 7329369714..dd41e27b88 100644 --- a/detections/endpoint/detect_regsvcs_with_network_connection.yml +++ b/detections/endpoint/detect_regsvcs_with_network_connection.yml @@ -1,7 +1,7 @@ name: Detect Regsvcs with Network Connection id: e3e7a1c0-f2b9-445c-8493-f30a63522d1a -version: 1 -date: '2021-02-16' +version: 2 +date: '2022-02-18' author: Michael Haag, Splunk type: TTP datamodel: [] @@ -15,10 +15,12 @@ description: The following analytic identifies Regsvcs.exe with a network connec and review accordingly. Review the reputation of the remote IP or domain and block as needed. regsvcs.exe and regasm.exe are natively found in C:\Windows\Microsoft.NET\Framework\v*\regasm|regsvcs.exe and C:\Windows\Microsoft.NET\Framework64\v*\regasm|regsvcs.exe. -search: '`sysmon` EventID=3 dest_ip!=10.0.0.0/12 dest_ip!=172.16.0.0/12 dest_ip!=192.168.0.0/16 - process_name=regsvcs.exe | rename Computer as dest | stats count min(_time) as firstTime - max(_time) as lastTime by dest, User, process_name, src_ip, dest_host, dest_ip | - `security_content_ctime(firstTime)` | `security_content_ctime(lastTime)` | `detect_regsvcs_with_network_connection_filter`' +search: '`sysmon` EventID=3 dest_ip!=10.0.0.0/12 dest_ip!=172.16.0.0/12 dest_ip!=192.168.0.0/16 process_name=regsvcs.exe + | rename Computer as dest + | stats count min(_time) as firstTime max(_time) as lastTime by dest, user, process_name, src_ip, dest_ip + | `security_content_ctime(firstTime)` + | `security_content_ctime(lastTime)` + | `detect_regsvcs_with_network_connection_filter`' how_to_implement: To successfully implement this search, you need to be ingesting logs with the process name, parent process, and command-line executions from your endpoints. If you are using Sysmon, you must have at least version 6.0.4 of the @@ -59,7 +61,7 @@ tags: type: User role: - Victim - - name: Computer + - name: dest type: Hostname role: - Victim @@ -77,7 +79,7 @@ tags: - dest_ip - process_name - Computer - - User + - user - src_ip - dest_host risk_score: 80 diff --git a/detections/endpoint/interactive_session_on_remote_endpoint_with_powershell.yml b/detections/endpoint/interactive_session_on_remote_endpoint_with_powershell.yml index eb2c580a34..40bdb14ab8 100644 --- a/detections/endpoint/interactive_session_on_remote_endpoint_with_powershell.yml +++ b/detections/endpoint/interactive_session_on_remote_endpoint_with_powershell.yml @@ -1,7 +1,7 @@ name: Interactive Session on Remote Endpoint with PowerShell id: a4e8f3a4-48b2-11ec-bcfc-3e22fbd008af -version: 1 -date: '2021-11-18' +version: 2 +date: '2022-02-18' author: Mauricio Velazco, Splunk type: TTP datamodel: [] @@ -10,9 +10,10 @@ description: The following analytic utilizes PowerShell Script Block Logging (Ev an interactive session on a remote endpoint leveraging the WinRM protocol. Red Teams and adversaries alike may abuse WinRM and `Enter-PSSession` for lateral movement and remote code execution. -search: powershell` EventCode=4104 (Message="*Enter-PSSession*" AND Message="*-ComputerName*") - | stats count min(_time) as firstTime max(_time) as lastTime by EventCode Message - ComputerName User | `security_content_ctime(firstTime)` | `interactive_session_on_remote_endpoint_with_powershell_filter` +search: '`powershell` EventCode=4104 (Message="*Enter-PSSession*" AND Message="*-ComputerName*") + | stats count min(_time) as firstTime max(_time) as lastTime by EventCode Message ComputerName User + | `security_content_ctime(firstTime)` + | `interactive_session_on_remote_endpoint_with_powershell_filter`' how_to_implement: To successfully implement this analytic, you will need to enable PowerShell Script Block Logging on some or all endpoints. Additional setup instructions can be found https://docs.splunk.com/Documentation/UBA/5.0.4.1/GetDataIn/AddPowerShell#Configure_module_logging_for_PowerShell. diff --git a/detections/endpoint/linux_at_application_execution.yml b/detections/endpoint/linux_at_application_execution.yml index 07a98f45d8..075783c9f9 100644 --- a/detections/endpoint/linux_at_application_execution.yml +++ b/detections/endpoint/linux_at_application_execution.yml @@ -13,7 +13,7 @@ description: The following analytic identifies a suspicious process creation of and after this process execution, when it was executed and what schedule task it will execute. search: '| tstats `security_content_summariesonly` count from datamodel=Endpoint.Processes - where Processes.process_name = at OR Processes.parent_process_name = at by Processes.dest + where Processes.process_name IN ("at", "atd") OR Processes.parent_process_name IN ("at", "atd") by Processes.dest Processes.user Processes.parent_process_name Processes.process_name Processes.process Processes.process_id Processes.parent_process_id Processes.process_guid | `drop_dm_object_name(Processes)` | `security_content_ctime(firstTime)` | `security_content_ctime(lastTime)` | `linux_at_application_execution_filter`' diff --git a/detections/endpoint/linux_common_process_for_elevation_control.yml b/detections/endpoint/linux_common_process_for_elevation_control.yml index 28eacca381..e0f2c65939 100644 --- a/detections/endpoint/linux_common_process_for_elevation_control.yml +++ b/detections/endpoint/linux_common_process_for_elevation_control.yml @@ -16,10 +16,10 @@ search: '| tstats `security_content_summariesonly` count min(_time) as firstTime as lastTime from datamodel=Endpoint.Processes where Processes.process_name IN ("chmod", "chown", "fchmod", "fchmodat", "fchown", "fchownat", "fremovexattr", "fsetxattr", "lchown", "lremovexattr", "lsetxattr", "removexattr", "setuid", "setgid", "setreuid", - "setregid") OR Processes.process IN ("*chmod *", "*chown *", "*fchmod *", "*fchmodat + "setregid", "chattr") OR Processes.process IN ("*chmod *", "*chown *", "*fchmod *", "*fchmodat *", "*fchown *", "*fchownat *", "*fremovexattr *", "*fsetxattr *", "*lchown *", "*lremovexattr *", "*lsetxattr *", "*removexattr *", "*setuid *", "*setgid *", "*setreuid - *", "*setregid *", "*setcap *") by Processes.dest Processes.user Processes.parent_process_name + *", "*setregid *", "*setcap *", "*chattr *") by Processes.dest Processes.user Processes.parent_process_name Processes.process_name Processes.process Processes.process_id Processes.parent_process_id Processes.process_guid | `drop_dm_object_name(Processes)` | `security_content_ctime(firstTime)` | `security_content_ctime(lastTime)` | `linux_common_process_for_elevation_control_filter`' diff --git a/detections/endpoint/linux_dd_file_overwrite.yml b/detections/endpoint/linux_dd_file_overwrite.yml new file mode 100644 index 0000000000..5ec2767a4b --- /dev/null +++ b/detections/endpoint/linux_dd_file_overwrite.yml @@ -0,0 +1,70 @@ +name: Linux DD File Overwrite +id: 9b6aae5e-8d85-11ec-b2ae-acde48001122 +version: 1 +date: '2022-02-14' +author: Teoderick Contreras, Splunk +type: TTP +datamodel: +- Endpoint +description: This analytic is to look for dd command to overwrite file. This technique was abused by adversaries or + threat actor to destroy files or data on specific system or in a large number of host within network to interrupt host avilability, + services and many more. This is also used to destroy data where it make the file irrecoverable by forensic techniques through overwriting files, + data or local and remote drives. +search: '| tstats `security_content_summariesonly` count min(_time) as firstTime max(_time) as lastTime from datamodel=Endpoint.Processes + where Processes.process_name = "dd" AND Processes.process = "*of=*" + by Processes.dest Processes.user Processes.parent_process_name Processes.process_name Processes.process Processes.process_id Processes.parent_process_id Processes.process_guid + | `drop_dm_object_name(Processes)` + | `security_content_ctime(firstTime)` + | `security_content_ctime(lastTime)` + | `linux_dd_file_overwrite_filter`' +how_to_implement: To successfully implement this search, you need to be ingesting + logs with the process name, parent process, and command-line executions from your + endpoints. If you are using Sysmon, you can use the Add-on for Linux Sysmon from + Splunkbase. +known_false_positives: Administrator or network operator can execute this command. + Please update the filter macros to remove false positives. +references: +- https://gtfobins.github.io/gtfobins/dd/ +- https://github.com/redcanaryco/atomic-red-team/blob/master/atomics/T1485/T1485.md +tags: + analytic_story: + - Data Destruction + dataset: + - https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1485/linux_dd_file_overwrite/sysmon_linux.log + kill_chain_phases: + - Exploitation + mitre_attack_id: + - T1485 + product: + - Splunk Enterprise + - Splunk Enterprise Security + - Splunk Cloud + required_fields: + - _time + - Processes.dest + - Processes.user + - Processes.parent_process_name + - Processes.process_name + - Processes.process + - Processes.process_id + - Processes.parent_process_id + security_domain: endpoint + impact: 80 + confidence: 80 + # (impact * confidence)/100 + risk_score: 64 + context: + - Source:Endpoint + - Stage:Impact + message: A commandline $process$ executed on $dest$ + observable: + - name: dest + type: Hostname + role: + - Victim + nist: + - DE.CM + cis20: + - CIS 3 + - CIS 5 + - CIS 16 \ No newline at end of file diff --git a/detections/endpoint/linux_system_network_discovery.yml b/detections/endpoint/linux_system_network_discovery.yml new file mode 100644 index 0000000000..93f8d5c2f8 --- /dev/null +++ b/detections/endpoint/linux_system_network_discovery.yml @@ -0,0 +1,70 @@ +name: Linux System Network Discovery +id: 535cb214-8b47-11ec-a2c7-acde48001122 +version: 1 +date: '2022-02-11' +author: Teoderick Contreras, Splunk +type: Anomaly +datamodel: +- Endpoint +description: This analytic is to look for possible enumeration of local network configuration. + This technique is commonly used as part of recon of adversaries or threat actor to know some network information for its next or further + attack. This anomaly detections may capture normal event made by administrator during auditing or testing network connection of specific + host or network to network. +search: '| tstats `security_content_summariesonly` + count values(Processes.process_name) as process_name_list values(Processes.process) as process_list + values(Processes.process_id) as process_id_list values(Processes.parent_process_id) as parent_process_id_list + values(Processes.process_guid) as process_guid_list dc(Processes.process_name) as process_name_count from datamodel=Endpoint.Processes + where Processes.process_name IN ("arp", "ifconfig", "ip", "netstat", "firewall-cmd", "ufw", "iptables", "ss", "route") + by _time span=30m Processes.dest Processes.user + | where process_name_count >=4 + | `drop_dm_object_name(Processes)`| `linux_system_network_discovery_filter`' +how_to_implement: To successfully implement this search, you need to be ingesting + logs with the process name, parent process, and command-line executions from your + endpoints. If you are using Sysmon, you can use the Add-on for Linux Sysmon from + Splunkbase. +known_false_positives: Administrator or network operator can execute this command. + Please update the filter macros to remove false positives. +references: +- https://github.com/redcanaryco/atomic-red-team/blob/master/atomics/T1016/T1016.md +tags: + analytic_story: + - Network Discovery + dataset: + - https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1016/atomic_red_team/linux_net_discovery/sysmon_linux.log + kill_chain_phases: + - Reconnaissance + mitre_attack_id: + - T1016 + product: + - Splunk Enterprise + - Splunk Enterprise Security + - Splunk Cloud + required_fields: + - _time + - Processes.dest + - Processes.user + - Processes.parent_process_name + - Processes.process_name + - Processes.process + - Processes.process_id + - Processes.parent_process_id + security_domain: endpoint + impact: 30 + confidence: 30 + # (impact * confidence)/100 + risk_score: 9 + context: + - Source:Endpoint + - Stage:Reconaissance + message: A commandline $process$ executed on $dest$ + observable: + - name: dest + type: Hostname + role: + - Victim + nist: + - DE.CM + cis20: + - CIS 3 + - CIS 5 + - CIS 16 \ No newline at end of file diff --git a/detections/endpoint/net_profiler_uac_bypass.yml b/detections/endpoint/net_profiler_uac_bypass.yml index 6a198c5242..dc81093e1c 100644 --- a/detections/endpoint/net_profiler_uac_bypass.yml +++ b/detections/endpoint/net_profiler_uac_bypass.yml @@ -1,7 +1,7 @@ name: NET Profiler UAC bypass id: 0252ca80-e30d-11eb-8aa3-acde48001122 -version: 1 -date: '2021-07-12' +version: 2 +date: '2022-02-18' author: Teoderick Contreras, Splunk type: TTP datamodel: @@ -12,11 +12,14 @@ description: This search is to detect modification of registry to bypass UAC win the registry key and values in the detection area. It may happened that windows update some dll related to mmc.exe and add dll path in this registry. In this case filtering is needed. -search: '| tstats `security_content_summariesonly` count min(_time) as firstTime max(_time) - as lastTime from datamodel=Endpoint.Registry where Registry.registry_path= "*\\Environment\\COR_PROFILER_PATH" - Registry.registry_value_name = "*.dll" by Registry.registry_path Registry.registry_key_name - Registry.registry_value_name Registry.dest | `drop_dm_object_name(Registry)` | `security_content_ctime(firstTime)` - | `security_content_ctime(lastTime)` | `net_profiler_uac_bypass_filter`' +search: '| tstats `security_content_summariesonly` count min(_time) as firstTime max(_time) as lastTime + from datamodel=Endpoint.Registry where + Registry.registry_path= "*\\Environment\\COR_PROFILER_PATH" Registry.registry_value_data = "*.dll" + by Registry.registry_path Registry.registry_key_name Registry.registry_value_data Registry.dest + | `drop_dm_object_name(Registry)` + | `security_content_ctime(firstTime)` + | `security_content_ctime(lastTime)` + | `net_profiler_uac_bypass_filter`' how_to_implement: To successfully implement this search you need to be ingesting information on process that include the name of the process responsible for the changes from your endpoints into the `Endpoint` datamodel in the `Registry` node. Also make sure diff --git a/detections/endpoint/powershell_remove_windows_defender_directory.yml b/detections/endpoint/powershell_remove_windows_defender_directory.yml index fe08533824..4a76a32e9a 100644 --- a/detections/endpoint/powershell_remove_windows_defender_directory.yml +++ b/detections/endpoint/powershell_remove_windows_defender_directory.yml @@ -1,7 +1,7 @@ name: Powershell Remove Windows Defender Directory id: adf47620-79fa-11ec-b248-acde48001122 -version: 1 -date: '2022-01-20' +version: 2 +date: '2022-01-18' author: Teoderick Contreras, Splunk type: TTP datamodel: @@ -11,10 +11,11 @@ description: This analytic will identify a suspicious PowerShell command used to campaign where it used Nirsofts advancedrun.exe to gain administrative privileges to then execute a PowerShell command to delete the Windows Defender folder. This is a good indicator the offending process is trying corrupt a Windows Defender installation. -search: '`powershell` EventCode=4104 Message = "* rmdir *" AND Message = "*\\Microsoft\\Windows - Defender*" | stats count min(_time) as firstTime max(_time) as lastTime by EventCode - Message ComputerName User | `security_content_ctime(firstTime)` | `security_content_ctime(lastTime)` - | `powershell_remove_windows_defender_directory_filter`' +search: '`powershell` EventCode=4104 Message = "*rmdir *" AND Message = "*\\Microsoft\\Windows Defender*" + | stats count min(_time) as firstTime max(_time) as lastTime by EventCode Message ComputerName User + | `security_content_ctime(firstTime)` + | `security_content_ctime(lastTime)` + | `powershell_remove_windows_defender_directory_filter` ' how_to_implement: To successfully implement this analytic, you will need to enable PowerShell Script Block Logging on some or all endpoints. Additional setup here https://docs.splunk.com/Documentation/UBA/5.0.4.1/GetDataIn/AddPowerShell#Configure_module_logging_for_PowerShell. diff --git a/detections/endpoint/process_deleting_its_process_file_path.yml b/detections/endpoint/process_deleting_its_process_file_path.yml index 92a9f3a6d7..7a93edb934 100644 --- a/detections/endpoint/process_deleting_its_process_file_path.yml +++ b/detections/endpoint/process_deleting_its_process_file_path.yml @@ -1,7 +1,7 @@ name: Process Deleting Its Process File Path id: f7eda4bc-871c-11eb-b110-acde48001122 -version: 1 -date: '2021-03-17' +version: 2 +date: '2022-02-18' author: Teoderick Contreras type: TTP datamodel: @@ -11,11 +11,13 @@ description: This detection is to identify a suspicious process that tries to de evasion once a certain condition of malware is satisfied or not. Clop ransomware use this technique where it will try to delete its process file path using a .bat command if the keyboard layout is not the layout it tries to infect. -search: '`sysmon` EventCode=1 cmdline = "* /c *" cmdline = "* del*" Image = "*\\cmd.exe" - |eval result = if(like(process,"%".parent_process."%"), "Found", "Not Found") | - stats min(_time) as firstTime max(_time) as lastTime count by Computer user ParentImage - ParentCommandLine Image cmdline EventCode ProcessID result | where result = "Found" - | `security_content_ctime(firstTime)` | `security_content_ctime(lastTime)` | `process_deleting_its_process_file_path_filter`' +search: '`sysmon` EventCode=1 CommandLine = "* /c *" CommandLine = "* del*" Image = "*\\cmd.exe" + | eval result = if(like(process,"%".parent_process."%"), "Found", "Not Found") + | stats min(_time) as firstTime max(_time) as lastTime count by Computer user ParentImage ParentCommandLine Image CommandLine EventCode ProcessID result + | where result = "Found" + | `security_content_ctime(firstTime)` + | `security_content_ctime(lastTime)` + | `process_deleting_its_process_file_path_filter`' how_to_implement: You must be ingesting data that records process activity from your hosts to populate the Endpoint data model in the Processes node. You must also be ingesting logs with both the process name and command line from your endpoints. @@ -34,8 +36,8 @@ tags: automated_detection_testing: passed confidence: 100 context: - - source:endpoint - - stage: Credential Access + - Source:Endpoint + - Stage:Credential Access dataset: - https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/malware/clop/clop_a/windows-sysmon.log impact: 60 @@ -51,7 +53,7 @@ tags: role: - Victim - name: user - type: user + type: User role: - Victim product: diff --git a/detections/endpoint/registry_keys_used_for_persistence.yml b/detections/endpoint/registry_keys_used_for_persistence.yml index a1d2b48115..c7f934b2fd 100644 --- a/detections/endpoint/registry_keys_used_for_persistence.yml +++ b/detections/endpoint/registry_keys_used_for_persistence.yml @@ -2,14 +2,14 @@ name: Registry Keys Used For Persistence id: f5f6af30-7aa7-4295-bfe9-07fe87c01a4b version: 7 date: '2022-01-26' -author: Jose Hernandez, David Dorsey, Teoderick Contreras, Splunk +author: Jose Hernandez, David Dorsey, Teoderick Contreras, Rod Soto, Splunk type: TTP datamodel: - Endpoint description: The search looks for modifications to registry keys that can be used to launch an application or service at system startup. search: '| tstats `security_content_summariesonly` count FROM datamodel=Endpoint.Registry - where (Registry.registry_path=*\\currentversion\\run* OR Registry.registry_path=*\\currentVersion\\Windows\\Appinit_Dlls* + where (Registry.registry_path=*\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\RunOnce OR Registry.registry_path=*\\currentversion\\run* OR Registry.registry_path=*\\currentVersion\\Windows\\Appinit_Dlls* OR Registry.registry_path=*\\CurrentVersion\\Winlogon\\Shell* OR Registry.registry_path=*\\CurrentVersion\\Winlogon\\Notify* OR Registry.registry_path=*\\CurrentVersion\\Winlogon\\Userinit* OR Registry.registry_path=*\\CurrentVersion\\Winlogon\\VmApplet* OR Registry.registry_path=*\\currentversion\\policies\\explorer\\run* OR Registry.registry_path=*\\currentversion\\runservices* @@ -65,6 +65,7 @@ tags: - Privilege Escalation dataset: - https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1547.001/atomic_red_team/windows-sysmon.log + - https://raw.githubusercontent.com/splunk/attack_data/master/datasets/attack_techniques/T1547.001/atomic_red_team/t1547001-runonce.log impact: 80 kill_chain_phases: - Actions on Objectives diff --git a/detections/endpoint/rundll32_dnsquery.yml b/detections/endpoint/rundll32_dnsquery.yml index d542ef031f..72503d8052 100644 --- a/detections/endpoint/rundll32_dnsquery.yml +++ b/detections/endpoint/rundll32_dnsquery.yml @@ -1,7 +1,7 @@ name: Rundll32 DNSQuery id: f1483f5e-ee29-11eb-9d23-acde48001122 -version: 1 -date: '2021-07-26' +version: 2 +date: '2022-02-18' author: Teoderick Contreras, Splunk type: TTP datamodel: @@ -11,9 +11,10 @@ description: This search is to detect a suspicious rundll32.exe process having a malware where the rundll32 that execute its payload will contact amazon.com to check internet connect and to communicate to its C&C server to download config and other file component. -search: '`sysmon` EventCode=22 process_name="rundll32.exe" | stats count min(_time) - as firstTime max(_time) as lastTime by Image QueryName QueryStatus ProcessId direction - Computer | `security_content_ctime(firstTime)` | `security_content_ctime(lastTime)` +search: '`sysmon` EventCode=22 process_name="rundll32.exe" + | stats count min(_time) as firstTime max(_time) as lastTime by Image QueryName QueryStatus ProcessId Computer + | `security_content_ctime(firstTime)` + | `security_content_ctime(lastTime)` | `rundll32_dnsquery_filter`' how_to_implement: To successfully implement this search, you need to be ingesting logs with the process name and eventcode = 22 dnsquery executions from your endpoints. @@ -59,7 +60,6 @@ tags: - QueryName - QueryStatus - ProcessId - - direction - Computer risk_score: 56 security_domain: endpoint diff --git a/detections/endpoint/scheduled_task_deleted_or_created_via_cmd.yml b/detections/endpoint/scheduled_task_deleted_or_created_via_cmd.yml index 12618ad7a7..04834ce99f 100644 --- a/detections/endpoint/scheduled_task_deleted_or_created_via_cmd.yml +++ b/detections/endpoint/scheduled_task_deleted_or_created_via_cmd.yml @@ -1,31 +1,29 @@ name: Scheduled Task Deleted Or Created via CMD id: d5af132c-7c17-439c-9d31-13d55340f36c -version: 5 -date: '2020-12-17' +version: 6 +date: '2022-02-22' author: Bhavin Patel, Splunk type: TTP datamodel: - Endpoint -description: This search looks for flags passed to schtasks.exe on the command-line - that indicate a task was created via command like. This has been associated with +description: The following analytic identifies the creation or deletion of a scheduled task using schtasks.exe with flags - create or delete being passed on the command-line. This has been associated with the Dragonfly threat actor, and the SUNBURST attack against Solarwinds. + This analytic replaces "Scheduled Task used in BadRabbit Ransomware". search: '| tstats `security_content_summariesonly` count values(Processes.process) as process values(Processes.parent_process) as parent_process min(_time) as firstTime max(_time) as lastTime from datamodel=Endpoint.Processes where Processes.process_name=schtasks.exe (Processes.process=*delete* OR Processes.process=*create*) by Processes.user Processes.process_name Processes.parent_process_name Processes.dest | `drop_dm_object_name(Processes)` | `security_content_ctime(firstTime)`| `security_content_ctime(lastTime)` | `scheduled_task_deleted_or_created_via_cmd_filter` ' -how_to_implement: You must be ingesting endpoint data that tracks process activity, - including parent-child relationships from your endpoints to populate the Endpoint - data model in the Processes node. The command-line arguments are mapped to the "process" - field in the Endpoint data model. -known_false_positives: Tasks should not be manually created via CLI, this is rarely - done by admins as well -references: [] +how_to_implement: To successfully implement this search you need to be ingesting information on process that include the name of the process responsible for the changes from your endpoints into the `Endpoint` datamodel in the `Processes` node. In addition, confirm the latest CIM App 4.20 or higher is installed and the latest TA for the endpoint product. +known_false_positives: It is possible scripts or administrators may trigger this analytic. Filter as needed based on parent process, application. +references: + - https://thedfirreport.com/2022/02/21/qbot-and-zerologon-lead-to-full-domain-compromise/ tags: analytic_story: - DHS Report TA18-074A - NOBELIUM Group + - Windows Persistence Techniques asset_type: Endpoint automated_detection_testing: passed cis20: diff --git a/detections/endpoint/set_default_powershell_execution_policy_to_unrestricted_or_bypass.yml b/detections/endpoint/set_default_powershell_execution_policy_to_unrestricted_or_bypass.yml index 2f6666750a..09fdaff5e6 100644 --- a/detections/endpoint/set_default_powershell_execution_policy_to_unrestricted_or_bypass.yml +++ b/detections/endpoint/set_default_powershell_execution_policy_to_unrestricted_or_bypass.yml @@ -1,18 +1,20 @@ name: Set Default PowerShell Execution Policy To Unrestricted or Bypass id: c2590137-0b08-4985-9ec5-6ae23d92f63d -version: 6 -date: '2020-11-06' +version: 7 +date: '2022-02-18' author: Patrick Bareiss, Splunk type: TTP datamodel: - Endpoint description: Monitor for changes of the ExecutionPolicy in the registry to the values "unrestricted" or "bypass," which allows the execution of malicious scripts. -search: '| tstats `security_content_summariesonly` count min(_time) as firstTime max(_time) - as lastTime from datamodel=Endpoint.Registry where Registry.registry_path=*Software\\Microsoft\\Powershell\\1\\ShellIds\\Microsoft.PowerShell* - Registry.registry_key_name=ExecutionPolicy (Registry.registry_value_name=Unrestricted - OR Registry.registry_value_name=Bypass) by Registry.registry_path Registry.registry_key_name - Registry.registry_value_name Registry.dest | `drop_dm_object_name(Registry)` | `security_content_ctime(firstTime)`|`security_content_ctime(lastTime)` +search: '| tstats `security_content_summariesonly` count min(_time) as firstTime max(_time) as lastTime from + datamodel=Endpoint.Registry where Registry.registry_path=*Software\\Microsoft\\Powershell\\1\\ShellIds\\Microsoft.PowerShell* + Registry.registry_value_name=ExecutionPolicy (Registry.registry_value_data=Unrestricted OR Registry.registry_value_data=Bypass) + by Registry.registry_path Registry.registry_value_name Registry.registry_value_data Registry.dest + | `drop_dm_object_name(Registry)` + | `security_content_ctime(firstTime)` + | `security_content_ctime(lastTime)` | `set_default_powershell_execution_policy_to_unrestricted_or_bypass_filter`' how_to_implement: You must be ingesting data that records process activity from your hosts to populate the Endpoint data model in the Registry node. You must also be diff --git a/detections/endpoint/ssa___prohibited_apps_spawning_cmdprompt.yml b/detections/endpoint/ssa___prohibited_apps_spawning_cmdprompt.yml index 0be577e79f..c4f2447379 100644 --- a/detections/endpoint/ssa___prohibited_apps_spawning_cmdprompt.yml +++ b/detections/endpoint/ssa___prohibited_apps_spawning_cmdprompt.yml @@ -11,7 +11,6 @@ description: The following analytic identifies parent processes, browsers, Windo many applications spawn cmd.exe natively or built into macros. Much of this will need to be tuned to further enhance the risk. search: '| from read_ssa_enriched_events() - | eval timestamp=parse_long(ucast(map_get(input_event, "_time"), "string", null)) | eval process_name=ucast(map_get(input_event, "process_name"), "string", null), parent_process=lower(ucast(map_get(input_event, "parent_process_name"), "string", @@ -23,7 +22,7 @@ search: '| from read_ssa_enriched_events() OR ParentBaseFileName="powerpnt.exe" OR ParentBaseFileName="visio.exe" OR ParentBaseFileName="mspub.exe" OR ParentBaseFileName="acrobat.exe" OR ParentBaseFileName="acrord32.exe" OR ParentBaseFileName="iexplore.exe" OR ParentBaseFileName="opera.exe" OR ParentBaseFileName="firefox.exe" OR (ParentBaseFileName="java.exe" - AND (cmd_line IS NULL OR (cmd_line IS NOT NULL AND NOT like(cmd_line, "%patch1-Hotfix1a%")))) + AND (cmd_line IS NULL OR (cmd_line IS NOT NULL AND match_regex(cmd_line, /(?i)patch1-Hotfix1a/)=false))) OR ParentBaseFileName="powershell.exe" OR (ParentBaseFileName="chrome.exe" AND (cmd_line IS NULL OR (cmd_line IS NOT NULL AND NOT like(cmd_line, "%chrome-extension%")))) | eval start_time=timestamp, end_time=timestamp, entities=mvappend(dest_device_id, diff --git a/detections/endpoint/ssa___windows_diskshadow_proxy_execution.yml b/detections/endpoint/ssa___windows_diskshadow_proxy_execution.yml new file mode 100644 index 0000000000..55a5e8feb3 --- /dev/null +++ b/detections/endpoint/ssa___windows_diskshadow_proxy_execution.yml @@ -0,0 +1,63 @@ +name: Windows Diskshadow Proxy Execution +id: aa502688-9037-11ec-842d-acde48001122 +version: 1 +date: '2022-02-17' +author: Lou Stella, Splunk +type: Anomaly +datamodel: +- Endpoint_Processes +description: DiskShadow.exe is a Microsoft Signed binary present on Windows Server. It has a scripting mode intended for complex scripted backup operations. This feature also allows for execution of arbitrary unsigned code. This analytic looks for the usage of the scripting mode flags in executions of DiskShadow. During triage, compare to known backup behavior in your environment and then review the scripts called by diskshadow. +search: '| from read_ssa_enriched_events() | where "Endpoint_Processes" IN(_datamodels) | eval timestamp=parse_long(ucast(map_get(input_event, "_time"), "string", null)), cmd_line=lower(ucast(map_get(input_event, "process"), "string", null)), process_name=lower(ucast(map_get(input_event, "process_name"), "string", null)), process_path=ucast(map_get(input_event, "process_path"), "string", null), parent_process_name=ucast(map_get(input_event, "parent_process_name"), "string", null), event_id=ucast(map_get(input_event, "event_id"), "string", null) | where cmd_line IS NOT NULL AND process_name IS NOT NULL AND process_name="diskshadow.exe" AND (like (cmd_line, "%-s%") OR like (cmd_line, "%/s%")) | eval start_time=timestamp, end_time=timestamp, entities=mvappend(ucast(map_get(input_event, "dest_user_id"), "string", null), ucast(map_get(input_event, "dest_device_id"), "string", null)) | eval body=create_map(["event_id", event_id, "cmd_line", cmd_line, "process_name", process_name, "parent_process_name", parent_process_name, "process_path", process_path]) | into write_ssa_detected_events();' +how_to_implement: To successfully implement this search you need to be ingesting information on processes that include the name of the process responsible for the changes from your endpoints into the `Endpoint_Processess` datamodel. +known_false_positives: Administrators using the DiskShadow tool in their infrastructure as a main backup tool with scripts will cause false positives +references: +- https://bohops.com/2018/03/26/diskshadow-the-return-of-vss-evasion-persistence-and-active-directory-database-extraction/ +tags: + analytic_story: + - Living Off The Land + dataset: + - https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1218/diskshadow/windows-security.log + kill_chain_phases: + - Exploitation + mitre_attack_id: + - T1218 + product: + - Splunk Behavioral Analytics + required_fields: + - _time + - dest_device_id + - process_name + - parent_process_name + - process_path + - dest_user_id + - process + - cmd_line + security_domain: endpoint + impact: 70 + confidence: 70 + risk_score: 49 + context: + - Source:Endpoint + - Stage:Execution + message: An instance of $parent_process_name$ spawning $process_name$ was identified on endpoint $dest_device_id$ by user $dest_user_id$ attempting to run a script. + observable: + - name: dest_device_id + type: Hostname + role: + - Victim + - name: dest_user_id + type: User + role: + - Victim + - name: parent_process_name + type: Parent Process + role: + - Parent Process + - name: process_name + type: Process + role: + - Child Process + nist: + - DE.CM + cis20: + - CIS 8 diff --git a/stories/data_destruction.yml b/stories/data_destruction.yml new file mode 100644 index 0000000000..45cd0ac30d --- /dev/null +++ b/stories/data_destruction.yml @@ -0,0 +1,22 @@ +name: Data Destruction +id: 4ae5c0d1-cebd-47d1-bfce-71bf096e38aa +version: 1 +date: '2022-02-14' +author: Teoderick Contreras, Splunk +description: Leverage searches that allow you to detect and investigate unusual activities + that might relate to the data destruction, including deleting files, overwriting files, wiping disk and encrypting files. +narrative: Adversaries may use this technique to maximize the impact on the target organization in operations where network wide availability interruption + is the goal. +references: +- https://attack.mitre.org/techniques/T1485/ +- https://researchcenter.paloaltonetworks.com/2018/09/unit42-xbash-combines-botnet-ransomware-coinmining-worm-targets-linux-windows/ +- https://www.picussecurity.com/blog/a-brief-history-and-further-technical-analysis-of-sodinokibi-ransomware +tags: + analytic_story: Data Destruction + category: + - Malware + product: + - Splunk Enterprise + - Splunk Enterprise Security + - Splunk Cloud + usecase: Advanced Threat Detection diff --git a/stories/network_discovery.yml b/stories/network_discovery.yml new file mode 100644 index 0000000000..9949ce6952 --- /dev/null +++ b/stories/network_discovery.yml @@ -0,0 +1,23 @@ +name: Network Discovery +id: af228995-f182-49d7-90b3-2a732944f00f +version: 1 +date: '2022-02-14' +author: Teoderick Contreras, Splunk +description: Leverage searches that allow you to detect and investigate unusual activities + that might relate to the network discovery, including looking for network configuration, settings such as IP, MAC address, + firewall settings and many more. +narrative: Adversaries may use the information from System Network Configuration Discovery during automated discovery to shape follow-on behaviors, + including determining certain access within the target network and what actions to do next. +references: +- https://attack.mitre.org/techniques/T1016/ +- https://www.welivesecurity.com/wp-content/uploads/2021/01/ESET_Kobalos.pdf +- https://researchcenter.paloaltonetworks.com/2018/09/unit42-xbash-combines-botnet-ransomware-coinmining-worm-targets-linux-windows/ +tags: + analytic_story: Network Discovery + category: + - Malware + product: + - Splunk Enterprise + - Splunk Enterprise Security + - Splunk Cloud + usecase: Advanced Threat Detection \ No newline at end of file diff --git a/tests/endpoint/linux_dd_file_overwrite.test.yml b/tests/endpoint/linux_dd_file_overwrite.test.yml new file mode 100644 index 0000000000..719a157be4 --- /dev/null +++ b/tests/endpoint/linux_dd_file_overwrite.test.yml @@ -0,0 +1,12 @@ +name: Linux DD File Overwrite Unit Test +tests: +- name: Linux DD File Overwrite + file: endpoint/linux_dd_file_overwrite.yml + pass_condition: '| stats count | where count > 0' + earliest_time: '-24h' + latest_time: 'now' + attack_data: + - file_name: sysmon_linux.log + data: https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1485/linux_dd_file_overwrite/sysmon_linux.log + source: Syslog:Linux-Sysmon/Operational + sourcetype: sysmon_linux \ No newline at end of file diff --git a/tests/endpoint/linux_system_network_discovery.test.yml b/tests/endpoint/linux_system_network_discovery.test.yml new file mode 100644 index 0000000000..ed79995680 --- /dev/null +++ b/tests/endpoint/linux_system_network_discovery.test.yml @@ -0,0 +1,12 @@ +name: Linux System Network Discovery Unit Test +tests: +- name: Linux System Network Discovery + file: endpoint/linux_system_network_discovery.yml + pass_condition: '| stats count | where count > 0' + earliest_time: '-24h' + latest_time: 'now' + attack_data: + - file_name: sysmon_linux.log + data: https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1016/atomic_red_team/linux_net_discovery/sysmon_linux.log + source: Syslog:Linux-Sysmon/Operational + sourcetype: sysmon_linux \ No newline at end of file diff --git a/tests/endpoint/process_execution_via_wmi.test.yml b/tests/endpoint/process_execution_via_wmi.test.yml index 3c1b9ee8b9..23f82aae83 100644 --- a/tests/endpoint/process_execution_via_wmi.test.yml +++ b/tests/endpoint/process_execution_via_wmi.test.yml @@ -2,7 +2,7 @@ name: Process execution via wmi Unit Test tests: - name: Process execution via wmi file: endpoint/process_execution_via_wmi.yml - pass_condition: '| stats count | where count = 1' + pass_condition: '| stats count | where count > 0' earliest_time: '-24h' latest_time: 'now' attack_data: diff --git a/tests/endpoint/ssa___windows_diskshadow_proxy_execution.test.yml b/tests/endpoint/ssa___windows_diskshadow_proxy_execution.test.yml new file mode 100644 index 0000000000..1b8c7d0d1b --- /dev/null +++ b/tests/endpoint/ssa___windows_diskshadow_proxy_execution.test.yml @@ -0,0 +1,9 @@ +name: BA Windows Diskshadow Proxy Execution Unit Test +tests: +- name: BA Windows Diskshadow Proxy Execution + file: endpoint/ssa___windows_diskshadow_proxy_execution.yml + pass_condition: '@count_gt(0)' + attack_data: + - file_name: windows-security.log + data: https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1218/diskshadow/windows-security.log + source: WinEventLog:Security