mirror of
https://github.com/splunk/security_content
synced 2026-06-08 17:32:49 +00:00
Merge branch 'develop' into ssa_detection_testing
This commit is contained in:
@@ -2,8 +2,6 @@
|
||||
import git
|
||||
import os
|
||||
import logging
|
||||
from os import path
|
||||
import sys
|
||||
|
||||
|
||||
# Logger
|
||||
@@ -25,7 +23,6 @@ class GithubService:
|
||||
repo_obj = git.Repo.clone_from(url, project, branch=branch)
|
||||
return repo_obj
|
||||
|
||||
|
||||
def get_changed_test_files_ssa(self):
|
||||
branch1 = self.security_content_branch
|
||||
branch2 = 'develop'
|
||||
@@ -35,8 +32,6 @@ class GithubService:
|
||||
|
||||
changed_ssa_test_files = []
|
||||
|
||||
#tests = self.read_security_content_test_files()
|
||||
|
||||
for file_path in changed_files:
|
||||
# added or changed test files
|
||||
if file_path.startswith('tests'):
|
||||
@@ -47,7 +42,10 @@ class GithubService:
|
||||
# changed detections
|
||||
if file_path.startswith('detections'):
|
||||
if os.path.basename(file_path).startswith('ssa'):
|
||||
file_path_new = os.path.splitext(file_path)[0].replace('detections', 'tests') + '.test.yml'
|
||||
file_path_base = os.path.splitext(file_path)[0].replace('detections', 'tests') + '.test'
|
||||
file_path_new = file_path_base + '.yml'
|
||||
if not os.path.exists(file_path_new):
|
||||
file_path_new = file_path_base + '.yaml'
|
||||
if file_path_new not in changed_ssa_test_files:
|
||||
changed_ssa_test_files.append(file_path_new)
|
||||
|
||||
|
||||
@@ -17,4 +17,5 @@
|
||||
| eval start_time = timestamp,
|
||||
end_time = timestamp,
|
||||
entities = mvappend(dest_device_id, dest_user_id),
|
||||
body = "TBD";
|
||||
body = "TBD"
|
||||
| into write_ssa_detected_events();
|
||||
@@ -17,4 +17,5 @@
|
||||
| eval start_time = timestamp,
|
||||
end_time = timestamp,
|
||||
entities = mvappend(dest_device_id, dest_user_id),
|
||||
body = "TBD";
|
||||
body = "TBD"
|
||||
| into write_ssa_detected_events();
|
||||
|
||||
@@ -1 +1 @@
|
||||
| from read_splunk_firehose();
|
||||
| from read_splunk_firehose();
|
||||
@@ -1 +1 @@
|
||||
| from read_ssa_enriched_events();
|
||||
| from read_ssa_enriched_events() | into write_ssa_detected_events();
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
@@ -6,8 +5,7 @@ import sys
|
||||
|
||||
from http import HTTPStatus
|
||||
from modules.streams_service_api_helper import DSPApi
|
||||
from modules.utils import manipulate_spl, read_spl, read_data
|
||||
|
||||
from modules.utils import check_source_sink, manipulate_spl, read_spl, read_data
|
||||
|
||||
# Logger
|
||||
logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO"))
|
||||
@@ -22,6 +20,7 @@ MAX_EXECUTION_TIME_LIMIT = 600 # per detection test
|
||||
|
||||
TEST_DATASET = 'windows-security_small.txt'
|
||||
|
||||
|
||||
class SSADetectionTesting:
|
||||
|
||||
def __init__(self, env, tenant, header_token):
|
||||
@@ -45,7 +44,7 @@ class SSADetectionTesting:
|
||||
]
|
||||
|
||||
test_results = []
|
||||
for i in range(0,len(test_spls)):
|
||||
for i in range(0, len(test_spls)):
|
||||
self.max_execution_time = MAX_EXECUTION_TIME_LIMIT
|
||||
test_result = self.ssa_detection_test(read_spl(file_path_spl, test_spls[i]), file_path_data, test_names[i])
|
||||
test_results.append(test_result.copy())
|
||||
@@ -62,17 +61,16 @@ class SSADetectionTesting:
|
||||
|
||||
return passed
|
||||
|
||||
|
||||
def test_ssa_detections(self, test_obj):
|
||||
LOGGER.info('Test SSA Detection: ' + test_obj["detection_obj"]["name"])
|
||||
self.max_execution_time = MAX_EXECUTION_TIME_LIMIT
|
||||
file_path_attack_data = os.path.join(os.path.dirname(__file__), "../", test_obj["attack_data_file_path"])
|
||||
|
||||
test_results = self.ssa_detection_test(test_obj["detection_obj"]["search"], file_path_attack_data, "SSA Smoke Test " + test_obj["test_obj"]["name"])
|
||||
test_results = self.ssa_detection_test(test_obj["detection_obj"]["search"], file_path_attack_data,
|
||||
"SSA Smoke Test " + test_obj["test_obj"]["name"])
|
||||
|
||||
return test_results
|
||||
|
||||
|
||||
## Helper Functions ##
|
||||
|
||||
def update_execution_time(self, time_frame):
|
||||
@@ -86,58 +84,45 @@ class SSADetectionTesting:
|
||||
time.sleep(time_in_s)
|
||||
return self.update_execution_time(time_in_s)
|
||||
|
||||
def check_result(self, condition, error_message):
|
||||
try:
|
||||
assert condition
|
||||
except:
|
||||
self.execution_passed = False
|
||||
LOGGER.error(error_message)
|
||||
|
||||
def write_test_results(self, test_name):
|
||||
if not self.execution_passed:
|
||||
msg = f"Detection test failed for {test_name}"
|
||||
LOGGER.error(msg)
|
||||
self.test_results["msg"] = msg
|
||||
self.test_results["result"] = False
|
||||
else:
|
||||
msg = f"Detection test successful for {test_name}"
|
||||
LOGGER.info(msg)
|
||||
self.test_results["msg"] = msg
|
||||
|
||||
|
||||
def ssa_detection_test_init(self):
|
||||
self.test_results["result"] = True
|
||||
self.test_results["msg"] = ""
|
||||
self.results_index = self.api.create_temp_index("mc")
|
||||
|
||||
self.created_pipelines = []
|
||||
self.activated_pipelines = []
|
||||
|
||||
def ssa_detection_test_main(self, spl, source, test_name):
|
||||
self.execution_passed = True
|
||||
|
||||
self.wait_time(SLEEP_TIME_CREATE_INDEX)
|
||||
|
||||
check_ssa_spl = check_source_sink(spl)
|
||||
spl = manipulate_spl(self.api.env, spl, self.results_index)
|
||||
self.check_result(spl is not None, "fail to manipulate spl file")
|
||||
assert spl is not None, "fail to manipulate spl file"
|
||||
|
||||
pipeline_id = self.api.create_pipeline_from_spl(spl)
|
||||
self.check_result(pipeline_id is not None, "failed to create a pipeline")
|
||||
assert pipeline_id is not None, "failed to create a pipeline"
|
||||
|
||||
_pipeline_status = self.api.pipeline_status(pipeline_id)
|
||||
self.check_result(_pipeline_status=="CREATED", f"Current status of pipeline {pipeline_id} should be CREATED")
|
||||
assert _pipeline_status == "CREATED", f"Current status of pipeline {pipeline_id} should be CREATED"
|
||||
self.created_pipelines.append(pipeline_id)
|
||||
|
||||
response_body = self.api.activate_pipeline(pipeline_id)
|
||||
self.check_result(response_body.get("activated")==pipeline_id, f"pipeline {pipeline_id} should be successfully activate.")
|
||||
assert response_body.get("activated") == pipeline_id, f"pipeline {pipeline_id} should be successfully activate."
|
||||
self.activated_pipelines.append(pipeline_id)
|
||||
|
||||
self.wait_time(SLEEP_TIME_ACTIVATE_PIPELINE)
|
||||
|
||||
if not check_ssa_spl:
|
||||
msg = f"Detection test successful for {test_name}"
|
||||
LOGGER.warning(f"Test not completed. Detection seems deprecated, and will not send messages to SSA")
|
||||
self.test_results["msg"] = msg
|
||||
return self.test_results
|
||||
|
||||
data = read_data(source)
|
||||
LOGGER.info("Sending (%d) events" % (len(data)))
|
||||
|
||||
if len(data) == 0:
|
||||
LOGGER.warning("No events to send, skip to next test.")
|
||||
self.execution_passed = False
|
||||
self.write_test_results(test_name)
|
||||
return self.test_results
|
||||
assert len(data) > 0, "No events to send, skip to next test."
|
||||
|
||||
for d in data:
|
||||
response_body = self.api.ingest_data(d)
|
||||
@@ -151,8 +136,8 @@ class SSADetectionTesting:
|
||||
max_execution_time_reached = self.wait_time(WAIT_CYCLE)
|
||||
query = f"from indexes('{self.results_index['name']}') | search source!=\"Search Catalog\" "
|
||||
sid = self.api.submit_search_job(self.results_index['module'], query)
|
||||
self.check_result(sid is not None, f"Failed to create a Search Job")
|
||||
|
||||
assert sid is not None, f"Failed to create a Search Job"
|
||||
|
||||
job_finished = False
|
||||
while not job_finished:
|
||||
self.wait_time(WAIT_CYCLE)
|
||||
@@ -162,33 +147,53 @@ class SSADetectionTesting:
|
||||
results = self.api.get_search_job_results(sid)
|
||||
search_results = (len(results) > 0)
|
||||
if not search_results:
|
||||
LOGGER.info(f"Search didn't return any results. Retrying in {WAIT_CYCLE}s, max execution time left {self.max_execution_time}s")
|
||||
|
||||
self.check_result(len(results) > 0, "Search job didn't return any results")
|
||||
LOGGER.info(
|
||||
f"Search didn't return any results. Retrying in {WAIT_CYCLE}s, max execution time left {self.max_execution_time}s")
|
||||
|
||||
response, response_body = self.api.deactivate_pipeline(pipeline_id)
|
||||
self.check_result(response.status_code == HTTPStatus.OK, f"The pipeline {pipeline_id} fails to deactivated.")
|
||||
assert len(results) > 0, "Search job didn't return any results"
|
||||
|
||||
response = self.api.delete_pipeline(pipeline_id)
|
||||
self.check_result(response.status_code == HTTPStatus.NO_CONTENT, f"Fail to delete pipeline {pipeline_id}.")
|
||||
|
||||
self.write_test_results(test_name)
|
||||
msg = f"Detection test successful for {test_name}"
|
||||
LOGGER.info(msg)
|
||||
self.test_results["msg"] = msg
|
||||
|
||||
return self.test_results
|
||||
|
||||
|
||||
def ssa_detection_test_teardown(self):
|
||||
pass
|
||||
self.api.delete_temp_index(self.results_index["id"])
|
||||
|
||||
"""
|
||||
Deactivate and deletes pipelines, deletes results indexes,
|
||||
and when it fails it shows pipelines and result indexes that were not removed.
|
||||
:return:
|
||||
None
|
||||
"""
|
||||
deactivate_pipeline = lambda p: self.api.deactivate_pipeline(p)[0].status_code == HTTPStatus.OK
|
||||
delete_pipeline = lambda p: self.api.delete_pipeline(p).status_code == HTTPStatus.NO_CONTENT
|
||||
delete_index = lambda p: self.api.delete_temp_index(p["id"]) == HTTPStatus.NO_CONTENT
|
||||
self.activated_pipelines = [p for p in self.activated_pipelines if not deactivate_pipeline(p)]
|
||||
self.created_pipelines = [p for p in self.created_pipelines if not delete_pipeline(p)]
|
||||
if len(self.activated_pipelines) > 0 or len(self.created_pipelines) > 0 or not delete_index(self.results_index):
|
||||
LOGGER.warning("Not all SCS resources fred up")
|
||||
LOGGER.info(f"Created Pipelines: {','.join(self.created_pipelines)}")
|
||||
LOGGER.info(f"Active Pipelines: {','.join(self.activated_pipelines)}")
|
||||
LOGGER.info(f"Result Indexes: {self.results_index}")
|
||||
else:
|
||||
LOGGER.info("Testing successfully cleaned up")
|
||||
|
||||
def ssa_detection_test(self, spl, source, test_name):
|
||||
self.ssa_detection_test_init()
|
||||
test_result = self.ssa_detection_test_main(spl, source, test_name)
|
||||
self.ssa_detection_test_teardown()
|
||||
return test_result
|
||||
|
||||
|
||||
try:
|
||||
test_result = self.ssa_detection_test_main(spl, source, test_name)
|
||||
self.ssa_detection_test_teardown()
|
||||
return test_result
|
||||
except AssertionError as e:
|
||||
self.ssa_detection_test_teardown()
|
||||
LOGGER.error(e.args[0])
|
||||
return {"result": False,
|
||||
"msg": f"Detection test failure for {test_name}"}
|
||||
except Exception as e:
|
||||
self.ssa_detection_test_teardown()
|
||||
LOGGER.error(e)
|
||||
return {"result": False,
|
||||
"msg": f"Detection test failure for {test_name} (perhaps SCS problems)"}
|
||||
|
||||
# only for troubleshooting
|
||||
# def ssa_detection_in_dsp_with_preview_session(self, spl, source, test_name):
|
||||
@@ -213,4 +218,4 @@ class SSADetectionTesting:
|
||||
|
||||
# response = self.api.stop_preview_session(preview_id)
|
||||
|
||||
# self.write_test_results(test_name)
|
||||
# self.write_test_results(test_name)
|
||||
|
||||
@@ -75,14 +75,26 @@ def request_headers(header_token):
|
||||
return headers
|
||||
|
||||
|
||||
def check_source_sink(spl):
|
||||
match_source = re.match(r"^\s*\|\s+from\s+read_ssa_enriched_events\(\s*\)", spl)
|
||||
match_sink = re.search(r"\|\s*into\s+write_ssa_detected_events\(\s*\)\s*;", spl)
|
||||
return match_source and match_sink
|
||||
|
||||
|
||||
def manipulate_spl(env, spl, results_index):
|
||||
spl = replace_ssa_macros(env, spl)
|
||||
# Obtain the SSA source
|
||||
pulsar_source_connection_id, pulsar_source_topic = return_macros(env)
|
||||
source = READ_SSA_ENRICHED_EVENTS_EXPANDED\
|
||||
.replace("__PULSAR_SOURCE_CONNECTION_ID__", pulsar_source_connection_id)\
|
||||
.replace("__PULSAR_SOURCE_TOPIC__", pulsar_source_topic)
|
||||
# Obtain the test sink
|
||||
sink = ";"
|
||||
if results_index is not None:
|
||||
# When an index is defined for a test, it writes the output of this pipeline to this index.
|
||||
# original_pipeline; => original_pipeline | into index("module", "index");
|
||||
module = results_index["module"]
|
||||
index = results_index["name"]
|
||||
spl = spl[:spl.rindex(";")] + f" | into index(\"{module}\", \"{index}\");"
|
||||
sink = f"| into index(\"{module}\", \"{index}\");"
|
||||
# Replace spl template with its `source` and `sink`
|
||||
spl = replace_ssa_macros(source, sink, spl)
|
||||
LOGGER.info(f"spl: {spl}")
|
||||
return spl
|
||||
|
||||
@@ -93,13 +105,9 @@ def read_spl(file_path, file_name):
|
||||
return spl
|
||||
|
||||
|
||||
def replace_ssa_macros(env, spl):
|
||||
pulsar_source_connection_id, pulsar_source_topic = return_macros(env)
|
||||
macro_expanded = READ_SSA_ENRICHED_EVENTS_EXPANDED.replace("__PULSAR_SOURCE_CONNECTION_ID__", pulsar_source_connection_id)
|
||||
macro_expanded = macro_expanded.replace("__PULSAR_SOURCE_TOPIC__", pulsar_source_topic)
|
||||
spl = spl.replace(READ_SSA_ENRICHED_EVENTS, macro_expanded)
|
||||
spl = spl.replace(WRITE_SSA_DETECTED_EVENTS, ";")
|
||||
#spl = spl.replace("\n", " ")
|
||||
def replace_ssa_macros(source, sink, spl):
|
||||
spl = spl.replace(READ_SSA_ENRICHED_EVENTS, source)
|
||||
spl = spl.replace(WRITE_SSA_DETECTED_EVENTS, sink)
|
||||
return spl
|
||||
|
||||
|
||||
|
||||
@@ -38,7 +38,7 @@ def main(args):
|
||||
for test_file in test_files_ssa:
|
||||
LOGGER.info(test_file)
|
||||
|
||||
if len(test_files_ssa)==0:
|
||||
if len(test_files_ssa) == 0:
|
||||
LOGGER.info('Nothing to test for SSA smoke test.')
|
||||
sys.exit(0)
|
||||
|
||||
|
||||
+16
-17
@@ -1,30 +1,27 @@
|
||||
name: Detect Exchange Web Shell
|
||||
id: 44465d80-810b-11eb-aa30-acde48001122
|
||||
version: 1
|
||||
id: 8c14eeee-2af1-4a4b-bda8-228da0f4862a
|
||||
version: 2
|
||||
date: '2021-03-09'
|
||||
author: Michael Haag, Splunk
|
||||
author: Michael Haag, Shannon Davis, Splunk
|
||||
type: batch
|
||||
datamodel:
|
||||
- Endpoint
|
||||
description: 'The following query identifies suspicious .aspx created in 3 paths identified
|
||||
by Microsoft as known drop locations for Exchange exploitation related to HAFNIUM
|
||||
group. Paths include: `\HttpProxy\owa\auth\`, `\inetpub\wwwroot\aspnet_client\`,
|
||||
and `\HttpProxy\OAB\`. Upon triage, the suspicious .aspx file will have a randomized
|
||||
name of 8 characters long. Review the file for suspect commands. Identify additional
|
||||
and `\HttpProxy\OAB\`. Upon triage, the suspicious .aspx file will likely look obvious
|
||||
on the surface. inspect the contents for script code inside. Identify additional
|
||||
log sources, IIS included, to review source and other potential exploitation.'
|
||||
search: '| tstats `security_content_summariesonly` count min(_time) as firstTime max(_time)
|
||||
search: '| tstats `security_content_summariesonly` count FROM datamodel=Endpoint.Processes
|
||||
where Processes.process_name=System by _time span=1h Processes.process_id Processes.process_name
|
||||
Processes.dest | `drop_dm_object_name(Processes)` | join process_guid, _time [|
|
||||
tstats `security_content_summariesonly` count min(_time) as firstTime max(_time)
|
||||
as lastTime FROM datamodel=Endpoint.Filesystem where Filesystem.file_path IN ("*\\HttpProxy\\owa\\auth\\*",
|
||||
"*\\inetpub\\wwwroot\\aspnet_client\\*", "*\\HttpProxy\\OAB\\*") Filesystem.file_name="*.aspx"
|
||||
by _time span=1h Filesystem.process_id Filesystem.file_name Filesystem.file_path
|
||||
Filesystem.file_hash Filesystem.user | `drop_dm_object_name(Filesystem)` | rename
|
||||
process_id as aspx_pid | join aspx_pid, _time [| tstats `security_content_summariesonly`
|
||||
count FROM datamodel=Endpoint.Processes where Processes.process_name=w3wp.exe by
|
||||
_time span=1h Processes.parent_process_id Processes.process_id Processes.process_name
|
||||
Processes.dest Processes.process_path Processes.process | `drop_dm_object_name(Processes)`
|
||||
| rename parent_process_id as aspx_pid | fields _time aspx_pid process_id dest process_name
|
||||
process_path process] | `security_content_ctime(firstTime)` | `security_content_ctime(lastTime)`
|
||||
| table firstTime, lastTime, aspx_pid, process_id, user, dest, file_name, file_path,
|
||||
process_name, process, process_path, file_hash | `detect_exchange_web_shell_filter`'
|
||||
by _time span=1h Filesystem.dest Filesystem.file_create_time Filesystem.file_name
|
||||
Filesystem.file_path | `drop_dm_object_name(Filesystem)` | fields _time dest file_create_time
|
||||
file_name file_path process_name process_path process] | dedup file_create_time
|
||||
| table dest file_create_time, file_name, file_path, process_name | `detect_exchange_web_shell_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 `Processes` node and `Filesystem`
|
||||
@@ -37,7 +34,8 @@ references:
|
||||
tags:
|
||||
analytic_story:
|
||||
- HAFNIUM Group
|
||||
dataset: []
|
||||
dataset:
|
||||
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1505.003/windows-sysmon_proxylogon.log
|
||||
kill_chain_phases:
|
||||
- Exploitation
|
||||
mitre_attack_id:
|
||||
@@ -54,3 +52,4 @@ tags:
|
||||
- Filesystem.file_hash
|
||||
- Filesystem.user
|
||||
security_domain: endpoint
|
||||
automated_detection_testing: passed
|
||||
@@ -47,7 +47,7 @@ search: '| from read_ssa_enriched_events() | eval timestamp=parse_long(ucast(map
|
||||
OR process_name="pcalua.exe" OR process_name="cmdkey.exe" OR process_name="msconfig.exe")
|
||||
|
||||
| eval start_time = timestamp, end_time = timestamp, entities = mvappend(dest_device_id,
|
||||
dest_user_id), body = "TBD" | into write_ssa_detected_events();'
|
||||
dest_user_id), body = "TBD" | into write_null();'
|
||||
how_to_implement: Collect endpoint data such as sysmon or 4688 events.
|
||||
known_false_positives: 'Some custom tools used by admins could be used rarely to launch
|
||||
remotely applications. This might trigger false positives at the beginning when
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
name: Detect Exchange Web Shell Unit Test
|
||||
tests:
|
||||
- name: Detect Exchange Web Shell, ProxyLogon
|
||||
file: endpoint/detect_exchange_web_shell.yml
|
||||
pass_condition: '| stats count | where count > 0'
|
||||
earliest_time: '-24h'
|
||||
latest_time: 'now'
|
||||
attack_data:
|
||||
- file_name: windows-sysmon.log
|
||||
data: https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1505.003/windows-sysmon_proxylogon.log
|
||||
source: XmlWinEventLog:Microsoft-Windows-Sysmon/Operational
|
||||
sourcetype: xmlwineventlog
|
||||
@@ -2,7 +2,7 @@ name: Rare Parent/Child Process Relationship - SSA Unit Test
|
||||
tests:
|
||||
- name: Access LSASS Memory for Dump Creation
|
||||
file: endpoint/ssa___rare_parent_process_relationship_lolbas.yml
|
||||
pass_condition: '@count_gt(0)'
|
||||
pass_condition: '@count_eq(0)'
|
||||
description: Test detection looking for LOLBAS processes spawned by other processes that are rarely seen together
|
||||
attack_data:
|
||||
- file_name: T1059.all.labeled.lolbas-test.json
|
||||
|
||||
Reference in New Issue
Block a user