rebase with ssa end to end testing

This commit is contained in:
P4T12ICK
2021-03-17 17:58:11 +01:00
37 changed files with 524019 additions and 13 deletions
+64
View File
@@ -1,7 +1,17 @@
variables:
GIT_SUBMODULE_STRATEGY: recursive
K8S_PLAYGROUND_ROLE: k8s:v1/app-play1/namespace/threat-research/role/deployer
K8S_STAGING_ROLE: k8s:v1/app-stage1/namespace/tr-st-deploy/role/deployer
DOCKER_ROLE: docker:v1/registry/prefix/threat-research
K8_DEPLOYER_CONTAINER: docker.repo.splunkdev.net/threat-research/k8-deployer
SMOKETEST_RUNNER: docker.repo.splunkdev.net/threat-research/smoketest-runner
SRCBRANCH: $CI_COMMIT_REF_NAME
stages:
- publish_smoketest_runner
- publish_deployer
- smoketest_staging
- smoketest_play
- ssa-validate
validate:
@@ -14,3 +24,57 @@ validate:
- python3 bin/ssa_validate.py --skip-error detections/*/*
- python3 bin/ssa_test.py --skip-error tests/*/*
- python3 bin/testing_coverage.py --type streaming --min-coverage 1.0
publish_deployer:
stage: publish_deployer
image: docker.repo.splunkdev.net/ci-cd/ci-container:alpine-3.11
before_script:
- apk add --update docker
script:
- eval $(go-go vault -a ${DOCKER_ROLE})
- cd bin/ssa-end-to-end-testing/k8s-deployer
- docker build . -t ${K8_DEPLOYER_CONTAINER}:${CI_COMMIT_SHORT_SHA}
- docker push ${K8_DEPLOYER_CONTAINER}:${CI_COMMIT_SHORT_SHA}
publish_smoketest_runner:
stage: publish_smoketest_runner
image: docker.repo.splunkdev.net/ci-cd/ci-container:alpine-3.11
before_script:
- apk add --update docker
script:
- eval $(go-go vault -a ${DOCKER_ROLE})
- docker build bin/ssa-end-to-end-testing/smoke-test-runner -t ${SMOKETEST_RUNNER}:${CI_COMMIT_SHORT_SHA} --build-arg SRCBRANCH=$CI_COMMIT_REF_NAME
- docker push ${SMOKETEST_RUNNER}:${CI_COMMIT_SHORT_SHA}
smoketest_play:
stage: smoketest_play
dependencies:
- publish_smoketest_runner
- publish_deployer
image: ${K8_DEPLOYER_CONTAINER}:${CI_COMMIT_SHORT_SHA}
allow_failure: true
script:
- eval $(go-go vault -a ${K8S_PLAYGROUND_ROLE})
- eval $(go-go vault -a ${DOCKER_ROLE})
- cd /deployer/k8s
- ./deployer.sh
- ./check_deploy_status.sh
variables:
SCSENV: app_play1
SMOKETEST_RUNNER_IMAGE: ${SMOKETEST_RUNNER}:${CI_COMMIT_SHORT_SHA}
smoketest_staging:
stage: smoketest_staging
dependencies:
- publish_smoketest_runner
- publish_deployer
image: ${K8_DEPLOYER_CONTAINER}:${CI_COMMIT_SHORT_SHA}
script:
- eval $(go-go vault -a ${K8S_STAGING_ROLE})
- eval $(go-go vault -a ${DOCKER_ROLE})
- cd /deployer/k8s
- ./deployer.sh
- ./check_deploy_status.sh
variables:
SCSENV: app_staging1
SMOKETEST_RUNNER_IMAGE: ${SMOKETEST_RUNNER}:${CI_COMMIT_SHORT_SHA}
+8 -8
View File
@@ -1,4 +1,4 @@
ansible==2.10.7
ansible==3.0.0
ansible-runner==1.4.7
apipkg==1.5
aspy.yaml==1.3.0
@@ -6,22 +6,22 @@ atomicwrites==1.4.0
attackcti==0.3.4.3
attrs==20.3.0
azure-common==1.1.26
azure-core==1.10.0
azure-core==1.11.0
azure-identity==1.5.0
azure-mgmt-compute==18.2.0
azure-mgmt-core==1.2.2
azure-mgmt-network==17.1.0
azure-mgmt-resource==15.0.0
azure-mgmt-resource==16.0.0
bcrypt==3.2.0
boto3==1.16.61
botocore==1.19.63
boto3==1.16.62
botocore==1.20.0
certifi==2020.12.5
cffi==1.14.5
cfgv==2.0.1
chardet==4.0.0
configparser==5.0.2
contextlib2==0.6.0.post1
cryptography==3.3.2
cryptography==3.4.4
Deprecated==1.2.11
dnspython==2.1.0
docutils==0.16
@@ -36,7 +36,7 @@ jmespath==0.10.0
lockfile==0.12.2
MarkupSafe==1.1.1
mock==4.0.3
more-itertools==8.6.0
more-itertools==8.7.0
mysql-connector-python==8.0.23
nodeenv==1.3.4
ntlm-auth==1.5.0
@@ -47,7 +47,7 @@ path==15.1.2
path.py==12.5.0
pexpect==4.8.0
pluggy==0.13.1
pre-commit==2.9.3
pre-commit==2.10.0
protobuf==3.15.1
psutil==5.8.0
ptyprocess==0.7.0
+2 -2
View File
@@ -145,11 +145,11 @@ search = {{ baseline.search }}
{% for response_task in response_tasks %}
{% if response_task.search is defined %}
[{{ response_task.type | default('ESCU') }} - {{ response_task.name }} - Response Task]
[ESCU - {{ response_task.name }} - Response Task]
action.escu = 0
action.escu.enabled = 1
action.escu.search_type = investigative
action.escu.full_search_name = {{ response_task.type | default('ESCU') }} - {{ response_task.name }} - Response Task
action.escu.full_search_name = ESCU - {{ response_task.name }} - Response Task
description = {{ response_task.description }}
action.escu.creation_date = {{ response_task.date }}
action.escu.modification_date = {{ response_task.date }}
+11 -1
View File
@@ -34,4 +34,14 @@ def build_humvee(path):
log(logging.DEBUG, "Humvee details", detail=latest_humvee_object)
urllib.request.urlretrieve(latest_humvee_object['downloadUri'], humvee_path)
else:
log(logging.DEBUG, "Already latest checksum %s" % humvee_md5, detail=latest_humvee_object)
log(logging.DEBUG, "Already latest checksum %s" % humvee_md5, detail=latest_humvee_object)
#def convert_to_ssa(detection):
# '''
# curl -H 'Content-type: text/yaml' -H 'Authorization: Bearer TOKEN'
# https://app-admin.playground.scp.splunk.com/secanalytics/ssa-tenant-management/v1alpha1/admin/detection-spl/research2
# --data-binary @detections/endpoint/ssa___first_time_seen_cmd_line.yml
# @param detection:
# @return:
# '''
+11
View File
@@ -0,0 +1,11 @@
FROM docker.repo.splunkdev.net/ci-cd/ci-container:python-3.7-buster
RUN apt-get update
RUN apt-get install -y python3-dev git python-dev unzip python3-pip
RUN apt-get install -y python-gitdb
WORKDIR /app
RUN pip3 install -r requirements.txt
ENTRYPOINT ["python3", "detection_service.py"]
CMD ["-tfn", "T1003_002"]
+47
View File
@@ -0,0 +1,47 @@
# Running tests
You can run these tests using `pytest`. The required parameters a DSP `token`, the DSP `environment`, and the DSP `tenant`.
## Run all tests in `playground`
```angular2
pytest --env playground --tenant research2 --token <YOUR_DSP_TOKEN>
```
## Run all tests in `staging`
```angular2
pytest --env staging --tenant research --token <YOUR_DSP_TOKEN>
```
## Run specific tests
If you add the argument `-k` to pytest, then it can filter tests to run the tests specified.
```angular2
pytest --env playground --tenant research2 --token <YOUR_DSP_TOKEN> -k test_data_ingestion_preview
```
It also can run tests by category if naming conditions are maintained.
```angular2
pytest --env playground --tenant research2 --token <YOUR_DSP_TOKEN> -k test_data_ingestion
```
Will run tests `test_data_ingestion_preview` and `test_data_ingestion_index`.
## Best Practices
Save your token to an environment variable. Go to *playground* console and copy your token. Then..
```angular2
export SCLOUD_TOKEN_PLAY=$(pbpaste)
```
Go to your *staging* console and copy your token. Then..
```angular2
export SCLOUD_TOKEN_STAGING=$(pbpaste)
```
Now you can easily run tests on both playground, and staging.
+18
View File
@@ -0,0 +1,18 @@
def pytest_addoption(parser):
parser.addoption("--token", action="store", default="scs token")
parser.addoption("--env", action="store", default="scs environment")
parser.addoption("--tenant", action="store", default="environment's tenant")
def pytest_generate_tests(metafunc):
# This is called for every test. Only get/set command line arguments
# if the argument is specified in the list of test "fixturenames".
option_value = metafunc.config.option.token
if 'token' in metafunc.fixturenames and option_value is not None:
metafunc.parametrize("token", [option_value])
env = metafunc.config.option.env
if 'tenant' in metafunc.fixturenames and env is not None:
metafunc.parametrize("env", [env])
tenant = metafunc.config.option.tenant
if 'tenant' in metafunc.fixturenames and tenant is not None:
metafunc.parametrize("tenant", [tenant])
@@ -0,0 +1,30 @@
FROM docker.repo.splunkdev.net/ci-cd/ci-container:debian-buster
ARG SRCBRANCH=develop
ARG SCSENV=app_play1
ARG KUBECTL_VERSION=v1.10.9
ENV QBEC_VERSION 0.12.1
ENV QBEC_SHA 6fbec2c33ff15c603585a22c100aa839461680cf775a1f90336078bf79c6a42f
ENV SRCBRANCH=${SRCBRANCH}
ENV SCSENV=${SCSENV}
RUN mkdir /deployer && \
apt-get update && \
apt-get install -y bash tar curl && \
# Install kubetctl
curl -L https://dl.k8s.io/release/v${KUBECTL_VERSION}/bin/linux/amd64/kubectl -o /usr/local/bin/kubectl && \
chmod +x /usr/local/bin/kubectl && \
# Install qbec
curl -L -O https://github.com/splunk/qbec/releases/download/v${QBEC_VERSION}/qbec-linux-amd64.tar.gz && \
echo "${QBEC_SHA} qbec-linux-amd64.tar.gz" | sha256sum -c && \
tar -xzf qbec-linux-amd64.tar.gz -C /usr/local/bin qbec && rm -rf qbec-linux-amd64.tar.gz
COPY k8s /deployer/k8s
RUN chmod +x /deployer/k8s/deployer.sh
RUN chmod +x /deployer/k8s/check_deploy_status.sh
WORKDIR /deployer/k8s
@@ -0,0 +1,60 @@
#!/bin/bash
set -o pipefail
set -o nounset
set -o errexit
#This script is used to check the status of the smoketest k8s job: the tests pass if {.status.succeeded} returns 1.
# Then retrieve the job logs once it's completed.
smoketest_k8s_job=$(kubectl get jobs -o json | jq -r --arg JOBID "${CI_JOB_ID}" '.items[] | select(.kind=="Job") | select(.metadata.labels.ciJobId==$JOBID) | .metadata.name')
echo >&2 "Create smoketest k8s job: $smoketest_k8s_job"
smoketest_k8s_pod=$(kubectl get pods -o custom-columns=:metadata.name | grep ${smoketest_k8s_job})
echo >&2 "Create smoketest k8s pod: $smoketest_k8s_pod"
echo >&2 "Checking if smoketest starts running..."
set +o errexit
#Wait for job ready to run as background process, returns 1 if smoketest job still in Pod Initializing stage.
#Will exit if the pod is not up after 5 minutes(max_retries = 30)
job_running=1
counter=1
MAX_RETRIES=30
while [[ $job_running -ne 0 ]] && [[ $counter -le $MAX_RETRIES ]]; do
kubectl wait --for=condition=ready pod/${smoketest_k8s_pod} --timeout=10s
job_running=$?
echo >&2 "Checking if smoketest job starts running (return 0 if the job is ready to run):" $job_running
((counter++))
if [[ $counter -eq $MAX_RETRIES ]]; then
kubectl get pods
echo "Smoke test pod is not up after 5 minutes. Will exit."
exit 1
fi
done
set -o errexit
kubectl logs -f ${smoketest_k8s_pod}
echo >&2 "Complete retrieving smoketest job logs."
#Add more loggings to capture more info for k8s error that happens sporadically
set +o errexit
echo >&2 "Printing command: kubectl get job/${smoketest_k8s_job}"
echo >&2 "$(kubectl get job/${smoketest_k8s_job})"
echo >&2 "$(kubectl get job/${smoketest_k8s_job} -o jsonpath='{.status}')"
echo >&2 "Printing command with extra space: kubectl get job/${smoketest_k8s_job} "
echo >&2 "$(kubectl get job/${smoketest_k8s_job} )"
echo >&2 "$(kubectl get job/${smoketest_k8s_job} -o jsonpath='{.status}')"
echo >&2 "Printing command kubectl get jobs ${smoketest_k8s_job} -o jsonpath='{.status}'"
echo >&2 "$(kubectl get jobs ${smoketest_k8s_job} -o jsonpath='{.status}')"
echo >&2 "*****End of logging for k8s error debugging *****"
set -o errexit
SUCCESS=$(kubectl get job/${smoketest_k8s_job} -o jsonpath='{.status.succeeded}')
if [[ $SUCCESS -ne 1 ]]; then
echo "Smoke test failed. Please refer to the test logs."
exit 1
fi
echo "All smoke tests passed!"
@@ -0,0 +1,12 @@
local params = import '../params.libsonnet';
[
{
apiVersion: 'v1',
kind: 'ServiceAccount',
metadata: {
name: params.components.serviceAccountName,
},
},
]
@@ -0,0 +1,73 @@
local params = import '../params.libsonnet';
local job = {
apiVersion: 'batch/v1',
kind: 'Job',
metadata: {
generateName: 'ssa-smoketest-',
labels: {
name: 'ssa-smoketest',
uploaderLabel: std.extVar('CI_COMMIT_SHORT_SHA'),
ciJobId: std.extVar('CI_JOB_ID'),
},
},
spec: {
activeDeadlineSeconds: 600,
ttlSecondsAfterFinished: 100,
template: {
metadata: {
annotations: {
'splunk8s.io.vault/init-container': 'true',
},
},
spec: {
serviceAccountName: params.components.serviceAccountName,
restartPolicy: 'Never',
containers: [
{
name: 'ssa-smoke-test',
image: std.extVar('SMOKETEST_RUNNER_IMAGE'),
imagePullPolicy: 'Always',
env: [
{
"name": 'SRCBRANCH',
"value": std.extVar('SRCBRANCH')
},
{
"name": 'SMOKETEST_VAULT_READ_PATH',
"value": params.components.vaultReadPath
},
{
"name": 'DSP_ENV',
"value": params.components.dspEnv
},
{
"name": 'TENANT',
"value": params.components.tenant
},
{
"name": 'ENV',
"value": std.extVar('qbec.io/env')
},
],
resources: {
limits: {
cpu: '1',
memory: '1000Mi'
},
requests: {
cpu: '800m',
memory: '750Mi'
},
},
command: ['/bin/bash', '-c'],
args: ['./run_ssa_smoketest_helper.sh'],
},
],
},
},
},
};
job
@@ -0,0 +1,3 @@
#!/bin/bash
qbec --yes apply $SCSENV --vm:ext-str SRCBRANCH --vm:ext-str CI_COMMIT_SHORT_SHA --vm:ext-str CI_JOB_ID --vm:ext-str SMOKETEST_RUNNER_IMAGE -c service-account -c smoketest --wait-timeout "1m"
@@ -0,0 +1,7 @@
// this file has the baseline default parameters
{
components: {
serviceAccountName: "sa-tr"
},
}
@@ -0,0 +1,12 @@
// this file has the param overrides for the default environment
local base = import './base.libsonnet';
base {
components +: {
serviceAccountName: "sa-tr-playground",
vaultReadPath: 'scpauth-app-play1/token/threat-research-test.app-play1',
tenant: 'research2',
dspEnv: 'playground',
}
}
@@ -0,0 +1,12 @@
// this file has the param overrides for the default environment
local base = import './base.libsonnet';
base {
components +: {
serviceAccountName: "sa-tr-staging",
vaultReadPath: 'scpauth-app-stage1/token/threat-research-test.app-stage1',
tenant: 'research',
dspEnv: 'staging',
}
}
@@ -0,0 +1,13 @@
// this file returns the params for the current qbec environment
// you need to add an entry here every time you add a new environment.
local env = std.extVar('qbec.io/env');
local paramsMap = {
_: import './environments/base.libsonnet',
app_play1: import './environments/smoke-test-playground.libsonnet',
app_staging1: import './environments/smoke-test-staging.libsonnet',
};
if std.objectHas(paramsMap, env) then paramsMap[env] else error 'environment ' + env + ' not defined in ' + std.thisFile
@@ -0,0 +1,20 @@
apiVersion: qbec.io/v1alpha1
kind: App
metadata:
name: security-content-stream-smoke-test
spec:
environments:
app_play1:
defaultNamespace: threat-research
server: https://internal.api.app-play1.dev.us-west-2.splunk8s.io
app_staging1:
defaultNamespace: tr-st-deploy
server: https://internal.api.app-stage1.stage.us-west-2.splunk8s.io
vars:
external:
- name: SCBRANCH
default: develop
secret: false
- name: SMOKE_TEST
default: test
secret: false
@@ -0,0 +1,41 @@
03/12/2021 01:19:11 AM
LogName=Security
SourceName=Microsoft Windows security auditing.
EventCode=4688
EventType=0
Type=Information
ComputerName=win-dc-5236904.attackrange.local
TaskCategory=Process Creation
OpCode=Info
RecordNumber=540110
Keywords=Audit Success
Message=A new process has been created.
Creator Subject:
Security ID: ATTACKRANGE\SuperUser01
Account Name: SuperUser01
Account Domain: ATTACKRANGE
Logon ID: 0x1DB69D
Target Subject:
Security ID: NULL SID
Account Name: -
Account Domain: -
Logon ID: 0x0
Process Information:
New Process ID: 0xd4c
New Process Name: C:\Windows\system32\cmd.exe
Token Elevation Type: %%1936
Mandatory Label: Mandatory Label\High Mandatory Level
Creator Process ID: 0xc88
Creator Process Name: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
Process Command Line: "C:\Windows\system32\cmd.exe" /c "reg save HKLM\sam %temp%\sam & reg save HKLM\system %temp%\system & reg save HKLM\Security %temp%\security"
Token Elevation Type indicates the type of token that was assigned to the new process in accordance with User Account Control policy.
Type 3 is a full token with no privileges removed or groups disabled. A full token is only used if User Account Control is disabled or if the user is the built-in Administrator account or a service account.
Type 2 is an elevated token with no privileges removed or groups disabled. An elevated token is used when User Account Control is enabled and the user chooses to start the program using Run as administrator. An elevated token is also used when an application is configured to always require administrative privilege or to always require maximum privilege, and the user is a member of the Administrators group.
Type 3 is a limited token with administrative privileges removed and administrative groups disabled. The limited token is used when User Account Control is enabled, the application does not require administrative privilege, and the user does not choose to start the program using Run as administrator.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,52 @@
import json
from datetime import datetime
from datetime import timedelta
from pytz import timezone
import fileinput
import os
import re
import io
class DataManipulation:
def manipulate_timestamp(self, file_path, sourcetype, source):
if source == 'WinEventLog:Security':
return self.manipulate_timestamp_windows_event_log_raw(file_path)
def manipulate_timestamp_windows_event_log_raw(self, file_path):
path = file_path
f = io.open(path, "r", encoding="utf-8")
pst = timezone('US/Pacific')
self.now = datetime.now(pst)
self.now = self.now.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
self.now = datetime.strptime(self.now,"%Y-%m-%dT%H:%M:%S.%fZ")
# read raw logs
regex = r'\d{2}/\d{2}/\d{4} \d{2}:\d{2}:\d{2} [AP]M'
data = f.read()
lst_matches = re.findall(regex, data)
if len(lst_matches) > 0:
latest_event = datetime.strptime(lst_matches[-1],"%m/%d/%Y %I:%M:%S %p")
self.difference = self.now - latest_event
f.close()
result = re.sub(regex, self.replacement_function, data)
with io.open(path + ".swp", "w+", encoding='utf8') as f:
f.write(result)
return path + ".swp"
else:
f.close()
return path
def replacement_function(self, match):
try:
event_time = datetime.strptime(match.group(),"%m/%d/%Y %I:%M:%S %p")
new_time = self.difference + event_time
return new_time.strftime("%m/%d/%Y %I:%M:%S %p")
except Exception as e:
return match.group()
@@ -0,0 +1,56 @@
import git
import os
import logging
from os import path
import sys
# Logger
logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO"))
LOGGER = logging.getLogger(__name__)
SECURITY_CONTENT_URL = f"https://github.com/splunk/security_content"
class GithubService:
def __init__(self, security_content_branch):
self.security_content_branch = security_content_branch
self.security_content_repo_obj = self.clone_project(SECURITY_CONTENT_URL, f"security_content", f"develop")
self.security_content_repo_obj.git.checkout(security_content_branch)
def clone_project(self, url, project, branch):
LOGGER.info(f"Clone Security Content Project")
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'
g = git.Git('security_content')
differ = g.diff('--name-only', branch1, branch2)
changed_files = differ.splitlines()
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'):
if os.path.basename(file_path).startswith('ssa'):
if file_path not in changed_ssa_test_files:
changed_ssa_test_files.append(file_path)
# 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'
if file_path_new not in changed_ssa_test_files:
changed_ssa_test_files.append(file_path_new)
return changed_ssa_test_files
@@ -0,0 +1,58 @@
import sys
import yaml
import time
import os
import requests
import shutil
from .data_manipulation import DataManipulation
def load_file(file_path):
with open(file_path, 'r', encoding="utf-8") as stream:
try:
file = list(yaml.safe_load_all(stream))[0]
except yaml.YAMLError as exc:
sys.exit("ERROR: reading {0}".format(file_path))
return file
def prepare_test(file_path):
# read test file and return as object
test_obj = load_file('security_content/' + file_path)
detection_obj = load_file('security_content/detections/' + test_obj['tests'][0]['file'])
# download attack data
epoch_time = str(int(time.time()))
folder_name = "attack_data_" + epoch_time
os.mkdir(folder_name)
for test in test_obj['tests']:
for attack_data in test['attack_data']:
url = attack_data['data']
r = requests.get(url, allow_redirects=True)
attack_data_file_path = folder_name + '/' + attack_data['file_name']
open(folder_name + '/' + attack_data['file_name'], 'wb').write(r.content)
# Update timestamps before replay
if 'update_timestamp' in attack_data:
if attack_data['update_timestamp'] == True:
data_manipulation = DataManipulation()
data_manipulation.manipulate_timestamp(folder_name + '/' + attack_data['file_name'], attack_data['sourcetype'], attack_data['source'])
dict_test = {
"test_obj": test_obj,
"detection_obj": detection_obj,
"attack_data_file_path": attack_data_file_path
}
return dict_test, folder_name
def remove_security_content():
shutil.rmtree('security_content')
def remove_attack_data(file_path):
# remove attack data
shutil.rmtree(file_path)
@@ -0,0 +1,20 @@
| from read_ssa_enriched_events()
| eval timestamp=parse_long(ucast(map_get(input_event, "_time"), "string", null))
| eval process_name=lower(ucast(map_get(input_event, "process_name"), "string", null)),
cmd_line=ucast(map_get(input_event, "process"), "string", null),
dest_user_id=ucast(map_get(input_event, "dest_user_id"), "string", null),
dest_device_id=ucast(map_get(input_event, "dest_device_id"), "string", null)
| where process_name="cmd.exe" OR process_name="reg.exe"
| where cmd_line != null AND
match_regex(cmd_line, /(?i)save\s+/)=true AND
( match_regex(cmd_line, /(?i)HKLM\\Security/)=true OR
match_regex(cmd_line, /(?i)HKLM\\SAM/)=true OR
match_regex(cmd_line, /(?i)HKLM\\System/)=true OR
match_regex(cmd_line, /(?i)HKEY_LOCAL_MACHINE\\Security/)=true OR
match_regex(cmd_line, /(?i)HKEY_LOCAL_MACHINE\\SAM/)=true OR
match_regex(cmd_line, /(?i)HKEY_LOCAL_MACHINE\\System/)=true
)
| eval start_time = timestamp,
end_time = timestamp,
entities = mvappend(dest_device_id, dest_user_id),
body = "TBD";
@@ -0,0 +1,20 @@
| from read_ssa_enriched_events()
| eval timestamp=parse_long(ucast(map_get(input_event, "_time"), "string", null))
| eval process_name=lower(ucast(map_get(input_event, "process_name"), "string", null)),
cmd_line=ucast(map_get(input_event, "process"), "string", null),
dest_user_id=ucast(map_get(input_event, "dest_user_id"), "string", null),
dest_device_id=ucast(map_get(input_event, "dest_device_id"), "string", null)
| where process_name="cmd.exe" OR process_name="reg.exe"
| where cmd_line != null AND
match_regex(cmd_line, /(?i)save\s+/)=true AND
( match_regex(cmd_line, /(?i)HKLM\\Security/)=true OR
match_regex(cmd_line, /(?i)HKLM\\SAM/)=true OR
match_regex(cmd_line, /(?i)HKLM\\System/)=true OR
match_regex(cmd_line, /(?i)HKEY_LOCAL_MACHINE\\Security/)=true OR
match_regex(cmd_line, /(?i)HKEY_LOCAL_MACHINE\\SAM/)=true OR
match_regex(cmd_line, /(?i)HKEY_LOCAL_MACHINE\\System/)=true
)
| eval start_time = timestamp,
end_time = timestamp,
entities = mvappend(dest_device_id, dest_user_id),
body = "TBD";
@@ -0,0 +1 @@
| from read_splunk_firehose();
@@ -0,0 +1 @@
| from read_splunk_firehose();
@@ -0,0 +1 @@
| from read_ssa_enriched_events();
@@ -0,0 +1,504 @@
"""
helper functions to use the Streams Service API (v3beta1) to perform create, read and delete operations on
data pipeline.
API doc: https://dev.splunk.com/enterprise/reference/api/streams/v3beta1
"""
import logging
import os
import uuid
import requests
from http import HTTPStatus
#from constants import ML_MODEL_CONNECTOR_UUID
from modules.utils import request_headers
# Logger
logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO"))
LOGGER = logging.getLogger(__name__)
TENANT_PLAYGROUND = f"research2"
TENANT_STAGING = f"research"
BASE_URL_PLAYGROUND = f"https://api.playground.scp.splunk.com/"
BASE_URL_STAGING = f"https://api.staging.scp.splunk.com/"
# Streaming Pipelines REST endpoints
CONNECTIONS_ENDPOINT = f"streams/v3beta1/connections"
PIPELINES_ENDPOINT = f"streams/v3beta1/pipelines"
PIPELINES_COMPILE_ENDPOINT = f"streams/v3beta1/pipelines/compile"
PIPELINES_VALIDATE_ENDPOINT = f"streams/v3beta1/pipelines/validate"
PIPELINES_REGISTRY_ENDPOINT = f"streams/v3beta1/pipelines/registry"
PREVIEW_SESSION_ENDPOINT = f"streams/v3beta1/preview-session"
PREVIEW_DATA_ENDPOINT = f"streams/v3beta1/preview-data"
INGEST_ENDPOINT = f"ingest/v1beta2/events"
SUBMIT_SEARCH_ENDPOINT = f"search/v2beta1/jobs"
DATASETS_ENDPOINT = f"catalog/v2beta1/datasets"
class DSPApi:
def __init__(self, env, tenant, header_token):
self.env = env
self.tenant = tenant
self.header_token = header_token
def return_api_endpoint(self, endpoint):
if self.env == 'playground':
return f"{BASE_URL_PLAYGROUND}{TENANT_PLAYGROUND}/{endpoint}"
else:
return f"{BASE_URL_STAGING}{TENANT_STAGING}/{endpoint}"
def compile_spl(self, spl):
"""
Compile SPL text to a UPL JSON
Parameters
----------
spl: str
the SPL representation of a pipeline or function parameter to be compiled
Returns
-------
upl:
JSON representation of the compiled AST
"""
data = {"spl": spl}
LOGGER.debug(f"Compiling SPL into UPL")
#LOGGER.info(f"{spl}")
response = requests.post(self.return_api_endpoint(PIPELINES_COMPILE_ENDPOINT), json=data, headers=request_headers(self.header_token))
upl = response.json()
#LOGGER.info(f"POST compile response_body is: {upl}")
LOGGER.info(f"Successfully compile spl to upl")
return upl, response
def validate_upl(self, upl):
"""
Validate whether the JSON representation of a pipeline is valid
Parameters
----------
upl: JSON
JSON representation of the compiled AST of a pipeline
Returns
-------
response_body: JSON
returns whether or not the pipeline id valid. If valid, the response body returns 'success'
"""
headers = {"Content-Type": "application/json", "Authorization": self.header_token}
data = {"upl": upl}
response = requests.post(self.return_api_endpoint(PIPELINES_VALIDATE_ENDPOINT), json=data, headers=headers)
response_body = response.json()
if response.status_code == HTTPStatus.OK:
LOGGER.info(f"UPL is validated.")
return upl, response_body
else:
LOGGER.error(f"UPL validation failed: {response_body}")
return response_body
def create_pipeline(self, upl):
"""
POST pipelines endpoint to create a pipeline based on the valid upl
Parameters
----------
upl: JSON
JSON representation of the validated pipeline details
Returns
-------
pipline_id: UUID
id of the created pipeline
"""
headers = {"Content-Type": "application/json", "Authorization": self.header_token}
set_test_id = uuid.uuid4().hex
data = {
"name": f"ssa_smoke_test_pipeline_helper_{set_test_id}",
"description": "ssa_test_pipeline_description",
"bypassValidation": "true",
"data": upl
}
response = requests.post(self.return_api_endpoint(PIPELINES_ENDPOINT), json=data, headers=headers)
response_body = response.json()
if response.status_code == HTTPStatus.CREATED:
pipeline_id = response_body.get("id")
#LOGGER.info(f"Pipeline {pipeline_id} successfully created")
return pipeline_id
def create_pipeline_from_spl(self, spl):
"""
helper function to compile and validate from spl text, then create the pipeline
"""
upl, _ = self.compile_spl(spl)
validated_upl, _ = self.validate_upl(upl)
pipeline_id = self.create_pipeline(validated_upl)
LOGGER.info(f"pipeline id created is: {pipeline_id}")
return pipeline_id
def activate_pipeline(self, pipeline_id):
"""
POST pipelines/activate endpoint to activate an existing pipeline
Parameters
----------
pipeline_id: str
pipeline UUID to activate
Returns
-------
response_body: response JSON
response body that contains pipeline status ACTIVATED
"""
assert(pipeline_id is not None), "Must specify a 'pipeline_id'"
headers = {"Content-Type": "application/json", "Authorization": self.header_token}
pipelines_activate_endpoint = self.return_api_endpoint(PIPELINES_ENDPOINT) + "/" + pipeline_id + "/activate"
data = {
"activateLatestVersion": "true",
"allowNonRestoredState": "true",
"skipRestoreState": "true"
}
response = requests.post(pipelines_activate_endpoint, json=data, headers=headers)
response_body = response.json()
if response.status_code == HTTPStatus.OK:
pipeline_id = response_body.get("id")
LOGGER.info(f"Pipeline {pipeline_id} successfully created")
return response_body
else:
LOGGER.error(f"Failed to activate pipeline {pipeline_id}")
return
def deactivate_pipeline(self, pipeline_id):
"""
POST pipelines/deactivate endpoint to deactivate an existing pipeline
Parameters
----------
pipeline_id:
pipeline UUID to deactivate
Returns
-------
response_body: response JSON
response body that contains the status of the pipeline after deactivation
"""
assert(pipeline_id is not None), "Must specify a 'pipeline_id'"
headers = {"Content-Type": "application/json", "Authorization": self.header_token}
pipelines_deactivate_endpoint = self.return_api_endpoint(PIPELINES_ENDPOINT) + "/" + pipeline_id + "/deactivate"
data = {
"skipSavepoint": "true"
}
response = requests.post(pipelines_deactivate_endpoint, json=data, headers=headers)
response_body = response.json()
return response, response_body
def delete_pipeline(self, pipeline_id):
"""
Delete an existing pipeline using its pipeline UUID
Parameters
----------
pipeline_id:
pipeline UUID of an existing pipeline
Returns
-------
response status code
"""
assert(pipeline_id is not None), "Must specify a 'pipeline_id'"
headers = {"Content-Type": "application/json", "Authorization": self.header_token}
delete_pipeline_endpoint = self.return_api_endpoint(PIPELINES_ENDPOINT) + "/" + pipeline_id
response = requests.delete(delete_pipeline_endpoint, headers=headers)
LOGGER.info(f"DELETE pipeline response status code is: {response.status_code}")
return response
def pipeline_status(self, pipeline_id):
"""
Returns the statues of an existing pipeline
Parameters
----------
pipeline_id:
pipeline UUID of an existing pipeline.
Returns
-------
pipeline_status: str
pipeline status can be CREATED, ACTIVATED, FINISHED, RESTARTING, FAILED.
"""
assert(pipeline_id is not None), "Must specify a 'pipeline_id'"
headers = {"Content-Type": "application/json", "Authorization": self.header_token}
pipelines_status_endpoint = self.return_api_endpoint(PIPELINES_ENDPOINT) + "/" + pipeline_id
response = requests.get(pipelines_status_endpoint, headers=headers)
response_body = response.json()
if response.status_code == HTTPStatus.OK:
pipeline_status = response_body.get("status")
return pipeline_status
else:
LOGGER.error(f"Fail to get current status of pipeline pipeline {pipeline_id}")
return
def get_preview_id(self, upl):
"""
POST preview-session endpoint to create a preview session for a pipeline
Parameters
----------
upl: JSON
JSON representation of the pipeline details
Returns
-------
preview_id: str
preview session id
"""
headers = {"Content-Type": "application/json", "Authorization": self.header_token}
data = {
"upl": upl
}
response = requests.post(self.return_api_endpoint(PREVIEW_SESSION_ENDPOINT), json=data, headers=headers)
response_body = response.json()
# The preview sessions was started successfully
if response.status_code == HTTPStatus.CREATED:
preview_id = response_body.get("previewId")
assert(preview_id is not None), "Must return a 'preview_id'"
return preview_id
def get_preview_data(self, preview_id):
"""
POST preview-data endpoint to get the preview data for a preview session
Parameters
----------
preview_id: str
preview session id
Returns
-------
response_body: response JSON
the response would contain current number of records, preview data
"""
assert(preview_id is not None), "Must specify a 'preview_id'"
headers = {"Content-Type": "application/json", "Authorization": self.header_token}
preview_data_endpoint = self.return_api_endpoint(PREVIEW_DATA_ENDPOINT) + "/" + str(preview_id)
response = requests.get(preview_data_endpoint, headers=headers)
response_body = response.json()
if response.status_code != HTTPStatus.OK:
LOGGER.error(f"Failed to preview data from the pipeline. Please check if the operator has been properly "
f"uploaded to DSP.")
return
else:
return response, response_body
def stop_preview_session(self, preview_id):
"""
Delete an existing pipeline using its pipeline UUID
Parameters
----------
preview_id: str
preview session id
Returns
-------
Boolean: True if preview session is stopped.
"""
assert(preview_id is not None), "Must specify a 'preview_id'"
headers = {"Content-Type": "application/json", "Authorization": self.header_token}
stop_preview_session_endpoint = self.return_api_endpoint(PREVIEW_SESSION_ENDPOINT) + "/" + str(preview_id)
response = requests.delete(stop_preview_session_endpoint, headers=headers)
LOGGER.info(f"DELETE/preview-session response status code is: {response.status_code}")
return response
def get_preview_id_from_spl(self, spl):
"""
helper function to compile and validate from spl text, then create the pipeline preview session
"""
upl, _ = self.compile_spl(spl)
validated_upl, _ = self.validate_upl(upl)
preview_id = self.get_preview_id(validated_upl)
LOGGER.info(f"preview id created is: {preview_id}")
return preview_id
def ingest_data(self, data):
"""
Send events
Parameters
----------
data: str
datafile
Returns
-------
response
response body in JSON format
"""
data = [{
"body": data,
"sourcetype": "WinEventLog"
}]
response = requests.post(self.return_api_endpoint(INGEST_ENDPOINT), json=data, headers=request_headers(self.header_token))
return response.json()
def submit_search_job(self, module, query):
"""
Submit Search job
Parameters
----------
module: str
module where this query will be run on (default: `mc`)
query: str
SPL of the search query
Returns
-------
sid
sid to search job
"""
data = {
"query": query,
"module": module
}
LOGGER.info(f"Submit Search Job")
response = requests.post(self.return_api_endpoint(SUBMIT_SEARCH_ENDPOINT), json=data, headers=request_headers(self.header_token))
if response.status_code != HTTPStatus.CREATED:
LOGGER.error(f"Submit search job failed.")
return None
else:
response_body = response.json()
return response_body.get("sid")
def check_search_job_finished(self, sid):
"""
Check if a search job finished
Parameters
----------
sid: str
ID of the search. Returned value from `submit_search_job`
Returns
-------
response
boolean true or false
"""
LOGGER.info(f"Check Search job results")
results_check_search_job = self.return_api_endpoint(SUBMIT_SEARCH_ENDPOINT) + "/" + sid
response = requests.get(results_check_search_job, headers=request_headers(self.header_token))
if response.status_code != HTTPStatus.OK:
LOGGER.error(f"Failed to get status of search job")
return None
else:
response_json = response.json()
LOGGER.info(f"Check if search is finished.")
if response_json.get("status") == "done":
return True
else:
return False
def get_search_job_results(self, sid):
"""
Get Search Job Results
Parameters
----------
sid: str
ID of the search. Returned value from `submit_search_job`
Returns
-------
results
results of search
"""
LOGGER.info(f"Get Search job results")
results_search_job_endpoint = self.return_api_endpoint(SUBMIT_SEARCH_ENDPOINT) + "/" + sid + "/results"
response = requests.get(results_search_job_endpoint, headers=request_headers(self.header_token))
if response.status_code != HTTPStatus.OK:
LOGGER.error(f"Failed to get search results")
return None
else:
response_body = response.json()
return response_body.get("results")
def create_temp_index(self, module):
"""
Creates an index under module
Parameters
@param module: str
module under this index will be created
@return:
index object dictionary
"""
index_name = f"temp_st_{uuid.uuid1()}".replace("-", "_")
data = {
"module": module,
"name": index_name,
"kind": "index",
"disabled": False
}
response = requests.post(self.return_api_endpoint(DATASETS_ENDPOINT), headers=request_headers(self.header_token), json=data)
LOGGER.info(f"Create Temp Index {index_name}")
return response.json()
def delete_temp_index(self, index_id):
"""
Deletes an index
@param index_id:
Index ID
@return:
response status code from API
"""
LOGGER.info(f"Delete Temp Index")
datasets_endpoint_api = self.return_api_endpoint(DATASETS_ENDPOINT)
delete_url = f"{datasets_endpoint_api}/{index_id}"
response = requests.delete(delete_url, headers=request_headers(self.header_token))
return response.status_code
@@ -0,0 +1,214 @@
import logging
import os
import time
import sys
from http import HTTPStatus
from modules.streams_service_api_helper import DSPApi
from modules.utils import manipulate_spl, read_spl, read_data
# Logger
logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO"))
LOGGER = logging.getLogger(__name__)
# MACROS
SLEEP_TIME_CREATE_INDEX = 10
SLEEP_TIME_ACTIVATE_PIPELINE = 10
SLEEP_TIME_SEND_DATA = 30
WAIT_CYCLE = 20
MAX_EXECUTION_TIME_LIMIT = 1200 # per detection test
TEST_DATASET = 'windows-security_small.txt'
class SSADetectionTesting:
def __init__(self, env, tenant, header_token):
self.execution_passed = True
self.max_execution_time = MAX_EXECUTION_TIME_LIMIT
self.env = env
self.tenant = tenant
self.header_token = f"Bearer {header_token}"
self.api = DSPApi(env, tenant, self.header_token)
self.test_results = {}
def test_dsp_pipeline(self):
file_path_data = os.path.join(os.path.dirname(__file__), 'data', TEST_DATASET)
file_path_spl = os.path.join(os.path.dirname(__file__), 'spl')
test_spls = ['troubleshoot.spl', 'detection.spl']
test_names = [
"SSA Index Test Minimal",
"SSA Index Detection Testing Example"
]
test_results = []
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())
passed = True
LOGGER.info('-----------------------------------')
LOGGER.info('-------- test DSP Pipeline --------')
LOGGER.info('-----------------------------------')
for test_result in test_results:
LOGGER.info(test_result['msg'])
passed = passed and test_result['result']
LOGGER.info('-----------------------------------')
return passed
def test_ssa_detections(self, test_obj):
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"])
return test_results
## Helper Functions ##
def update_execution_time(self, time_frame):
self.max_execution_time = self.max_execution_time - WAIT_CYCLE
if self.max_execution_time < 0:
return True
else:
return False
def wait_time(self, time_in_s):
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")
def ssa_detection_test_main(self, spl, source, test_name):
self.execution_passed = True
self.wait_time(SLEEP_TIME_CREATE_INDEX)
spl = manipulate_spl(self.api.env, spl, self.results_index)
self.check_result(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")
_pipeline_status = self.api.pipeline_status(pipeline_id)
self.check_result(_pipeline_status=="CREATED", f"Current status of pipeline {pipeline_id} should be CREATED")
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.")
self.wait_time(SLEEP_TIME_ACTIVATE_PIPELINE)
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.")
return self.test_results
for d in data:
response_body = self.api.ingest_data(d)
self.wait_time(SLEEP_TIME_SEND_DATA)
search_results = False
max_execution_time_reached = False
while not (search_results or max_execution_time_reached):
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")
job_finished = False
while not job_finished:
self.wait_time(WAIT_CYCLE)
result = self.api.check_search_job_finished(sid)
job_finished = result
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")
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.")
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)
return self.test_results
def ssa_detection_test_teardown(self):
pass
self.api.delete_temp_index(self.results_index["id"])
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
# only for troubleshooting
# def ssa_detection_in_dsp_with_preview_session(self, spl, source, test_name):
# self.execution_passed = True
# spl = manipulate_spl(self.api.env, spl)
# self.check_result(spl is not None, "fail to read dummy spl file")
# preview_id = self.api.get_preview_id_from_spl(spl)
# self.check_result(preview_id is not None, "failed to create a preview session %s" % spl)
# time.sleep(SLEEP_TIME_SHORT)
# data = read_data(source)
# response_body = self.api.ingest_data(data)
# time.sleep(SLEEP_TIME_LONG)
# response, response_body = self.api.get_preview_data(preview_id)
# self.check_result(response_body.get("currentNumberOfRecords") > 0, "Missing records in preview session.")
# response = self.api.stop_preview_session(preview_id)
# self.write_test_results(test_name)
+126
View File
@@ -0,0 +1,126 @@
import json
import logging
import os
import fileinput
import re
from .data_manipulation import DataManipulation
# Logger
logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO"))
LOGGER = logging.getLogger(__name__)
# Macros
PULSAR_SOURCE_CONNECTION_ID_PLAYGROUND = f"29fb61f1-9342-48f5-9793-1afa008c377b"
PULSAR_SOURCE_TOPIC_PLAYGROUND = f"persistent://ssa/egress/decorated-events-research2"
PULSAR_SOURCE_CONNECTION_ID_STAGING = f"fd92bf9f-5d40-4c2e-bb75-bf0c3fc13980"
PULSAR_SOURCE_TOPIC_STAGING = f"persistent://ssa/egress/decorated-events-research"
READ_SSA_ENRICHED_EVENTS = f"| from read_ssa_enriched_events()"
READ_SSA_ENRICHED_EVENTS_EXPANDED = (
f"| from pulsar(\"__PULSAR_SOURCE_CONNECTION_ID__\", \"__PULSAR_SOURCE_TOPIC__\")"
f"| eval input_event=deserialize_json_object(value)"
f"| select input_event"
f"| eval _datamodels=ucast(map_get(input_event, \"_datamodels\"), \"collection<string>\", [])"
f",body={{}}"
)
# not used in the moment
# PULSAR_SINK_CONNECTION_ID = f"29fb61f1-9342-48f5-9793-1afa008c377b"
# PULSAR_SINK_TOPIC = f"persistent://ssa/ingress/detection-events-research2"
WRITE_SSA_DETECTED_EVENTS = f"| into write_ssa_detected_events();"
# ## dummy values ##
# DETECTION_TYPE = f"anomaly"
# DETECTION_ID = f"93fbec4e-0375-440c-8db3-4508eca470c4"
# DETECTION_VERSION = f"1"
# RISK_SEVERITY = f"low"
# ## dummy values ##
# WRITE_SSA_DETECTED_EVENTS_EXPANDED = (
# f"| eval create_time=time()"
# f", type=\"{DETECTION_TYPE}\""
# f", detection_id=\"{DETECTION_ID}\""
# f", detection_version=\"{DETECTION_VERSION}\""
# f", risk_severity=\"{RISK_SEVERITY}\""
# f"| select create_time"
# f", start_time"
# f", end_time"
# f", type"
# f", detection_id"
# f", detection_version"
# f", risk_severity"
# f", entities"
# f", body"
# f"| eval id=concat(\"sha256:\", base64_encode(sha256(serialize_json())))"
# f"| select id, serialize_json() AS value"
# f"| into pulsar(\"{PULSAR_SINK_CONNECTION_ID}\", \"{PULSAR_SINK_TOPIC}\", id, value);"
# )
def fetch_token(file):
with open(file) as f:
data = json.load(f)
try:
token = data["data"]["token"]
except RuntimeError as e:
LOGGER.error(f"JSON is invalid, no IAC token found in Vault JSON output: {e}")
return token
def request_headers(header_token):
headers = {"Content-Type": "application/json", "Authorization": header_token}
return headers
def manipulate_spl(env, spl, results_index):
spl = replace_ssa_macros(env, spl)
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}\");"
LOGGER.info(f"spl: {spl}")
return spl
def read_spl(file_path, file_name):
full_path = os.path.join(file_path, file_name)
spl = open(full_path, "r").read()
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", " ")
return spl
def read_data(file_name):
file_path = os.path.join(os.path.dirname(__file__), 'data', file_name)
data_manipulation = DataManipulation()
modified_file = data_manipulation.manipulate_timestamp(file_path, 'xmlwineventlog', 'WinEventLog:Security')
data = []
event = ""
date_rex = r'\d{2}/\d{2}/\d{4} \d{2}:\d{2}:\d{2} [AP]M'
for line in fileinput.input(files=modified_file):
if event != "" and re.match(date_rex, line):
data.append(event)
event = line
else:
event = event + line
return data
def return_macros(env):
if env == "playground":
return PULSAR_SOURCE_CONNECTION_ID_PLAYGROUND, PULSAR_SOURCE_TOPIC_PLAYGROUND
else:
return PULSAR_SOURCE_CONNECTION_ID_STAGING, PULSAR_SOURCE_TOPIC_STAGING
@@ -0,0 +1,23 @@
attrs==20.3.0
certifi==2020.12.5
chardet==4.0.0
gitdb==4.0.5
GitPython==3.1.14
idna==2.10
iniconfig==1.1.1
install==1.3.4
packaging==20.9
pluggy==0.13.1
py==1.10.0
pyparsing==2.4.7
pytest==6.2.2
pytest-check==1.0.1
pytest-expect==1.1.0
pytz==2021.1
PyYAML==5.4.1
requests==2.25.1
six==1.15.0
smmap==3.0.5
toml==0.10.2
u-msgpack-python==2.7.1
urllib3==1.26.3
@@ -0,0 +1,77 @@
import os
import sys
import argparse
import logging
from modules.github_service import GithubService
from modules.test_ssa_detections import SSADetectionTesting
from modules.security_content_handler import prepare_test, remove_attack_data, remove_security_content
# Logger
logging.basicConfig(level=os.environ.get("LOGLEVEL", "INFO"))
LOGGER = logging.getLogger(__name__)
def main(args):
parser = argparse.ArgumentParser(description="SSA detection smoke test")
parser.add_argument("-t", "--token", required=True,
help="specify the scloud token")
parser.add_argument("-e", "--env", required=True,
help="specify the environment")
parser.add_argument("-s", "--tenant", required=True,
help="specify the tenant in the environment")
parser.add_argument("-b", "--branch", required=True,
help="specify the security content branch")
args = parser.parse_args()
token = args.token
env = args.env
tenant = args.tenant
branch = args.branch
# Retrieve Security Content
github_service = GithubService(branch)
test_files_ssa = github_service.get_changed_test_files_ssa()
LOGGER.info('changed/added GitHub files:')
for test_file in test_files_ssa:
LOGGER.info(test_file)
if len(test_files_ssa)==0:
LOGGER.info('Nothing to test for SSA smoke test.')
sys.exit(0)
# test DSP and SSA pipeline
ssa_detection_testing = SSADetectionTesting(env, tenant, token)
test_result_passed = ssa_detection_testing.test_dsp_pipeline()
if not test_result_passed:
sys.exit(1)
# # test SSA detections
test_results = []
test_passed = True
for test_file in test_files_ssa:
test_obj, attack_data_folder = prepare_test(test_file)
test_result = ssa_detection_testing.test_ssa_detections(test_obj)
test_results.append(test_result)
remove_attack_data(attack_data_folder)
LOGGER.info('-----------------------------------')
LOGGER.info('------- test SSA detections -------')
LOGGER.info('-----------------------------------')
for test_result in test_results:
test_passed = test_passed and test_result['result']
LOGGER.info(test_result['msg'])
LOGGER.info('-----------------------------------')
remove_security_content()
exit_code = not test_passed
sys.exit(exit_code)
if __name__ == "__main__":
main(sys.argv[1:])
@@ -0,0 +1,9 @@
#!/bin/bash
export VAULT_TOKEN=`cat /vault/.vault-token`
SCLOUD_TOKEN=$(vault read -format json $SMOKETEST_VAULT_READ_PATH tenant=$TENANT | jq -r '.data.token')
virtualenv -p python3 smoketest && source smoketest/bin/activate && pip3 install -r requirements.txt
python run_ssa_smoketest.py -t $SCLOUD_TOKEN -e $DSP_ENV -s $TENANT -b $SRCBRANCH
@@ -0,0 +1,18 @@
# Base image that support pod to communicate with SCS Vault to fetch IAC token as well to support python3 pytest framework
FROM docker.repo.splunkdev.net/ci-cd/ci-container:python-3.7-buster
ARG SRCBRANCH=develop
RUN mkdir smoketest && \
/usr/local/bin/python -m pip install --upgrade pip && \
pip install pytest && pip install requests && pip install virtualenv
RUN git clone https://github.com/splunk/security_content.git -b ${SRCBRANCH}
RUN chmod +x security_content/bin/ssa-end-to-end-testing/run_ssa_smoketest_helper.sh
RUN curl -L https://github.com/splunk/splunk-cloud-sdk-go/releases/download/v1.11.1/scloud_v7.1.0_linux_amd64.tar.gz | tar -xz -C /usr/bin
COPY --from=docker.repo.splunkdev.net/kub/debug-tools:latest /usr/local/bin/vault /usr/local/bin/vault
WORKDIR /security_content/bin/ssa-end-to-end-testing
+2 -2
View File
@@ -20,10 +20,10 @@ importlib-resources==5.1.2
Jinja2==2.11.3
jsonschema==3.2.0
MarkupSafe==1.1.1
more-itertools==8.6.0
more-itertools==8.7.0
nodeenv==1.5.0
pathlib2==2.3.5
pre-commit==2.9.3
pre-commit==2.10.0
prompt-toolkit==1.0.14
Pygments==2.8.0
PyInquirer==1.0.3