mirror of
https://github.com/splunk/security_content
synced 2026-06-08 17:32:49 +00:00
Merge pull request #2077 from splunk/refactored_security_content_new
Refactored security content
This commit is contained in:
@@ -10,8 +10,6 @@
|
||||
#https://docs.github.com/en/actions/guides/storing-workflow-data-as-artifacts
|
||||
#In CircleCI, this was different (store_artifacts vs persist_to_workspace)
|
||||
|
||||
|
||||
|
||||
name: build-and-validate
|
||||
on:
|
||||
push:
|
||||
@@ -83,43 +81,41 @@ jobs:
|
||||
python3 -m pip install --upgrade pip
|
||||
python3 -m pip install wheel
|
||||
python3 -m pip install -q -r requirements.txt
|
||||
|
||||
|
||||
|
||||
- name: run validate
|
||||
run: |
|
||||
source venv/bin/activate
|
||||
python3 contentctl.py --path . --verbose validate
|
||||
python3 contentctl.py -p . validate -pr ESCU
|
||||
python3 contentctl.py -p . validate -pr SSA
|
||||
|
||||
- name: Get CTI Repo for Mitre context
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: mitre/cti #check out https://github.com/mitre/cti.git, defaults to HEAD
|
||||
path: "cti/"
|
||||
|
||||
# - name: Get CTI Repo for Mitre context
|
||||
# uses: actions/checkout@v2
|
||||
# with:
|
||||
# repository: mitre/cti #check out https://github.com/mitre/cti.git, defaults to HEAD
|
||||
# path: "cti/"
|
||||
|
||||
|
||||
#Now generate the documentation (uses Node)
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '14' #can easily be changed to a different version
|
||||
|
||||
- name: Generate documentation
|
||||
run: |
|
||||
ls -lah
|
||||
|
||||
#Enter the virtualenv and run the docgen
|
||||
source venv/bin/activate
|
||||
python3 bin/doc_gen.py --path . --output docs -v
|
||||
|
||||
python3 contentctl.py -p . docgen -o docs
|
||||
#Now generate the spec docs
|
||||
npm install -g @adobe/jsonschema2md
|
||||
jsonschema2md -d spec -o docs/spec -f yaml -e spec.json -x -
|
||||
|
||||
#Clean up extra properties on docs
|
||||
rm -rf docs/spec/*-*.md
|
||||
|
||||
echo "****** BRANCH INFORMATION ******"
|
||||
git branch
|
||||
git branch --show-current
|
||||
|
||||
|
||||
build-sources:
|
||||
runs-on: ubuntu-latest
|
||||
needs: validate-content
|
||||
@@ -127,11 +123,16 @@ jobs:
|
||||
- name: Checkout Repo
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.9' #Available versions here - https://github.com/actions/python-versions/releases easy to change/make a matrix/use pypy
|
||||
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
|
||||
|
||||
- name: Install System Packages
|
||||
run: |
|
||||
sudo apt update -qq
|
||||
sudo apt install jq -qq
|
||||
|
||||
|
||||
- name: Install Python Dependencies
|
||||
run: |
|
||||
#Get the virtualenv set up
|
||||
@@ -141,22 +142,13 @@ jobs:
|
||||
python3 -m pip install --upgrade pip
|
||||
python3 -m pip install wheel
|
||||
python3 -m pip install -q -r requirements.txt
|
||||
|
||||
|
||||
- name: Run Generate
|
||||
run: |
|
||||
source venv/bin/activate
|
||||
python3 contentctl.py --path . --verbose generate --product ESCU --output dist/escu
|
||||
python3 contentctl.py --path . --verbose generate --product SSA --output dist/ssa
|
||||
|
||||
- name: Copy lookups .csv files
|
||||
run: |
|
||||
# clean up current lookups
|
||||
rm -rf dist/escu/lookups
|
||||
mkdir dist/escu/lookups
|
||||
#copy over lookups
|
||||
cd lookups
|
||||
cp -rv *.csv ../dist/escu/lookups
|
||||
|
||||
python3 contentctl.py --path . generate --product ESCU --output dist/escu
|
||||
python3 contentctl.py --path . generate --product SSA --output dist/ssa
|
||||
|
||||
- name: Copy lookups .mlmodel files
|
||||
run: |
|
||||
cd lookups
|
||||
@@ -165,7 +157,7 @@ jobs:
|
||||
then cp -rv *.mlmodel ../dist/escu/lookups
|
||||
else echo "No mlmodel files to copy"
|
||||
fi
|
||||
|
||||
|
||||
#Tag is '' for non-tagged push and the tag name for a tagged release
|
||||
- name: Set tag
|
||||
id: vars
|
||||
@@ -179,7 +171,7 @@ jobs:
|
||||
echo "Release is NOT TAGGED!"
|
||||
echo "::set-output name=tag::"
|
||||
fi
|
||||
|
||||
|
||||
- name: Update Version and Build number
|
||||
run : |
|
||||
# check if tag is set, get build number from the tag if set
|
||||
@@ -196,10 +188,9 @@ jobs:
|
||||
sed -i "s/\"version\": .*$/\"version\": \"$CONTENT_VERSION\"/g" dist/escu/app.manifest
|
||||
sed -i "s/version = .*$/version = $CONTENT_VERSION/g" dist/escu/default/content-version.conf
|
||||
tar -czf content-pack-build-escu.tar.gz dist/escu/*
|
||||
|
||||
# update build number and version for ssa
|
||||
tar -czf content-pack-build-ssa.tar.gz dist/ssa/*
|
||||
|
||||
|
||||
- name: Persist to Workspace
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
@@ -207,8 +198,7 @@ jobs:
|
||||
path: |
|
||||
content-pack-build-escu.tar.gz
|
||||
content-pack-build-ssa.tar.gz
|
||||
|
||||
|
||||
|
||||
build-package:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [validate-content, build-sources]
|
||||
@@ -228,13 +218,13 @@ jobs:
|
||||
- name: Get virtualenv for Python 2.7
|
||||
run: |
|
||||
sudo apt install virtualenv
|
||||
|
||||
|
||||
- name: Grab Splunk Packaging Toolkit
|
||||
run : |
|
||||
curl -Ls https://download.splunk.com/misc/packaging-toolkit/splunk-packaging-toolkit-0.9.0.tar.gz -o splunk-packaging-toolkit-latest.tar.gz
|
||||
mkdir slim-latest
|
||||
tar -zxf splunk-packaging-toolkit-latest.tar.gz -C slim-latest --strip-components=1
|
||||
|
||||
|
||||
- name: Install Splunk Packaging Toolkit (slim)
|
||||
run: |
|
||||
cd slim-latest
|
||||
@@ -244,36 +234,32 @@ jobs:
|
||||
python2 -m pip install wheel
|
||||
python2 -m pip install semantic_version
|
||||
python2 -m pip install .
|
||||
|
||||
- name: Create a .spl for this Build Using Slim
|
||||
run: |
|
||||
source slim-latest/venv/bin/activate
|
||||
cd build
|
||||
tar -zxf content-pack-build-escu.tar.gz
|
||||
tar -zxf content-pack-build-ssa.tar.gz
|
||||
|
||||
mv dist/escu DA-ESS-ContentUpdate
|
||||
mv dist/ssa SSA_Content
|
||||
|
||||
slim package -o upload DA-ESS-ContentUpdate
|
||||
|
||||
cp upload/DA-ESS-ContentUpdate-*.tar.gz DA-ESS-ContentUpdate-latest.tar.gz
|
||||
sha256sum DA-ESS-ContentUpdate-latest.tar.gz > checksum.txt
|
||||
|
||||
#Do this copy so that we conform as much as possible, and have to make
|
||||
#as few changes as possible, once we start generating this as a real,
|
||||
#properly packaged app
|
||||
tar -zcf upload/SSA_Content-NO_SLIM.tar.gz SSA_Content
|
||||
cp upload/SSA_Content-*.tar.gz SSA_Content-latest.tar.gz
|
||||
sha256sum SSA_Content-latest.tar.gz >> checksum.txt
|
||||
|
||||
|
||||
|
||||
- name: store_artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: package
|
||||
path: |
|
||||
build/upload
|
||||
|
||||
- name: store_artifacts_two
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
@@ -282,13 +268,8 @@ jobs:
|
||||
build/DA-ESS-ContentUpdate-latest.tar.gz
|
||||
build/SSA_Content-latest.tar.gz
|
||||
build/checksum.txt
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#Everything below this line should ONLY run on a tag and nothing else
|
||||
#We still want all of the above checks to run and pass before running these
|
||||
run-appinspect:
|
||||
@@ -309,16 +290,11 @@ jobs:
|
||||
name: content-latest
|
||||
path: build/
|
||||
|
||||
|
||||
|
||||
|
||||
- name: Install System Packages
|
||||
run: |
|
||||
sudo apt update -qq
|
||||
sudo apt install jq -qq
|
||||
|
||||
|
||||
|
||||
|
||||
- name: Submit ESCU to AppInspect API
|
||||
env:
|
||||
APPINSPECT_USERNAME: ${{ secrets.AppInspectUsername }}
|
||||
@@ -328,20 +304,20 @@ jobs:
|
||||
#Enclose in quotes in case there are any special characters in the username/password
|
||||
#Better not to pass these arguments on the command line, if possible
|
||||
./appinspect.sh ../ DA-ESS-ContentUpdate-latest.tar.gz "$APPINSPECT_USERNAME" "$APPINSPECT_PASSWORD"
|
||||
|
||||
|
||||
- name: Create report artifact
|
||||
if: always()
|
||||
run: |
|
||||
#Always create this, regardless of whether success or failure above
|
||||
tar -cvzf report.tar.gz report/
|
||||
|
||||
|
||||
- name: store_artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: appinspect_reports
|
||||
path: |
|
||||
report.tar.gz
|
||||
|
||||
|
||||
#Still store the report, even if we have failed (otherwise we don't know why/how we failed)
|
||||
- name: store_artifacts_on_failure
|
||||
uses: actions/upload-artifact@v2
|
||||
@@ -350,7 +326,8 @@ jobs:
|
||||
name: appinspect_reports_failure
|
||||
path: |
|
||||
report.tar.gz
|
||||
|
||||
|
||||
|
||||
create-report:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [validate-content, build-sources, build-package, run-appinspect]
|
||||
@@ -362,12 +339,11 @@ jobs:
|
||||
with:
|
||||
ref: 'develop'
|
||||
|
||||
|
||||
- name: Install System Packages
|
||||
run: |
|
||||
sudo apt update -qq
|
||||
sudo apt install jq -qq
|
||||
|
||||
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.9' #Available versions here - https://github.com/actions/python-versions/releases easy to change/make a matrix/use pypy
|
||||
@@ -382,12 +358,12 @@ jobs:
|
||||
python3 -m pip install --upgrade pip
|
||||
python3 -m pip install wheel
|
||||
python3 -m pip install -q -r requirements.txt
|
||||
|
||||
|
||||
- name: run reporting
|
||||
run: |
|
||||
source venv/bin/activate
|
||||
python3 bin/reporting.py
|
||||
|
||||
python3 contentctl.py -p . reporting
|
||||
|
||||
#Official, Verified Amazon-AWS Github Account Provided Action
|
||||
- uses: aws-actions/configure-aws-credentials@v1
|
||||
with:
|
||||
@@ -399,7 +375,7 @@ jobs:
|
||||
- name: Upload Reporting
|
||||
run: |
|
||||
aws s3 cp bin/reporting s3://security-content/reporting --recursive --exclude "*" --include "*.svg"
|
||||
|
||||
|
||||
update-sources-github:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [validate-content, build-sources, build-package, run-appinspect, create-report]
|
||||
@@ -422,13 +398,13 @@ jobs:
|
||||
with:
|
||||
name: content-latest
|
||||
|
||||
|
||||
- name: Stage artifacts in proper directories
|
||||
run: |
|
||||
mkdir latest-escu
|
||||
tar -zxf DA-ESS-ContentUpdate-latest.tar.gz -C latest-escu --strip-components=1
|
||||
mkdir latest-ssa
|
||||
tar -zxf SSA_Content-latest.tar.gz -C latest-ssa --strip-components=1
|
||||
|
||||
- name: Install Python Dependencies
|
||||
run: |
|
||||
#Get the virtualenv set up
|
||||
@@ -439,21 +415,15 @@ jobs:
|
||||
python3 -m pip install wheel
|
||||
python3 -m pip install -q -r requirements.txt
|
||||
|
||||
- name: Get CTI Repo for Mitre context
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
repository: mitre/cti #check out https://github.com/mitre/cti.git, defaults to HEAD
|
||||
path: "cti/"
|
||||
|
||||
- name: Get branch and PR required for detection testing main.py
|
||||
id: vars
|
||||
run: |
|
||||
echo "::set-output name=branch::${GITHUB_REF#refs/heads/}"
|
||||
|
||||
|
||||
- name: Run doc-gen
|
||||
run: |
|
||||
source venv/bin/activate
|
||||
python3 bin/doc_gen.py --path . --output docs -v
|
||||
python3 contentctl.py -p . docgen -o docs
|
||||
|
||||
# this step is creating some TOKEN issues while tagging a build, commenting this out for now
|
||||
|
||||
@@ -465,16 +435,6 @@ jobs:
|
||||
# python3 -m pip install -r security_content_automation/requirements.txt
|
||||
# python3 security_content_automation/enrich_detections.py
|
||||
|
||||
- name: Make YAMLs Pretty
|
||||
run: |
|
||||
source venv/bin/activate
|
||||
python3 bin/pretty_yaml.py --path . -v
|
||||
|
||||
- name: Run generate-coverage-map
|
||||
run: |
|
||||
source venv/bin/activate
|
||||
python3 bin/generate-coverage-map.py --projects_path . --output docs/mitre-map
|
||||
|
||||
- name: Update github with new docs and package bits
|
||||
run: |
|
||||
rm -rf dist
|
||||
@@ -492,8 +452,7 @@ jobs:
|
||||
# Push quietly to prevent showing the token in log
|
||||
#No need to provide any credentials
|
||||
git push
|
||||
|
||||
|
||||
|
||||
publish-github-release:
|
||||
#Github-maintained release action is in archived state: https://github.com/actions/create-release
|
||||
#They recommend several and we use the following with the most stars: https://github.com/softprops/action-gh-release
|
||||
@@ -512,8 +471,6 @@ jobs:
|
||||
with:
|
||||
name: appinspect_reports
|
||||
|
||||
|
||||
|
||||
#Rename those artifacts appropriately
|
||||
- name: Set tag
|
||||
id: vars
|
||||
@@ -523,12 +480,10 @@ jobs:
|
||||
run: |
|
||||
cp DA-ESS-ContentUpdate-latest.tar.gz DA-ESS-ContentUpdate-${{ steps.vars.outputs.tag }}.tar.gz
|
||||
cp SSA_Content-latest.tar.gz SSA_Content-${{ steps.vars.outputs.tag }}.tar.gz
|
||||
|
||||
#No checksum on the reports
|
||||
cp report.tar.gz report-${{ steps.vars.outputs.tag }}.tar.gz
|
||||
|
||||
cp checksum.txt checksum-${{ steps.vars.outputs.tag }}.txt
|
||||
|
||||
|
||||
#Upload all of the artifacts that we have created using the third party
|
||||
#action recommended bu Github
|
||||
- name: Upload Release Artifacts
|
||||
@@ -539,9 +494,7 @@ jobs:
|
||||
SSA_Content-${{ steps.vars.outputs.tag }}.tar.gz
|
||||
report-${{ steps.vars.outputs.tag }}.tar.gz
|
||||
checksum-${{ steps.vars.outputs.tag }}.txt
|
||||
|
||||
|
||||
|
||||
|
||||
attack-range-update:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [validate-content, build-sources, build-package, run-appinspect, create-report, update-sources-github, publish-github-release]
|
||||
@@ -554,7 +507,6 @@ jobs:
|
||||
with:
|
||||
name: content-latest
|
||||
|
||||
|
||||
#Official, Verified Amazon-AWS Github Account Provided Action
|
||||
- uses: aws-actions/configure-aws-credentials@v1
|
||||
with:
|
||||
@@ -568,7 +520,7 @@ jobs:
|
||||
aws s3 cp DA-ESS-ContentUpdate-latest.tar.gz s3://attack-range-appbinaries/
|
||||
# make the file public since it is not by default
|
||||
aws s3api put-object-acl --bucket attack-range-appbinaries --key DA-ESS-ContentUpdate-latest.tar.gz --acl public-read
|
||||
|
||||
|
||||
master-api-update:
|
||||
runs-on: ubuntu-latest
|
||||
needs: [validate-content, build-sources, build-package, run-appinspect, create-report, update-sources-github, publish-github-release, attack-range-update]
|
||||
@@ -596,16 +548,11 @@ jobs:
|
||||
python3 -m pip install wheel
|
||||
python3 -m pip install -q -r requirements.txt
|
||||
|
||||
- name: Create Baseline Folder
|
||||
run: |
|
||||
source venv/bin/activate
|
||||
python3 bin/create_baseline_folder.py
|
||||
|
||||
- name: Create YML to JSON Folder
|
||||
run: |
|
||||
source venv/bin/activate
|
||||
python3 contentctl.py --path . --verbose generate --product API --output dist/api
|
||||
|
||||
python3 contentctl.py --path . generate --product API --output dist/api
|
||||
|
||||
#Official, Verified Amazon-AWS Github Account Provided Action
|
||||
- uses: aws-actions/configure-aws-credentials@v1
|
||||
with:
|
||||
@@ -627,6 +574,7 @@ jobs:
|
||||
aws s3 cp macros s3://security-content/macros --recursive --exclude "*" --include "*.yml"
|
||||
aws s3 cp deployments s3://security-content/deployments --recursive --exclude "*" --include "*.yml"
|
||||
aws s3 cp dist/api s3://security-content/json --recursive --exclude "*" --include "*.json"
|
||||
|
||||
- name: Security Content API Smoke Test
|
||||
run: |
|
||||
API_URL='https://content.splunkresearch.com/detections'
|
||||
@@ -635,4 +583,4 @@ jobs:
|
||||
if [ "$API_STATUS" != "200" ]; then
|
||||
echo "Error [Security Content API status: $API_STATUS]"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
@@ -0,0 +1,38 @@
|
||||
name: code-testing
|
||||
on:
|
||||
push:
|
||||
paths:
|
||||
- 'bin/contentctl_project/**'
|
||||
|
||||
jobs:
|
||||
code-testing:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out the repository code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: '3.9' #Available versions here - https://github.com/actions/python-versions/releases easy to change/make a matrix/use pypy
|
||||
architecture: 'x64' # optional x64 or x86. Defaults to x64 if not specified
|
||||
|
||||
- name: Install System Packages
|
||||
run: |
|
||||
sudo apt update -qq
|
||||
sudo apt install jq -qq
|
||||
|
||||
- name: Install Python Dependencies
|
||||
run: |
|
||||
#Get the virtualenv set up
|
||||
rm -rf venv
|
||||
python3 -m venv --clear venv
|
||||
source venv/bin/activate
|
||||
python3 -m pip install --upgrade pip
|
||||
python3 -m pip install wheel
|
||||
python3 -m pip install -q -r requirements.txt
|
||||
|
||||
- name: test code with pytest
|
||||
run: |
|
||||
source venv/bin/activate
|
||||
export PYTHONPATH=$PYTHONPATH:/home/runner/work/security_content
|
||||
pytest -s bin/contentctl_project
|
||||
@@ -1,45 +0,0 @@
|
||||
# This file uses .gitignore syntax:
|
||||
#
|
||||
# To ignore a file anywhere it occurs in your project, enter a
|
||||
# glob pattern here. E.g. "*.min.js".
|
||||
#
|
||||
# To ignore a directory anywhere it occurs in your project, add
|
||||
# a trailing slash to the file name. E.g. "dist/".
|
||||
#
|
||||
# To ignore a file or directory only relative to the project root,
|
||||
# include a slash anywhere except the last character. E.g.
|
||||
# "/dist/", or "src/generated".
|
||||
#
|
||||
# Some parts of .gitignore syntax are not supported, and patterns
|
||||
# using this syntax will be dropped from the ignore list:
|
||||
# - Explicit "include syntax", e.g. "!kept/".
|
||||
# - Multi-character expansion syntax, e.g. "*.py[cod]"
|
||||
#
|
||||
# To include ignore patterns from another file, start a line
|
||||
# with ':include', followed by the path of the file. E.g.
|
||||
# ":include path/to/other/ignore/file".
|
||||
#
|
||||
# To ignore a file with a literal ':' character, escape it with
|
||||
# a backslash, e.g. "\:foo".
|
||||
|
||||
# Ignore git items
|
||||
.gitignore
|
||||
.git/
|
||||
:include .gitignore
|
||||
|
||||
#spl files may contain eval and other statements that should NOT trigger semgrep warnings
|
||||
*.spl
|
||||
|
||||
#Ignore Markdown and Wiki Pages
|
||||
*.md
|
||||
*.wiki
|
||||
|
||||
#Temporarily ignoring this directory as we discuss a path moving forward
|
||||
#for Splunk Packaging Toolkit Update Strategy
|
||||
/dist/
|
||||
|
||||
#Don't check yaml files in these directories
|
||||
/response_tasks
|
||||
/detections
|
||||
|
||||
|
||||
@@ -46,11 +46,12 @@ curl -s https://content.splunkresearch.com | jq
|
||||
### contentctl.py
|
||||
The Content Control tool allows you to manipulate Splunk Security Content via the following actions:
|
||||
|
||||
1. **new** - Creates new content (detection, story, baseline)
|
||||
1. **new_content** - Creates new content (detection, story, baseline)
|
||||
2. **validate** - Validates written content
|
||||
3. **generate** - Generates a deployment package for different platforms (splunk_app)
|
||||
|
||||
### pre-requisites
|
||||
Make sure you use python version 3.9 or higher.
|
||||
|
||||
```
|
||||
git clone git@github.com:splunk/security_content.git
|
||||
@@ -60,19 +61,19 @@ virtualenv venv
|
||||
source venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
### Architecture details for the tooling
|
||||
- [WIKI](https://github.com/splunk/security_content/wiki/Security-Content-Code)
|
||||
|
||||
### create a new detection
|
||||
`python contentctl.py new`
|
||||
`python contentctl.py -p . new_content -t detection`
|
||||
|
||||
for a more indepth write up on how to write content see our [guide](https://github.com/splunk/security_content/wiki/Developing-Content).
|
||||
|
||||
### create a new analytic story
|
||||
`python contentctl.py new -t story`
|
||||
|
||||
### validate written content a new analytic story
|
||||
`python contentctl.py --verbose validate`
|
||||
### validate security content
|
||||
`python contentctl.py -p . validate -pr ESCU`
|
||||
|
||||
### generate a splunk app from current content
|
||||
`python contentctl.py --path . --verbose generate --output package`
|
||||
`python contentctl.py -p . generate -o dist/escu -pr ESCU`
|
||||
|
||||
# MITRE ATT&CK ⚔️
|
||||
### Detection Coverage
|
||||
@@ -91,9 +92,9 @@ A complete use case, specifically built to detect, investigate, and respond to a
|
||||
* [detections/](detections/): Contains all 209 detection searches to-date and growing.
|
||||
* [stories/](stories/): All Analytic Stories that are group detections or also known as Use Cases
|
||||
* [deployments/](deployments/): Configuration for the schedule and alert action for all content
|
||||
* [responses/](responses/): Incident Response Playbooks/Workflow for responding to a specific Use Case or Threat.
|
||||
* [response_tasks/](response_tasks/): Individual steps in responses that help the user investigate via a Splunk search, automate via a phantom playbook, and visualize via dashboards threats.
|
||||
* [playbooks/](playbooks/): Incident Response Playbooks/Workflow for responding to a specific Use Case or Threat.
|
||||
* [baselines/](baselines/): Searches that must be executed before a detection runs. It is specifically useful for collecting data on a system before running your detection on the collected data.
|
||||
* [investigations/](investigations/): Investigations to further analysis the output from detections.
|
||||
* [dashboards/](dashboards/): JSON definitions of Mission Control dashboards, to be used as a response task. Currently not used.
|
||||
* [macros/](macros/): Implements Splunk’s search macros, shortcuts to commonly used search patterns like sysmon source type. More on how macros are used to customize content below.
|
||||
* [lookups/](lookups/): Implements Splunk’s lookup, usually to provide a list of static values like commonly used ransomware extensions.
|
||||
@@ -115,7 +116,7 @@ If you have questions or need support, you can:
|
||||
|
||||
|
||||
## License
|
||||
Copyright 2020 Splunk Inc.
|
||||
Copyright 2022 Splunk Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
||||
+1
-1
@@ -26,8 +26,8 @@ references: []
|
||||
tags:
|
||||
analytic_story:
|
||||
- AWS Network ACL Activity
|
||||
- Command and Control
|
||||
- Suspicious AWS Traffic
|
||||
- Command and Control
|
||||
deployments:
|
||||
- Daily Cache Updates
|
||||
detections:
|
||||
-1
@@ -39,7 +39,6 @@ tags:
|
||||
detections:
|
||||
- Abnormally High Number Of Cloud Infrastructure API Calls
|
||||
product:
|
||||
- Splunk Security Analytics for AWS
|
||||
- Splunk Enterprise
|
||||
- Splunk Enterprise Security
|
||||
- Splunk Cloud
|
||||
-1
@@ -42,7 +42,6 @@ tags:
|
||||
detections:
|
||||
- Abnormally High Number Of Cloud Instances Destroyed
|
||||
product:
|
||||
- Splunk Security Analytics for AWS
|
||||
- Splunk Enterprise
|
||||
- Splunk Enterprise Security
|
||||
- Splunk Cloud
|
||||
-1
@@ -42,7 +42,6 @@ tags:
|
||||
detections:
|
||||
- Abnormally High Number Of Cloud Instances Launched
|
||||
product:
|
||||
- Splunk Security Analytics for AWS
|
||||
- Splunk Enterprise
|
||||
- Splunk Enterprise Security
|
||||
- Splunk Cloud
|
||||
-1
@@ -38,7 +38,6 @@ tags:
|
||||
detections:
|
||||
- Abnormally High Number Of Cloud Security Group API Calls
|
||||
product:
|
||||
- Splunk Security Analytics for AWS
|
||||
- Splunk Enterprise
|
||||
- Splunk Enterprise Security
|
||||
- Splunk Cloud
|
||||
+1
-1
@@ -27,9 +27,9 @@ known_false_positives: none
|
||||
references: []
|
||||
tags:
|
||||
analytic_story:
|
||||
- Command and Control
|
||||
- Hidden Cobra Malware
|
||||
- Suspicious DNS Traffic
|
||||
- Command and Control
|
||||
deployments:
|
||||
- Daily Cache Updates
|
||||
detections:
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
name: Baseline of S3 Bucket deletion activity by ARN
|
||||
id: fc0edd96-ff2b-48b0-9f1f-63eq3783fd63
|
||||
id: 841b102c-8866-494b-a704-87b674fe9b09
|
||||
version: 1
|
||||
date: '2018-07-17'
|
||||
author: Bhavin Patel, Splunk
|
||||
+6
@@ -16,8 +16,14 @@ how_to_implement: To successfully implement this search, you must be ingesting n
|
||||
known_false_positives: none
|
||||
references: []
|
||||
tags:
|
||||
analytic_story:
|
||||
- Prohibited Traffic Allowed or Protocol Mismatch
|
||||
- Ransomware
|
||||
- Command and Control
|
||||
deployments:
|
||||
- Daily Cache Updates
|
||||
detections:
|
||||
- Prohibited Network Traffic Allowed
|
||||
product:
|
||||
- Splunk Enterprise
|
||||
- Splunk Enterprise Security
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
name: Create a list of approved AWS service accounts
|
||||
id: fc0edc95-ff2b-48b1-5f6f-63ga3789fd43
|
||||
id: 08ef80f5-6555-474b-bb2d-22e2aa4206a4
|
||||
version: 2
|
||||
date: '2018-12-03'
|
||||
author: Bhavin Patel, Splunk
|
||||
+14
@@ -20,6 +20,7 @@ tags:
|
||||
- 'Emotet Malware DHS Report TA18-201A '
|
||||
- Monitor for Unauthorized Software
|
||||
- SamSam Ransomware
|
||||
asset_type: Endpoint
|
||||
deployments:
|
||||
- Daily Cache Updates
|
||||
detections:
|
||||
@@ -31,3 +32,16 @@ tags:
|
||||
required_fields:
|
||||
- _time
|
||||
security_domain: endpoint
|
||||
kill_chain_phases:
|
||||
- Exploitation
|
||||
confidence: 50
|
||||
impact: 50
|
||||
risk_score: 25
|
||||
context:
|
||||
- Unknown
|
||||
message: tbd
|
||||
observable:
|
||||
- name: field
|
||||
type: Unknown
|
||||
role:
|
||||
- Unknown
|
||||
+13
@@ -35,3 +35,16 @@ tags:
|
||||
- eventType
|
||||
- userIdentity.arn
|
||||
security_domain: network
|
||||
kill_chain_phases:
|
||||
- Exploitation
|
||||
confidence: 50
|
||||
impact: 50
|
||||
risk_score: 25
|
||||
context:
|
||||
- Unknown
|
||||
message: tbd
|
||||
observable:
|
||||
- name: field
|
||||
type: Unknown
|
||||
role:
|
||||
- Unknown
|
||||
+13
@@ -47,3 +47,16 @@ tags:
|
||||
- errorCode
|
||||
- src_user
|
||||
security_domain: network
|
||||
kill_chain_phases:
|
||||
- Exploitation
|
||||
confidence: 50
|
||||
impact: 50
|
||||
risk_score: 25
|
||||
context:
|
||||
- Unknown
|
||||
message: tbd
|
||||
observable:
|
||||
- name: field
|
||||
type: Unknown
|
||||
role:
|
||||
- Unknown
|
||||
+13
@@ -47,3 +47,16 @@ tags:
|
||||
- errorCode
|
||||
- src_user
|
||||
security_domain: network
|
||||
kill_chain_phases:
|
||||
- Exploitation
|
||||
confidence: 50
|
||||
impact: 50
|
||||
risk_score: 25
|
||||
context:
|
||||
- Unknown
|
||||
message: tbd
|
||||
observable:
|
||||
- name: field
|
||||
type: Unknown
|
||||
role:
|
||||
- Unknown
|
||||
+14
-1
@@ -1,5 +1,5 @@
|
||||
name: Previously seen API call per user roles in CloudTrail
|
||||
id: fc0edc95-fq2c-48b0-9f6f-63da3289fd03
|
||||
id: 02add098-efa3-428d-b2e2-4ed0831c92f4
|
||||
version: 1
|
||||
date: '2018-04-16'
|
||||
author: Bhavin Patel, Splunk
|
||||
@@ -38,3 +38,16 @@ tags:
|
||||
- userName
|
||||
- eventName
|
||||
security_domain: network
|
||||
kill_chain_phases:
|
||||
- Exploitation
|
||||
confidence: 50
|
||||
impact: 50
|
||||
risk_score: 25
|
||||
context:
|
||||
- Unknown
|
||||
message: tbd
|
||||
observable:
|
||||
- name: field
|
||||
type: Unknown
|
||||
role:
|
||||
- Unknown
|
||||
+13
@@ -36,3 +36,16 @@ tags:
|
||||
- eventName
|
||||
- sourceIPAddress
|
||||
security_domain: network
|
||||
kill_chain_phases:
|
||||
- Exploitation
|
||||
confidence: 50
|
||||
impact: 50
|
||||
risk_score: 25
|
||||
context:
|
||||
- Unknown
|
||||
message: tbd
|
||||
observable:
|
||||
- name: field
|
||||
type: Unknown
|
||||
role:
|
||||
- Unknown
|
||||
+13
@@ -32,3 +32,16 @@ tags:
|
||||
- errorCode
|
||||
- requestParameters.instancesSet.items{}.imageId
|
||||
security_domain: network
|
||||
kill_chain_phases:
|
||||
- Exploitation
|
||||
confidence: 50
|
||||
impact: 50
|
||||
risk_score: 25
|
||||
context:
|
||||
- Unknown
|
||||
message: tbd
|
||||
observable:
|
||||
- name: field
|
||||
type: Unknown
|
||||
role:
|
||||
- Unknown
|
||||
+13
@@ -32,3 +32,16 @@ tags:
|
||||
- errorCode
|
||||
- requestParameters.instanceType
|
||||
security_domain: network
|
||||
kill_chain_phases:
|
||||
- Exploitation
|
||||
confidence: 50
|
||||
impact: 50
|
||||
risk_score: 25
|
||||
context:
|
||||
- Unknown
|
||||
message: tbd
|
||||
observable:
|
||||
- name: field
|
||||
type: Unknown
|
||||
role:
|
||||
- Unknown
|
||||
+13
@@ -33,3 +33,16 @@ tags:
|
||||
- errorCode
|
||||
- requestParameters.instanceType
|
||||
security_domain: network
|
||||
kill_chain_phases:
|
||||
- Exploitation
|
||||
confidence: 50
|
||||
impact: 50
|
||||
risk_score: 25
|
||||
context:
|
||||
- Unknown
|
||||
message: tbd
|
||||
observable:
|
||||
- name: field
|
||||
type: Unknown
|
||||
role:
|
||||
- Unknown
|
||||
+13
@@ -40,3 +40,16 @@ tags:
|
||||
- userIdentity.arn
|
||||
- src
|
||||
security_domain: network
|
||||
kill_chain_phases:
|
||||
- Exploitation
|
||||
confidence: 50
|
||||
impact: 50
|
||||
risk_score: 25
|
||||
context:
|
||||
- Unknown
|
||||
message: tbd
|
||||
observable:
|
||||
- name: field
|
||||
type: Unknown
|
||||
role:
|
||||
- Unknown
|
||||
+13
@@ -42,3 +42,16 @@ tags:
|
||||
- userIdentity.arn
|
||||
- src
|
||||
security_domain: network
|
||||
kill_chain_phases:
|
||||
- Exploitation
|
||||
confidence: 50
|
||||
impact: 50
|
||||
risk_score: 25
|
||||
context:
|
||||
- Unknown
|
||||
message: tbd
|
||||
observable:
|
||||
- name: field
|
||||
type: Unknown
|
||||
role:
|
||||
- Unknown
|
||||
+14
@@ -19,6 +19,7 @@ tags:
|
||||
analytic_story:
|
||||
- Brand Monitoring
|
||||
- Suspicious Emails
|
||||
asset_type: Endpoint
|
||||
deployments:
|
||||
- Daily Cache Updates
|
||||
detections:
|
||||
@@ -29,6 +30,19 @@ tags:
|
||||
- Splunk Enterprise
|
||||
- Splunk Enterprise Security
|
||||
- Splunk Cloud
|
||||
kill_chain_phases:
|
||||
- Exploitation
|
||||
required_fields:
|
||||
- _time
|
||||
security_domain: network
|
||||
confidence: 50
|
||||
impact: 50
|
||||
risk_score: 25
|
||||
context:
|
||||
- Unknown
|
||||
message: tbd
|
||||
observable:
|
||||
- name: dest
|
||||
type: Other
|
||||
role:
|
||||
- Other
|
||||
+7
@@ -16,8 +16,15 @@ how_to_implement: To successfully implement this search, you must ingest network
|
||||
known_false_positives: none
|
||||
references: []
|
||||
tags:
|
||||
analytic_story:
|
||||
- SamSam Ransomware
|
||||
- Ryuk Ransomware
|
||||
- Hidden Cobra Malware
|
||||
- Active Directory Lateral Movement
|
||||
deployments:
|
||||
- Daily Cache Updates
|
||||
detections:
|
||||
- Remote Desktop Network Traffic
|
||||
product:
|
||||
- Splunk Enterprise
|
||||
- Splunk Enterprise Security
|
||||
+7
@@ -17,8 +17,15 @@ how_to_implement: To successfully implement this search you must ingest network
|
||||
known_false_positives: none
|
||||
references: []
|
||||
tags:
|
||||
analytic_story:
|
||||
- SamSam Ransomware
|
||||
- Ryuk Ransomware
|
||||
- Hidden Cobra Malware
|
||||
- Active Directory Lateral Movement
|
||||
deployments:
|
||||
- Daily Cache Updates
|
||||
detections:
|
||||
- Remote Desktop Network Traffic
|
||||
product:
|
||||
- Splunk Enterprise
|
||||
- Splunk Enterprise Security
|
||||
+7
@@ -16,8 +16,15 @@ how_to_implement: To successfully implement this search you must be ingesting en
|
||||
known_false_positives: none
|
||||
references: []
|
||||
tags:
|
||||
analytic_story:
|
||||
- SamSam Ransomware
|
||||
- Ryuk Ransomware
|
||||
- Hidden Cobra Malware
|
||||
- Active Directory Lateral Movement
|
||||
deployments:
|
||||
- Daily Cache Updates
|
||||
detections:
|
||||
- Remote Desktop Network Traffic
|
||||
product:
|
||||
- Splunk Enterprise
|
||||
- Splunk Enterprise Security
|
||||
-1
@@ -31,7 +31,6 @@ tags:
|
||||
detections:
|
||||
- AWS Cross Account Activity From Previously Unseen Account
|
||||
product:
|
||||
- Splunk Security Analytics for AWS
|
||||
- Splunk Enterprise
|
||||
- Splunk Enterprise Security
|
||||
- Splunk Cloud
|
||||
-1
@@ -32,7 +32,6 @@ tags:
|
||||
detections:
|
||||
- AWS Cross Account Activity From Previously Unseen Account
|
||||
product:
|
||||
- Splunk Security Analytics for AWS
|
||||
- Splunk Enterprise
|
||||
- Splunk Enterprise Security
|
||||
- Splunk Cloud
|
||||
-1
@@ -27,7 +27,6 @@ tags:
|
||||
detections:
|
||||
- Cloud API Calls From Previously Unseen User Roles
|
||||
product:
|
||||
- Splunk Security Analytics for AWS
|
||||
- Splunk Enterprise
|
||||
- Splunk Enterprise Security
|
||||
- Splunk Cloud
|
||||
-1
@@ -29,7 +29,6 @@ tags:
|
||||
detections:
|
||||
- Cloud API Calls From Previously Unseen User Roles
|
||||
product:
|
||||
- Splunk Security Analytics for AWS
|
||||
- Splunk Enterprise
|
||||
- Splunk Enterprise Security
|
||||
- Splunk Cloud
|
||||
-1
@@ -24,7 +24,6 @@ tags:
|
||||
detections:
|
||||
- Cloud Compute Instance Created By Previously Unseen User
|
||||
product:
|
||||
- Splunk Security Analytics for AWS
|
||||
- Splunk Enterprise
|
||||
- Splunk Enterprise Security
|
||||
- Splunk Cloud
|
||||
-1
@@ -27,7 +27,6 @@ tags:
|
||||
detections:
|
||||
- Cloud Compute Instance Created By Previously Unseen User
|
||||
product:
|
||||
- Splunk Security Analytics for AWS
|
||||
- Splunk Enterprise
|
||||
- Splunk Enterprise Security
|
||||
- Splunk Cloud
|
||||
-1
@@ -26,7 +26,6 @@ tags:
|
||||
detections:
|
||||
- Cloud Compute Instance Created With Previously Unseen Image
|
||||
product:
|
||||
- Splunk Security Analytics for AWS
|
||||
- Splunk Enterprise
|
||||
- Splunk Enterprise Security
|
||||
- Splunk Cloud
|
||||
-1
@@ -27,7 +27,6 @@ tags:
|
||||
detections:
|
||||
- Cloud Compute Instance Created With Previously Unseen Image
|
||||
product:
|
||||
- Splunk Security Analytics for AWS
|
||||
- Splunk Enterprise
|
||||
- Splunk Enterprise Security
|
||||
- Splunk Cloud
|
||||
-1
@@ -25,7 +25,6 @@ tags:
|
||||
detections:
|
||||
- Cloud Compute Instance Created With Previously Unseen Instance Type
|
||||
product:
|
||||
- Splunk Security Analytics for AWS
|
||||
- Splunk Enterprise
|
||||
- Splunk Enterprise Security
|
||||
- Splunk Cloud
|
||||
-1
@@ -27,7 +27,6 @@ tags:
|
||||
detections:
|
||||
- Cloud Compute Instance Created With Previously Unseen Instance Type
|
||||
product:
|
||||
- Splunk Security Analytics for AWS
|
||||
- Splunk Enterprise
|
||||
- Splunk Enterprise Security
|
||||
- Splunk Cloud
|
||||
-1
@@ -25,7 +25,6 @@ tags:
|
||||
detections:
|
||||
- Cloud Instance Modified By Previously Unseen User
|
||||
product:
|
||||
- Splunk Security Analytics for AWS
|
||||
- Splunk Enterprise
|
||||
- Splunk Enterprise Security
|
||||
- Splunk Cloud
|
||||
-1
@@ -29,7 +29,6 @@ tags:
|
||||
detections:
|
||||
- Cloud Instance Modified By Previously Unseen User
|
||||
product:
|
||||
- Splunk Security Analytics for AWS
|
||||
- Splunk Enterprise
|
||||
- Splunk Enterprise Security
|
||||
- Splunk Cloud
|
||||
-1
@@ -32,7 +32,6 @@ tags:
|
||||
- Cloud Provisioning Activity From Previously Unseen Country
|
||||
- Cloud Provisioning Activity From Previously Unseen Region
|
||||
product:
|
||||
- Splunk Security Analytics for AWS
|
||||
- Splunk Enterprise
|
||||
- Splunk Enterprise Security
|
||||
- Splunk Cloud
|
||||
-1
@@ -37,7 +37,6 @@ tags:
|
||||
- Cloud Provisioning Activity From Previously Unseen Country
|
||||
- Cloud Provisioning Activity From Previously Unseen Region
|
||||
product:
|
||||
- Splunk Security Analytics for AWS
|
||||
- Splunk Enterprise
|
||||
- Splunk Enterprise Security
|
||||
- Splunk Cloud
|
||||
-1
@@ -27,7 +27,6 @@ tags:
|
||||
detections:
|
||||
- Cloud Compute Instance Created In Previously Unused Region
|
||||
product:
|
||||
- Splunk Security Analytics for AWS
|
||||
- Splunk Enterprise
|
||||
- Splunk Enterprise Security
|
||||
- Splunk Cloud
|
||||
-1
@@ -30,7 +30,6 @@ tags:
|
||||
detections:
|
||||
- Cloud Compute Instance Created In Previously Unused Region
|
||||
product:
|
||||
- Splunk Security Analytics for AWS
|
||||
- Splunk Enterprise
|
||||
- Splunk Enterprise Security
|
||||
- Splunk Cloud
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
name: Previously seen S3 bucket access by remote IP
|
||||
id: fc0edc15-fq2c-48b0-9f6f-63qa1281fd03
|
||||
id: 54c40c6a-9a5b-4a79-9291-85977f713961
|
||||
version: 1
|
||||
date: '2018-06-28'
|
||||
author: Bhavin Patel, Splunk
|
||||
-1
@@ -33,7 +33,6 @@ tags:
|
||||
- Detect AWS Console Login by User from New City
|
||||
- Detect AWS Console Login by New User
|
||||
product:
|
||||
- Splunk Security Analytics for AWS
|
||||
- Splunk Enterprise
|
||||
- Splunk Enterprise Security
|
||||
- Splunk Cloud
|
||||
-1
@@ -33,7 +33,6 @@ tags:
|
||||
- Detect AWS Console Login by User from New City
|
||||
- Detect AWS Console Login by New User
|
||||
product:
|
||||
- Splunk Security Analytics for AWS
|
||||
- Splunk Enterprise
|
||||
- Splunk Enterprise Security
|
||||
- Splunk Cloud
|
||||
+4
@@ -15,8 +15,12 @@ how_to_implement: You must be ingesting your Windows Update Logs
|
||||
known_false_positives: none
|
||||
references: []
|
||||
tags:
|
||||
analytic_story:
|
||||
- Monitor for Updates
|
||||
deployments:
|
||||
- Daily Cache Updates
|
||||
detections:
|
||||
- No Windows Updates in a time frame
|
||||
product:
|
||||
- Splunk Enterprise
|
||||
- Splunk Enterprise Security
|
||||
+4
@@ -15,8 +15,12 @@ how_to_implement: You must be ingesting your Windows Update Logs
|
||||
known_false_positives: none
|
||||
references: []
|
||||
tags:
|
||||
analytic_story:
|
||||
- Monitor for Updates
|
||||
deployments:
|
||||
- Daily Cache Updates
|
||||
detections:
|
||||
- No Windows Updates in a time frame
|
||||
product:
|
||||
- Splunk Enterprise
|
||||
- Splunk Enterprise Security
|
||||
@@ -1,75 +0,0 @@
|
||||
import glob
|
||||
import yaml
|
||||
import sys
|
||||
import re
|
||||
import argparse
|
||||
|
||||
from os import path
|
||||
|
||||
|
||||
def load_objects(file_path, REPO_PATH):
|
||||
files = []
|
||||
manifest_files = path.join(path.expanduser(REPO_PATH), file_path)
|
||||
for file in sorted(glob.glob(manifest_files)):
|
||||
files.append(load_file(file))
|
||||
return files
|
||||
|
||||
|
||||
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:
|
||||
print(exc)
|
||||
sys.exit("ERROR: reading {0}".format(file_path))
|
||||
return file
|
||||
|
||||
|
||||
def load_content(old_project):
|
||||
stories = load_objects("stories/*.yml", old_project)
|
||||
macros = load_objects("macros/*.yml", old_project)
|
||||
lookups = load_objects("lookups/*.yml", old_project)
|
||||
baselines = load_objects("baselines/*.yml", old_project)
|
||||
responses = load_objects("responses/*.yml", old_project)
|
||||
response_tasks = load_objects("response_tasks/*.yml", old_project)
|
||||
deployments = load_objects("deployments/*.yml", old_project)
|
||||
|
||||
# process all detections
|
||||
detections = []
|
||||
detections = load_objects("detections/*/*.yml", old_project)
|
||||
detections.extend(load_objects("detections/*/*/*.yml", old_project))
|
||||
|
||||
#print(len(detections))
|
||||
|
||||
return detections, stories, macros, lookups, baselines, responses, response_tasks, deployments
|
||||
|
||||
|
||||
def add_required_field(detections, new_project):
|
||||
#for detection in detections:
|
||||
matches = re.findall(r'(?<key>[^\s]*)=', detections[0])
|
||||
for match in matches:
|
||||
print(match)
|
||||
|
||||
|
||||
def main(new_project, old_project, change):
|
||||
|
||||
detections, stories, macros, lookups, baselines, responses, response_tasks, deployments = load_content(old_project)
|
||||
|
||||
if change == "add_required_field":
|
||||
add_required_field(detections, new_project)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
parser = argparse.ArgumentParser(description="applies security content changes to the whole project")
|
||||
parser.add_argument("-np", "--new_project", required=True, help="the security content project to write the new configs in to")
|
||||
parser.add_argument("-op", "--old_project", required=True, help="the security content project to read the files from")
|
||||
parser.add_argument("-c", "--change", required=True, help="the name of your change")
|
||||
|
||||
# parse them
|
||||
args = parser.parse_args()
|
||||
new_project = args.new_project
|
||||
old_project = args.old_project
|
||||
change = args.change
|
||||
|
||||
main(new_project, old_project, change)
|
||||
@@ -1,218 +0,0 @@
|
||||
import glob
|
||||
import yaml
|
||||
import sys
|
||||
import re
|
||||
import argparse
|
||||
import requests
|
||||
import csv
|
||||
from requests.auth import HTTPBasicAuth
|
||||
from urllib3.exceptions import InsecureRequestWarning
|
||||
|
||||
from os import path
|
||||
|
||||
BASE_URL = f"https://ip:8089"
|
||||
SEARCH_PARSER_ENDPOINT = f"/services/search/parser"
|
||||
USER = f"admin"
|
||||
PASSWORD = f"password"
|
||||
parsed_fields = dict()
|
||||
|
||||
|
||||
def load_objects(file_path, REPO_PATH):
|
||||
files = []
|
||||
manifest_files = path.join(path.expanduser(REPO_PATH), file_path)
|
||||
for file in sorted(glob.glob(manifest_files)):
|
||||
files.append(load_file(file))
|
||||
return files
|
||||
|
||||
|
||||
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:
|
||||
print(exc)
|
||||
sys.exit("ERROR: reading {0}".format(file_path))
|
||||
return file
|
||||
|
||||
|
||||
def load_content(old_project):
|
||||
|
||||
# process all detections
|
||||
detections = []
|
||||
detections = load_objects("detections/*/*.yml", old_project)
|
||||
detections.extend(load_objects("detections/*/*/*.yml", old_project))
|
||||
|
||||
#print(len(detections))
|
||||
|
||||
return detections
|
||||
|
||||
|
||||
def analysis_detection(detections):
|
||||
|
||||
for detection in detections:#
|
||||
if detection['type'] != 'streaming':
|
||||
#if detection['name'] == 'Attempted Credential Dump From Registry via Reg exe':
|
||||
print('Analysis Detection: ' + detection['name'])
|
||||
call_splunk_parser_api(detection)
|
||||
|
||||
# sort parsed fields by occurence
|
||||
sorted_dict = {k: v for k, v in sorted(parsed_fields.items(), key=lambda item: item[1], reverse=True)}
|
||||
|
||||
with open('output_fields_ordered_by_usage.csv', mode='w') as csv_file:
|
||||
writer = csv.writer(csv_file, delimiter=',', quotechar='"', quoting=csv.QUOTE_ALL)
|
||||
|
||||
writer.writerow(['field_name', 'occurence'])
|
||||
|
||||
for field_name in sorted_dict:
|
||||
writer.writerow([field_name, sorted_dict[field_name]])
|
||||
|
||||
# sort parsed fields by name
|
||||
sorted_dict_2 = sorted(parsed_fields.items())
|
||||
|
||||
with open('output_fields_ordered_by_keys.csv', mode='w') as csv_file:
|
||||
writer = csv.writer(csv_file, delimiter=',', quotechar='"', quoting=csv.QUOTE_ALL)
|
||||
|
||||
writer.writerow(['field_name', 'occurence'])
|
||||
|
||||
for field_name in sorted_dict_2:
|
||||
writer.writerow([field_name[0], field_name[1]])
|
||||
|
||||
|
||||
def call_splunk_parser_api(detection):
|
||||
requests.packages.urllib3.disable_warnings(category=InsecureRequestWarning)
|
||||
spl = ''
|
||||
if detection['search'].startswith('| tstats'):
|
||||
spl = detection['search']
|
||||
else:
|
||||
spl = 'search ' + detection['search']
|
||||
data = {
|
||||
"output_mode": "json",
|
||||
"q": spl,
|
||||
"parse_only": "true"
|
||||
}
|
||||
#Have semgrep ignore the following line. It will complain about the verify=false, but the server is hosted on localhost
|
||||
response = requests.post(BASE_URL + SEARCH_PARSER_ENDPOINT, data=data, auth=(USER, PASSWORD), verify=False, headers={"Content-Type": "application/x-www-form-urlencoded"}) # nosemgrep
|
||||
if response.status_code != 200:
|
||||
print(response.json())
|
||||
print('ERROR: parser endpoint problems')
|
||||
return
|
||||
parse_commands(response.json())
|
||||
|
||||
|
||||
def parse_commands(api_response):
|
||||
tmp_parsed_fields = {}
|
||||
|
||||
last_stat_command = ''
|
||||
rename_command_after_stats_arr = []
|
||||
|
||||
for command in api_response['commands']:
|
||||
if command['command'] in ['tstats', 'stats', 'table']:
|
||||
last_stat_command = command
|
||||
if (command['command'] == 'rename') and last_stat_command:
|
||||
rename_command_after_stats_arr.append(command)
|
||||
|
||||
|
||||
if not last_stat_command:
|
||||
print('ERROR: could not find stats table or tasts command')
|
||||
return
|
||||
|
||||
# last command table
|
||||
if last_stat_command['command'] == 'table':
|
||||
matches = re.findall(r'([0-9a-zA-Z_]+)', last_stat_command['rawargs'])
|
||||
for match in matches:
|
||||
if match in tmp_parsed_fields:
|
||||
tmp_parsed_fields[match] = tmp_parsed_fields[match] + 1
|
||||
else:
|
||||
tmp_parsed_fields[match] = 1
|
||||
|
||||
# last command stats
|
||||
if last_stat_command['command'] == 'stats':
|
||||
match = re.match(r'(.*)by', last_stat_command['rawargs'])
|
||||
if match:
|
||||
args_one = match.group(1)
|
||||
matches = re.findall(r'(?:min|max|values)\(([0-9a-zA-Z_]+)\)', args_one)
|
||||
if matches:
|
||||
for match in matches:
|
||||
if match in tmp_parsed_fields:
|
||||
tmp_parsed_fields[match] = tmp_parsed_fields[match] + 1
|
||||
else:
|
||||
tmp_parsed_fields[match] = 1
|
||||
match = re.match(r'.*by(.*)$', last_stat_command['rawargs'])
|
||||
if match:
|
||||
args_two = match.group(1)
|
||||
matches = re.findall(r'([0-9a-zA-Z_]+)', args_two)
|
||||
if matches:
|
||||
for match in matches:
|
||||
if match in tmp_parsed_fields:
|
||||
tmp_parsed_fields[match] = tmp_parsed_fields[match] + 1
|
||||
else:
|
||||
tmp_parsed_fields[match] = 1
|
||||
|
||||
# tstats command
|
||||
if last_stat_command['command'] == 'tstats':
|
||||
match = re.match(r'(.*)(?:from|FROM)', last_stat_command['rawargs'])
|
||||
if match:
|
||||
args_one = match.group(1)
|
||||
matches = re.findall(r'(?:min|max|values)\(([0-9a-zA-Z_]+)\.([0-9a-zA-Z_]+)\)', args_one)
|
||||
if matches:
|
||||
for match in matches:
|
||||
field = match[0] + '.' + match[1]
|
||||
if field in tmp_parsed_fields:
|
||||
tmp_parsed_fields[field] = tmp_parsed_fields[field] + 1
|
||||
else:
|
||||
tmp_parsed_fields[field] = 1
|
||||
|
||||
match = re.match(r'.*by(.*)$', last_stat_command['rawargs'])
|
||||
if match:
|
||||
args_two = match.group(1)
|
||||
matches = re.findall(r'([0-9_a-zA-Z]+)\.([0-9a-zA-Z_]+)', args_two)
|
||||
if matches:
|
||||
for match in matches:
|
||||
field = match[0] + '.' + match[1]
|
||||
if field in tmp_parsed_fields:
|
||||
tmp_parsed_fields[field] = tmp_parsed_fields[field] + 1
|
||||
else:
|
||||
tmp_parsed_fields[field] = 1
|
||||
|
||||
match = re.match(r'.*where(.*)by.*$', last_stat_command['rawargs'])
|
||||
if match:
|
||||
args_three = match.group(1)
|
||||
matches = re.findall(r'([0-9_a-zA-Z]+)\.([0-9a-zA-Z_]+)=', args_three)
|
||||
if matches:
|
||||
for match in matches:
|
||||
field = match[0] + '.' + match[1]
|
||||
if field in tmp_parsed_fields:
|
||||
tmp_parsed_fields[field] = tmp_parsed_fields[field] + 1
|
||||
else:
|
||||
tmp_parsed_fields[field] = 1
|
||||
|
||||
|
||||
# rename occured
|
||||
for rename_command_after_stats in rename_command_after_stats_arr:
|
||||
if rename_command_after_stats:
|
||||
renamed_field = {}
|
||||
matches = re.findall(r'(?:(([0-9a-zA-Z_]+)\s+as\s+([0-9a-zA-Z_]+)))', rename_command_after_stats['rawargs'])
|
||||
for match in matches:
|
||||
renamed_field[match[1]] = match[2]
|
||||
|
||||
for key in renamed_field:
|
||||
if key in tmp_parsed_fields:
|
||||
tmp_parsed_fields[renamed_field[key]] = tmp_parsed_fields.pop(key)
|
||||
|
||||
# write to global parsed fields var
|
||||
for key in tmp_parsed_fields:
|
||||
if key in parsed_fields:
|
||||
parsed_fields[key] = parsed_fields[key] + tmp_parsed_fields[key]
|
||||
else:
|
||||
parsed_fields[key] = tmp_parsed_fields[key]
|
||||
|
||||
|
||||
def main(project):
|
||||
|
||||
detections = load_content(project)
|
||||
analysis_detection(detections)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
main("../")
|
||||
@@ -0,0 +1,9 @@
|
||||
import abc
|
||||
|
||||
from bin.contentctl_project.contentctl_core.domain.entities.enums.enums import SecurityContentType
|
||||
|
||||
class Adapter(abc.ABC):
|
||||
|
||||
@abc.abstractmethod
|
||||
def writeObjects(self, objects: list, output_path: str, type: SecurityContentType = None) -> None:
|
||||
pass
|
||||
@@ -0,0 +1,25 @@
|
||||
import abc
|
||||
|
||||
from bin.contentctl_project.contentctl_core.domain.entities.enums.enums import SecurityContentType
|
||||
from bin.contentctl_project.contentctl_core.domain.entities.baseline import Baseline
|
||||
from bin.contentctl_project.contentctl_core.domain.entities.enums.enums import SecurityContentProduct
|
||||
from bin.contentctl_project.contentctl_core.domain.entities.security_content_object import SecurityContentObject
|
||||
|
||||
|
||||
class BaselineBuilder(abc.ABC):
|
||||
|
||||
@abc.abstractmethod
|
||||
def addDeployment(self, deployments: list) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def setObject(self, path: str) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def reset(self) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def getObject(self) -> SecurityContentObject:
|
||||
pass
|
||||
@@ -0,0 +1,22 @@
|
||||
import abc
|
||||
|
||||
from bin.contentctl_project.contentctl_core.domain.entities.security_content_object import SecurityContentObject
|
||||
from bin.contentctl_project.contentctl_core.domain.entities.enums.enums import SecurityContentType
|
||||
|
||||
# https://refactoring.guru/design-patterns/builder
|
||||
|
||||
class BasicBuilder(abc.ABC):
|
||||
|
||||
@abc.abstractmethod
|
||||
def setObject(self, path: str, type: SecurityContentType) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def reset(self) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def getObject(self) -> SecurityContentObject:
|
||||
pass
|
||||
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import abc
|
||||
|
||||
from bin.contentctl_project.contentctl_core.domain.entities.enums.enums import SecurityContentProduct
|
||||
from bin.contentctl_project.contentctl_core.domain.entities.security_content_object import SecurityContentObject
|
||||
from bin.contentctl_project.contentctl_core.domain.entities.enums.enums import SecurityContentType
|
||||
|
||||
# https://refactoring.guru/design-patterns/builder
|
||||
|
||||
class DetectionBuilder(abc.ABC):
|
||||
|
||||
@abc.abstractmethod
|
||||
def addDeployment(self, deployments: list) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def addRBA(self) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def addNesFields(self) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def addMappings(self) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def addAnnotations(self) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def addPlaybook(self, playbooks: list) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def addBaseline(self, baselines: list) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def addUnitTest(self, tests: list) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def addMitreAttackEnrichment(self) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def addMacros(self, macros: list) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def addLookups(self, lookups: list) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def addCve(self) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def setObject(self, path: str) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def reset(self) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def getObject(self) -> SecurityContentObject:
|
||||
pass
|
||||
@@ -0,0 +1,51 @@
|
||||
import abc
|
||||
|
||||
from bin.contentctl_project.contentctl_core.application.builder.basic_builder import BasicBuilder
|
||||
from bin.contentctl_project.contentctl_core.application.builder.detection_builder import DetectionBuilder
|
||||
from bin.contentctl_project.contentctl_core.application.builder.baseline_builder import BaselineBuilder
|
||||
from bin.contentctl_project.contentctl_core.application.builder.investigation_builder import InvestigationBuilder
|
||||
from bin.contentctl_project.contentctl_core.application.builder.story_builder import StoryBuilder
|
||||
from bin.contentctl_project.contentctl_core.application.builder.playbook_builder import PlaybookBuilder
|
||||
from bin.contentctl_project.contentctl_core.domain.entities.enums.enums import SecurityContentProduct
|
||||
|
||||
class Director(abc.ABC):
|
||||
|
||||
@abc.abstractmethod
|
||||
def constructDetection(self, builder: DetectionBuilder, path: str, deployments: list, playbooks: list, baselines: list, tests: list, attack_enrichment: dict, macros: list) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def constructBaseline(self, builder: BaselineBuilder, path: str, deployments: list) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def constructDeployment(self, builder: BasicBuilder, path: str) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def constructLookup(self, builder: BasicBuilder, path: str) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def constructMacro(self, builder: BasicBuilder, path: str) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def constructPlaybook(self, builder: PlaybookBuilder, path: str, detections: list) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def constructTest(self, builder: BasicBuilder, path: str) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def constructStory(self, builder: StoryBuilder, path: str, detections: list, baselines: list, investigations: list) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def constructInvestigation(self, builder: InvestigationBuilder, path: str) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def constructObjects(self, builder: BasicBuilder, path: str) -> None:
|
||||
pass
|
||||
@@ -0,0 +1,25 @@
|
||||
import abc
|
||||
|
||||
from bin.contentctl_project.contentctl_core.domain.entities.security_content_object import SecurityContentObject
|
||||
|
||||
class InvestigationBuilder(abc.ABC):
|
||||
|
||||
@abc.abstractmethod
|
||||
def setObject(self, path: str) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def reset(self) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def getObject(self) -> SecurityContentObject:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def addInputs(self) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def addLowercaseName(self) -> None:
|
||||
pass
|
||||
@@ -0,0 +1,23 @@
|
||||
import abc
|
||||
|
||||
from bin.contentctl_project.contentctl_core.domain.entities.security_content_object import SecurityContentObject
|
||||
|
||||
# https://refactoring.guru/design-patterns/builder
|
||||
|
||||
class PlaybookBuilder(abc.ABC):
|
||||
|
||||
@abc.abstractmethod
|
||||
def setObject(self, path: str) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def addDetections(self, detections : list) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def reset(self) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def getObject(self) -> SecurityContentObject:
|
||||
pass
|
||||
@@ -0,0 +1,39 @@
|
||||
import abc
|
||||
|
||||
from bin.contentctl_project.contentctl_core.domain.entities.security_content_object import SecurityContentObject
|
||||
from bin.contentctl_project.contentctl_core.domain.entities.enums.enums import SecurityContentType
|
||||
|
||||
|
||||
class StoryBuilder(abc.ABC):
|
||||
|
||||
@abc.abstractmethod
|
||||
def addDetections(self, detections: list) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def addInvestigations(self, investigations: list) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def addAuthorCompanyName(self) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def addBaselines(self, baselines: list) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def addInvestigations(self, investigations: list) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def setObject(self, path: str) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def reset(self) -> None:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def getObject(self) -> SecurityContentObject:
|
||||
pass
|
||||
@@ -0,0 +1,58 @@
|
||||
import os
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from bin.contentctl_project.contentctl_core.domain.entities.enums.enums import SecurityContentType
|
||||
from bin.contentctl_project.contentctl_core.application.builder.basic_builder import BasicBuilder
|
||||
from bin.contentctl_project.contentctl_core.application.builder.detection_builder import DetectionBuilder
|
||||
from bin.contentctl_project.contentctl_core.application.builder.story_builder import StoryBuilder
|
||||
from bin.contentctl_project.contentctl_core.application.builder.director import Director
|
||||
from bin.contentctl_project.contentctl_core.application.factory.utils.utils import Utils
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BAFactoryInputDto:
|
||||
input_path: str
|
||||
basic_builder: BasicBuilder
|
||||
detection_builder: DetectionBuilder
|
||||
director: Director
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class BAFactoryOutputDto:
|
||||
detections: list
|
||||
tests: list
|
||||
|
||||
class BAFactory():
|
||||
input_dto: BAFactoryInputDto
|
||||
output_dto: BAFactoryOutputDto
|
||||
|
||||
def __init__(self, output_dto: BAFactoryOutputDto) -> None:
|
||||
self.output_dto = output_dto
|
||||
|
||||
def execute(self, input_dto: BAFactoryInputDto) -> None:
|
||||
self.input_dto = input_dto
|
||||
|
||||
self.createSecurityContent(SecurityContentType.unit_tests)
|
||||
self.createSecurityContent(SecurityContentType.detections)
|
||||
|
||||
|
||||
|
||||
def createSecurityContent(self, type: SecurityContentType) -> list:
|
||||
objects = []
|
||||
if type == SecurityContentType.unit_tests:
|
||||
files = Utils.get_all_yml_files_from_directory(os.path.join(self.input_dto.input_path, 'tests'))
|
||||
else:
|
||||
files = Utils.get_all_yml_files_from_directory(os.path.join(self.input_dto.input_path, str(type.name)))
|
||||
|
||||
for file in files:
|
||||
if 'ssa__' in file:
|
||||
if type == SecurityContentType.detections:
|
||||
self.input_dto.director.constructDetection(self.input_dto.detection_builder, file, [], [], [], self.output_dto.tests, {}, [], [])
|
||||
detection = self.input_dto.detection_builder.getObject()
|
||||
if not detection.deprecated and not detection.experimental:
|
||||
self.output_dto.detections.append(detection)
|
||||
elif type == SecurityContentType.unit_tests:
|
||||
self.input_dto.director.constructTest(self.input_dto.basic_builder, file)
|
||||
test = self.input_dto.basic_builder.getObject()
|
||||
self.output_dto.tests.append(test)
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
import os
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from bin.contentctl_project.contentctl_core.domain.entities.enums.enums import SecurityContentProduct
|
||||
from bin.contentctl_project.contentctl_core.domain.entities.enums.enums import SecurityContentType
|
||||
from bin.contentctl_project.contentctl_core.application.builder.basic_builder import BasicBuilder
|
||||
from bin.contentctl_project.contentctl_core.application.builder.detection_builder import DetectionBuilder
|
||||
from bin.contentctl_project.contentctl_core.application.builder.story_builder import StoryBuilder
|
||||
from bin.contentctl_project.contentctl_core.application.builder.baseline_builder import BaselineBuilder
|
||||
from bin.contentctl_project.contentctl_core.application.builder.investigation_builder import InvestigationBuilder
|
||||
from bin.contentctl_project.contentctl_core.application.builder.playbook_builder import PlaybookBuilder
|
||||
from bin.contentctl_project.contentctl_core.application.builder.director import Director
|
||||
from bin.contentctl_project.contentctl_core.application.factory.utils.utils import Utils
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FactoryInputDto:
|
||||
input_path: str
|
||||
basic_builder: BasicBuilder
|
||||
detection_builder: DetectionBuilder
|
||||
story_builder: StoryBuilder
|
||||
baseline_builder: BaselineBuilder
|
||||
investigation_builder: InvestigationBuilder
|
||||
playbook_builder: PlaybookBuilder
|
||||
director: Director
|
||||
attack_enrichment: dict
|
||||
|
||||
|
||||
@dataclass()
|
||||
class FactoryOutputDto:
|
||||
detections: list
|
||||
stories: list
|
||||
baselines: list
|
||||
investigations: list
|
||||
playbooks: list
|
||||
deployments: list
|
||||
macros: list
|
||||
lookups: list
|
||||
tests: list
|
||||
|
||||
|
||||
class Factory():
|
||||
input_dto: FactoryInputDto
|
||||
output_dto: FactoryOutputDto
|
||||
|
||||
|
||||
def __init__(self, output_dto: FactoryOutputDto) -> None:
|
||||
self.output_dto = output_dto
|
||||
|
||||
|
||||
def execute(self, input_dto: FactoryInputDto) -> None:
|
||||
self.input_dto = input_dto
|
||||
|
||||
# order matters to load and enrich security content types
|
||||
self.createSecurityContent(SecurityContentType.unit_tests)
|
||||
self.createSecurityContent(SecurityContentType.lookups)
|
||||
self.createSecurityContent(SecurityContentType.macros)
|
||||
self.createSecurityContent(SecurityContentType.deployments)
|
||||
self.createSecurityContent(SecurityContentType.baselines)
|
||||
self.createSecurityContent(SecurityContentType.investigations)
|
||||
self.createSecurityContent(SecurityContentType.detections) # execution only for playbook enrichment
|
||||
self.createSecurityContent(SecurityContentType.playbooks)
|
||||
self.output_dto.detections = []
|
||||
self.createSecurityContent(SecurityContentType.detections)
|
||||
self.createSecurityContent(SecurityContentType.stories)
|
||||
|
||||
|
||||
def createSecurityContent(self, type: SecurityContentType) -> list:
|
||||
objects = []
|
||||
if type == SecurityContentType.deployments:
|
||||
files = Utils.get_all_yml_files_from_directory(os.path.join(self.input_dto.input_path, str(type.name), 'ESCU'))
|
||||
elif type == SecurityContentType.unit_tests:
|
||||
files = Utils.get_all_yml_files_from_directory(os.path.join(self.input_dto.input_path, 'tests'))
|
||||
else:
|
||||
files = Utils.get_all_yml_files_from_directory(os.path.join(self.input_dto.input_path, str(type.name)))
|
||||
|
||||
for file in files:
|
||||
if not 'ssa__' in file:
|
||||
if type == SecurityContentType.lookups:
|
||||
self.input_dto.director.constructLookup(self.input_dto.basic_builder, file)
|
||||
self.output_dto.lookups.append(self.input_dto.basic_builder.getObject())
|
||||
|
||||
elif type == SecurityContentType.macros:
|
||||
self.input_dto.director.constructMacro(self.input_dto.basic_builder, file)
|
||||
self.output_dto.macros.append(self.input_dto.basic_builder.getObject())
|
||||
|
||||
elif type == SecurityContentType.deployments:
|
||||
self.input_dto.director.constructDeployment(self.input_dto.basic_builder, file)
|
||||
self.output_dto.deployments.append(self.input_dto.basic_builder.getObject())
|
||||
|
||||
elif type == SecurityContentType.playbooks:
|
||||
self.input_dto.director.constructPlaybook(self.input_dto.playbook_builder, file, self.output_dto.detections)
|
||||
self.output_dto.playbooks.append(self.input_dto.playbook_builder.getObject())
|
||||
|
||||
elif type == SecurityContentType.baselines:
|
||||
self.input_dto.director.constructBaseline(self.input_dto.baseline_builder, file, self.output_dto.deployments)
|
||||
baseline = self.input_dto.baseline_builder.getObject()
|
||||
self.output_dto.baselines.append(baseline)
|
||||
|
||||
elif type == SecurityContentType.investigations:
|
||||
self.input_dto.director.constructInvestigation(self.input_dto.investigation_builder, file)
|
||||
investigation = self.input_dto.investigation_builder.getObject()
|
||||
self.output_dto.investigations.append(investigation)
|
||||
|
||||
elif type == SecurityContentType.stories:
|
||||
self.input_dto.director.constructStory(self.input_dto.story_builder, file,
|
||||
self.output_dto.detections, self.output_dto.baselines, self.output_dto.investigations)
|
||||
story = self.input_dto.story_builder.getObject()
|
||||
self.output_dto.stories.append(story)
|
||||
|
||||
elif type == SecurityContentType.detections:
|
||||
self.input_dto.director.constructDetection(self.input_dto.detection_builder, file,
|
||||
self.output_dto.deployments, self.output_dto.playbooks, self.output_dto.baselines,
|
||||
self.output_dto.tests, self.input_dto.attack_enrichment, self.output_dto.macros,
|
||||
self.output_dto.lookups)
|
||||
detection = self.input_dto.detection_builder.getObject()
|
||||
self.output_dto.detections.append(detection)
|
||||
|
||||
elif type == SecurityContentType.unit_tests:
|
||||
self.input_dto.director.constructTest(self.input_dto.basic_builder, file)
|
||||
test = self.input_dto.basic_builder.getObject()
|
||||
self.output_dto.tests.append(test)
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
import os
|
||||
import uuid
|
||||
|
||||
from dataclasses import dataclass
|
||||
from PyInquirer import prompt
|
||||
from datetime import datetime
|
||||
|
||||
from bin.contentctl_project.contentctl_core.domain.entities.enums.enums import SecurityContentType
|
||||
from bin.contentctl_project.contentctl_core.application.factory.utils.new_content_questions import NewContentQuestions
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class NewContentFactoryInputDto:
|
||||
type: SecurityContentType
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class NewContentFactoryOutputDto:
|
||||
obj: dict
|
||||
|
||||
|
||||
class NewContentFactory():
|
||||
|
||||
|
||||
def __init__(self, output_dto: NewContentFactoryOutputDto) -> None:
|
||||
self.output_dto = output_dto
|
||||
|
||||
|
||||
def execute(self, input_dto: NewContentFactoryInputDto) -> None:
|
||||
if input_dto.type == SecurityContentType.detections:
|
||||
questions = NewContentQuestions.get_questions_detection()
|
||||
answers = prompt(questions)
|
||||
self.output_dto.obj['name'] = answers['detection_name']
|
||||
self.output_dto.obj['id'] = str(uuid.uuid4())
|
||||
self.output_dto.obj['version'] = 1
|
||||
self.output_dto.obj['date'] = datetime.today().strftime('%Y-%m-%d')
|
||||
self.output_dto.obj['author'] = answers['detection_author']
|
||||
self.output_dto.obj['type'] = answers['detection_type']
|
||||
self.output_dto.obj['datamodel'] = answers['datamodels']
|
||||
self.output_dto.obj['description'] = 'UPDATE_DESCRIPTION'
|
||||
file_name = self.output_dto.obj['name'].replace(' ', '_').replace('-','_').replace('.','_').replace('/','_').lower()
|
||||
self.output_dto.obj['search'] = answers['detection_search'] + ' | `' + file_name + '_filter`'
|
||||
self.output_dto.obj['how_to_implement'] = 'UPDATE_HOW_TO_IMPLEMENT'
|
||||
self.output_dto.obj['known_false_positives'] = 'UPDATE_KNOWN_FALSE_POSITIVES'
|
||||
self.output_dto.obj['references'] = ['REFERENCE']
|
||||
self.output_dto.obj['tags'] = dict()
|
||||
self.output_dto.obj['tags']['analytic_story'] = ['UPDATE_STORY_NAME']
|
||||
self.output_dto.obj['tags']['asset_type'] = 'UPDATE asset_type'
|
||||
self.output_dto.obj['tags']['cis20'] = ['CIS 3', 'CIS 5', 'CIS 16']
|
||||
self.output_dto.obj['tags']['confidence'] = 'UPDATE value between 1-100'
|
||||
self.output_dto.obj['tags']['context'] = ['Update context']
|
||||
self.output_dto.obj['tags']['dataset'] = ['UPDATE_DATASET_URL']
|
||||
self.output_dto.obj['tags']['impact'] = 'UPDATE value between 1-100'
|
||||
self.output_dto.obj['tags']['kill_chain_phases'] = answers['kill_chain_phases']
|
||||
self.output_dto.obj['tags']['message'] = 'UPDATE message'
|
||||
self.output_dto.obj['tags']['mitre_attack_id'] = [x.strip() for x in answers['mitre_attack_ids'].split(',')]
|
||||
self.output_dto.obj['tags']['nist'] = ['DE.CM']
|
||||
self.output_dto.obj['tags']['observable'] = [{'name': 'UPDATE', 'type': 'UPDATE', 'role': ['UPDATE']}]
|
||||
self.output_dto.obj['tags']['product'] = ['Splunk Enterprise','Splunk Enterprise Security','Splunk Cloud']
|
||||
self.output_dto.obj['tags']['required_fields'] = ['UPDATE']
|
||||
self.output_dto.obj['tags']['risk_score'] = 'UPDATE (impact * confidence)/100'
|
||||
self.output_dto.obj['tags']['security_domain'] = answers['security_domain']
|
||||
self.output_dto.obj['source'] = answers['detection_kind']
|
||||
|
||||
elif input_dto.type == SecurityContentType.stories:
|
||||
questions = NewContentQuestions.get_questions_story()
|
||||
answers = prompt(questions)
|
||||
self.output_dto.obj['name'] = answers['story_name']
|
||||
self.output_dto.obj['id'] = str(uuid.uuid4())
|
||||
self.output_dto.obj['version'] = 1
|
||||
self.output_dto.obj['date'] = datetime.today().strftime('%Y-%m-%d')
|
||||
self.output_dto.obj['author'] = answers['story_author']
|
||||
self.output_dto.obj['description'] = 'UPDATE_DESCRIPTION'
|
||||
self.output_dto.obj['narrative'] = 'UPDATE_NARRATIVE'
|
||||
self.output_dto.obj['references'] = []
|
||||
self.output_dto.obj['tags']['analytic_story'] = self.output_dto.obj['name']
|
||||
self.output_dto.obj['tags']['category'] = answers['category']
|
||||
self.output_dto.obj['tags']['product'] = ['Splunk Enterprise','Splunk Enterprise Security','Splunk Cloud']
|
||||
self.output_dto.obj['tags']['usecase'] = answers['usecase']
|
||||
@@ -0,0 +1,29 @@
|
||||
import os
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from bin.contentctl_project.contentctl_core.application.builder.basic_builder import BasicBuilder
|
||||
from bin.contentctl_project.contentctl_core.application.builder.director import Director
|
||||
from bin.contentctl_project.contentctl_core.domain.entities.enums.enums import SecurityContentType
|
||||
from bin.contentctl_project.contentctl_core.application.factory.utils.utils import Utils
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ObjectFactoryInputDto:
|
||||
input_path: str
|
||||
builder: BasicBuilder
|
||||
director: Director
|
||||
|
||||
|
||||
class ObjectFactory():
|
||||
objects: list
|
||||
|
||||
def __init__(self, objects: list) -> None:
|
||||
self.objects = objects
|
||||
|
||||
def execute(self, input_dto: ObjectFactoryInputDto) -> None:
|
||||
self.input_path = input_dto.input_path
|
||||
|
||||
files = Utils.get_all_yml_files_from_directory(input_dto.input_path)
|
||||
for file in files:
|
||||
input_dto.director.constructObjects(input_dto.builder, file)
|
||||
self.objects.append(input_dto.builder.getObject())
|
||||
+266
@@ -0,0 +1,266 @@
|
||||
|
||||
|
||||
class NewContentQuestions():
|
||||
|
||||
@classmethod
|
||||
def get_questions_detection(self) -> list:
|
||||
questions = [
|
||||
{
|
||||
'type': 'list',
|
||||
'message': 'what kind of detection is this',
|
||||
'name': 'detection_kind',
|
||||
'choices': [
|
||||
{
|
||||
'name': 'endpoint'
|
||||
},
|
||||
{
|
||||
'name': 'cloud'
|
||||
},
|
||||
{
|
||||
'name': 'application'
|
||||
},
|
||||
{
|
||||
'name': 'network'
|
||||
},
|
||||
{
|
||||
'name': 'web'
|
||||
},
|
||||
{
|
||||
'name': 'experimental'
|
||||
},
|
||||
|
||||
],
|
||||
'default': 'endpoint'
|
||||
},
|
||||
{
|
||||
'type': 'input',
|
||||
'message': 'enter detection name',
|
||||
'name': 'detection_name',
|
||||
'default': 'Powershell Encoded Command',
|
||||
},
|
||||
{
|
||||
'type': 'input',
|
||||
'message': 'enter author name',
|
||||
'name': 'detection_author',
|
||||
},
|
||||
{
|
||||
'type': 'list',
|
||||
'message': 'select a detection type',
|
||||
'name': 'detection_type',
|
||||
'choices': [
|
||||
{
|
||||
'name': 'TTP'
|
||||
},
|
||||
{
|
||||
'name': 'Anomaly'
|
||||
},
|
||||
{
|
||||
'name': 'Hunting'
|
||||
},
|
||||
{
|
||||
'name': 'Baseline'
|
||||
},
|
||||
{
|
||||
'name': 'Investigation'
|
||||
},
|
||||
{
|
||||
'name': 'Correlation'
|
||||
}
|
||||
|
||||
],
|
||||
'default': 'TTP'
|
||||
},
|
||||
{
|
||||
'type': 'checkbox',
|
||||
'message': 'select the datamodels used in the detection',
|
||||
'name': 'datamodels',
|
||||
'choices': [
|
||||
{
|
||||
'name': 'Endpoint',
|
||||
'checked': True
|
||||
},
|
||||
{
|
||||
'name': 'Authentication'
|
||||
},
|
||||
{
|
||||
'name': 'Change'
|
||||
},
|
||||
{
|
||||
'name': 'Email'
|
||||
},
|
||||
{
|
||||
'name': 'Network_Resolution'
|
||||
},
|
||||
{
|
||||
'name': 'Network_Traffic'
|
||||
},
|
||||
{
|
||||
'name': 'Network_Sessions'
|
||||
},
|
||||
{
|
||||
'name': 'Updates'
|
||||
},
|
||||
{
|
||||
'name': 'Vulnerabilities'
|
||||
},
|
||||
{
|
||||
'name': 'Web'
|
||||
},
|
||||
{
|
||||
'name': 'Risk'
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'type': 'input',
|
||||
'message': 'enter search (spl)',
|
||||
'name': 'detection_search',
|
||||
'default': '| UPDATE_SPL'
|
||||
},
|
||||
{
|
||||
'type': 'input',
|
||||
'message': 'enter MITRE ATT&CK Technique IDs related to the detection, comma delimited for multiple',
|
||||
'name': 'mitre_attack_ids',
|
||||
'default': 'T1003.002'
|
||||
},
|
||||
{
|
||||
'type': 'checkbox',
|
||||
'message': 'select kill chain phases related to the detection',
|
||||
'name': 'kill_chain_phases',
|
||||
'choices': [
|
||||
|
||||
{
|
||||
'name': 'Reconnaissance'
|
||||
},
|
||||
{
|
||||
'name': 'Intrusion'
|
||||
},
|
||||
{
|
||||
'name': 'Exploitation',
|
||||
'checked': True
|
||||
},
|
||||
{
|
||||
'name': 'Privilege Escalation'
|
||||
},
|
||||
{
|
||||
'name': 'Lateral Movement'
|
||||
},
|
||||
{
|
||||
'name': 'Obfuscation'
|
||||
},
|
||||
{
|
||||
'name': 'Denial of Service'
|
||||
},
|
||||
{
|
||||
'name': 'Exfiltration'
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'type': 'list',
|
||||
'message': 'security_domain for detection',
|
||||
'name': 'security_domain',
|
||||
'choices': [
|
||||
{
|
||||
'name': 'access'
|
||||
},
|
||||
{
|
||||
'name': 'endpoint'
|
||||
},
|
||||
{
|
||||
'name': 'network'
|
||||
},
|
||||
{
|
||||
'name': 'threat'
|
||||
},
|
||||
{
|
||||
'name': 'identity'
|
||||
},
|
||||
{
|
||||
'name': 'audit'
|
||||
},
|
||||
|
||||
],
|
||||
'default': 'endpoint'
|
||||
},
|
||||
]
|
||||
return questions
|
||||
|
||||
|
||||
@classmethod
|
||||
def get_questions_story(self) -> list:
|
||||
questions = [
|
||||
{
|
||||
'type': 'input',
|
||||
'message': 'enter story name',
|
||||
'name': 'story_name',
|
||||
'default': 'Suspicious Powershell Behavior',
|
||||
},
|
||||
{
|
||||
'type': 'input',
|
||||
'message': 'enter author name',
|
||||
'name': 'story_author',
|
||||
},
|
||||
{
|
||||
'type': 'checkbox',
|
||||
'message': 'select a category',
|
||||
'name': 'category',
|
||||
'choices': [
|
||||
{
|
||||
'name': 'Adversary Tactics',
|
||||
'checked': True
|
||||
},
|
||||
{
|
||||
'name': 'Account Compromise'
|
||||
},
|
||||
{
|
||||
'name': 'Unauthorized Software'
|
||||
},
|
||||
{
|
||||
'name': 'Best Practices'
|
||||
},
|
||||
{
|
||||
'name': 'Cloud Security'
|
||||
},
|
||||
{
|
||||
'name': 'Command and Control'
|
||||
},
|
||||
{
|
||||
'name': 'Lateral Movement'
|
||||
},
|
||||
{
|
||||
'name': 'Ransomware'
|
||||
},
|
||||
{
|
||||
'name': 'Privilege Escalation'
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
'type': 'list',
|
||||
'message': 'select a use case',
|
||||
'name': 'usecase',
|
||||
'choices': [
|
||||
{
|
||||
'name': 'Advanced Threat Detection',
|
||||
'checked': True
|
||||
},
|
||||
{
|
||||
'name': 'Security Monitoring'
|
||||
},
|
||||
{
|
||||
'name': 'Compliance'
|
||||
},
|
||||
{
|
||||
'name': 'Insider Threat'
|
||||
},
|
||||
{
|
||||
'name': 'Application Security'
|
||||
},
|
||||
{
|
||||
'name': 'Other'
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
return questions
|
||||
@@ -0,0 +1,13 @@
|
||||
import os
|
||||
|
||||
class Utils:
|
||||
|
||||
@staticmethod
|
||||
def get_all_yml_files_from_directory(path: str) -> list:
|
||||
listOfFiles = list()
|
||||
for (dirpath, dirnames, filenames) in os.walk(path):
|
||||
for file in filenames:
|
||||
if file.endswith(".yml"):
|
||||
listOfFiles.append(os.path.join(dirpath, file))
|
||||
|
||||
return sorted(listOfFiles)
|
||||
@@ -0,0 +1,189 @@
|
||||
import re
|
||||
import uuid
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from bin.contentctl_project.contentctl_core.application.factory.object_factory import ObjectFactory, ObjectFactoryInputDto
|
||||
from bin.contentctl_project.contentctl_core.application.adapter.adapter import Adapter
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ContentChangerInputDto:
|
||||
adapter : Adapter
|
||||
factory_input_dto : ObjectFactoryInputDto
|
||||
converter_func_name : str
|
||||
|
||||
|
||||
class ContentChanger:
|
||||
|
||||
def execute(self, input_dto: ContentChangerInputDto) -> None:
|
||||
objects = list()
|
||||
factory = ObjectFactory(objects)
|
||||
factory.execute(input_dto.factory_input_dto)
|
||||
|
||||
converter_func = getattr(self, input_dto.converter_func_name)
|
||||
converter_func(objects)
|
||||
|
||||
input_dto.adapter.writeObjectsInPlace(objects)
|
||||
|
||||
|
||||
# Define Converter Functions here
|
||||
|
||||
def example_converter_func(self, objects : list) -> None:
|
||||
for obj in objects:
|
||||
obj['author'] = obj['author'].upper()
|
||||
|
||||
def add_default_risk_values(self, objects : list) -> None:
|
||||
for obj in objects:
|
||||
if not 'confidence' in obj['tags']:
|
||||
obj['tags']['confidence'] = 50
|
||||
if not 'impact' in obj['tags']:
|
||||
obj['tags']['impact'] = 50
|
||||
if not 'risk_score' in obj['tags']:
|
||||
obj['tags']['risk_score'] = 25
|
||||
|
||||
def add_unknown_context(self, objects : list) -> None:
|
||||
for obj in objects:
|
||||
if not 'context' in obj['tags']:
|
||||
obj['tags']['context'] = ['Unknown']
|
||||
|
||||
def add_default_message(self, objects : list) -> None:
|
||||
for obj in objects:
|
||||
if not 'message' in obj['tags']:
|
||||
obj['tags']['message'] = 'tbd'
|
||||
|
||||
def add_default_observable(self, objects : list) -> None:
|
||||
for obj in objects:
|
||||
if not 'observable' in obj['tags'] or ('observable' in obj['tags'] and len(obj['tags']['observable']) == 0):
|
||||
observables = []
|
||||
regexp_user = re.compile(r'user')
|
||||
if regexp_user.search(obj['search']):
|
||||
observables.append({'name': 'user', 'type': 'User', 'role': ['Victim']})
|
||||
regexp_user = re.compile(r'dest')
|
||||
if regexp_user.search(obj['search']):
|
||||
observables.append({'name': 'dest', 'type': 'Hostname', 'role': ['Victim']})
|
||||
if len(observables) == 0:
|
||||
observables.append({'name': 'dest', 'type': 'Other', 'role': ['Other']})
|
||||
obj['tags']['observable'] = observables
|
||||
|
||||
def add_default_cis(self, objects : list) -> None:
|
||||
for obj in objects:
|
||||
if not 'cis20' in obj['tags']:
|
||||
obj['tags']['cis20'] = ['CIS 3', 'CIS 5', 'CIS 16']
|
||||
|
||||
def add_default_nist(self, objects : list) -> None:
|
||||
for obj in objects:
|
||||
if not 'nist' in obj['tags']:
|
||||
obj['tags']['nist'] = ['DE.CM']
|
||||
|
||||
def fix_broken_uuids(self, objects : list) -> None:
|
||||
for obj in objects:
|
||||
try:
|
||||
uuid.UUID(str(obj['id']))
|
||||
except:
|
||||
obj['id'] = str(uuid.uuid4())
|
||||
|
||||
def fix_wrong_kill_chain_phases(self, objects : list) -> None:
|
||||
valid_kill_chain_phases = [
|
||||
'Reconnaissance', 'Weaponization', 'Delivery',
|
||||
'Exploitation', 'Installation', 'Command and Control',
|
||||
'Actions on Objectives']
|
||||
for obj in objects:
|
||||
if 'kill_chain_phases' in obj['tags']:
|
||||
for value in obj['tags']['kill_chain_phases']:
|
||||
if value not in valid_kill_chain_phases:
|
||||
obj['tags']['kill_chain_phases'] = ['Exploitation']
|
||||
break
|
||||
|
||||
def add_default_kill_chain_phases(self, objects : list) -> None:
|
||||
for obj in objects:
|
||||
if 'kill_chain_phases' not in obj['tags']:
|
||||
obj['tags']['kill_chain_phases'] = ['Exploitation']
|
||||
if obj['tags']['kill_chain_phases'] == ['Privilege Escalation']:
|
||||
obj['tags']['kill_chain_phases'] = ['Exploitation']
|
||||
|
||||
def fix_wrong_calculated_risk_score(self, objects : list) -> None:
|
||||
for obj in objects:
|
||||
calculated_risk_score = (int(obj['tags']['impact']))*(int(obj['tags']['confidence']))/100
|
||||
if calculated_risk_score != int(obj['tags']['risk_score']):
|
||||
obj['tags']['risk_score'] = calculated_risk_score
|
||||
|
||||
def add_asset_type_to_endpoint_detections(self, objects : list) -> None:
|
||||
for obj in objects:
|
||||
if 'asset_type' not in obj['tags']:
|
||||
if '/endpoint/' in obj['file_path']:
|
||||
obj['tags']['asset_type'] = 'Endpoint'
|
||||
|
||||
def fix_observables(self, objects : list) -> None:
|
||||
for obj in objects:
|
||||
if 'observable' in obj['tags']:
|
||||
for observable in obj['tags']['observable']:
|
||||
if observable['type'] == 'Parent Process':
|
||||
observable['type'] = 'Process'
|
||||
if observable['type'] == 'user':
|
||||
observable['type'] = 'User'
|
||||
if observable['type'] == 'process name':
|
||||
observable['type'] = 'Process'
|
||||
|
||||
def fix_context(self, objects : list) -> None:
|
||||
for obj in objects:
|
||||
if 'context' in obj['tags']:
|
||||
new_context = []
|
||||
for context in obj['tags']['context']:
|
||||
if context == 'Stage:Exploitation':
|
||||
context = 'Stage:Execution'
|
||||
new_context.append(context)
|
||||
|
||||
obj['tags']['context'] = list(dict.fromkeys(new_context))
|
||||
|
||||
def add_default_values_deprecated(self, objects : list) -> None:
|
||||
for obj in objects:
|
||||
if 'context' not in obj['tags']:
|
||||
obj['tags']['context'] = ['Unknown']
|
||||
if 'message' not in obj['tags']:
|
||||
obj['tags']['message'] = 'tbd'
|
||||
if 'observable' not in obj['tags']:
|
||||
obj['tags']['observable'] = [{'name': 'field', 'type': 'Unknown', 'role': ['Unknown']}]
|
||||
|
||||
def fix_story(self, objects : list) -> None:
|
||||
for obj in objects:
|
||||
if 'type' not in obj:
|
||||
print(obj['name'])
|
||||
if isinstance(obj['tags']['analytic_story'], list):
|
||||
obj['tags']['analytic_story'] = obj['tags']['analytic_story'][0]
|
||||
|
||||
def remove_SAAWS(self, objects : list) -> None:
|
||||
for obj in objects:
|
||||
if 'Splunk Security Analytics for AWS' in obj['tags']['product']:
|
||||
obj['tags']['product'].remove('Splunk Security Analytics for AWS')
|
||||
|
||||
def remove_testing_passed(self, objects : list) -> None:
|
||||
for obj in objects:
|
||||
if 'automated_detection_testing' in obj['tags']:
|
||||
obj['tags'].pop('automated_detection_testing')
|
||||
|
||||
def change_test_file_format(self, objects : list) -> None:
|
||||
for obj in objects:
|
||||
obj['name'] = obj['name'] + ' Unit Test'
|
||||
|
||||
def fix_kill_chain(self, objects : list) -> None:
|
||||
for obj in objects:
|
||||
if 'kill_chain_phases' in obj['tags']:
|
||||
if obj['tags']['kill_chain_phases'] == 'Exploitation':
|
||||
obj['tags']['kill_chain_phases'] = ['Exploitation']
|
||||
|
||||
def add_default_confidence_impact_risk_score(self, objects : list) -> None:
|
||||
for obj in objects:
|
||||
if 'confidence' not in obj['tags']:
|
||||
obj['tags']['confidence'] = 50
|
||||
if 'impact' not in obj['tags']:
|
||||
obj['tags']['impact'] = 50
|
||||
if 'risk_score' not in obj['tags']:
|
||||
calculated_risk_score = (int(obj['tags']['impact']))*(int(obj['tags']['confidence']))/100
|
||||
obj['tags']['risk_score'] = calculated_risk_score
|
||||
|
||||
def fix_cc(self, objects : list) -> None:
|
||||
for obj in objects:
|
||||
if 'Command & Control' in obj['tags']['analytic_story']:
|
||||
obj['tags']['analytic_story'].remove('Command & Control')
|
||||
obj['tags']['analytic_story'].append('Command and Control')
|
||||
@@ -0,0 +1,23 @@
|
||||
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from bin.contentctl_project.contentctl_core.application.factory.factory import FactoryInputDto, Factory, FactoryOutputDto
|
||||
from bin.contentctl_project.contentctl_core.application.adapter.adapter import Adapter
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DocGenInputDto:
|
||||
output_path: str
|
||||
factory_input_dto: FactoryInputDto
|
||||
adapter : Adapter
|
||||
|
||||
|
||||
class DocGen:
|
||||
|
||||
def execute(self, input_dto: DocGenInputDto) -> None:
|
||||
factory_output_dto = FactoryOutputDto([],[],[],[],[],[],[],[],[])
|
||||
factory = Factory(factory_output_dto)
|
||||
factory.execute(input_dto.factory_input_dto)
|
||||
|
||||
input_dto.adapter.writeObjects([factory_output_dto.stories, factory_output_dto.detections, factory_output_dto.playbooks], input_dto.output_path)
|
||||
@@ -0,0 +1,58 @@
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from bin.contentctl_project.contentctl_core.domain.entities.enums.enums import SecurityContentProduct, SecurityContentType
|
||||
from bin.contentctl_project.contentctl_core.application.adapter.adapter import Adapter
|
||||
from bin.contentctl_project.contentctl_core.application.factory.factory import FactoryInputDto, Factory, FactoryOutputDto
|
||||
from bin.contentctl_project.contentctl_core.application.factory.ba_factory import BAFactoryInputDto, BAFactory, BAFactoryOutputDto
|
||||
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GenerateInputDto:
|
||||
output_path: str
|
||||
factory_input_dto: FactoryInputDto
|
||||
ba_factory_input_dto: BAFactoryInputDto
|
||||
adapter : Adapter
|
||||
product: SecurityContentProduct
|
||||
|
||||
|
||||
class Generate:
|
||||
|
||||
def execute(self, input_dto: GenerateInputDto) -> None:
|
||||
|
||||
if input_dto.product == SecurityContentProduct.ESCU:
|
||||
factory_output_dto = FactoryOutputDto([],[],[],[],[],[],[],[],[])
|
||||
factory = Factory(factory_output_dto)
|
||||
factory.execute(input_dto.factory_input_dto)
|
||||
input_dto.adapter.writeHeaders(input_dto.output_path)
|
||||
input_dto.adapter.writeObjects(factory_output_dto.detections, input_dto.output_path, SecurityContentType.detections)
|
||||
input_dto.adapter.writeObjects(factory_output_dto.stories, input_dto.output_path, SecurityContentType.stories)
|
||||
input_dto.adapter.writeObjects(factory_output_dto.baselines, input_dto.output_path, SecurityContentType.baselines)
|
||||
input_dto.adapter.writeObjects(factory_output_dto.investigations, input_dto.output_path, SecurityContentType.investigations)
|
||||
input_dto.adapter.writeObjects(factory_output_dto.lookups, input_dto.output_path, SecurityContentType.lookups)
|
||||
input_dto.adapter.writeObjects(factory_output_dto.macros, input_dto.output_path, SecurityContentType.macros)
|
||||
|
||||
elif input_dto.product == SecurityContentProduct.SSA:
|
||||
shutil.rmtree(input_dto.output_path + '/srs/', ignore_errors=True)
|
||||
shutil.rmtree(input_dto.output_path + '/complex/', ignore_errors=True)
|
||||
os.makedirs(input_dto.output_path + '/complex/')
|
||||
os.makedirs(input_dto.output_path + '/srs/')
|
||||
factory_output_dto = BAFactoryOutputDto([],[])
|
||||
factory = BAFactory(factory_output_dto)
|
||||
factory.execute(input_dto.ba_factory_input_dto)
|
||||
input_dto.adapter.writeObjects(factory_output_dto.detections, input_dto.output_path)
|
||||
|
||||
elif input_dto.product == SecurityContentProduct.API:
|
||||
factory_output_dto = FactoryOutputDto([],[],[],[],[],[],[],[],[])
|
||||
factory = Factory(factory_output_dto)
|
||||
factory.execute(input_dto.factory_input_dto)
|
||||
input_dto.adapter.writeObjects(factory_output_dto.detections, input_dto.output_path, SecurityContentType.detections)
|
||||
input_dto.adapter.writeObjects(factory_output_dto.stories, input_dto.output_path, SecurityContentType.stories)
|
||||
input_dto.adapter.writeObjects(factory_output_dto.baselines, input_dto.output_path, SecurityContentType.baselines)
|
||||
input_dto.adapter.writeObjects(factory_output_dto.investigations, input_dto.output_path, SecurityContentType.investigations)
|
||||
input_dto.adapter.writeObjects(factory_output_dto.lookups, input_dto.output_path, SecurityContentType.lookups)
|
||||
input_dto.adapter.writeObjects(factory_output_dto.macros, input_dto.output_path, SecurityContentType.macros)
|
||||
input_dto.adapter.writeObjects(factory_output_dto.deployments, input_dto.output_path, SecurityContentType.deployments)
|
||||
@@ -0,0 +1,22 @@
|
||||
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from bin.contentctl_project.contentctl_core.application.factory.new_content_factory import NewContentFactory, NewContentFactoryInputDto, NewContentFactoryOutputDto
|
||||
from bin.contentctl_project.contentctl_core.application.adapter.adapter import Adapter
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class NewContentInputDto:
|
||||
factory_input_dto: NewContentFactoryInputDto
|
||||
adapter : Adapter
|
||||
|
||||
|
||||
class NewContent:
|
||||
|
||||
def execute(self, input_dto: NewContentInputDto) -> None:
|
||||
factory_output_dto = NewContentFactoryOutputDto(dict())
|
||||
factory = NewContentFactory(factory_output_dto)
|
||||
factory.execute(input_dto.factory_input_dto)
|
||||
|
||||
input_dto.adapter.writeObjectNewContent(factory_output_dto.obj, input_dto.factory_input_dto.type)
|
||||
@@ -0,0 +1,24 @@
|
||||
import os
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from bin.contentctl_project.contentctl_core.application.factory.factory import FactoryInputDto, Factory, FactoryOutputDto
|
||||
from bin.contentctl_project.contentctl_core.application.adapter.adapter import Adapter
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ReportingInputDto:
|
||||
factory_input_dto: FactoryInputDto
|
||||
adapter_svg : Adapter
|
||||
adapter_attack : Adapter
|
||||
|
||||
|
||||
class Reporting:
|
||||
|
||||
def execute(self, input_dto: ReportingInputDto) -> None:
|
||||
factory_output_dto = FactoryOutputDto([],[],[],[],[],[],[],[],[])
|
||||
factory = Factory(factory_output_dto)
|
||||
factory.execute(input_dto.factory_input_dto)
|
||||
|
||||
input_dto.adapter_svg.writeObjects(factory_output_dto.detections, os.path.join(os.path.dirname(__file__), '../../../../reporting'))
|
||||
input_dto.adapter_attack.writeObjects(factory_output_dto.detections, os.path.join(os.path.dirname(__file__), '../../../../../docs/mitre-map'))
|
||||
@@ -0,0 +1,47 @@
|
||||
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
from pydantic import ValidationError
|
||||
|
||||
from bin.contentctl_project.contentctl_core.domain.entities.enums.enums import SecurityContentProduct
|
||||
from bin.contentctl_project.contentctl_core.application.factory.factory import FactoryInputDto, Factory, FactoryOutputDto
|
||||
from bin.contentctl_project.contentctl_core.application.factory.ba_factory import BAFactoryInputDto, BAFactory, BAFactoryOutputDto
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ValidateInputDto:
|
||||
factory_input_dto: FactoryInputDto
|
||||
ba_factory_input_dto: BAFactoryInputDto
|
||||
product: SecurityContentProduct
|
||||
|
||||
|
||||
class Validate:
|
||||
|
||||
def execute(self, input_dto: ValidateInputDto) -> None:
|
||||
if input_dto.product == SecurityContentProduct.ESCU:
|
||||
factory_output_dto = FactoryOutputDto([],[],[],[],[],[],[],[],[])
|
||||
factory = Factory(factory_output_dto)
|
||||
factory.execute(input_dto.factory_input_dto)
|
||||
|
||||
elif input_dto.product == SecurityContentProduct.SSA:
|
||||
factory_output_dto = BAFactoryOutputDto([],[])
|
||||
factory = BAFactory(factory_output_dto)
|
||||
factory.execute(input_dto.ba_factory_input_dto)
|
||||
|
||||
|
||||
# validate detections
|
||||
|
||||
# validate tests
|
||||
self.validate_detection_exist_for_test(factory_output_dto.tests, factory_output_dto.detections)
|
||||
|
||||
|
||||
def validate_detection_exist_for_test(self, tests : list, detections: list):
|
||||
for test in tests:
|
||||
found_detection = False
|
||||
for detection in detections:
|
||||
if test.tests[0].file in detection.file_path:
|
||||
found_detection = True
|
||||
|
||||
if not found_detection:
|
||||
ValueError("detection doesn't exist for test file: " + test.name)
|
||||
@@ -0,0 +1,98 @@
|
||||
|
||||
|
||||
SES_CONTEXT_MAPPING = {
|
||||
"Unknown": 0,
|
||||
"Source:Endpoint": 10,
|
||||
"Source:AD": 11,
|
||||
"Source:Firewall": 12,
|
||||
"Source:Application Log": 13,
|
||||
"Source:IPS": 14,
|
||||
"Source:Cloud Data": 15,
|
||||
"Source:Correlation": 16,
|
||||
"Source:Printer": 17,
|
||||
"Source:Badge": 18,
|
||||
"Scope:Internal": 20,
|
||||
"Scope:External": 21,
|
||||
"Scope:Inbound": 22,
|
||||
"Scope:Outbound": 23,
|
||||
"Scope:Local": 24,
|
||||
"Scope:Network": 25,
|
||||
"Outcome:Blocked": 30,
|
||||
"Outcome:Allowed": 31,
|
||||
"Stage:Recon": 40,
|
||||
"Stage:Initial Access": 41,
|
||||
"Stage:Execution": 42,
|
||||
"Stage:Persistence": 43,
|
||||
"Stage:Privilege Escalation": 44,
|
||||
"Stage:Defense Evasion": 45,
|
||||
"Stage:Credential Access": 46,
|
||||
"Stage:Discovery": 47,
|
||||
"Stage:Lateral Movement": 48,
|
||||
"Stage:Collection": 49,
|
||||
"Stage:Exfiltration": 50,
|
||||
"Stage:Command And Control": 51,
|
||||
"Consequence:Infection": 60,
|
||||
"Consequence:Reduced Visibility": 61,
|
||||
"Consequence:Data Destruction": 62,
|
||||
"Consequence:Denial Of Service": 63,
|
||||
"Consequence:Loss Of Control": 64,
|
||||
"Rares:Rare User": 70,
|
||||
"Rares:Rare Process": 71,
|
||||
"Rares:Rare Device": 72,
|
||||
"Rares:Rare Domain": 73,
|
||||
"Rares:Rare Network": 74,
|
||||
"Rares:Rare Location": 75,
|
||||
"Other:Peer Group": 80,
|
||||
"Other:Brute Force": 81,
|
||||
"Other:Policy Violation": 82,
|
||||
"Other:Threat Intelligence": 83,
|
||||
"Other:Flight Risk": 84,
|
||||
"Other:Removable Storage": 85
|
||||
}
|
||||
|
||||
SES_KILL_CHAIN_MAPPINGS = {
|
||||
"Unknown": 0,
|
||||
"Reconnaissance": 1,
|
||||
"Weaponization": 2,
|
||||
"Delivery": 3,
|
||||
"Exploitation": 4,
|
||||
"Installation": 5,
|
||||
"Command & Control": 6,
|
||||
"Actions on Objectives": 7
|
||||
}
|
||||
|
||||
SES_OBSERVABLE_ROLE_MAPPING = {
|
||||
"Other": -1,
|
||||
"Unknown": 0,
|
||||
"Actor": 1,
|
||||
"Target": 2,
|
||||
"Attacker": 3,
|
||||
"Victim": 4,
|
||||
"Parent Process": 5,
|
||||
"Child Process": 6,
|
||||
"Known Bad": 7,
|
||||
"Data Loss": 8,
|
||||
"Observer": 9
|
||||
}
|
||||
|
||||
SES_OBSERVABLE_TYPE_MAPPING = {
|
||||
"Other": -1,
|
||||
"Unknown": 0,
|
||||
"Device": 1,
|
||||
"Container": 2,
|
||||
"Endpoint": 3,
|
||||
"Hostname": 4,
|
||||
"IP Address": 5,
|
||||
"User": 6,
|
||||
"Username": 7,
|
||||
"Email": 8,
|
||||
"Email Address": 9,
|
||||
"URL": 10,
|
||||
"URL Domain": 11,
|
||||
"File": 12,
|
||||
"File Name": 13,
|
||||
"File Hash": 14,
|
||||
"Process": 15,
|
||||
"Process Name": 16,
|
||||
"Location": 17
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import string
|
||||
import uuid
|
||||
import requests
|
||||
|
||||
from pydantic import BaseModel, validator, ValidationError
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
|
||||
from bin.contentctl_project.contentctl_core.domain.entities.security_content_object import SecurityContentObject
|
||||
from bin.contentctl_project.contentctl_core.domain.entities.enums.enums import DataModel
|
||||
from bin.contentctl_project.contentctl_core.domain.entities.baseline_tags import BaselineTags
|
||||
from bin.contentctl_project.contentctl_core.domain.entities.deployment import Deployment
|
||||
|
||||
|
||||
class Baseline(BaseModel, SecurityContentObject):
|
||||
# baseline spec
|
||||
name: str
|
||||
id: str
|
||||
version: int
|
||||
date: str
|
||||
author: str
|
||||
type: str
|
||||
datamodel: list
|
||||
description: str
|
||||
search: str
|
||||
how_to_implement: str
|
||||
known_false_positives: str
|
||||
references: list
|
||||
tags: BaselineTags
|
||||
|
||||
# enrichment
|
||||
deployment: Deployment = None
|
||||
|
||||
|
||||
@validator('name')
|
||||
def name_max_length(cls, v):
|
||||
if len(v) > 75:
|
||||
raise ValueError('name is longer then 75 chars: ' + v)
|
||||
return v
|
||||
|
||||
@validator('name')
|
||||
def name_invalid_chars(cls, v):
|
||||
invalidChars = set(string.punctuation.replace("-", ""))
|
||||
if any(char in invalidChars for char in v):
|
||||
raise ValueError('invalid chars used in name: ' + v)
|
||||
return v
|
||||
|
||||
@validator('id')
|
||||
def id_check(cls, v, values):
|
||||
try:
|
||||
uuid.UUID(str(v))
|
||||
except:
|
||||
raise ValueError('uuid is not valid: ' + values["name"])
|
||||
return v
|
||||
|
||||
@validator('date')
|
||||
def date_valid(cls, v, values):
|
||||
try:
|
||||
datetime.strptime(v, "%Y-%m-%d")
|
||||
except:
|
||||
raise ValueError('date is not in format YYYY-MM-DD: ' + values["name"])
|
||||
return v
|
||||
|
||||
@validator('type')
|
||||
def type_valid(cls, v, values):
|
||||
if v != "Baseline":
|
||||
raise ValueError('not valid analytics type: ' + values["name"])
|
||||
return v
|
||||
|
||||
@validator('datamodel')
|
||||
def datamodel_valid(cls, v, values):
|
||||
for datamodel in v:
|
||||
if datamodel not in [el.name for el in DataModel]:
|
||||
raise ValueError('not valid data model: ' + values["name"])
|
||||
return v
|
||||
|
||||
@validator('description', 'how_to_implement')
|
||||
def encode_error(cls, v, values, field):
|
||||
try:
|
||||
v.encode('ascii')
|
||||
except UnicodeEncodeError:
|
||||
raise ValueError('encoding error in ' + field.name + ': ' + values["name"])
|
||||
return v
|
||||
|
||||
@validator('references')
|
||||
def references_check(cls, v, values):
|
||||
for reference in v:
|
||||
try:
|
||||
get = requests.get(reference)
|
||||
if not get.status_code == 200:
|
||||
raise ValueError('Reference ' + reference + ' is not reachable: ' + values["name"])
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise ValueError('Reference ' + reference + ' is not reachable: ' + values["name"])
|
||||
|
||||
return v
|
||||
|
||||
@validator('search')
|
||||
def search_validate(cls, v, values):
|
||||
# write search validator
|
||||
return v
|
||||
@@ -0,0 +1,25 @@
|
||||
|
||||
from pydantic import BaseModel, validator, ValidationError
|
||||
|
||||
|
||||
|
||||
class BaselineTags(BaseModel):
|
||||
analytic_story: list
|
||||
deployments: list = None
|
||||
detections: list
|
||||
product: list
|
||||
required_fields: list
|
||||
security_domain: str
|
||||
|
||||
|
||||
@validator('product')
|
||||
def tags_product(cls, v, values):
|
||||
valid_products = [
|
||||
"Splunk Enterprise", "Splunk Enterprise Security", "Splunk Cloud",
|
||||
"Splunk Security Analytics for AWS", "Splunk Behavioral Analytics"
|
||||
]
|
||||
|
||||
for value in v:
|
||||
if value not in valid_products:
|
||||
raise ValueError('product is not valid for ' + values['name'] + '. valid products are ' + str(valid_products))
|
||||
return v
|
||||
@@ -0,0 +1,60 @@
|
||||
|
||||
import uuid
|
||||
import string
|
||||
|
||||
from pydantic import BaseModel, validator, ValidationError
|
||||
from datetime import datetime
|
||||
|
||||
from bin.contentctl_project.contentctl_core.domain.entities.security_content_object import SecurityContentObject
|
||||
from bin.contentctl_project.contentctl_core.domain.entities.deployment_scheduling import DeploymentScheduling
|
||||
from bin.contentctl_project.contentctl_core.domain.entities.deployment_email import DeploymentEmail
|
||||
from bin.contentctl_project.contentctl_core.domain.entities.deployment_notable import DeploymentNotable
|
||||
from bin.contentctl_project.contentctl_core.domain.entities.deployment_rba import DeploymentRBA
|
||||
from bin.contentctl_project.contentctl_core.domain.entities.deployment_slack import DeploymentSlack
|
||||
from bin.contentctl_project.contentctl_core.domain.entities.deployment_phantom import DeploymentPhantom
|
||||
|
||||
class Deployment(BaseModel, SecurityContentObject):
|
||||
name: str
|
||||
id: str
|
||||
date: str
|
||||
author: str
|
||||
description: str
|
||||
scheduling: DeploymentScheduling = None
|
||||
email: DeploymentEmail = None
|
||||
notable: DeploymentNotable = None
|
||||
rba: DeploymentRBA = None
|
||||
slack: DeploymentSlack = None
|
||||
phantom: DeploymentPhantom = None
|
||||
tags: dict
|
||||
|
||||
|
||||
@validator('name')
|
||||
def name_invalid_chars(cls, v):
|
||||
invalidChars = set(string.punctuation.replace("-", ""))
|
||||
if any(char in invalidChars for char in v):
|
||||
raise ValueError('invalid chars used in name: ' + v)
|
||||
return v
|
||||
|
||||
@validator('id')
|
||||
def id_check(cls, v, values):
|
||||
try:
|
||||
uuid.UUID(str(v))
|
||||
except:
|
||||
raise ValueError('uuid is not valid: ' + values["name"])
|
||||
return v
|
||||
|
||||
@validator('date')
|
||||
def date_valid(cls, v, values):
|
||||
try:
|
||||
datetime.strptime(v, "%Y-%m-%d")
|
||||
except:
|
||||
raise ValueError('date is not in format YYYY-MM-DD: ' + values["name"])
|
||||
return v
|
||||
|
||||
@validator('description')
|
||||
def encode_error(cls, v, values, field):
|
||||
try:
|
||||
v.encode('ascii')
|
||||
except UnicodeEncodeError:
|
||||
raise ValueError('encoding error in ' + field.name + ': ' + values["name"])
|
||||
return v
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
from pydantic import BaseModel, validator, ValidationError
|
||||
|
||||
|
||||
class DeploymentEmail(BaseModel):
|
||||
message: str
|
||||
subject: str
|
||||
to: str
|
||||
@@ -0,0 +1,8 @@
|
||||
|
||||
from pydantic import BaseModel, validator, ValidationError
|
||||
|
||||
|
||||
class DeploymentNotable(BaseModel):
|
||||
rule_description: str
|
||||
rule_title: str
|
||||
nes_fields: list
|
||||
@@ -0,0 +1,10 @@
|
||||
|
||||
from pydantic import BaseModel, validator, ValidationError
|
||||
|
||||
|
||||
class DeploymentPhantom(BaseModel):
|
||||
cam_workers : str
|
||||
label : str
|
||||
phantom_server : str
|
||||
sensitivity : str
|
||||
severity : str
|
||||
@@ -0,0 +1,7 @@
|
||||
|
||||
|
||||
from pydantic import BaseModel, validator, ValidationError
|
||||
|
||||
|
||||
class DeploymentRBA(BaseModel):
|
||||
enabled: str
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user