Branch was auto-updated.

This commit is contained in:
srv-rr-gh-researchbt
2024-02-15 13:44:24 -08:00
committed by GitHub
1032 changed files with 4595 additions and 126631 deletions
+1 -1
View File
@@ -151,4 +151,4 @@ Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
limitations under the License.
-83
View File
@@ -1,83 +0,0 @@
#!/bin/bash
# simple script to run an appinspect API check
EXPECTED_ARGS=4
E_BADARGS=65
if [ $# -lt 4 ]
then
echo "Usage: `basename $0` <app_path> <package_name> <username> <password>"
echo "Example `basename $0` ~/ DA-ESS-ContentUpdate-latest.tar.gz doomguy R1p&T3ar"
exit $E_BADARGS
fi
if [ $# -gt $EXPECTED_ARGS ]
then
echo "Too many arguments"
exit $E_BADARGS
fi
APP_PATH=$1
PACKAGE_NAME=$2
USERNAME=$3
PASSWORD=$4
#PACKAGE_PATH="/home/circleci/"$PACKAGE_NAME
PACKAGE_PATH="build/"$PACKAGE_NAME
cd $APP_PATH
# check if report exists
if [ -d report ]
then
echo "report/ Directory exists"
else
mkdir report
fi
# get a JWT token
AUTH_TOKEN=$(echo -n "$USERNAME:$PASSWORD" | base64)
APPINSPECT_TOKEN=$(curl -s --location --request GET 'https://api.splunk.com/2.0/rest/login/splunk' --header "Authorization: Basic $AUTH_TOKEN" | jq -r '.data | .token')
sleep 1
# submit a inspection job EXPECTS app on same directory
#REQUEST_ID=$(curl -s --location --request POST 'https://appinspect.splunk.com/v1/app/validate' --header "Authorization: bearer $APPINSPECT_TOKEN" --form 'app_package=@"/home/circleci/DA-ESS-ContentUpdate-latest.tar.gz"' | jq -r '.request_id')
echo "The PACKAGE_PATH is "$PACKAGE_PATH
ls -lah $PACKAGE_PATH
REQUEST_ID=$(curl -s --location --request POST 'https://appinspect.splunk.com/v1/app/validate' --header "Authorization: bearer $APPINSPECT_TOKEN" --form 'included_tags="cloud"' --form 'app_package=@'$PACKAGE_PATH | jq -r '.request_id')
echo "app inspect request: $REQUEST_ID"
sleep 5
STATUS=$(curl -s --location --request GET https://appinspect.splunk.com/v1/app/validate/status/$REQUEST_ID --header "Authorization: bearer $APPINSPECT_TOKEN" | jq -r '.status')
while :
do
STATUS=$(curl -s --location --request GET https://appinspect.splunk.com/v1/app/validate/status/$REQUEST_ID --header "Authorization: bearer $APPINSPECT_TOKEN" | jq -r '.status')
if [ "$STATUS" == "PROCESSING" ] || [ "$STATUS" == "PREPARING" ]
then
echo "appinspect PROCESSING request: $REQUEST_ID"
elif [ "$STATUS" == "SUCCESS" ]
# REPORT FINISHED CHECK RESULTS
then
echo "appinspect completed inspection"
curl -s --location --request GET https://appinspect.splunk.com/v1/app/report/$REQUEST_ID --header "Authorization: bearer $APPINSPECT_TOKEN" --header 'Content-Type: text/html' -o report/appinspect_report_$PACKAGE_NAME.html
FAILS=$(curl -s --location --request GET https://appinspect.splunk.com/v1/app/report/$REQUEST_ID --header "Authorization: bearer $APPINSPECT_TOKEN" --header 'Content-Type: application/json' | jq -r '.summary | .failure')
ERRORS=$(curl -s --location --request GET https://appinspect.splunk.com/v1/app/report/$REQUEST_ID --header "Authorization: bearer $APPINSPECT_TOKEN" --header 'Content-Type: application/json' | jq -r '.summary | .error')
if [ $FAILS -gt 0 -o $ERRORS -gt 0 ]
then
echo "ERROR appinspect had $FAILS failures and or $ERRORS errors, see summary report under job artifacts for details"
#print out the report so that we know what went wrong
cat report/appinspect_report_$PACKAGE_NAME.html
exit 1
else
echo "appinspect passed successfully, see summary report under job artifacts for details"
exit 0
fi
else
echo "there was an error with app inspect report please see below:"
curl -s --location --request GET https://appinspect.splunk.com/v1/app/report/$REQUEST_ID --header "Authorization: bearer $APPINSPECT_TOKEN" --header 'Content-Type: application/json' | jq -r
exit 1
fi
sleep 60
done
exit 0
@@ -1,9 +0,0 @@
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
@@ -1,25 +0,0 @@
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
@@ -1,22 +0,0 @@
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
@@ -1,93 +0,0 @@
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 addProvidingTechnologies(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) -> 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 addSplunkApp(self) -> None:
pass
@abc.abstractmethod
def setObject(self, path: str) -> None:
pass
@abc.abstractmethod
def addCIS(self) -> None:
pass
@abc.abstractmethod
def addKillChainPhase(self) -> None:
pass
@abc.abstractmethod
def addNist(self) -> None:
pass
@abc.abstractmethod
def reset(self) -> None:
pass
@abc.abstractmethod
def addDatamodel(self) -> None:
pass
@abc.abstractmethod
def getObject(self) -> SecurityContentObject:
pass
@@ -1,51 +0,0 @@
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, 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
@@ -1,25 +0,0 @@
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
@@ -1,23 +0,0 @@
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
@@ -1,39 +0,0 @@
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
@@ -1,123 +0,0 @@
import os
import sys
from webbrowser import get
from pydantic import ValidationError
from pydantic.error_wrappers import ErrorWrapper
from dataclasses import dataclass
from typing import Sequence, Tuple
import pathlib
from bin.contentctl_project.contentctl_core.domain.entities.enums.enums import SecurityContentType
from bin.contentctl_project.contentctl_core.domain.entities.detection_tags import DetectionTags
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
attack_enrichment: dict
@dataclass(frozen=True)
class BAFactoryOutputDto:
detections: list
class BAFactory():
input_dto: BAFactoryInputDto
output_dto: BAFactoryOutputDto
ids: dict[str,list[pathlib.Path]] = {}
def __init__(self, output_dto: BAFactoryOutputDto) -> None:
self.output_dto = output_dto
def execute(self, input_dto: BAFactoryInputDto) -> None:
self.input_dto = input_dto
print("Creating Security Content - SSA. This may take some time...")
validation_errors = self.createSecurityContent(SecurityContentType.detections)
if len(validation_errors) != 0:
print(f"There were [{len(validation_errors)}] error(s) found while parsing security_content")
for ve in validation_errors:
file_path = ve[0]
error = ve[1]
print(f'\nValidation Error for file [{file_path}]:\n{str(error)}')
#raise(Exception("Error(s) validating Security Content"))
def createSecurityContent(self, type: SecurityContentType) -> list[Tuple[pathlib.Path, ValidationError]]:
objects = []
files = Utils.get_all_yml_files_from_directory(os.path.join(self.input_dto.input_path, 'ssa_detections'))
validation_errors:list[Tuple[pathlib.Path, ValidationError]] = []
files_with_ssa = [f for f in files if f.name.startswith('ssa_')]
already_ran = False
progress_percent = 0
type_string = "UNKNOWN TYPE"
for index,file in enumerate(files_with_ssa):
#Index + 1 because we are zero indexed, not 1 indexed. This ensures
# that printouts end at 100%, not some other number
progress_percent = ((index+1)/len(files_with_ssa)) * 100
progress_percent = ((index+1)/len(files_with_ssa)) * 100
#try:
type_string = "UNKNOWN TYPE"
if type == SecurityContentType.detections:
type_string = "Detections"
self.input_dto.director.constructDetection(self.input_dto.detection_builder, file, [], [], [], self.input_dto.attack_enrichment, [], [])
detection = self.input_dto.detection_builder.getObject()
Utils.add_id(self.ids, detection, file)
tag_and_nist_errors = []
if detection.tags.cis20 == None:
error = TypeError(f"Detection Tags missing cis20 field")
tag_and_nist_errors.append(ErrorWrapper(error, loc="cis20"))
if detection.tags.nist == None:
error = TypeError(f"Detection Tags missing nist field")
tag_and_nist_errors.append(ErrorWrapper(error, loc="nist"))
if len(tag_and_nist_errors) > 0:
raise ValidationError( tag_and_nist_errors , DetectionTags)
if detection.status in ["production","validation"]:
self.output_dto.detections.append(detection)
else:
raise(Exception(f"Unsupported content type: [{type}]"))
if (sys.stdout.isatty() and sys.stdin.isatty() and sys.stderr.isatty()) or not already_ran:
already_ran = True
print(f"\r{f'{type_string} Progress'.rjust(23)}: [{progress_percent:3.0f}%]...", end="", flush=True)
# except ValidationError as e:
# validation_errors.append((pathlib.Path(file), e))
# except Exception as e:
# print(f"Unknown exception caught while Creating BA Security Content: {str(e)}")
# sys.exit(1)
print(f"\r{f'{type_string} Progress'.rjust(23)}: [{progress_percent:3.0f}%]...", end="", flush=True)
print("Done!")
return validation_errors
@@ -1,204 +0,0 @@
import os
import sys
from pydantic import ValidationError
from dataclasses import dataclass
import pathlib
from typing import Tuple
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
from bin.contentctl_project.contentctl_core.domain.entities.link_validator import LinkValidator
from bin.contentctl_project.contentctl_core.domain.entities.security_content_object import SecurityContentObject
@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
force_cached_or_offline: bool = True
@dataclass()
class FactoryOutputDto:
detections: list
stories: list
baselines: list
investigations: list
playbooks: list
deployments: list
macros: list
lookups: list
class Factory():
input_dto: FactoryInputDto
output_dto: FactoryOutputDto
ids: dict[str,list[pathlib.Path]] = {}
def __init__(self, output_dto: FactoryOutputDto) -> None:
self.output_dto = output_dto
def execute(self, input_dto: FactoryInputDto) -> None:
self.input_dto = input_dto
print("Creating Security Content - ESCU. This may take some time...")
#Accumulate any validation errors that may occur while creating security_contnet
validation_errors = []
# order matters to load and enrich security content types
validation_errors.extend(self.createSecurityContent(SecurityContentType.lookups))
lookups_directory = pathlib.Path(input_dto.input_path) / "lookups"
csv_lookups = [
l.name
for l in Utils.get_all_csv_files_from_directory(str(lookups_directory))
]
try:
csv_lookups.remove("mitre_enrichment.csv")
except ValueError as e:
# mitre_enrichment.csv didn't exist in the lookups directory. That's okay.
pass
for lookup in self.output_dto.lookups:
if lookup.filename != None and lookup.filename in csv_lookups:
csv_lookups.remove(lookup.filename)
if len(csv_lookups) > 0:
print("The following lookups were unused. Should they be removed?\n\t- ",end="")
print("\n\t- ".join([str(p) for p in csv_lookups]))
validation_errors.extend(self.createSecurityContent(SecurityContentType.macros))
validation_errors.extend(self.createSecurityContent(SecurityContentType.deployments))
validation_errors.extend(self.createSecurityContent(SecurityContentType.baselines))
validation_errors.extend(self.createSecurityContent(SecurityContentType.investigations))
validation_errors.extend(self.createSecurityContent(SecurityContentType.playbooks))
validation_errors.extend(self.createSecurityContent(SecurityContentType.detections))
validation_errors.extend(self.createSecurityContent(SecurityContentType.stories))
validation_errors.extend(Utils.check_ids_for_duplicates(self.ids))
LinkValidator.print_link_validation_errors()
if len(validation_errors) != 0:
print(f"There were [{len(validation_errors)}] error(s) found while parsing security_content")
for ve in validation_errors:
file_path = ve[0]
error = ve[1]
print(f'\nValidation Error for file [{file_path}]:\n{str(error)}')
raise(Exception("Error(s) validating Security Content"))
def createSecurityContent(self, type: SecurityContentType) -> list[Tuple[pathlib.Path, ValidationError]]:
objects = []
if type == SecurityContentType.deployments:
files = Utils.get_all_yml_files_from_directory(os.path.join(self.input_dto.input_path, str(type.name)))
elif type == SecurityContentType.detections:
files = Utils.get_all_yml_files_from_directory(os.path.join(self.input_dto.input_path, 'detections'))
else:
files = Utils.get_all_yml_files_from_directory(os.path.join(self.input_dto.input_path, str(type.name)))
# Instead of failing on the first error, just keep track of
# of all the exceptions that we generate. These exceptions
# will be returned from the function and should be printed
# by the caller.
validation_errors:list[Tuple[pathlib.Path, ValidationError]] = []
already_ran = False
progress_percent = 0
type_string = "UNKNOWN TYPE"
#Non threaded, production version of the construction code
files_without_ssa = [f for f in files if not f.name.startswith('ssa___')]
for index,file in enumerate(files_without_ssa):
#Index + 1 because we are zero indexed, not 1 indexed. This ensures
# that printouts end at 100%, not some other number
progress_percent = ((index+1)/len(files_without_ssa)) * 100
try:
type_string = "UNKNOWN TYPE"
if type == SecurityContentType.lookups:
type_string = "Lookups"
self.input_dto.director.constructLookup(self.input_dto.basic_builder, str(file))
lookup = self.input_dto.basic_builder.getObject()
Utils.add_id(self.ids, lookup, file)
self.output_dto.lookups.append(lookup)
elif type == SecurityContentType.macros:
type_string = "Macros"
self.input_dto.director.constructMacro(self.input_dto.basic_builder, str(file))
macro = self.input_dto.basic_builder.getObject()
Utils.add_id(self.ids, macro, file)
self.output_dto.macros.append(macro)
elif type == SecurityContentType.deployments:
type_string = "Deployments"
self.input_dto.director.constructDeployment(self.input_dto.basic_builder, str(file))
deployment = self.input_dto.basic_builder.getObject()
Utils.add_id(self.ids, deployment, file)
self.output_dto.deployments.append(deployment)
elif type == SecurityContentType.playbooks:
type_string = "Playbooks"
self.input_dto.director.constructPlaybook(self.input_dto.playbook_builder, str(file))
playbook = self.input_dto.playbook_builder.getObject()
Utils.add_id(self.ids, playbook, file)
self.output_dto.playbooks.append(playbook)
elif type == SecurityContentType.baselines:
type_string = "Baselines"
self.input_dto.director.constructBaseline(self.input_dto.baseline_builder, str(file), self.output_dto.deployments)
baseline = self.input_dto.baseline_builder.getObject()
Utils.add_id(self.ids, baseline, file)
self.output_dto.baselines.append(baseline)
elif type == SecurityContentType.investigations:
type_string = "Investigations"
self.input_dto.director.constructInvestigation(self.input_dto.investigation_builder, file)
investigation = self.input_dto.investigation_builder.getObject()
Utils.add_id(self.ids, investigation, file)
self.output_dto.investigations.append(investigation)
elif type == SecurityContentType.stories:
type_string = "Stories"
self.input_dto.director.constructStory(self.input_dto.story_builder, str(file),
self.output_dto.detections, self.output_dto.baselines, self.output_dto.investigations)
story = self.input_dto.story_builder.getObject()
Utils.add_id(self.ids, story, file)
self.output_dto.stories.append(story)
elif type == SecurityContentType.detections:
type_string = "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.input_dto.attack_enrichment, self.output_dto.macros,
self.output_dto.lookups, self.input_dto.force_cached_or_offline)
detection = self.input_dto.detection_builder.getObject()
Utils.add_id(self.ids, detection, file)
self.output_dto.detections.append(detection)
else:
raise Exception(f"Unsupported type: [{type}]")
if (sys.stdout.isatty() and sys.stdin.isatty() and sys.stderr.isatty()) or not already_ran:
already_ran = True
print(f"\r{f'{type_string} Progress'.rjust(23)}: [{progress_percent:3.0f}%]...", end="", flush=True)
except ValidationError as e:
validation_errors.append((pathlib.Path(file), e))
except Exception as e:
validation_errors.append((pathlib.Path(file), e))
print(f"\r{f'{type_string} Progress'.rjust(23)}: [{progress_percent:3.0f}%]...", end="", flush=True)
print("Done!")
return validation_errors
@@ -1,110 +0,0 @@
import os
import uuid
import questionary
from dataclasses import dataclass
from datetime import datetime
from bin.contentctl_project.contentctl_core.domain.entities.enums.enums import SecurityContentType
from bin.contentctl_project.contentctl_core.domain.entities.enums.enums import SecurityContentProduct
from bin.contentctl_project.contentctl_core.application.factory.utils.new_content_questions import NewContentQuestions
@dataclass(frozen=True)
class NewContentFactoryInputDto:
type: SecurityContentType
type: SecurityContentProduct
@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 = questionary.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['status'] = 'production'
self.output_dto.obj['type'] = answers['detection_type']
self.output_dto.obj['data_source'] = ['UPDATE_DATA_SOURCE']
self.output_dto.obj['description'] = 'UPDATE_DESCRIPTION'
if answers['detection_product'] == 'ESCU':
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']['atomic_guid'] = ['UPDATE atomic_guid']
self.output_dto.obj['tags']['confidence'] = 'UPDATE value between 1-100'
#self.output_dto.obj['tags']['drilldown_search'] = ['Add drilldown search']
self.output_dto.obj['tags']['impact'] = 'UPDATE value between 1-100'
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']['observable'] = [{'name': 'UPDATE', 'type': 'UPDATE', 'role': ['UPDATE']}]
if answers['detection_product'] == 'SSA':
self.output_dto.obj['tags']['risk_severity'] = 'UPDATE: <low>, <medium>, <high>'
if answers['detection_product'] == 'ESCU':
self.output_dto.obj['tags']['product'] = ['Splunk Enterprise','Splunk Enterprise Security','Splunk Cloud']
if answers['detection_product'] == 'SSA':
self.output_dto.obj['tags']['product'] = ['Splunk Behavioral Analytics']
self.output_dto.obj['tags']['risk_score'] = 'UPDATE (impact * confidence)/100'
self.output_dto.obj['tags']['required_fields'] = ['UPDATE_required_fields']
self.output_dto.obj['tags']['security_domain'] = answers['security_domain']
self.output_dto.obj['source'] = answers['detection_kind']
self.output_dto.obj['tests'] = list()
true_positive_test = dict()
true_positive_test["name"] = 'True Positive Test'
true_positive_test["attack_data"] = [{
"data": "UPDATE url to dataset",
"source": "UPDATE source",
"sourcetype": "UPDATE sourcetype"
}]
self.output_dto.obj['tests'].append(true_positive_test)
elif input_dto.type == SecurityContentType.stories:
questions = NewContentQuestions.get_questions_story()
answers = questionary.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'] = dict()
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']
elif input_dto.type == SecurityContentType.attack_data:
questions = NewContentQuestions.get_questions_attack_data()
answers = questionary.prompt(questions)
self.output_dto.obj['author'] = answers['author_name']
self.output_dto.obj['id'] = str(uuid.uuid4())
self.output_dto.obj['date'] = datetime.today().strftime('%Y-%m-%d')
self.output_dto.obj['description'] = "description"
self.output_dto.obj['environment'] = "attackrange"
self.output_dto.obj['dataset'] = "datasets"
self.output_dto.obj['sourcetypes'] = answers['data_src_category']
self.output_dto.obj['references'] = [answers['references']]
self.output_dto.obj['src_path'] = answers['src_file_path'].strip()
self.output_dto.obj['dst_path'] = answers['dest_file_path'].strip()
@@ -1,29 +0,0 @@
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, str(file))
self.objects.append(input_dto.builder.getObject())
@@ -1,268 +0,0 @@
class NewContentQuestions():
@classmethod
def get_questions_detection(self) -> list:
questions = [
{
'type': 'select',
'message': 'what product is this for',
'name': 'detection_product',
'choices': [
'ESCU',
'SSA'
],
'default': 'ESCU'
},
{
'type': 'select',
'message': 'what kind of detection is this',
'name': 'detection_kind',
'choices': [
'endpoint',
'cloud',
'application',
'network',
'web',
'experimental'
],
'default': 'endpoint'
},
{
'type': 'text',
'message': 'enter detection name',
'name': 'detection_name',
'default': 'Powershell Encoded Command',
},
{
'type': 'text',
'message': 'enter author name',
'name': 'detection_author',
},
{
'type': 'select',
'message': 'select a detection type',
'name': 'detection_type',
'choices': [
'TTP',
'Anomaly',
'Hunting',
'Baseline',
'Investigation',
'Correlation'
],
'default': 'TTP'
},
{
'type': 'checkbox',
'message': 'select the datamodels used in the detection',
'name': 'datamodels',
'choices': [
'Endpoint',
'Endpoint_Processes (SSA)',
'Endpoint_Registry (SSA)',
'Endpoint_Filesystem (SSA)',
'Endpoint_ResourceAccess (SSA)',
'Endpoint_AccountManagement (SSA)',
'Intrusion_Detection (SSA)',
'Authentication',
'Change',
'Email',
'Network_Resolution',
'Network_Traffic',
'Network_Sessions',
'Updates',
'Vulnerabilities',
'Web',
'Risk'
],
'default': 'Endpoint'
},
{
'type': 'text',
'message': 'enter search (spl)',
'name': 'detection_search',
'default': '| UPDATE_SPL'
},
{
'type': 'text',
'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': [
'Reconnaissance',
'Weaponization',
'Delivery',
'Exploitation',
'Installation',
'Command & Control',
'Actions on Objectives',
'Denial of Service'
],
'default': 'Exploitation'
},
{
'type': 'select',
'message': 'security_domain for detection',
'name': 'security_domain',
'choices': [
'access',
'endpoint',
'network',
'threat',
'identity',
'audit'
],
'default': 'endpoint'
},
]
return questions
@classmethod
def get_questions_story(self) -> list:
questions = [
{
'type': 'text',
'message': 'enter story name',
'name': 'story_name',
'default': 'Suspicious Powershell Behavior',
},
{
'type': 'text',
'message': 'enter author name',
'name': 'story_author',
},
{
'type': 'checkbox',
'message': 'select a category',
'name': 'category',
'choices': [
'Adversary Tactics',
'Account Compromise',
'Unauthorized Software',
'Best Practices',
'Cloud Security',
'Command And Control',
'Lateral Movement',
'Ransomware',
'Privilege Escalation'
]
},
{
'type': 'select',
'message': 'select a use case',
'name': 'usecase',
'choices': [
'Advanced Threat Detection',
'Security Monitoring',
'Compliance',
'Insider Threat',
'Application Security',
'Other'
],
},
]
return questions
@classmethod
def get_questions_attack_data(self) -> list:
questions = [
{
'type': 'input',
'message': 'enter the source file path of your attack_dataset (ex. ~/attack_range/attack_data/stext_sysmon/sysmon.log): ',
'name': 'src_file_path',
},
{
'type': 'input',
'message': 'enter the dest folder path for your attack_dataset (ex. ~/attack_data/datasets/malware/remcos/remcos_dynwrapx): ',
'name': 'dest_file_path',
},
{
'type': 'input',
'message': 'enter author name: ',
'name': 'author_name',
'default': 'STRT',
},
{
'type': 'checkbox',
'message': 'select the data source type',
'name': 'data_src_category',
'choices': [
{
'name': 'windows-sysmon.log',
'checked': True
},
{
'name': 'windows-security.log'
},
{
'name': 'windows-system.log'
},
{
'name': 'windows-powershell-xml.log'
},
{
'name': 'stream_http_events.log'
},
{
'name': 'aws_cloudtrail_events.json'
},
{
'name': 'o365_events.json'
},
{
'name': 'o365_exchange_events.json'
},
{
'name': 'kubernetes_events.json'
},
{
'name': 'security_hub_finding.json'
},
{
'name': 'gsuite_gmail_bigquery.json'
},
{
'name': 'gsuite_drive_json.json'
},
{
'name': 'github.json'
},
{
'name': 'kubernetes_nginx.json'
},
{
'name': 'circleci.json'
},
{
'name': 'sysmon_linux.log'
},
{
'name': 'xml-windows-security.log'
},
{
'name': 'xml-windows-system.log'
},
{
'name': 'xml-windows-application.log'
},
{
'name': 'xml-windows-directory-service.log'
},
],
},
{
'type': 'input',
'message': 'enter references: ',
'name': 'references',
},
]
return questions
@@ -1,62 +0,0 @@
import os
import pathlib
from typing import Tuple
from pydantic import ValidationError
from bin.contentctl_project.contentctl_core.domain.entities.security_content_object import (
SecurityContentObject,
)
class Utils:
@staticmethod
def get_all_csv_files_from_directory(path: str) -> list[pathlib.Path]:
listOfFiles: list[pathlib.Path] = []
for dirpath, dirnames, filenames in os.walk(path):
for file in filenames:
if file.endswith(".csv"):
listOfFiles.append(pathlib.Path(os.path.join(dirpath, file)))
return sorted(listOfFiles)
@staticmethod
def get_all_yml_files_from_directory(path: str) -> list[pathlib.Path]:
listOfFiles: list[pathlib.Path] = []
for dirpath, dirnames, filenames in os.walk(path):
for file in filenames:
if file.endswith(".yml"):
listOfFiles.append(pathlib.Path(os.path.join(dirpath, file)))
return sorted(listOfFiles)
@staticmethod
def add_id(
id_dict: dict[str, list[pathlib.Path]],
obj: SecurityContentObject,
path: pathlib.Path,
) -> None:
if hasattr(obj, "id"):
obj_id = obj.id
if obj_id in id_dict:
id_dict[obj_id].append(path)
else:
id_dict[obj_id] = [path]
# Otherwise, no ID so nothing to add....
@staticmethod
def check_ids_for_duplicates(
id_dict: dict[str, list[pathlib.Path]]
) -> list[Tuple[pathlib.Path, ValidationError]]:
validation_errors: list[Tuple[pathlib.Path, ValidationError]] = []
for key, values in id_dict.items():
if len(values) > 1:
error_file_path = pathlib.Path("MULTIPLE")
all_files = "\n\t".join(str(pathlib.Path(p)) for p in values)
exception = ValueError(
f"Error validating id [{key}] - duplicate ID was used in the following files: \n\t{all_files}"
)
validation_errors.append((error_file_path, exception))
return validation_errors
@@ -1,93 +0,0 @@
import subprocess
import sys
import tarfile
import os
from typing import TextIO
class Build:
def __init__(self, args):
base_path = args.path
if args.product == "ESCU":
self.source = os.path.join(base_path, "dist","escu")
self.app_name = "DA-ESS-ContentUpdate"
elif args.product == "SSA":
raise(Exception(f"{args.product} build not supported"))
else:
self.source = os.path.join(base_path, "dist", args.product)
self.app_name = args.product
if not os.path.exists(self.source):
raise(Exception(f"Attemping to build app from {self.source}, but it does not exist."))
print(f"Building Splunk App from source {self.source}")
self.output_dir_base = args.output_dir
self.output_dir_source = os.path.join(self.output_dir_base, self.app_name)
#self.output_package = os.path.join(self.output_dir_base, self.app_name+'.tar.gz')
self.copy_app_source()
self.validate_splunk_app()
self.build_splunk_app()
#self.archive_splunk_app()
def copy_app_source(self):
import shutil
try:
if os.path.exists(self.output_dir_source):
print(f"The directory {self.output_dir_source} exists. Deleting it in preparation to build the app... ", end='', flush=True)
try:
shutil.rmtree(self.output_dir_source)
print("Done!")
except Exception as e:
raise(Exception(f"Unable to delete {self.output_dir_source}"))
print(f"Copying Splunk App Source to {self.source} in preparation for building...", end='')
sys.stdout.flush()
shutil.copytree(self.source, self.output_dir_source, dirs_exist_ok=True)
print("done")
except Exception as e:
raise(Exception(f"Failed to copy Splunk app source from {self.source} -> {self.output_dir_source} : {str(e)}"))
def validate_splunk_app(self):
proc = "nothing..."
try:
print("Validating Splunk App...")
sys.stdout.flush()
nothing = subprocess.check_output(["slim", "validate", self.output_dir_source])
print("Package Validation Complete")
except Exception as e:
print(f"error: {str(e)} ")
raise(Exception(f"Error building Splunk App: {str(e)}"))
def build_splunk_app(self):
proc = "nothing..."
try:
print("Building Splunk App...")
sys.stdout.flush()
nothing = subprocess.check_output(["slim", "package", "-o", self.output_dir_base, self.output_dir_source])
print("Package Generation Complete")
except Exception as e:
print("error")
raise(Exception(f"Error building Splunk App: {str(e)}"))
'''
def archive_splunk_app(self):
try:
print(f"Creating Splunk app archive {self.output_package}...", end='')
sys.stdout.flush()
with tarfile.open(self.output_package, "w:gz") as tar:
tar.add(self.output_dir_build, arcname=os.path.basename(self.output_dir_build))
print("done")
except Exception as e:
print("error")
raise(Exception(f"Error creating {self.output_package}: {str(e)}"))
'''
@@ -1,219 +0,0 @@
import argparse
from ast import arg
import re
import uuid
import inspect
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
filter_key: str
filter_value: str
variables: list
class ContentChanger:
def execute(self, input_dto: ContentChangerInputDto) -> None:
objects = list()
factory = ObjectFactory(objects)
factory.execute(input_dto.factory_input_dto)
filtered_objects = objects
if input_dto.filter_key and input_dto.filter_value:
filtered_objects = self.apply_key_value_filter(objects, input_dto.filter_key, input_dto.filter_value)
converter_func = getattr(self, input_dto.converter_func_name)
converter_func(filtered_objects, input_dto.variables)
input_dto.adapter.writeObjectsInPlace(filtered_objects)
@staticmethod
def enumerate_content_changer_functions(exclude_functions: list[str] = ["enumerate_content_changer_functions", "execute", "example_converter_func"]) -> list[str]:
members = inspect.getmembers(ContentChanger, predicate=inspect.isfunction)
function_names = [function_object[0] for function_object in members if function_object[0] not in exclude_functions]
return function_names
def apply_key_value_filter(self, objects: list, key: str, value: str) -> None:
new_list = list()
for obj in objects:
if str(obj[key]) == value:
new_list.append(obj)
return new_list
def all(self, objects : list) -> None:
for func_name in ContentChanger.enumerate_content_changer_functions():
if func_name not in ["all", "change_test_file_format"]:
print(f"calling {func_name}")
func_object = getattr(self, func_name)
func_object(objects)
# Define Converter Functions here
# def example_converter_func(self, objects : list) -> None:
# for obj in objects:
# obj['author'] = obj['author'].upper()
# 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:
# #Risk score must be an integer, so we round it to the nearest integer
# calculated_risk_score = round((obj['tags']['impact'] * obj['tags']['confidence'])/100)
# if calculated_risk_score != round(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:
# updated = True
# if not 'confidence' in obj['tags']:
# updated = True
# obj['tags']['confidence'] = 50
# if not 'impact' in obj['tags']:
# updated = True
# obj['tags']['impact'] = 50
# if not 'risk_score' in obj['tags'] or updated == True:
# #Recalculate the risk score if we have added/updated
# #the confidence or impact fields OR the risk_score
# #was missing in the first place
# self.fix_wrong_calculated_risk_score([obj])
# 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')
def update_description(self, objects : list, input_vars: list) -> None:
for obj in objects:
obj["description"] = input_vars[0]
@@ -1,26 +0,0 @@
import sys
import shutil
import os
from dataclasses import dataclass
from bin.contentctl_project.contentctl_infrastructure.builder.sigma_converter import SigmaConverter, SigmaConverterInputDto, SigmaConverterOutputDto
from bin.contentctl_project.contentctl_infrastructure.adapter.yml_output import YmlOutput
@dataclass(frozen=True)
class ConvertInputDto:
sigma_converter_input_dto: SigmaConverterInputDto
output_path : str
class Convert:
def execute(self, input_dto: ConvertInputDto) -> None:
sigma_converter_output_dto = SigmaConverterOutputDto([])
sigma_converter = SigmaConverter(sigma_converter_output_dto)
sigma_converter.execute(input_dto.sigma_converter_input_dto)
yml_output = YmlOutput()
yml_output.writeDetections(sigma_converter_output_dto.detections, input_dto.output_path)
@@ -1,98 +0,0 @@
import splunklib.client as client
import multiprocessing
import http.server
import time
import sys
import subprocess
import os
class Deploy:
def __init__(self, args):
#First, check to ensure that the legal ack is correct. If not, quit
if args.acs_legal_ack != "Y":
raise(Exception(f"Error - must supply 'acs-legal-ack=Y', not 'acs-legal-ack={args.acs_legal_ack}'"))
self.acs_legal_ack = args.acs_legal_ack
self.app_package = args.app_package
if not os.path.exists(self.app_package):
raise(Exception(f"Error - app_package file {self.app_package} does not exist"))
self.username = args.username
self.password = args.password
self.server = args.server
self.deploy_to_splunk_cloud()
#self.http_process = self.start_http_server()
#self.install_app()
def deploy_to_splunk_cloud(self):
commandline = f"acs apps install private --acs-legal-ack={self.acs_legal_ack} "\
f"--app-package {self.app_package} --server {self.server} --username "\
f"{self.username} --password {self.password}"
try:
res = subprocess.run(args = commandline.split(' '), )
except Exception as e:
raise(Exception(f"Error deploying to Splunk Cloud Instance: {str(e)}"))
print(res.returncode)
if res.returncode != 0:
raise(Exception("Error deploying to Splunk Cloud Instance. Review output to diagnose error."))
'''
def install_app_local(self) -> bool:
#Connect to the service
time.sleep(1)
#self.http_process.start()
#time.sleep(2)
print(f"Connecting to server {self.host}")
try:
service = client.connect(host=self.host, port=self.api_port, username=self.username, password=self.password)
assert isinstance(service, client.Service)
except Exception as e:
raise(Exception(f"Failure connecting the Splunk Search Head: {str(e)}"))
#Install the app
try:
params = {'name': self.server_app_path}
res = service.post('apps/appinstall', **params)
#Check the result?
print(f"Successfully installed {self.server_app_path}!")
except Exception as e:
raise(Exception(f"Failure installing the app {self.server_app_path}: {str(e)}"))
#Query and list all of the installed apps
try:
all_apps = service.apps
except Exception as e:
print(f"Failed listing all apps: {str(e)}")
return False
print("Installed apps:")
for count, app in enumerate(all_apps):
print("\t{count}. {app.name}")
print(f"Installing app {self.path}")
self.http_process.terminate()
return True
'''
@@ -1,25 +0,0 @@
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)
print('Documentation generation of security content successful.')
@@ -1,61 +0,0 @@
import os
import shutil
from dataclasses import dataclass
from typing import Union
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: Union[FactoryInputDto,None]
ba_factory_input_dto: Union[BAFactoryInputDto,None]
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)
print('Generate of security content successful.')
@@ -1,393 +0,0 @@
from logging import shutdown
import re
import glob
import os
import copy
import json
import shutil
CONTENT_VERSION_FILE = '''
[content-version]
version = {version}
'''
APP_CONFIGURATION_FILE = '''
## Splunk app configuration file
[install]
is_configured = false
state = enabled
state_change_requires_restart = false
build = 7313
[triggers]
reload.analytic_stories = simple
reload.usage_searches = simple
reload.use_case_library = simple
reload.correlationsearches = simple
reload.analyticstories = simple
reload.governance = simple
reload.managed_configurations = simple
reload.postprocess = simple
reload.content-version = simple
reload.es_investigations = simple
[launcher]
author = {author}
version = {version}
description = {description}
[ui]
is_visible = true
label = {label}
[package]
id = {id}
'''
APP_MANIFEST_TEMPLATE = {
"schemaVersion": "1.0.0",
"info": {
"title": "TEMPLATE_TITLE",
"id": {
"group": None,
"name": "TEMPLATE_NAME",
"version": "TEMPLATE_VERSION"
},
"author": [
{
"name": "TEMPLATE_AUTHOR_NAME",
"email": "TEMPLATE_AUTHOR_EMAIL",
"company": "TEMPLATE_AUTHOR_COMPANY"
}
],
"releaseDate": None,
"description": "TEMPLATE_DESCRIPTION",
"classification": {
"intendedAudience": None,
"categories": [],
"developmentStatus": None
},
"commonInformationModels": None,
"license": {
"name": None,
"text": None,
"uri": None
},
"privacyPolicy": {
"name": None,
"text": None,
"uri": None
},
"releaseNotes": {
"name": None,
"text": "./README.md",
"uri": None
}
},
"dependencies": None,
"tasks": None,
"inputGroups": None,
"incompatibleApps": None,
"platformRequirements": None
}
#f-strings cannot include a backslash, so we include this as a constant
NEWLINE_INDENT = "\n\t"
class Initialize:
def __init__(self, args):
self.items_scanned = []
self.items_deleted = []
self.items_kept = []
self.items_deleted_failed = []
#Information that will be used for generation of a custom manifest
self.app_title = args.title
self.app_name = args.name
if not self.app_name.replace('-','').isalnum() and len(self.app_name.replace('-','')) > 0:
# Basic check to see if the app_name is alphanumeric (no spaces or symbols) and, after any
# - character(s) are removed it is still non-zero length
raise(Exception(f"Error - app_name {self.app_name} is not valid. Name must be alphanumeric (no symbols or spaces). The only allowed special character is -."))
self.app_version = args.version
self.app_description = args.description
self.app_author_name = args.author_name
self.app_author_email = args.author_email
self.app_author_company = args.author_company
self.app_description = args.description
self.path = args.path
self.dist_app_path = os.path.join(args.path, "dist", self.app_name)
self.escu_path = os.path.join(args.path, "dist", "escu")
self.copy_dist_escu_to_dist_app()
self.success = self.remove_all_content()
self.generate_files_and_directories()
self.print_results_summary()
def copy_dist_escu_to_dist_app(self):
print("Copying ESCU Template output dir to retain static app files...",end='')
shutil.copytree(self.escu_path, self.dist_app_path, dirs_exist_ok=True)
#delete all the contents in the lookups folder
lookups_path = os.path.join(self.dist_app_path, "lookups")
files = glob.glob(os.path.join(lookups_path, "*"))
for filename in files:
os.remove(filename)
print("done")
def simple_replace_line(self, filename:str, original:str,updated:str):
print(f"Performing update on file {filename}")
with open(filename,'r') as data:
contents=data.read()
updated_contents = contents.replace(original, updated)
with open(filename,'w') as data:
data.write(updated_contents)
def generate_files_and_directories(self):
#Generate files
self.generate_custom_manifest()
self.generate_app_configuration_file()
self.generate_readme()
self.generate_content_version_file()
raw = '''{app_name}'''
original = raw.format(app_name="DA-ESS-ContentUpdate")
updated = raw.format(app_name=self.app_name)
filename = os.path.join(self.dist_app_path,"default","data","ui","views","escu_summary.xml")
self.simple_replace_line(filename, original, updated)
raw = '''{app_name}'''
original = raw.format(app_name="ESCU")
updated = raw.format(app_name=self.app_name)
filename = os.path.join(self.dist_app_path,"default","data","ui","views","escu_summary.xml")
self.simple_replace_line(filename, original, updated)
raw ='''{app_name} - '''
original = raw.format(app_name="ESCU")
updated = raw.format(app_name=self.app_name)
filename_root = os.path.join(self.path,"bin/contentctl_project/contentctl_infrastructure/adapter/templates/")
for fname in ["savedsearches_investigations.j2", "savedsearches_detections.j2", "analyticstories_investigations.j2", "analyticstories_detections.j2", "savedsearches_baselines.j2"]:
full_path = os.path.join(filename_root, fname)
self.simple_replace_line(full_path, original, updated)
raw ='''.{app_name}'''
original = raw.format(app_name="ESCU".lower()) #
updated = raw.format(app_name=self.app_name.lower())
filename_root = os.path.join(self.path,"bin/contentctl_project/contentctl_infrastructure/adapter/templates/")
for fname in ["savedsearches_investigations.j2", "savedsearches_detections.j2", "savedsearches_baselines.j2"]:
full_path = os.path.join(filename_root, fname)
self.simple_replace_line(full_path, original, updated)
raw ='''.{app_name}.'''
original = raw.format(app_name="ESCU".lower()) #
updated = raw.format(app_name=self.app_name.lower())
filename_root = os.path.join(self.path,f"dist/{self.app_name}/default/data/ui/views/")
for fname in ["escu_summary.xml"]:
full_path = os.path.join(filename_root, fname)
self.simple_replace_line(full_path, original, updated)
def generate_content_version_file(self):
new_content_version = CONTENT_VERSION_FILE.format(version=self.app_version)
content_version_path = os.path.join(self.dist_app_path, "default", "content-version.conf")
try:
if not os.path.exists(os.path.dirname(content_version_path)):
os.makedirs(os.path.dirname(content_version_path), exist_ok = True)
with open(content_version_path, "w") as readme_file:
readme_file.write(new_content_version)
except Exception as e:
raise(Exception(f"Error writing config to {content_version_path}: {str(e)}"))
print(f"Created Custom Content Version File at: {content_version_path}")
def generate_readme(self):
readme_file_path = os.path.join(self.dist_app_path, "README.md")
readme_stub_text = "Empty Readme file"
try:
if not os.path.exists(os.path.dirname(readme_file_path)):
os.makedirs(os.path.dirname(readme_file_path), exist_ok = True)
with open(readme_file_path, "w") as readme_file:
readme_file.write(readme_stub_text)
except Exception as e:
raise(Exception(f"Error writing config to {readme_file_path}: {str(e)}"))
print(f"Created Custom App Configuration at: {readme_file_path}")
def generate_app_configuration_file(self):
new_configuration = APP_CONFIGURATION_FILE.format(author = self.app_author_company,
version=self.app_version,
description=self.app_description,
label=self.app_title,
id=self.app_name)
app_configuration_file_path = os.path.join(self.dist_app_path, "default", "app.conf")
try:
if not os.path.exists(os.path.dirname(app_configuration_file_path)):
os.makedirs(os.path.dirname(app_configuration_file_path), exist_ok = True)
with open(app_configuration_file_path, "w") as app_config:
app_config.write(new_configuration)
except Exception as e:
raise(Exception(f"Error writing config to {app_configuration_file_path}: {str(e)}"))
print(f"Created Custom App Configuration at: {app_configuration_file_path}")
def generate_custom_manifest(self):
#Set all the required fields
new_manifest = copy.copy(APP_MANIFEST_TEMPLATE)
try:
new_manifest['info']['title'] = self.app_title
new_manifest['info']['id']['name'] = self.app_name
new_manifest['info']['id']['version'] = self.app_version
new_manifest['info']['author'][0]['name'] = self.app_author_name
new_manifest['info']['author'][0]['email'] = self.app_author_email
new_manifest['info']['author'][0]['company'] = self.app_author_company
new_manifest['info']['description'] = self.app_description
except Exception as e:
raise(Exception(f"Failure setting field to generate custom manifest: {str(e)}"))
#Output the new manifest file
manifest_path = os.path.join(self.dist_app_path, "app.manifest")
try:
if not os.path.exists(os.path.dirname(manifest_path)):
os.makedirs(os.path.dirname(manifest_path), exist_ok = True)
with open(manifest_path, 'w') as manifest_file:
json.dump(new_manifest, manifest_file, indent=3)
except Exception as e:
raise(Exception(f"Failure writing manifest file {manifest_path}: {str(e)}"))
print(f"Created Custom App Manifest at : {manifest_path}")
def print_results_summary(self):
if self.success is True:
print(f"Repo has been initialized successfully for app [{self.app_name}] at path [{self.dist_app_path}]!\n"
"Ready for your custom constent!")
else:
print("**Failure(s) initializing repo - check log for details**")
'''
print(f"Summary:"
f"\n\tItems Scanned : {len(self.items_scanned)}"
f"\n\tItems Kept : {len(self.items_kept)}"
f"\n\tItems Deleted : {len(self.items_deleted)}"
f"\n\tDeletion Failed: {len(self.items_deleted_failed)}"
)
'''
def remove_all_content(self)-> bool:
errors = []
#List out all the steps we will have to take
steps = [(self.remove_detections,"Creating Detections"),
(self.remove_baselines,"Creating Baselines"),
(self.remove_investigations,"Creating Investigations"),
(self.remove_lookups,"Creating Lookups"),
(self.remove_macros,"Creating Macros"),
(self.remove_notebooks,"Creating Notebooks"),
(self.remove_playbooks,"Creating Playbooks"),
(self.remove_stories,"Creating Stores"),
(self.remove_tests,"Creating Tests"),
(self.remove_dist_lookups,"Creating Dist Lookups")]
#Sort the steps so they are performced alphabetically
steps.sort(key=lambda name: name[1])
for func, text in steps:
print(f"{text}...",end='')
success = func()
if success is True:
print("done")
else:
print("**ERROR!**")
errors.append(f"Error(s) in {func.__name__}")
if len(errors) == 0:
return True
else:
print(f"Clean failed on the following steps:{NEWLINE_INDENT}{NEWLINE_INDENT.join(errors)}")
return False
def remove_baselines(self, glob_patterns:list[str]=["baselines/**/*.yml"], keep:list[str]=[]) -> bool:
return self.remove_by_glob_patterns(glob_patterns, keep)
def remove_dist_lookups(self, glob_patterns:list[str]=["dist/escu/lookups/**/*.yml","dist/escu/lookups/**/*.csv", "dist/escu/lookups/**/*.*"], keep:list[str]=[]) -> bool:
return self.remove_by_glob_patterns(glob_patterns, keep)
def remove_detections(self, glob_patterns:list[str]=["detections/**/*.yml"], keep:list[str]=[]) -> bool:
return self.remove_by_glob_patterns(glob_patterns, keep)
def remove_investigations(self,glob_patterns:list[str]=["investigations/**/*.yml"], keep:list[str]=[]) -> bool:
return self.remove_by_glob_patterns(glob_patterns, keep)
def remove_lookups(self, glob_patterns:list[str]=["lookups/**/*.yml","lookups/**/*.csv", "lookups/**/*.*"], keep:list[str]=[]) -> bool:
return self.remove_by_glob_patterns(glob_patterns, keep)
def remove_macros(self,glob_patterns:list[str]=["macros/**/*.yml"], keep:list[str]=[]) -> bool:
return self.remove_by_glob_patterns(glob_patterns, keep)
def remove_notebooks(self, glob_patterns:list[str]=["notesbooks/**/*.ipynb"], keep:list[str]=[]) -> bool:
return self.remove_by_glob_patterns(glob_patterns, keep)
def remove_playbooks(self, glob_patterns:list[str]=["playbooks/**/*.*"], keep:list[str]=[]) -> bool:
return self.remove_by_glob_patterns(glob_patterns, keep)
def remove_stories(self, glob_patterns:list[str]=["stories/**/*.yml"], keep:list[str]=[]) -> bool:
return self.remove_by_glob_patterns(glob_patterns, keep)
def remove_tests(self, glob_patterns:list[str]=["tests/**/*.yml"], keep:list[str]=[]) -> bool:
return self.remove_by_glob_patterns(glob_patterns, keep)
def remove_by_glob_patterns(self, glob_patterns:list[str], keep:list[str]=[]) -> bool:
success = True
for pattern in glob_patterns:
success |= self.remove_by_glob_pattern(pattern, keep)
return success
def remove_by_glob_pattern(self, glob_pattern:str, keep:list[str]) -> bool:
success = True
try:
matched_filenames = glob.glob(glob_pattern, recursive=True)
for filename in matched_filenames:
self.items_scanned.append(filename)
success &= self.remove_file(filename, keep)
return success
except Exception as e:
print(f"Error running glob on the pattern {glob_pattern}: {str(e)}")
return False
def remove_file(self, filename:str, keep:list[str]) -> bool:
for keep_pattern in keep:
if re.search(keep_pattern, filename) is not None:
print(f"Preserving file {filename} which conforms to the keep regex {keep_pattern}")
self.items_kept.append(filename)
return True
#File will be deleted - it was not identified as a file to keep
#Note that, by design, we will not/cannot delete files with os.remove. We want to keep
#the folder hierarchy. If we want to delete folders, we will need to update this library
try:
os.remove(filename)
self.items_deleted.append(filename)
return True
except Exception as e:
print(f"Error deleting file {filename}: {str(e)}")
self.items_deleted_failed.append(filename)
return False
@@ -1,32 +0,0 @@
import subprocess
import os
class Inspect:
def __init__(self, args):
try:
import splunk_appinspect
except Exception as e:
print("Failed to import libmagic. If you're on macOS, you probably need to run 'brew install libmagic'")
raise(Exception(f"AppInspect Failed to import magic: str(e)"))
#Splunk appinspect does not have a documented python API... so we run it
#using the Command Line interface
self.package_path = args.package_path
proc = "no output produced..."
try:
proc = subprocess.run(["splunk-appinspect", "inspect", self.package_path])
if proc.returncode != 0:
raise(Exception(f"splunk-appinspect failed with return code {proc.returncode}"))
except Exception as e:
raise(Exception(f"Error running appinspect on {self.package_path}: {str(e)}"))
print(f"Appinspect on {self.package_path} was successful!")
@@ -1,32 +0,0 @@
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)
class NewAttackDataContent:
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.writeObjects(factory_output_dto.obj, input_dto.factory_input_dto.type)
@@ -1,27 +0,0 @@
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(input_dto.factory_input_dto.input_path, 'bin', 'reporting'))
input_dto.adapter_attack.writeObjects(factory_output_dto.detections, os.path.join(input_dto.factory_input_dto.input_path, 'docs', 'mitre-map'))
print('Reporting of security content successful.')
@@ -1,37 +0,0 @@
from dataclasses import dataclass
from pydantic import ValidationError
from typing import Union
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: Union[FactoryInputDto,None]
ba_factory_input_dto: Union[BAFactoryInputDto,None]
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
print('Validation of security content successful.')
@@ -1,135 +0,0 @@
ATTACK_TACTICS_KILLCHAIN_MAPPING = {
"Reconnaissance": "Reconnaissance",
"Resource Development": "Weaponization",
"Initial Access": "Delivery",
"Execution": "Installation",
"Persistence": "Installation",
"Privilege Escalation": "Exploitation",
"Defense Evasion": "Exploitation",
"Credential Access": "Exploitation",
"Discovery": "Exploitation",
"Lateral Movement": "Exploitation",
"Collection": "Exploitation",
"Command And Control": "Command And Control",
"Command And Control": "Command And Control",
"Exfiltration": "Actions on Objectives",
"Impact": "Actions on Objectives"
}
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 And 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 = {
"Unknown": 0,
"Hostname": 1,
"IP Address": 2,
"MAC Address": 3,
"User Name": 4,
"Email Address": 5,
"URL String": 6,
"File Name": 7,
"File Hash": 8,
"Process Name": 9,
"Ressource UID": 10,
"Endpoint": 20,
"User": 21,
"Email": 22,
"Uniform Resource Locator": 23,
"File": 24,
"Process": 25,
"Geo Location": 26,
"Container": 27,
"Registry Key": 28,
"Registry Value": 29,
"Other": 99
}
SES_ATTACK_TACTICS_ID_MAPPING = {
"Reconnaissance": "TA0043",
"Resource_Development": "TA0042",
"Initial_Access": "TA0001",
"Execution": "TA0002",
"Persistence": "TA0003",
"Privilege_Escalation": "TA0004",
"Defense_Evasion": "TA0005",
"Credential_Access": "TA0006",
"Discovery": "TA0007",
"Lateral_Movement": "TA0008",
"Collection": "TA0009",
"Command_and_Control": "TA0011",
"Exfiltration": "TA0010",
"Impact": "TA0040"
}
@@ -1,96 +0,0 @@
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
from bin.contentctl_project.contentctl_core.domain.entities.link_validator import LinkValidator
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
check_references: bool = False #Validation is done in order, this field must be defined first
references: list
tags: BaselineTags
# enrichment
deployment: Deployment = None
@validator('name')
def name_max_length(cls, v):
if len(v) > 67:
raise ValueError('name is longer then 67 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):
return LinkValidator.SecurityContentObject_validate_references(v, values)
@validator('search')
def search_validate(cls, v, values):
# write search validator
return v
@@ -1,25 +0,0 @@
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
@@ -1,23 +0,0 @@
from pydantic import BaseModel, validator, ValidationError
from dataclasses import dataclass
class DataSource(BaseModel):
name: str
id: str
date: str
author: str
type: str
source: str
sourcetype: str
category: str = None
product: str
service: str = None
supported_TA: list
references: list
raw_fields: list
field_mappings: list = None
convert_to_log_source: list = None
@@ -1,60 +0,0 @@
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):
name: str = None
id: str = None
date: str = None
author: str = None
description: str = None
scheduling: DeploymentScheduling = None
email: DeploymentEmail = None
notable: DeploymentNotable = None
rba: DeploymentRBA = None
slack: DeploymentSlack = None
phantom: DeploymentPhantom = None
tags: dict = None
@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
@@ -1,8 +0,0 @@
from pydantic import BaseModel, validator, ValidationError
class DeploymentEmail(BaseModel):
message: str
subject: str
to: str
@@ -1,8 +0,0 @@
from pydantic import BaseModel, validator, ValidationError
class DeploymentNotable(BaseModel):
rule_description: str
rule_title: str
nes_fields: list
@@ -1,10 +0,0 @@
from pydantic import BaseModel, validator, ValidationError
class DeploymentPhantom(BaseModel):
cam_workers : str
label : str
phantom_server : str
sensitivity : str
severity : str
@@ -1,7 +0,0 @@
from pydantic import BaseModel, validator, ValidationError
class DeploymentRBA(BaseModel):
enabled: str
@@ -1,10 +0,0 @@
from pydantic import BaseModel, validator, ValidationError
class DeploymentScheduling(BaseModel):
cron_schedule: str
earliest_time: str
latest_time: str
schedule_window: str
@@ -1,7 +0,0 @@
from pydantic import BaseModel, validator, ValidationError
class DeploymentSlack(BaseModel):
channel: str
message: str
@@ -1,195 +0,0 @@
import uuid
import string
import requests
import time
from pydantic import BaseModel, validator, root_validator
from dataclasses import dataclass
from datetime import datetime
from typing import Union
import re
from bin.contentctl_project.contentctl_core.domain.entities.security_content_object import (
SecurityContentObject,
)
from bin.contentctl_project.contentctl_core.domain.entities.enums.enums import (
AnalyticsType,
)
from bin.contentctl_project.contentctl_core.domain.entities.enums.enums import DataModel
from bin.contentctl_project.contentctl_core.domain.entities.enums.enums import (
DetectionStatus,
)
from bin.contentctl_project.contentctl_core.domain.entities.detection_tags import (
DetectionTags,
)
from bin.contentctl_project.contentctl_core.domain.entities.deployment import Deployment
from bin.contentctl_project.contentctl_core.domain.entities.unit_test import UnitTest
from bin.contentctl_project.contentctl_core.domain.entities.macro import Macro
from bin.contentctl_project.contentctl_core.domain.entities.lookup import Lookup
from bin.contentctl_project.contentctl_core.domain.entities.baseline import Baseline
from bin.contentctl_project.contentctl_core.domain.entities.playbook import Playbook
from bin.contentctl_project.contentctl_core.domain.entities.link_validator import (
LinkValidator,
)
from bin.contentctl_project.contentctl_core.domain.entities.deployment import Deployment
import sys
class Detection(BaseModel, SecurityContentObject):
# detection spec
name: str
id: str
version: int
date: str
author: str
type: str
status: DetectionStatus
description: str
data_source: list[str]
search: Union[str, dict]
how_to_implement: str
known_false_positives: str
references: list
tags: DetectionTags
tests: list[UnitTest] = None
# enrichments
datamodel: list = None
deprecated: bool = None
experimental: bool = None
deployment: Deployment = None
annotations: dict = None
risk: list = None
playbooks: list[Playbook] = None
baselines: list[Baseline] = None
mappings: dict = None
test: Union[UnitTest, dict] = None
macros: list[Macro] = None
lookups: list[Lookup] = None
cve_enrichment: list = None
splunk_app_enrichment: list = None
file_path: str = None
source: str = None
nes_fields: str = None
providing_technologies: list = None
runtime: str = None
internalVersion: str = None
# @validator('name')v
# def name_max_length(cls, v, values):
# if len(v) > 67:
# raise ValueError('name is longer then 67 chars: ' + v)
# return v
class Config:
use_enum_values = True
@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.lower() not in [el.name.lower() for el in AnalyticsType]:
raise ValueError("not valid analytics type: " + 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
# @root_validator
# def search_validation(cls, values):
# if 'ssa_' not in values['file_path']:
# if not '_filter' in values['search']:
# raise ValueError('filter macro missing in: ' + values["name"])
# if any(x in values['search'] for x in ['eventtype=', 'sourcetype=', ' source=', 'index=']):
# if not 'index=_internal' in values['search']:
# raise ValueError('Use source macro instead of eventtype, sourcetype, source or index in detection: ' + values["name"])
# return values
@root_validator
def name_max_length(cls, values):
# Check max length only for ESCU searches, SSA does not have that constraint
if "ssa_" not in values["file_path"]:
if len(values["name"]) > 67:
raise ValueError("name is longer then 67 chars: " + values["name"])
return values
@root_validator
def validation_for_ba_only(cls, values):
# Ensure that only a BA detection can have status: validation
if values["status"] == "validation":
if "ssa_" not in values["file_path"]:
raise ValueError(f"The following is NOT an ssa_ detection, but has 'status: {values['status']} which may ONLY be used for ssa_ detections:' {values['file_path']}")
else:
#This is an ssa_ validation detection
pass
return values
@root_validator
def new_line_check(cls, values):
# Check if there is a new line in description and how to implement that is not escaped
pattern = r'(?<!\\)\n'
if re.search(pattern, values["description"]):
match_obj = re.search(pattern,values["description"])
words = values["description"][:match_obj.span()[0]].split()[-10:]
newline_context = ' '.join(words)
raise ValueError(f"Field named 'description' contains new line that is not escaped using backslash. Add backslash at the end of the line after the words: '{newline_context}' in '{values['name']}'")
if re.search(pattern, values["how_to_implement"]):
match_obj = re.search(pattern,values["how_to_implement"])
words = values["how_to_implement"][:match_obj.span()[0]].split()[-10:]
newline_context = ' '.join(words)
raise ValueError(f"Field named 'how_to_implement' contains new line that is not escaped using backslash. Add backslash at the end of the line after the words: '{newline_context}' in '{values['name']}'")
return values
# @validator('references')
# def references_check(cls, v, values):
# return LinkValidator.SecurityContentObject_validate_references(v, values)
@root_validator
def missing_test_file(cls, values):
if values["status"] == DetectionStatus.production:
if "tests" not in values:
raise ValueError("Missing test file for detection: " + values["name"])
return values
@validator("search")
def search_validate(cls, v, values):
# write search validator
return v
@validator("tests")
def tests_validate(cls, v, values):
if values["status"] != DetectionStatus.production and not v:
raise ValueError(
"tests value is needed for production detection: " + values["name"]
)
return v
@@ -1,145 +0,0 @@
import re
from pydantic import BaseModel, validator, ValidationError, root_validator
from bin.contentctl_project.contentctl_core.domain.entities.mitre_attack_enrichment import MitreAttackEnrichment
from bin.contentctl_project.contentctl_core.domain.constants.constants import *
class DetectionTags(BaseModel):
# detection spec
name: str
analytic_story: list
asset_type: str
automated_detection_testing: str = None
cis20: list = None
confidence: str
impact: int
kill_chain_phases: list = None
message: str
mitre_attack_id: list = None
nist: list = None
observable: list
product: list
required_fields: list
risk_score: int
security_domain: str
risk_severity: str = None
cve: list = None
supported_tas: list = None
atomic_guid: list = None
drilldown_search: str = None
manual_test: str = None
# enrichment
mitre_attack_enrichments: list[MitreAttackEnrichment] = []
confidence_id: int = None
impact_id: int = None
context_ids: list = None
risk_level_id: int = None
risk_level: str = None
observable_str: str = None
evidence_str: str = None
analytics_story_str: str = None
kill_chain_phases_id: list = None
kill_chain_phases_str: str = None
research_site_url: str = None
event_schema: str = None
mappings: list = None
annotations: dict = None
@validator('cis20')
def tags_cis20(cls, v, values):
pattern = '^CIS ([0-9]|1[0-9]|20)$' #DO NOT match leading zeroes and ensure no extra characters before or after the string
for value in v:
if not re.match(pattern, value):
raise ValueError(f"CIS control '{value}' is not a valid Control ('CIS 1' -> 'CIS 20'): {values['name']}")
return v
@validator('nist')
def tags_nist(cls, v, values):
# Sourced Courtest of NIST: https://www.nist.gov/system/files/documents/cyberframework/cybersecurity-framework-021214.pdf (Page 19)
IDENTIFY = [f'ID.{category}' for category in ["AM", "BE", "GV", "RA", "RM"] ]
PROTECT = [f'PR.{category}' for category in ["AC", "AT", "DS", "IP", "MA", "PT"]]
DETECT = [f'DE.{category}' for category in ["AE", "CM", "DP"] ]
RESPOND = [f'RS.{category}' for category in ["RP", "CO", "AN", "MI", "IM"] ]
RECOVER = [f'RC.{category}' for category in ["RP", "IM", "CO"] ]
ALL_NIST_CATEGORIES = IDENTIFY + PROTECT + DETECT + RESPOND + RECOVER
for value in v:
if not value in ALL_NIST_CATEGORIES:
raise ValueError(f"NIST Category '{value}' is not a valid category")
return v
@validator('confidence')
def tags_confidence(cls, v, values):
v = int(v)
if not (v > 0 and v <= 100):
raise ValueError('confidence score is out of range 1-100: ' + values["name"])
else:
return v
@validator('impact')
def tags_impact(cls, v, values):
if not (v > 0 and v <= 100):
raise ValueError('impact score is out of range 1-100: ' + values["name"])
else:
return v
@validator('kill_chain_phases')
def tags_kill_chain_phases(cls, v, values):
valid_kill_chain_phases = SES_KILL_CHAIN_MAPPINGS.keys()
for value in v:
if value not in valid_kill_chain_phases:
raise ValueError('kill chain phase not valid for ' + values["name"] + '. valid options are ' + str(valid_kill_chain_phases))
return v
@validator('mitre_attack_id')
def tags_mitre_attack_id(cls, v, values):
pattern = 'T[0-9]{4}'
for value in v:
if not re.match(pattern, value):
raise ValueError('Mitre Attack ID are not following the pattern Txxxx: ' + values["name"])
return v
@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
@validator('risk_score')
def tags_calculate_risk_score(cls, v, values):
calculated_risk_score = round(values['impact'] * values['confidence'] / 100)
if calculated_risk_score != int(v):
raise ValueError(f"Risk Score must be calculated as round(confidence * impact / 100)"
f"\n Expected risk_score={calculated_risk_score}, found risk_score={int(v)}: {values['name']}")
return v
@root_validator
def tags_observable(cls, values):
valid_roles = SES_OBSERVABLE_ROLE_MAPPING.keys()
valid_types = SES_OBSERVABLE_TYPE_MAPPING.keys()
for value in values["observable"]:
if value['type'] in valid_types:
if 'Splunk Behavioral Analytics' in values["product"]:
continue
if 'role' not in value:
raise ValueError('Observable role is missing for ' + values["name"])
for role in value['role']:
if role not in valid_roles:
raise ValueError('Observable role ' + role + ' not valid for ' + values["name"] + '. valid options are ' + str(valid_roles))
else:
raise ValueError('Observable type ' + value['type'] + ' not valid for ' + values["name"] + '. valid options are ' + str(valid_types))
return values
@@ -1,62 +0,0 @@
import enum
class AnalyticsType(enum.Enum):
TTP = 1
anomaly = 2
hunting = 3
correlation = 4
class DataModel(enum.Enum):
Endpoint = 1
Network_Traffic = 2
Authentication = 3
Change = 4
Change_Analysis = 5
Email = 6
Network_Resolution = 7
Network_Sessions = 8
UEBA = 9
Updates = 10
Vulnerabilities = 11
Web = 12
Endpoint_Processes = 13
Endpoint_Filesystem = 14
Endpoint_Registry = 15
Risk = 16
Splunk_Audit = 17
class SecurityContentType(enum.Enum):
detections = 1
baselines = 2
stories = 3
playbooks = 4
macros = 5
lookups = 6
deployments = 7
investigations = 8
unit_tests = 9
attack_data = 10
class SecurityContentProduct(enum.Enum):
ESCU = 1
SSA = 2
API = 3
CUSTOM = 4
class SigmaConverterTarget(enum.Enum):
CIM = 1
RAW = 2
OCSF = 3
ALL = 4
class DetectionStatus(enum.Enum):
production = "production"
deprecated = "deprecated"
experimental = "experimental"
validation = "validation"
@@ -1,91 +0,0 @@
import enum
import uuid
import string
import re
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 AnalyticsType
from bin.contentctl_project.contentctl_core.domain.entities.enums.enums import DataModel
from bin.contentctl_project.contentctl_core.domain.entities.investigation_tags import InvestigationTags
from bin.contentctl_project.contentctl_core.domain.entities.link_validator import LinkValidator
class Investigation(BaseModel, SecurityContentObject):
# investigation 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
check_references: bool = False #Validation is done in order, this field must be defined first
references: list
inputs: list = None
tags: InvestigationTags
# enrichment
lowercase_name: str = 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('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):
return LinkValidator.SecurityContentObject_validate_references(v, values)
@validator('search')
def search_validate(cls, v, values):
# write search validator
return v
@@ -1,9 +0,0 @@
from pydantic import BaseModel, validator, ValidationError
class InvestigationTags(BaseModel):
analytic_story: list
product: list
required_fields: list
security_domain: str
@@ -1,174 +0,0 @@
import re
from tracemalloc import start
from unittest.mock import DEFAULT
from pydantic import BaseModel, validator, root_validator,Field
from typing import Union, Callable
import requests
import urllib3, urllib3.exceptions
import time
import abc
import os
import shelve
DEFAULT_USER_AGENT_STRING = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.41 Safari/537.36"
ALLOWED_HTTP_CODES = [200]
class LinkStats(BaseModel):
#Static Values
method: Callable = requests.get
allowed_http_codes: list[int] = ALLOWED_HTTP_CODES
access_count: int = 1 #when constructor is called, it has been accessed once!
timeout_seconds: int = 15
allow_redirects: bool = True
headers: dict = {"User-Agent": DEFAULT_USER_AGENT_STRING}
verify_ssl: bool = False
if verify_ssl is False:
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
#Values generated at runtime.
#We need to assign these some default values to get the
#validation working since ComputedField has not yet been
#introduced to Pydantic
reference: str
referencing_files: set[str]
redirect: Union[str,None] = None
status_code: int = 0
valid: bool = False
resolution_time: float = 0
def is_link_valid(self, referencing_file:str)->bool:
self.access_count += 1
self.referencing_files.add(referencing_file)
return self.valid
@root_validator
def check_reference(cls, values):
start_time = time.time()
#Get out all the fields names to make them easier to reference
method = values['method']
reference = values['reference']
timeout_seconds = values['timeout_seconds']
headers = values['headers']
allow_redirects = values['allow_redirects']
verify_ssl = values['verify_ssl']
allowed_http_codes = values['allowed_http_codes']
if not (reference.startswith("http://") or reference.startswith("https://")):
raise(ValueError(f"Reference {reference} does not begin with http(s). Only http(s) references are supported"))
try:
get = method(reference, timeout=timeout_seconds,
headers = headers,
allow_redirects=allow_redirects, verify=verify_ssl)
resolution_time = time.time() - start_time
values['status_code'] = get.status_code
values['resolution_time'] = resolution_time
if reference != get.url:
values['redirect'] = get.url
else:
values['redirect'] = None #None is also already the default
#Returns the updated values and sets them for the object
if get.status_code in allowed_http_codes:
values['valid'] = True
else:
#print(f"Unacceptable HTTP Status Code {get.status_code} received for {reference}")
values['valid'] = False
return values
except Exception as e:
resolution_time = time.time() - start_time
#print(f"Reference {reference} was not reachable after {resolution_time:.2f} seconds")
values['status_code'] = 0
values['valid'] = False
values['redirect'] = None
values['resolution_time'] = resolution_time
return values
class LinkValidator(abc.ABC):
cache: Union[dict[str,LinkStats], shelve.Shelf] = {}
uncached_checks: int = 0
total_checks: int = 0
#cache: dict[str,LinkStats] = {}
use_file_cache: bool = False
reference_cache_file: str ="lookups/REFERENCE_CACHE.db"
@staticmethod
def initialize_cache(use_file_cache: bool = False):
LinkValidator.use_file_cache = use_file_cache
if use_file_cache is False:
return
if not os.path.exists(LinkValidator.reference_cache_file):
print(f"Cache at {LinkValidator.reference_cache_file} not found - Creating it.")
try:
LinkValidator.cache = shelve.open(LinkValidator.reference_cache_file, flag='c', writeback=True)
except:
print(f"Failed to create the cache file {LinkValidator.reference_cache_file}. Reference info will not be cached.")
LinkValidator.cache = {}
#Remove all of the failures to force those resources to be resolved again
failed_refs = []
for ref in LinkValidator.cache.keys():
if LinkValidator.cache[ref].status_code not in ALLOWED_HTTP_CODES:
failed_refs.append(ref)
#can't remove it here because this will throw an error:
#cannot change size of dictionary while iterating over it
else:
#Set the reference count to 0 and referencing files to empty set
LinkValidator.cache[ref].access_count = 0
LinkValidator.cache[ref].referencing_files = set()
for ref in failed_refs:
del(LinkValidator.cache[ref])
@staticmethod
def close_cache():
if LinkValidator.use_file_cache:
LinkValidator.cache.close()
@staticmethod
def validate_reference(reference: str, referencing_file:str, raise_exception_if_failure: bool = False) -> bool:
LinkValidator.total_checks += 1
if reference not in LinkValidator.cache:
LinkValidator.uncached_checks += 1
LinkValidator.cache[reference] = LinkStats(reference=reference, referencing_files = set([referencing_file]))
result = LinkValidator.cache[reference].is_link_valid(referencing_file)
#print(f"Total Checks: {LinkValidator.total_checks}, Percent Cached: {100*(1 - LinkValidator.uncached_checks / LinkValidator.total_checks):.2f}")
if result is True:
return True
elif raise_exception_if_failure is True:
raise(Exception(f"Reference Link Failed: {reference}"))
else:
return False
@staticmethod
def print_link_validation_errors():
failures = [LinkValidator.cache[k] for k in LinkValidator.cache if LinkValidator.cache[k].valid is False]
failures.sort(key=lambda d: d.status_code)
for failure in failures:
print(f"Link {failure.reference} invalid with HTTP Status Code [{failure.status_code}] and referenced by the following files:")
for ref in failure.referencing_files:
print(f"\t* {ref}")
@staticmethod
def SecurityContentObject_validate_references(v:list, values: dict)->list:
if 'check_references' not in values:
raise(Exception("Member 'check_references' missing from Baseline!"))
elif values['check_references'] is False:
#Reference checking is enabled
pass
elif values['check_references'] is True:
for reference in v:
LinkValidator.validate_reference(reference, values['name'])
#Remove the check_references key from the values dict so that it is not
#output by the serialization code
del values['check_references']
return v
@@ -1,78 +0,0 @@
import pathlib
from pydantic import BaseModel, validator, root_validator, ValidationError
from bin.contentctl_project.contentctl_core.domain.entities.security_content_object import (
SecurityContentObject,
)
class Lookup(BaseModel, SecurityContentObject):
name: str
description: str
# Collection indicates a KV Store that will be created and/or updated during detection runtime
collection: str = None
fields_list: str = None
# Filename points to a lookup file that must exist during app build time
filename: str = None
default_match: str = None
match_type: str = None
min_matches: int = None
case_sensitive_match: str = None
@root_validator(pre=True)
def ensure_collection_or_filename_exists(cls, values):
# Exactly one of the fields "collection" or "filename" MUST be defined
# Check max length only for ESCU searches, SSA does not have that constraint
if (
values.get("collection", None) == None
and values.get("filename", None) == None
):
raise ValueError(
"Error in lookup. Eaxctly one of 'collection' or 'filename' filename MUST be defined, but NEITHER was defined."
)
if (
values.get("collection", None) != None
and values.get("filename", None) != None
):
raise ValueError(
"Error in lookup. Exactly one of 'collection' or 'filename' filename MUST be defined, but BOTH were defined."
)
return values
@validator("filename")
def filename_validate(cls, v, values):
lookup_file_path = pathlib.Path(".") / "lookups" / str(v)
if not lookup_file_path.is_file():
raise ValueError(
f"Lookup references lookup file '{lookup_file_path}', but that file does not exist."
)
#Also check the format of the lookup file. It MUST be a valid CSV. Valid CSV must have the
#correct number of fields (each row has the same number of columns, even if empty, as the
# number of columns declared at the top of the file)
import csv
with open(lookup_file_path, "r") as csv_file_obj:
reader = csv.reader(csv_file_obj, delimiter=',',quoting=csv.QUOTE_ALL)
try:
reader_list = list(reader)
except Exception as e:
raise ValueError(f"Error validating lookup file '{lookup_file_path}': the follow error was encountered when parsing the csv file: {str(e)}")
if len(reader_list)>0:
csv_keys = reader_list[0]
else:
raise ValueError(f"Error validating lookup file '{lookup_file_path}': 0 rows found in file. a csv MUST contain at least one row (which contains the field names)")
row_errors=[]
for index,row in enumerate(reader_list[1:]):
if len(row) != len(csv_keys) and len(row) != 0:
row_errors.append(f"Error in row {index+2}: expected {len(csv_keys)} columns but got {len(row)}.")
if len(row_errors) > 0:
condensed_string = '\n\t'.join(row_errors)
raise ValueError(f"Error validating lookup file '{lookup_file_path}':\n\t{condensed_string}.")
return v
@@ -1,13 +0,0 @@
from pydantic import BaseModel, validator, ValidationError
from bin.contentctl_project.contentctl_core.domain.entities.security_content_object import SecurityContentObject
class Macro(BaseModel, SecurityContentObject):
name: str
definition: str
description: str
arguments: list = None
@@ -1,8 +0,0 @@
from pydantic import BaseModel, validator, ValidationError
class MitreAttackEnrichment(BaseModel):
mitre_attack_id: str
mitre_attack_technique: str
mitre_attack_tactics: list
mitre_attack_groups: list
@@ -1,32 +0,0 @@
import uuid
import string
from pydantic import BaseModel, validator, ValidationError
from bin.contentctl_project.contentctl_core.domain.entities.security_content_object import SecurityContentObject
from bin.contentctl_project.contentctl_core.domain.entities.playbook_tags import PlaybookTag
from bin.contentctl_project.contentctl_core.domain.entities.link_validator import LinkValidator
class Playbook(BaseModel, SecurityContentObject):
name: str
id: str
version: int
date: str
author: str
type: str
description: str
how_to_implement: str
playbook: str
check_references: bool = False #Validation is done in order, this field must be defined first
references: list
app_list: list
tags: PlaybookTag
@validator('references')
def references_check(cls, v, values):
return LinkValidator.SecurityContentObject_validate_references(v, values)
@@ -1,13 +0,0 @@
from pydantic import BaseModel, validator, ValidationError
class PlaybookTag(BaseModel):
analytic_story: list = None
detections: list = None
platform_tags: list = None
playbook_fields: list = None
product: list = None
playbook_fields: list = None
detection_objects: list = None
@@ -1,7 +0,0 @@
import abc
from bin.contentctl_project.contentctl_core.domain.entities.enums.enums import SecurityContentType
class SecurityContentObject(abc.ABC):
type: SecurityContentType
@@ -1,68 +0,0 @@
import string
import uuid
import requests
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.story_tags import StoryTags
from bin.contentctl_project.contentctl_core.domain.entities.link_validator import LinkValidator
class Story(BaseModel, SecurityContentObject):
# story spec
name: str
id: str
version: int
date: str
author: str
description: str
narrative: str
check_references: bool = False #Validation is done in order, this field must be defined first
references: list
tags: StoryTags
# enrichments
detection_names: list = None
investigation_names: list = None
baseline_names: list = None
author_company: str = None
author_name: str = None
detections: list = None
investigations: list = None
@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', 'narrative')
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):
return LinkValidator.SecurityContentObject_validate_references(v, values)
@@ -1,32 +0,0 @@
from pydantic import BaseModel, validator, ValidationError
from bin.contentctl_project.contentctl_core.domain.entities.mitre_attack_enrichment import MitreAttackEnrichment
class StoryTags(BaseModel):
# story spec
name: str
analytic_story: str
category: list
product: list
usecase: str
# enrichment
mitre_attack_enrichments: list[MitreAttackEnrichment] = []
mitre_attack_tactics: list = []
datamodels: list = []
kill_chain_phases: list = []
@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
@@ -1,13 +0,0 @@
from pydantic import BaseModel, validator, ValidationError
from bin.contentctl_project.contentctl_core.domain.entities.security_content_object import SecurityContentObject
from bin.contentctl_project.contentctl_core.domain.entities.unit_test_attack_data import UnitTestAttackData
from bin.contentctl_project.contentctl_core.domain.entities.unit_test_baseline import UnitTestBaseline
class UnitTest(BaseModel):
name: str
baselines: list[UnitTestBaseline] = None
attack_data: list[UnitTestAttackData]
@@ -1,11 +0,0 @@
from pydantic import BaseModel, validator, ValidationError
class UnitTestAttackData(BaseModel):
file_name: str = None
data: str = None
source: str = None
sourcetype: str = None
update_timestamp: bool = None
@@ -1,11 +0,0 @@
from pydantic import BaseModel, validator, ValidationError
class UnitTestBaseline(BaseModel):
name: str
file: str
pass_condition: str
earliest_time: str
latest_time: str
@@ -1,12 +0,0 @@
from pydantic import BaseModel, validator, ValidationError
from bin.contentctl_project.contentctl_core.domain.entities.unit_test import UnitTest
class UnitTestOld(BaseModel):
name: str
tests: list[UnitTest]
@@ -1,15 +0,0 @@
from pydantic import BaseModel, validator, ValidationError
from bin.contentctl_project.contentctl_core.domain.entities.unit_test_attack_data import UnitTestAttackData
from bin.contentctl_project.contentctl_core.domain.entities.unit_test_baseline import UnitTestBaseline
class UnitTestTest(BaseModel):
name: str
file: str
pass_condition: str
earliest_time: str = None
latest_time: str = None
baselines: list[UnitTestBaseline] = None
attack_data: list[UnitTestAttackData]
@@ -1,28 +0,0 @@
import os
import pytest
from bin.contentctl_project.contentctl_core.application.factory.ba_factory import BAFactory, BAFactoryInputDto, BAFactoryOutputDto
from bin.contentctl_project.contentctl_infrastructure.builder.security_content_director import SecurityContentDirector
from bin.contentctl_project.contentctl_infrastructure.builder.security_content_basic_builder import SecurityContentBasicBuilder
from bin.contentctl_project.contentctl_infrastructure.builder.security_content_detection_builder import SecurityContentDetectionBuilder
def test_factory_BA():
input_path = os.path.join(os.path.dirname(__file__), '../../../../../..')
input_dto = BAFactoryInputDto(
input_path,
SecurityContentBasicBuilder(),
SecurityContentDetectionBuilder(),
SecurityContentDirector()
)
output_dto = BAFactoryOutputDto([],[])
factory = BAFactory(output_dto)
factory.execute(input_dto)
for detection in output_dto.detections:
if not detection.test:
raise AssertionError("test file missing for ssa detection: " + detection.name)
@@ -1,38 +0,0 @@
import os
from re import A
from bin.contentctl_project.contentctl_infrastructure.tests.test_constants import SECURITY_CONTENT_ROOT
from bin.contentctl_project.contentctl_core.application.factory.factory import FactoryInputDto
from bin.contentctl_project.contentctl_core.application.factory.factory import FactoryOutputDto
from bin.contentctl_project.contentctl_core.application.factory.factory import Factory
from bin.contentctl_project.contentctl_infrastructure.builder.security_content_director import SecurityContentDirector
from bin.contentctl_project.contentctl_infrastructure.builder.security_content_basic_builder import SecurityContentBasicBuilder
from bin.contentctl_project.contentctl_infrastructure.builder.security_content_detection_builder import SecurityContentDetectionBuilder
from bin.contentctl_project.contentctl_infrastructure.builder.security_content_story_builder import SecurityContentStoryBuilder
from bin.contentctl_project.contentctl_core.domain.entities.enums.enums import SecurityContentProduct
from bin.contentctl_project.contentctl_infrastructure.builder.security_content_investigation_builder import SecurityContentInvestigationBuilder
from bin.contentctl_project.contentctl_infrastructure.builder.security_content_baseline_builder import SecurityContentBaselineBuilder
from bin.contentctl_project.contentctl_infrastructure.builder.attack_enrichment import AttackEnrichment
from bin.contentctl_project.contentctl_infrastructure.builder.security_content_playbook_builder import SecurityContentPlaybookBuilder
def test_factory_ESCU():
input_path = os.path.join(os.path.dirname(__file__), '../../../../../..')
input_dto = FactoryInputDto(
input_path,
SecurityContentBasicBuilder(),
SecurityContentDetectionBuilder(),
SecurityContentStoryBuilder(),
SecurityContentBaselineBuilder(),
SecurityContentInvestigationBuilder(),
SecurityContentPlaybookBuilder(input_path = SECURITY_CONTENT_ROOT),
SecurityContentDirector(),
AttackEnrichment.get_attack_lookup(input_path = SECURITY_CONTENT_ROOT)
)
output_dto = FactoryOutputDto([],[],[],[],[],[],[],[],[])
factory = Factory(output_dto)
factory.execute(input_dto)
@@ -1,25 +0,0 @@
import os
from bin.contentctl_project.contentctl_core.domain.entities.enums.enums import SecurityContentType
from bin.contentctl_project.contentctl_core.application.factory.object_factory import ObjectFactoryInputDto
from bin.contentctl_project.contentctl_core.application.factory.object_factory import ObjectFactory
from bin.contentctl_project.contentctl_infrastructure.builder.security_content_object_builder import SecurityContentObjectBuilder
from bin.contentctl_project.contentctl_infrastructure.builder.security_content_director import SecurityContentDirector
def test_object_factory():
input_path = os.path.join(os.path.dirname(__file__), '../../../../../../detections')
input_dto = ObjectFactoryInputDto(
input_path,
SecurityContentObjectBuilder(),
SecurityContentDirector()
)
objects = list()
factory = ObjectFactory(objects)
factory.execute(input_dto)
#assert len(objects) == 959
@@ -1,88 +0,0 @@
name: Attempted Credential Dump From Registry via Reg exe
id: e9fb4a59-c5fb-440a-9f24-191fbc6b2911
version: 6
date: '2021-09-16'
author: PATRICK BAREISS, SPLUNK
type: TTP
datamodel:
- Endpoint
description: Monitor for execution of reg.exe with parameters specifying an export
of keys that contain hashed credentials that attackers may try to crack offline.
search: '| tstats `security_content_summariesonly` count min(_time) as firstTime max(_time)
as lastTime from datamodel=Endpoint.Processes where `process_reg` OR `process_cmd`
Processes.process=*save* (Processes.process=*HKEY_LOCAL_MACHINE\\Security* OR Processes.process=*HKEY_LOCAL_MACHINE\\SAM*
OR Processes.process=*HKEY_LOCAL_MACHINE\\System* OR Processes.process=*HKLM\\Security*
OR Processes.process=*HKLM\\System* OR Processes.process=*HKLM\\SAM*) by Processes.dest
Processes.user Processes.parent_process Processes.process_name Processes.original_file_name
Processes.process Processes.process_id Processes.parent_process_id | `drop_dm_object_name(Processes)`
| `security_content_ctime(firstTime)`| `security_content_ctime(lastTime)` | `attempted_credential_dump_from_registry_via_reg_exe_filter`'
how_to_implement: To successfully implement this search you need to be ingesting information
on process that include the name of the process responsible for the changes from
your endpoints into the `Endpoint` datamodel in the `Processes` node. In addition,
confirm the latest CIM App 4.20 or higher is installed and the latest TA for the
endpoint product.
known_false_positives: None identified.
references:
- https://github.com/redcanaryco/atomic-red-team/blob/master/atomics/T1003.002/T1003.002.md#atomic-test-1---registry-dump-of-sam-creds-and-secrets
tags:
analytic_story:
- Credential Dumping
- DarkSide Ransomware
asset_type: Endpoint
automated_detection_testing: passed
cis20:
- CIS 3
- CIS 5
- CIS 16
confidence: 100
context:
- Source:Endpoint
- Stage:Credential Access
dataset:
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1003.002/atomic_red_team/windows-sysmon.log
impact: 90
kill_chain_phases:
- Actions on Objectives
message: An instance of $parent_process_name$ spawning $process_name$ was identified
on endpoint $dest$ by user $user$ attempting to export the registry keys.
mitre_attack_id:
- T1003.002
- T1003
nist:
- DE.CM
observable:
- name: user
type: User
role:
- Victim
- name: dest
type: Hostname
role:
- Victim
- name: parent_process_name
type: Process
role:
- Parent Process
- name: process_name
type: Process
role:
- Child Process
product:
- Splunk Enterprise
- Splunk Enterprise Security
- Splunk Cloud
required_fields:
- _time
- Processes.dest
- Processes.user
- Processes.parent_process_name
- Processes.parent_process
- Processes.original_file_name
- Processes.process_name
- Processes.process
- Processes.process_id
- Processes.parent_process_path
- Processes.process_path
- Processes.parent_process_id
risk_score: 90
security_domain: endpoint
@@ -1,88 +0,0 @@
name: Attempted Credential Dump From Registry via Reg exe
id: e9fb4a59-c5fb-440a-9f24-191fbc6b2911
version: 6
date: '2021-09-16'
author: PATRICK BAREISS, SPLUNK
type: TTP
datamodel:
- Endpoint
description: Monitor for execution of reg.exe with parameters specifying an export
of keys that contain hashed credentials that attackers may try to crack offline.
search: '| tstats `security_content_summariesonly` count min(_time) as firstTime max(_time)
as lastTime from datamodel=Endpoint.Processes where `process_reg` OR `process_cmd`
Processes.process=*save* (Processes.process=*HKEY_LOCAL_MACHINE\\Security* OR Processes.process=*HKEY_LOCAL_MACHINE\\SAM*
OR Processes.process=*HKEY_LOCAL_MACHINE\\System* OR Processes.process=*HKLM\\Security*
OR Processes.process=*HKLM\\System* OR Processes.process=*HKLM\\SAM*) by Processes.dest
Processes.user Processes.parent_process Processes.process_name Processes.original_file_name
Processes.process Processes.process_id Processes.parent_process_id | `drop_dm_object_name(Processes)`
| `security_content_ctime(firstTime)`| `security_content_ctime(lastTime)` | `attempted_credential_dump_from_registry_via_reg_exe_filter`'
how_to_implement: To successfully implement this search you need to be ingesting information
on process that include the name of the process responsible for the changes from
your endpoints into the `Endpoint` datamodel in the `Processes` node. In addition,
confirm the latest CIM App 4.20 or higher is installed and the latest TA for the
endpoint product.
known_false_positives: None identified.
references:
- https://github.com/redcanaryco/atomic-red-team/blob/master/atomics/T1003.002/T1003.002.md#atomic-test-1---registry-dump-of-sam-creds-and-secrets
tags:
analytic_story:
- Credential Dumping
- DarkSide Ransomware
asset_type: Endpoint
automated_detection_testing: passed
cis20:
- CIS 3
- CIS 5
- CIS 16
confidence: 100
context:
- Source:Endpoint
- Stage:Credential Access
dataset:
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1003.002/atomic_red_team/windows-sysmon.log
impact: 90
kill_chain_phases:
- Actions on Objectives
message: An instance of $parent_process_name$ spawning $process_name$ was identified
on endpoint $dest$ by user $user$ attempting to export the registry keys.
mitre_attack_id:
- T1003.002
- T1003
nist:
- DE.CM
observable:
- name: user
type: User
role:
- Victim
- name: dest
type: Hostname
role:
- Victim
- name: parent_process_name
type: Process
role:
- Parent Process
- name: process_name
type: Process
role:
- Child Process
product:
- Splunk Enterprise
- Splunk Enterprise Security
- Splunk Cloud
required_fields:
- _time
- Processes.dest
- Processes.user
- Processes.parent_process_name
- Processes.parent_process
- Processes.original_file_name
- Processes.process_name
- Processes.process
- Processes.process_id
- Processes.parent_process_path
- Processes.process_path
- Processes.parent_process_id
risk_score: 90
security_domain: endpoint
@@ -1,35 +0,0 @@
import os
from bin.contentctl_project.contentctl_infrastructure.tests.test_constants import SECURITY_CONTENT_ROOT
from bin.contentctl_project.contentctl_core.application.use_cases.content_changer import ContentChanger, ContentChangerInputDto
from bin.contentctl_project.contentctl_core.application.factory.object_factory import ObjectFactoryInputDto
from bin.contentctl_project.contentctl_infrastructure.builder.security_content_object_builder import SecurityContentObjectBuilder
from bin.contentctl_project.contentctl_infrastructure.builder.security_content_director import SecurityContentDirector
from bin.contentctl_project.contentctl_infrastructure.adapter.obj_to_yml_adapter import ObjToYmlAdapter
from bin.contentctl_project.contentctl_infrastructure.builder.yml_reader import YmlReader
def test_content_changer_author_uppercase():
input_path = os.path.join(os.path.dirname(__file__),
'data_content_changer')
output_path = os.path.join(os.path.dirname(__file__),
'data_content_changer_ref')
factory_input_dto = ObjectFactoryInputDto(
input_path,
SecurityContentObjectBuilder(),
SecurityContentDirector()
)
input_dto = ContentChangerInputDto(
ObjToYmlAdapter(input_path = SECURITY_CONTENT_ROOT),
factory_input_dto,
'example_converter_func'
)
content_changer = ContentChanger()
content_changer.execute(input_dto)
yml_obj = YmlReader.load_file(os.path.join(output_path, 'attempted_credential_dump_from_registry_via_reg_exe.yml'))
assert yml_obj['author'] == 'PATRICK BAREISS, SPLUNK'
@@ -1,68 +0,0 @@
name: Unsigned Image Loaded by LSASS
id: 56ef054c-76ef-45f9-af4a-a634695dcd65
version: 1
date: '2019-12-06'
author: Patrick Bareiss, Splunk
type: TTP
datamodel: []
description: This search detects loading of unsigned images by LSASS. Deprecated because
too noisy.
search: '`sysmon` EventID=7 Image=*lsass.exe Signed=false | stats count min(_time)
as firstTime max(_time) as lastTime by Computer, Image, ImageLoaded, Signed, SHA1
| rename Computer as dest | `security_content_ctime(firstTime)`| `security_content_ctime(lastTime)`
| `unsigned_image_loaded_by_lsass_filter` '
how_to_implement: This search needs Sysmon Logs with a sysmon configuration, which
includes EventCode 7 with lsass.exe. This search uses an input macro named `sysmon`.
We strongly recommend that you specify your environment-specific configurations
(index, source, sourcetype, etc.) for Windows Sysmon logs. Replace the macro definition
with configurations for your Splunk environment. The search also uses a post-filter
macro designed to filter out known false positives.
known_false_positives: Other tools could load images into LSASS for legitimate reason.
But enterprise tools should always use signed DLLs.
references:
- https://attack.mitre.org/techniques/T1003/001/
tags:
analytic_story:
- Credential Dumping
asset_type: Windows
cis20:
- CIS 8
- CIS 16
confidence: 100
context:
- Source:Endpoint
- Stage:Credential Access
impact: 90
kill_chain_phases:
- Actions on Objectives
message: An instance of $parent_process_name$ spawning $process_name$ was identified
on endpoint $dest$ by user $user$ attempting to export the registry keys.
mitre_attack_id:
- T1003.001
nist:
- DE.CM
observable:
- name: user
type: User
role:
- Victim
- name: dest
type: Hostname
role:
- Victim
- name: parent_process_name
type: Process
role:
- Parent Process
- name: process_name
type: Process
role:
- Child Process
product:
- Splunk Enterprise
- Splunk Enterprise Security
- Splunk Cloud
required_fields:
- _time
risk_score: 90
security_domain: endpoint
@@ -1,88 +0,0 @@
name: Attempted Credential Dump From Registry via Reg exe
id: e9fb4a59-c5fb-440a-9f24-191fbc6b2911
version: 6
date: '2021-09-16'
author: Patrick Bareiss, Splunk
type: TTP
datamodel:
- Endpoint
description: Monitor for execution of reg.exe with parameters specifying an export
of keys that contain hashed credentials that attackers may try to crack offline.
search: '| tstats `security_content_summariesonly` count min(_time) as firstTime max(_time)
as lastTime from datamodel=Endpoint.Processes where `process_reg` OR `process_cmd`
Processes.process=*save* (Processes.process=*HKEY_LOCAL_MACHINE\\Security* OR Processes.process=*HKEY_LOCAL_MACHINE\\SAM*
OR Processes.process=*HKEY_LOCAL_MACHINE\\System* OR Processes.process=*HKLM\\Security*
OR Processes.process=*HKLM\\System* OR Processes.process=*HKLM\\SAM*) by Processes.dest
Processes.user Processes.parent_process Processes.process_name Processes.original_file_name
Processes.process Processes.process_id Processes.parent_process_id | `drop_dm_object_name(Processes)`
| `security_content_ctime(firstTime)`| `security_content_ctime(lastTime)` | `attempted_credential_dump_from_registry_via_reg_exe_filter`'
how_to_implement: To successfully implement this search you need to be ingesting information
on process that include the name of the process responsible for the changes from
your endpoints into the `Endpoint` datamodel in the `Processes` node. In addition,
confirm the latest CIM App 4.20 or higher is installed and the latest TA for the
endpoint product.
known_false_positives: None identified.
references:
- https://github.com/redcanaryco/atomic-red-team/blob/master/atomics/T1003.002/T1003.002.md#atomic-test-1---registry-dump-of-sam-creds-and-secrets
tags:
analytic_story:
- Credential Dumping
- DarkSide Ransomware
asset_type: Endpoint
automated_detection_testing: passed
cis20:
- CIS 3
- CIS 5
- CIS 16
confidence: 100
context:
- Source:Endpoint
- Stage:Credential Access
dataset:
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1003.002/atomic_red_team/windows-sysmon.log
impact: 90
kill_chain_phases:
- Actions on Objectives
message: An instance of $parent_process_name$ spawning $process_name$ was identified
on endpoint $dest$ by user $user$ attempting to export the registry keys.
mitre_attack_id:
- T1003.002
- T1003
nist:
- DE.CM
observable:
- name: user
type: User
role:
- Victim
- name: dest
type: Hostname
role:
- Victim
- name: parent_process_name
type: Process
role:
- Parent Process
- name: process_name
type: Process
role:
- Child Process
product:
- Splunk Enterprise
- Splunk Enterprise Security
- Splunk Cloud
required_fields:
- _time
- Processes.dest
- Processes.user
- Processes.parent_process_name
- Processes.parent_process
- Processes.original_file_name
- Processes.process_name
- Processes.process
- Processes.process_id
- Processes.parent_process_path
- Processes.process_path
- Processes.parent_process_id
risk_score: 90
security_domain: endpoint
@@ -1,5 +0,0 @@
arguments:
- field
definition: 'convert timeformat="%Y-%m-%dT%H:%M:%S" ctime($field$)'
description: convert epoch time to string
name: security_content_ctime
@@ -1,3 +0,0 @@
definition: summariesonly=false allow_old_summaries=true
description: search data model's summaries only
name: security_content_summariesonly
@@ -1,4 +0,0 @@
definition: sourcetype=XmlWinEventLog:Microsoft-Windows-Sysmon/Operational OR source=XmlWinEventLog:Microsoft-Windows-Sysmon/Operational
description: customer specific splunk configurations(eg- index, source, sourcetype).
Replace the macro definition with configurations for your Splunk Environmnent.
name: sysmon
@@ -1,48 +0,0 @@
name: Cobalt Strike
id: bcfd17e8-5461-400a-80a2-3b7d1459220c
version: 1
date: '2021-02-16'
author: Michael Haag, Splunk
description: Cobalt Strike is threat emulation software. Red teams and penetration testers use Cobalt Strike to demonstrate the risk of a breach and evaluate mature security programs. Most recently, Cobalt Strike has become the choice tool by threat groups due to its ease of use and extensibility.
narrative: 'This Analytic Story supports you to detect Tactics, Techniques and Procedures
(TTPs) from Cobalt Strike. Cobalt Strike has many ways to be enhanced by using aggressor scripts, malleable C2 profiles, default attack packages, and much more.
For endpoint behavior, Cobalt Strike is most commonly identified via named pipes, spawn to processes, and DLL function names. Many additional variables are provided for in memory operation of the beacon implant.
On the network, depending on the malleable C2 profile used, it is near infinite in the amount of ways to conceal the C2 traffic with Cobalt Strike.
Not every query may be specific to Cobalt Strike the tool, but the methodologies and techniques used by it.\
Splunk Threat Research reviewed all publicly available instances of Malleabe C2 Profiles and generated a list of the most commonly used spawnto and pipenames.\
`Spawnto_x86` and `spawnto_x64` is the process that Cobalt Strike will spawn and injects shellcode into.\
Pipename sets the named pipe name used in Cobalt Strikes Beacon SMB C2 traffic.\
With that, new detections were generated focused on these spawnto processes spawning without command line arguments. Similar, the named pipes most commonly used by Cobalt Strike added as a detection.
In generating content for Cobalt Strike, the following is considered:\
- Is it normal for spawnto_ value to have no command line arguments? No command line arguments and a network connection?\
- What is the default, or normal, process lineage for spawnto_ value?\
- Does the spawnto_ value make network connections?\
- Is it normal for spawnto_ value to load jscript, vbscript, Amsi.dll, and clr.dll?\
While investigating a detection related to this Analytic Story, keep in mind the parent process, process path, and any file modifications that may occur. Tuning may need to occur to remove any false positives.'
references:
- https://www.cobaltstrike.com/
- https://www.infocyte.com/blog/2020/09/02/cobalt-strike-the-new-favorite-among-thieves/
- https://bluescreenofjeff.com/2017-01-24-how-to-write-malleable-c2-profiles-for-cobalt-strike/
- https://blog.talosintelligence.com/2020/09/coverage-strikes-back-cobalt-strike-paper.html
- https://www.fireeye.com/blog/threat-research/2020/12/unauthorized-access-of-fireeye-red-team-tools.html
- https://github.com/MichaelKoczwara/Awesome-CobaltStrike-Defence
- https://github.com/zer0yu/Awesome-CobaltStrike
tags:
analytic_story: Cobalt Strike
category:
- Adversary Tactics
product:
- Splunk Enterprise
- Splunk Enterprise Security
- Splunk Cloud
usecase: Advanced Threat Detection
@@ -1,37 +0,0 @@
name: Credential Dumping
id: 854d78bf-d0e2-4f4e-b05c-640905f86d7a
version: 3
date: '2020-02-04'
author: Rico Valdez, Splunk
description: Uncover activity consistent with credential dumping, a technique wherein
attackers compromise systems and attempt to obtain and exfiltrate passwords. The
threat actors use these pilfered credentials to further escalate privileges and
spread throughout a target environment. The included searches in this Analytic Story
are designed to identify attempts to credential dumping.
narrative: 'Credential dumping&#151;gathering credentials from a target system, often
hashed or encrypted&#151;is a common attack technique. Even though the credentials
may not be in plain text, an attacker can still exfiltrate the data and set to cracking
it offline, on their own systems. The threat actors target a variety of sources
to extract them, including the Security Accounts Manager (SAM), Local Security Authority
(LSA), NTDS from Domain Controllers, or the Group Policy Preference (GPP) files.\
Once attackers obtain valid credentials, they use them to move throughout a target
network with ease, discovering new systems and identifying assets of interest. Credentials
obtained in this manner typically include those of privileged users, which may provide
access to more sensitive information and system operations.\
The detection searches in this Analytic Story monitor access to the Local Security
Authority Subsystem Service (LSASS) process, the usage of shadowcopies for credential
dumping and some other techniques for credential dumping.'
references:
- https://attack.mitre.org/wiki/Technique/T1003
- https://cyberwardog.blogspot.com/2017/03/chronicles-of-threat-hunter-hunting-for.html
tags:
analytic_story: Credential Dumping
category:
- Adversary Tactics
product:
- Splunk Enterprise
- Splunk Enterprise Security
- Splunk Cloud
usecase: Advanced Threat Detection
@@ -1,20 +0,0 @@
name: DarkSide Ransomware
id: 507edc74-13d5-4339-878e-b9114ded1f35
version: 1
date: '2021-05-12'
author: Bhavin Patel, Splunk
description: Leverage searches that allow you to detect and investigate unusual activities
that might relate to the DarkSide Ransomware
narrative: 'This story addresses Darkside ransomware. This ransomware payload has many similarities to common ransomware however there are certain items particular to it. The creation of a .TXT log that shows every item being encrypted as well as the creation of ransomware notes and files adding a machine ID created based on CRC32 checksum algorithm. This ransomware payload leaves machines in minimal operation level,enough to browse the attackers websites. A customized URI with leaked information is presented to each victim.This is the ransomware payload that shut down the Colonial pipeline. The story is composed of several detection searches covering similar items to other ransomware payloads and those particular to Darkside payload.'
references:
- https://www.splunk.com/en_us/blog/security/the-darkside-of-the-ransomware-pipeline.htmlbig-game-hunting-with-ryuk-another-lucrative-targeted-ransomware/
- https://www.mandiant.com/resources/shining-a-light-on-darkside-ransomware-operations
tags:
analytic_story: DarkSide Ransomware
category:
- Malware
product:
- Splunk Enterprise
- Splunk Enterprise Security
- Splunk Cloud
usecase: Advanced Threat Detection
@@ -1,23 +0,0 @@
name: Trickbot
id: 16f93769-8342-44c0-9b1d-f131937cce8e
version: 1
date: '2021-04-20'
author: Rod Soto, Teoderick Contreras, Splunk
description: Leverage searches that allow you to detect and investigate unusual activities
that might relate to the trickbot banking trojan, including looking for file writes associated
with its payload, process injection, shellcode execution and data collection even in LDAP environment.
narrative: trickbot banking trojan campaigns targeting banks and other vertical sectors.This malware is known
in Microsoft Windows OS where target security Microsoft Defender to prevent its detection and removal. steal
Verizon credentials and targeting banks using its multi component modules that collect and exfiltrate data.
references:
- https://en.wikipedia.org/wiki/Trickbot
- https://blog.checkpoint.com/2021/03/11/february-2021s-most-wanted-malware-trickbot-takes-over-following-emotet-shutdown/
tags:
analytic_story: Trickbot
category:
- Malware
product:
- Splunk Enterprise
- Splunk Enterprise Security
- Splunk Cloud
usecase: Advanced Threat Detection
@@ -1,88 +0,0 @@
name: Attempted Credential Dump From Registry via Reg exe
id: e9fb4a59-c5fb-440a-9f24-191fbc6b2911
version: 6
date: '2021-09-16'
author: PATRICK BAREISS, SPLUNK
type: TTP
datamodel:
- Endpoint
description: Monitor for execution of reg.exe with parameters specifying an export
of keys that contain hashed credentials that attackers may try to crack offline.
search: '| tstats `security_content_summariesonly` count min(_time) as firstTime max(_time)
as lastTime from datamodel=Endpoint.Processes where `process_reg` OR `process_cmd`
Processes.process=*save* (Processes.process=*HKEY_LOCAL_MACHINE\\Security* OR Processes.process=*HKEY_LOCAL_MACHINE\\SAM*
OR Processes.process=*HKEY_LOCAL_MACHINE\\System* OR Processes.process=*HKLM\\Security*
OR Processes.process=*HKLM\\System* OR Processes.process=*HKLM\\SAM*) by Processes.dest
Processes.user Processes.parent_process Processes.process_name Processes.original_file_name
Processes.process Processes.process_id Processes.parent_process_id | `drop_dm_object_name(Processes)`
| `security_content_ctime(firstTime)`| `security_content_ctime(lastTime)` | `attempted_credential_dump_from_registry_via_reg_exe_filter`'
how_to_implement: To successfully implement this search you need to be ingesting information
on process that include the name of the process responsible for the changes from
your endpoints into the `Endpoint` datamodel in the `Processes` node. In addition,
confirm the latest CIM App 4.20 or higher is installed and the latest TA for the
endpoint product.
known_false_positives: None identified.
references:
- https://github.com/redcanaryco/atomic-red-team/blob/master/atomics/T1003.002/T1003.002.md#atomic-test-1---registry-dump-of-sam-creds-and-secrets
tags:
analytic_story:
- Credential Dumping
- DarkSide Ransomware
asset_type: Endpoint
automated_detection_testing: passed
cis20:
- CIS 3
- CIS 5
- CIS 16
confidence: 100
context:
- Source:Endpoint
- Stage:Credential Access
dataset:
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1003.002/atomic_red_team/windows-sysmon.log
impact: 90
kill_chain_phases:
- Actions on Objectives
message: An instance of $parent_process_name$ spawning $process_name$ was identified
on endpoint $dest$ by user $user$ attempting to export the registry keys.
mitre_attack_id:
- T1003.002
- T1003
nist:
- DE.CM
observable:
- name: user
type: User
role:
- Victim
- name: dest
type: Hostname
role:
- Victim
- name: parent_process_name
type: Process
role:
- Parent Process
- name: process_name
type: Process
role:
- Child Process
product:
- Splunk Enterprise
- Splunk Enterprise Security
- Splunk Cloud
required_fields:
- _time
- Processes.dest
- Processes.user
- Processes.parent_process_name
- Processes.parent_process
- Processes.original_file_name
- Processes.process_name
- Processes.process
- Processes.process_id
- Processes.parent_process_path
- Processes.process_path
- Processes.parent_process_id
risk_score: 90
security_domain: endpoint
@@ -1,89 +0,0 @@
name: Cobalt Strike Named Pipes
id: 5876d429-0240-4709-8b93-ea8330b411b5
version: 2
date: '2022-07-15'
author: Michael Haag, Splunk
type: Hunting
datamodel: []
description: 'The following analytic identifies the use of default or publicly known
named pipes used with Cobalt Strike. A named pipe is a named, one-way or duplex
pipe for communication between the pipe server and one or more pipe clients. Cobalt
Strike uses named pipes in many ways and has default values used with the Artifact
Kit and Malleable C2 Profiles. The following query assists with identifying these
default named pipes. Each EDR product presents named pipes a little different. Consider
taking the values and generating a query based on the product of choice. \
Upon triage, review the process performing the named pipe. If it is explorer.exe,
It is possible it was injected into by another process. Review recent parallel processes
to identify suspicious patterns or behaviors. A parallel process may have a network
connection, review and follow the connection back to identify any file modifications.'
search: '`sysmon` EventID=17 OR EventID=18 PipeName IN (\\msagent_*, \\wkssvc*, \\DserNamePipe*,
\\srvsvc_*, \\mojo.*, \\postex_*, \\status_*, \\MSSE-*, \\spoolss_*, \\win_svc*,
\\ntsvcs*, \\winsock*, \\UIA_PIPE*) | stats count min(_time) as firstTime max(_time)
as lastTime by Computer, process_name, process_id process_path, PipeName | rename
Computer as dest | `security_content_ctime(firstTime)` | `security_content_ctime(lastTime)`
| `cobalt_strike_named_pipes_filter`'
how_to_implement: To successfully implement this search, you need to be ingesting
logs with the process name, parent process, and command-line executions from your
endpoints. If you are using Sysmon, you must have at least version 6.0.4 of the
Sysmon TA.
known_false_positives: The idea of using named pipes with Cobalt Strike is to blend
in. Therefore, some of the named pipes identified and added may cause false positives.
Filter by process name or pipe name to reduce false positives.
references:
- https://attack.mitre.org/techniques/T1218/009/
- https://docs.microsoft.com/en-us/windows/win32/ipc/named-pipes
- https://gist.github.com/MHaggis/6c600e524045a6d49c35291a21e10752
- https://www.mandiant.com/resources/shining-a-light-on-darkside-ransomware-operations
tags:
analytic_story:
- Cobalt Strike
- Trickbot
- DarkSide Ransomware
asset_type: Endpoint
automated_detection_testing: passed
cis20:
- CIS 8
confidence: 90
context:
- Source:Endpoint
- Stage:Defense Evasion
dataset:
- https://media.githubusercontent.com/media/splunk/attack_data/master/datasets/attack_techniques/T1055/cobalt_strike/windows-sysmon.log
impact: 80
kill_chain_phases:
- Actions on Objectives
message: An instance of $process_name$ was identified on endpoint $Computer$ by
user $user$ accessing known suspicious named pipes related to Cobalt Strike.
mitre_attack_id:
- T1055
nist:
- PR.PT
- DE.CM
observable:
- name: user
type: User
role:
- Victim
- name: Computer
type: Hostname
role:
- Victim
- name: process_name
type: Process
role:
- Parent Process
product:
- Splunk Enterprise
- Splunk Enterprise Security
- Splunk Cloud
required_fields:
- _time
- EventID
- PipeName
- Computer
- process_name
- process_path
- process_id
risk_score: 72
security_domain: endpoint
@@ -1,75 +0,0 @@
import json
VERSION = "4.3"
NAME = "Detection Coverage"
DESCRIPTION = "security_content detection coverage"
DOMAIN = "mitre-enterprise"
class AttackNavWriter():
@staticmethod
def writeAttackNavFile(mitre_techniques : dict, output_path : str) -> None:
max_count = 0
for technique_id in mitre_techniques.keys():
if mitre_techniques[technique_id]['score'] > max_count:
max_count = mitre_techniques[technique_id]['score']
layer_json = {
"version": VERSION,
"name": NAME,
"description": DESCRIPTION,
"domain": DOMAIN,
"techniques": []
}
layer_json["gradient"] = {
"colors": [
"#ffffff",
"#66b1ff",
"#096ed7"
],
"minValue": 0,
"maxValue": max_count
}
layer_json["filters"] = {
"platforms":
["Windows",
"Linux",
"macOS",
"AWS",
"GCP",
"Azure",
"Office 365",
"SaaS"
]
}
layer_json["legendItems"] = [
{
"label": "NO available detections",
"color": "#ffffff"
},
{
"label": "Some detections available",
"color": "#66b1ff"
}
]
layer_json['showTacticRowBackground'] = True
layer_json['tacticRowBackground'] = "#dddddd"
layer_json["sorting"] = 3
for technique_id in mitre_techniques.keys():
layer_technique = {
"techniqueID": technique_id,
"score": mitre_techniques[technique_id]['score'],
"comment": "\n\n".join(mitre_techniques[technique_id]['file_paths'])
}
layer_json["techniques"].append(layer_technique)
with open(output_path, 'w') as outfile:
json.dump(layer_json, outfile, ensure_ascii=False, indent=4)
@@ -1,61 +0,0 @@
import datetime
import os
from jinja2 import Environment, FileSystemLoader
from bin.contentctl_project.contentctl_core.domain.entities.security_content_object import SecurityContentObject
class ConfWriter():
@staticmethod
def writeConfFileHeader(output_path : str) -> None:
utc_time = datetime.datetime.utcnow().replace(microsecond=0).isoformat()
j2_env = Environment(
loader=FileSystemLoader(os.path.join(os.path.dirname(__file__), 'templates')),
trim_blocks=True)
template = j2_env.get_template('header.j2')
output = template.render(time=utc_time)
with open(output_path, 'w') as f:
output = output.encode('ascii', 'ignore').decode('ascii')
f.write(output)
@staticmethod
def writeConfFileHeaderEmpty(output_path : str) -> None:
with open(output_path, 'w') as f:
f.write('')
@staticmethod
def writeConfFile(template_name : str, output_path : str, objects : list) -> None:
def custom_jinja2_enrichment_filter(string, object):
customized_string = string
for key in dir(object):
if type(key) is not str:
key = key.decode()
if not key.startswith('__') and not key == "_abc_impl" and not callable(getattr(object, key)):
if hasattr(object, key):
customized_string = customized_string.replace("%" + key + "%", str(getattr(object, key)))
for key in dir(object.tags):
if type(key) is not str:
key = key.decode()
if not key.startswith('__') and not key == "_abc_impl" and not callable(getattr(object.tags, key)):
if hasattr(object.tags, key):
customized_string = customized_string.replace("%" + key + "%", str(getattr(object.tags, key)))
return customized_string
j2_env = Environment(
loader=FileSystemLoader(os.path.join(os.path.dirname(__file__), 'templates')),
trim_blocks=True)
j2_env.filters['custom_jinja2_enrichment_filter'] = custom_jinja2_enrichment_filter
template = j2_env.get_template(template_name)
output = template.render(objects=objects)
with open(output_path, 'a') as f:
output = output.encode('ascii', 'ignore').decode('ascii')
f.write(output)
@@ -1,89 +0,0 @@
import os
import re
from jinja2 import Environment, FileSystemLoader
from bin.contentctl_project.contentctl_core.domain.entities.detection import Detection
from bin.contentctl_project.contentctl_core.domain.constants.constants import *
class FindingReportObject():
@staticmethod
def writeFindingReport(detection : Detection) -> None:
if detection.tags.confidence < 33:
detection.tags.confidence_id = 1
elif detection.tags.confidence < 66:
detection.tags.confidence_id = 2
else:
detection.tags.confidence_id = 3
if detection.tags.impact < 20:
detection.tags.impact_id = 1
elif detection.tags.impact < 40:
detection.tags.impact_id = 2
elif detection.tags.impact < 60:
detection.tags.impact_id = 3
elif detection.tags.impact < 80:
detection.tags.impact_id = 4
else:
detection.tags.impact_id = 5
detection.tags.kill_chain_phases_id = dict()
for kill_chain_phase in detection.tags.kill_chain_phases:
detection.tags.kill_chain_phases_id[kill_chain_phase] = SES_KILL_CHAIN_MAPPINGS[kill_chain_phase]
kill_chain_phase_str = "["
i = 0
for kill_chain_phase in detection.tags.kill_chain_phases_id.keys():
kill_chain_phase_str = kill_chain_phase_str + '{"phase": "' + kill_chain_phase + '", "phase_id": ' + str(detection.tags.kill_chain_phases_id[kill_chain_phase]) + "}"
if not i == (len(detection.tags.kill_chain_phases_id.keys()) - 1):
kill_chain_phase_str = kill_chain_phase_str + ', '
i = i + 1
kill_chain_phase_str = kill_chain_phase_str + ']'
detection.tags.kill_chain_phases_str = kill_chain_phase_str
if detection.tags.risk_score < 20:
detection.tags.risk_level_id = 0
detection.tags.risk_level = "Info"
elif detection.tags.risk_score < 40:
detection.tags.risk_level_id = 1
detection.tags.risk_level = "Low"
elif detection.tags.risk_score < 60:
detection.tags.risk_level_id = 2
detection.tags.risk_level = "Medium"
elif detection.tags.risk_score < 80:
detection.tags.risk_level_id = 3
detection.tags.risk_level = "High"
else:
detection.tags.risk_level_id = 4
detection.tags.risk_level = "Critical"
evidence_str = "{"
for i in range(len(detection.tags.observable)):
evidence_str = evidence_str + '"' + detection.tags.observable[i]["name"] + '": ' + detection.tags.observable[i]["name"].replace(".", "_")
if not i == (len(detection.tags.observable) - 1):
evidence_str = evidence_str + ', '
evidence_str = evidence_str + '}'
detection.tags.evidence_str = evidence_str
analytics_story_str = "["
for i in range(len(detection.tags.analytic_story)):
analytics_story_str = analytics_story_str + '"' + detection.tags.analytic_story[i] + '"'
if not i == (len(detection.tags.analytic_story) - 1):
analytics_story_str = analytics_story_str + ', '
analytics_story_str = analytics_story_str + ']'
detection.tags.analytics_story_str = analytics_story_str
if "actor.user.name" in detection.tags.required_fields:
actor_user_name = "actor_user_name"
else:
actor_user_name = "\"Unknown\""
j2_env = Environment(
loader=FileSystemLoader(os.path.join(os.path.dirname(__file__), 'templates')),
trim_blocks=True)
template = j2_env.get_template('finding_report.j2')
body = template.render(detection=detection, attack_tactics_id_mapping=SES_ATTACK_TACTICS_ID_MAPPING, actor_user_name=actor_user_name)
return body
@@ -1,33 +0,0 @@
import os
from jinja2 import Environment, FileSystemLoader
class JinjaWriter:
@staticmethod
def writeObjectsList(template_name : str, output_path : str, objects : list) -> None:
j2_env = Environment(
loader=FileSystemLoader(os.path.join(os.path.dirname(__file__), 'templates')),
trim_blocks=False)
template = j2_env.get_template(template_name)
output = template.render(objects=objects)
with open(output_path, 'w') as f:
output = output.encode('ascii', 'ignore').decode('ascii')
f.write(output)
@staticmethod
def writeObject(template_name : str, output_path : str, object : dict) -> None:
j2_env = Environment(
loader=FileSystemLoader(os.path.join(os.path.dirname(__file__), 'templates')),
trim_blocks=False)
template = j2_env.get_template(template_name)
output = template.render(object=object)
with open(output_path, 'w') as f:
output = output.encode('ascii', 'ignore').decode('ascii')
f.write(output)
@@ -1,10 +0,0 @@
import json
class JsonWriter():
@staticmethod
def writeJsonObject(file_path : str, obj) -> None:
with open(file_path, 'w') as outfile:
json.dump(obj, outfile, ensure_ascii=False)
@@ -1,36 +0,0 @@
import os
from bin.contentctl_project.contentctl_core.application.adapter.adapter import Adapter
from bin.contentctl_project.contentctl_core.domain.entities.enums.enums import SecurityContentType
from bin.contentctl_project.contentctl_infrastructure.adapter.attack_nav_writer import AttackNavWriter
class ObjToAttackNavAdapter(Adapter):
def writeObjects(self, objects: list, output_path: str, type: SecurityContentType = None) -> None:
techniques = dict()
for detection in objects:
if detection.tags.mitre_attack_enrichments:
for mitre_attack_enrichment in detection.tags.mitre_attack_enrichments:
if not mitre_attack_enrichment.mitre_attack_id in techniques:
techniques[mitre_attack_enrichment.mitre_attack_id] = {
'score': 1,
'file_paths': ['https://github.com/splunk/security_content/blob/develop/detections/' + detection.source + '/' + self.convertNameToFileName(detection.name)]
}
else:
techniques[mitre_attack_enrichment.mitre_attack_id]['score'] = techniques[mitre_attack_enrichment.mitre_attack_id]['score'] + 1
techniques[mitre_attack_enrichment.mitre_attack_id]['file_paths'].append('https://github.com/splunk/security_content/blob/develop/detections/' + detection.source + '/' + self.convertNameToFileName(detection.name))
AttackNavWriter.writeAttackNavFile(techniques, os.path.join(output_path, 'coverage.json'))
def convertNameToFileName(self, name: str):
file_name = name \
.replace(' ', '_') \
.replace('-','_') \
.replace('.','_') \
.replace('/','_') \
.lower()
file_name = file_name + '.yml'
return file_name
@@ -1,116 +0,0 @@
import os
import re
from pathlib import Path
from bin.contentctl_project.contentctl_infrastructure.adapter.yml_writer import YmlWriter
from bin.contentctl_project.contentctl_core.application.adapter.adapter import Adapter
from bin.contentctl_project.contentctl_core.domain.entities.enums.enums import SecurityContentType
import shutil
class ObjToAttackDataYmlAdapter(Adapter):
def __init__(self):
self.ATTACK_DATASET_LINK = "https://media.githubusercontent.com/media/splunk/attack_data/master/datasets"
self.sourcetype_dict = {
'windows-sysmon.log':'XmlWinEventLog:Microsoft-Windows-Sysmon/Operational',
'windows-security.log': 'WinEventLog:Security',
'windows-system.log': 'WinEventLog:system',
'windows-powershell-xml.log' :'XmlWinEventLog:Microsoft-Windows-PowerShell/Operational',
'stream_http_events.log' :'stream:http',
'aws_cloudtrail_events.json' :'aws:cloudtrail',
'o365_events.json' :'o365:management:activity',
'o365_exchange_events.json' :'o365:management:activity',
'kubernetes_events.json' :'kubernetes',
'security_hub_finding.json' :'aws:securityhub:finding',
'gsuite_gmail_bigquery.json' :'gsuite:gmail:bigquery',
'gsuite_drive_json.json':'gsuite:drive:json',
'github.json' : 'aws:firehose:json',
'kubernetes_nginx.json' :'kube:container:controller',
'circleci.json' :'circleci',
'sysmon_linux.log' :'Syslog:Linux-Sysmon/Operational',
'xml-windows-security.log': 'XmlWinEventLog:Security',
'xml-windows-system.log': 'XmlWinEventLog:System',
'xml-windows-application.log': 'XmlWinEventLog:Application',
'xml-windows-directory-service.log': 'XmlWinEventLog:Directory Service'
}
return
def banner(self):
print("""
inspired from contentctl.py ...
running attack dataset utility helper.
warming up "Millenium Falcon"...
c==o
_/____\_
_.,--'" ||^ || "`z._
/_/^ ___\|| || _/o\ "`-._
_/ ]. L_| || .|| \_/_ . _`--._
/_~7 _ . " ||. || /] \ ]. (_) . "`--.
|__7~.(_)_ []|+--+|/____T_____________L|
|__| _^(_) /^ __\____e_ _|
|__| (_){_) J ]K{__ L___0_ _]
|__| . _(_) \v /__________|________
l__l_ (_). []|+-+-<\^ L . _ - ---L|
\__\ __. ||^l \Y] /_] (_) . _,--'
\~_] L_| || .\ .\\/~. _,--'"
\_\ . __/|| |\ \`-+-<'"
"`---._|J__L|X o~~|[\\
-Row \____/ \___|[//
`--' `--+-'
""")
def expand_path(self, in_path: str) -> str:
if "~" in in_path:
return str(in_path).replace("~", str(Path.home()))
else:
return in_path
def extract_base_path(self, in_path: str) -> str:
return os.path.basename(os.path.normpath(self.expand_path(in_path)))
def gen_attack_data_descp(self, in_path: str) -> str:
descp = "Generated datasets for {} in attack range.".format(self.extract_base_path(self.expand_path(in_path)).replace("_"," "))
return descp
def writeObjects(self, objects: list, output_path: str, type: SecurityContentType = None) -> None:
## check if src_path exist
expanded_src_path = self.expand_path(objects['src_path'])
expanded_dst_path = self.expand_path(objects['dst_path'])
try:
st = os.stat(expanded_src_path)
except os.error:
print("[x] ERROR: File {0} is not exist".format(objects['src_path']))
exit()
## check if dest_path exist
if not os.path.isdir(expanded_dst_path):
os.makedirs(expanded_dst_path, exist_ok=True)
objects['description'] = self.gen_attack_data_descp(objects['dst_path'])
objects['dataset'] = [self.ATTACK_DATASET_LINK + objects['dst_path'].split("datasets")[1] + os.sep + self.extract_base_path(objects['src_path'])]
objects['sourcetypes'] = [self.sourcetype_dict[objects['sourcetypes'][0]]]
attack_data_yml_file = expanded_dst_path + os.sep + self.extract_base_path(objects['dst_path']).replace(" ", "_") + ".yml"
## copy the dataset to the destination folder
shutil.copy(expanded_src_path, expanded_dst_path)
objects.pop('src_path')
objects.pop('dst_path')
YmlWriter.writeYmlFile(attack_data_yml_file, objects)
## read attackdata file
with open(attack_data_yml_file, 'r') as f:
self.banner()
print("[+] ----------- generated attack data yml file ------------\n")
print(f.read())
@@ -1,109 +0,0 @@
from dataclasses import dataclass
import os
import glob
import shutil
from typing import Union
from bin.contentctl_project.contentctl_core.application.adapter.adapter import Adapter
from bin.contentctl_project.contentctl_infrastructure.adapter.conf_writer import ConfWriter
from bin.contentctl_project.contentctl_core.domain.entities.enums.enums import SecurityContentType
class ObjToConfAdapter(Adapter):
input_path: str
def __init__(self, input_path: str):
self.input_path = input_path
def writeHeaders(self, output_folder: str) -> None:
ConfWriter.writeConfFileHeader(os.path.join(output_folder, 'default/analyticstories.conf'))
ConfWriter.writeConfFileHeader(os.path.join(output_folder, 'default/savedsearches.conf'))
ConfWriter.writeConfFileHeader(os.path.join(output_folder, 'default/collections.conf'))
ConfWriter.writeConfFileHeader(os.path.join(output_folder, 'default/es_investigations.conf'))
ConfWriter.writeConfFileHeader(os.path.join(output_folder, 'default/macros.conf'))
ConfWriter.writeConfFileHeader(os.path.join(output_folder, 'default/transforms.conf'))
ConfWriter.writeConfFileHeader(os.path.join(output_folder, 'default/workflow_actions.conf'))
def writeObjects(self, objects: list, output_path: str, type: SecurityContentType = None) -> None:
if type == SecurityContentType.detections:
ConfWriter.writeConfFile('savedsearches_detections.j2',
os.path.join(output_path, 'default/savedsearches.conf'),
objects)
ConfWriter.writeConfFile('analyticstories_detections.j2',
os.path.join(output_path, 'default/analyticstories.conf'),
objects)
ConfWriter.writeConfFile('macros_detections.j2',
os.path.join(output_path, 'default/macros.conf'),
objects)
elif type == SecurityContentType.stories:
ConfWriter.writeConfFile('analyticstories_stories.j2',
os.path.join(output_path, 'default/analyticstories.conf'),
objects)
elif type == SecurityContentType.baselines:
ConfWriter.writeConfFile('savedsearches_baselines.j2',
os.path.join(output_path, 'default/savedsearches.conf'),
objects)
elif type == SecurityContentType.investigations:
ConfWriter.writeConfFile('savedsearches_investigations.j2',
os.path.join(output_path, 'default/savedsearches.conf'),
objects)
ConfWriter.writeConfFile('analyticstories_investigations.j2',
os.path.join(output_path, 'default/analyticstories.conf'),
objects)
workbench_panels = []
for investigation in objects:
if investigation.inputs:
response_file_name_xml = investigation.lowercase_name + "___response_task.xml"
workbench_panels.append(investigation)
investigation.search = investigation.search.replace(">","&gt;")
investigation.search = investigation.search.replace("<","&lt;")
ConfWriter.writeConfFileHeaderEmpty(os.path.join(output_path,
'default/data/ui/panels/', str("workbench_panel_" + response_file_name_xml)))
ConfWriter.writeConfFile('panel.j2',
os.path.join(output_path,
'default/data/ui/panels/', str("workbench_panel_" + response_file_name_xml)),
[investigation.search])
ConfWriter.writeConfFile('es_investigations_investigations.j2',
os.path.join(output_path, 'default/es_investigations.conf'),
workbench_panels)
ConfWriter.writeConfFile('workflow_actions.j2',
os.path.join(output_path, 'default/workflow_actions.conf'),
workbench_panels)
elif type == SecurityContentType.lookups:
ConfWriter.writeConfFile('collections.j2',
os.path.join(output_path, 'default/collections.conf'),
objects)
ConfWriter.writeConfFile('transforms.j2',
os.path.join(output_path, 'default/transforms.conf'),
objects)
if self.input_path is None:
raise(Exception(f"input_path is required for lookups, but received [{self.input_path}]"))
files = glob.iglob(os.path.join(self.input_path, 'lookups', '*.csv'))
for file in files:
if os.path.isfile(file):
shutil.copy(file, os.path.join(output_path, 'lookups'))
files = glob.iglob(os.path.join(self.input_path, 'lookups', '*.mlmodel'))
for file in files:
if os.path.isfile(file):
shutil.copy(file, os.path.join(output_path, 'lookups'))
elif type == SecurityContentType.macros:
ConfWriter.writeConfFile('macros.j2',
os.path.join(output_path, 'default/macros.conf'),
objects)
@@ -1,120 +0,0 @@
import os
import json
import sys
from bin.contentctl_project.contentctl_core.application.adapter.adapter import Adapter
from bin.contentctl_project.contentctl_infrastructure.adapter.json_writer import JsonWriter
from bin.contentctl_project.contentctl_core.domain.entities.enums.enums import SecurityContentType
class ObjToJsonAdapter(Adapter):
def writeObjects(self, objects: list, output_path: str, type: SecurityContentType = None) -> None:
if type == SecurityContentType.detections:
obj_array = []
for detection in objects:
obj_array.append(detection.dict(exclude_none=True,
exclude =
{
"deprecated": True,
"experimental": True,
"annotations": True,
"risk": True,
"playbooks": True,
"baselines": True,
"mappings": True,
"test": True,
"deployment": True,
"type": True,
"status": True,
"data_source": True,
"tests": True,
"cve_enrichment": True,
"tags":
{
"file_path": True,
"required_fields": True,
"confidence": True,
"impact": True,
"product": True,
"cve": True
}
}
))
JsonWriter.writeJsonObject(os.path.join(output_path, 'detections.json'), {'detections': obj_array })
### Code to be added to contentctl to ship filter macros to macros.json
obj_array = []
for detection in objects:
detection_dict = detection.dict()
if "macros" in detection_dict:
for macro in detection_dict["macros"]:
obj_array.append(macro)
uniques:set[str] = set()
for obj in obj_array:
if obj.get("arguments",None) != None:
uniques.add(json.dumps(obj,sort_keys=True))
else:
obj.pop("arguments")
uniques.add(json.dumps(obj, sort_keys=True))
obj_array = []
for item in uniques:
obj_array.append(json.loads(item))
JsonWriter.writeJsonObject(os.path.join(output_path, 'macros.json'), {'macros': obj_array})
elif type == SecurityContentType.stories:
obj_array = []
for story in objects:
obj_array.append(story.dict(exclude_none=True,
exclude =
{
"investigations": True
}
))
JsonWriter.writeJsonObject(os.path.join(output_path, 'stories.json'), {'stories': obj_array })
elif type == SecurityContentType.baselines:
obj_array = []
for baseline in objects:
obj_array.append(baseline.dict(
exclude =
{
"deployment": True
}
))
JsonWriter.writeJsonObject(os.path.join(output_path, 'baselines.json'), {'baselines': obj_array })
elif type == SecurityContentType.investigations:
obj_array = []
for investigation in objects:
obj_array.append(investigation.dict(exclude_none=True))
JsonWriter.writeJsonObject(os.path.join(output_path, 'response_tasks.json'), {'response_tasks': obj_array })
elif type == SecurityContentType.lookups:
obj_array = []
for lookup in objects:
obj_array.append(lookup.dict(exclude_none=True))
JsonWriter.writeJsonObject(os.path.join(output_path, 'lookups.json'), {'lookups': obj_array })
elif type == SecurityContentType.deployments:
obj_array = []
for deployment in objects:
obj_array.append(deployment.dict(exclude_none=True))
JsonWriter.writeJsonObject(os.path.join(output_path, 'deployments.json'), {'deployments': obj_array })
@@ -1,81 +0,0 @@
import os
import asyncio
import sys
from bin.contentctl_project.contentctl_core.application.adapter.adapter import Adapter
from bin.contentctl_project.contentctl_core.domain.entities.enums.enums import SecurityContentType
from bin.contentctl_project.contentctl_infrastructure.adapter.jinja_writer import JinjaWriter
class ObjToMdAdapter(Adapter):
index = 0
files_to_write = 0
def writeObjects(self, objects: list, output_path: str, type: SecurityContentType = None) -> None:
self.files_to_write = sum([len(obj) for obj in objects])
self.index = 0
progress_percent = ((self.index+1)/self.files_to_write) * 100
if (sys.stdout.isatty() and sys.stdin.isatty() and sys.stderr.isatty()):
print(f"\r{'Docgen Progress'.rjust(23)}: [{progress_percent:3.0f}%]...", end="", flush=True)
attack_tactics = set()
datamodels = set()
categories = set()
for story in objects[0]:
if story.tags.category:
categories.update(story.tags.category)
for detection in objects[1]:
if detection.tags.mitre_attack_enrichments:
for attack in detection.tags.mitre_attack_enrichments:
attack_tactics.update(attack.mitre_attack_tactics)
if detection.datamodel:
datamodels.update(detection.datamodel)
JinjaWriter.writeObjectsList('doc_navigation.j2', os.path.join(output_path, '_data/navigation.yml'),
{
'attack_tactics': sorted(list(attack_tactics)),
'datamodels': sorted(list(datamodels)),
'categories': sorted(list(categories))
}
)
self.writeNavigationPageObjects(sorted(list(datamodels)), output_path)
self.writeNavigationPageObjects(sorted(list(attack_tactics)), output_path)
self.writeNavigationPageObjects(sorted(list(categories)), output_path)
JinjaWriter.writeObjectsList('doc_story_page.j2', os.path.join(output_path, '_pages/stories.md'), sorted(objects[0], key=lambda x: x.name))
self.writeObjectsMd(objects[0], os.path.join(output_path, '_stories'), 'doc_stories.j2')
JinjaWriter.writeObjectsList('doc_detection_page.j2', os.path.join(output_path, '_pages/detections.md'), sorted(objects[1], key=lambda x: x.name))
self.writeDetectionsMd(objects[1], os.path.join(output_path, '_posts'), 'doc_detections.j2')
JinjaWriter.writeObjectsList('doc_playbooks_page.j2', os.path.join(output_path, '_pages/paybooks.md'), sorted(objects[2], key=lambda x: x.name))
self.writeObjectsMd(objects[2], os.path.join(output_path, '_playbooks'), 'doc_playbooks.j2')
print("Done!")
def writeNavigationPageObjects(self, objects: list, output_path: str) -> None:
for obj in objects:
JinjaWriter.writeObject('doc_navigation_pages.j2', os.path.join(output_path, '_pages', obj.lower().replace(' ', '_') + '.md'),
{
'name': obj
}
)
def writeObjectsMd(self, objects, output_path: str, template_name: str) -> None:
for obj in objects:
progress_percent = ((self.index+1)/self.files_to_write) * 100
self.index+=1
if (sys.stdout.isatty() and sys.stdin.isatty() and sys.stderr.isatty()):
print(f"\r{'Docgen Progress'.rjust(23)}: [{progress_percent:3.0f}%]...", end="", flush=True)
JinjaWriter.writeObject(template_name, os.path.join(output_path, obj.name.lower().replace(' ', '_') + '.md'), obj)
def writeDetectionsMd(self, objects, output_path: str, template_name: str) -> None:
for obj in objects:
progress_percent = ((self.index+1)/self.files_to_write) * 100
self.index+=1
if (sys.stdout.isatty() and sys.stdin.isatty() and sys.stderr.isatty()):
print(f"\r{'Docgen Progress'.rjust(23)}: [{progress_percent:3.0f}%]...", end="", flush=True)
JinjaWriter.writeObject(template_name, os.path.join(output_path, obj.date + '-' + obj.name.lower().replace(' ', '_') + '.md'), obj)
@@ -1,33 +0,0 @@
import os
from bin.contentctl_project.contentctl_core.application.adapter.adapter import Adapter
from bin.contentctl_project.contentctl_core.domain.entities.enums.enums import SecurityContentType
from bin.contentctl_project.contentctl_infrastructure.adapter.jinja_writer import JinjaWriter
class ObjToSvgAdapter(Adapter):
def writeObjects(self, objects: list, output_path: str, type: SecurityContentType = None) -> None:
detections_tmp = objects
detection_without_test = 0
detections = []
obj = dict()
for detection in detections_tmp:
if not (detection.status == "deprecated"):
detections.append(detection)
if not detection.test and not (detection.status == "experimental"):
detection_without_test = detection_without_test + 1
obj['count'] = len(detections)
obj['coverage'] = (obj['count'] - detection_without_test)/obj['count']
obj['coverage'] = "{:.0%}".format(obj['coverage'])
JinjaWriter.writeObject('detection_count.j2', os.path.join(output_path, 'detection_count.svg'), obj)
JinjaWriter.writeObject('detection_coverage.j2', os.path.join(output_path, 'detection_coverage.svg'), obj)
@@ -1,180 +0,0 @@
import os
import re
from urllib.parse import urlparse
from bin.contentctl_project.contentctl_infrastructure.adapter.yml_writer import YmlWriter
from bin.contentctl_project.contentctl_core.application.adapter.adapter import Adapter
from bin.contentctl_project.contentctl_core.domain.entities.enums.enums import SecurityContentType
from bin.contentctl_project.contentctl_infrastructure.adapter.finding_report_writer import FindingReportObject
from bin.contentctl_project.contentctl_core.domain.entities.unit_test_old import UnitTestOld
class ObjToYmlAdapter(Adapter):
input_path: str
def __init__(self, input_path:str):
self.input_path = input_path
def writeObjectsInPlace(self, objects: list) -> None:
for object in objects:
file_path = object['file_path']
object.pop('file_path')
object.pop('deprecated')
object.pop('experimental')
YmlWriter.writeYmlFile(file_path, object)
def writeObjects(self, objects: list, output_path: str, type: SecurityContentType = None) -> None:
for obj in objects:
file_name = "ssa___" + self.convertNameToFileName(obj.name, obj.tags)
if self.isComplexBARule(obj.search):
file_path = os.path.join(output_path, 'complex', file_name)
else:
file_path = os.path.join(output_path, 'srs', file_name)
# add research object
RESEARCH_SITE_BASE = 'https://research.splunk.com/'
research_site_url = RESEARCH_SITE_BASE + obj.source + "/" + obj.id + "/"
obj.tags.research_site_url = research_site_url
# add ocsf schema tag
obj.tags.event_schema = 'ocsf'
body = FindingReportObject.writeFindingReport(obj)
if obj.test:
test_dict = {
"name": obj.name + " Unit Test",
"tests": [obj.test.dict()]
}
test_dict["tests"][0]["name"] = obj.name
for count in range(len(test_dict["tests"][0]["attack_data"])):
a = urlparse(test_dict["tests"][0]["attack_data"][count]["data"])
test_dict["tests"][0]["attack_data"][count]["file_name"] = os.path.basename(a.path)
test = UnitTestOld.parse_obj(test_dict)
obj.test = test
# create annotations object
obj.tags.annotations = {
"analytic_story": obj.tags.analytic_story,
"cis20": obj.tags.cis20,
"kill_chain_phases": obj.tags.kill_chain_phases,
"mitre_attack_id": obj.tags.mitre_attack_id,
"nist": obj.tags.nist
}
obj.runtime = "SPL2"
obj.internalVersion = 2
# remove unncessary fields
YmlWriter.writeYmlFile(file_path, obj.dict(
exclude_none=True,
include =
{
"name": True,
"id": True,
"eventSchema": True,
"version": True,
"status": True,
"description": True,
"search": True,
"how_to_implement": True,
"known_false_positives": True,
"references": True,
"runtime": True,
"internalVersion": True,
"tags":
{
#"analytic_story": True,
#"cis20" : True,
#"nist": True,
#"kill_chain_phases": True,
"annotations": True,
"mappings": True,
#"mitre_attack_id": True,
"risk_severity": True,
"risk_score": True,
"security_domain": True,
"required_fields": True,
"research_site_url": True,
"event_schema": True
},
"test":
{
"name": True,
"tests": {
'__all__':
{
"name": True,
"file": True,
"pass_condition": True,
"attack_data": {
'__all__':
{
"file_name": True,
"data": True,
"source": True
}
}
}
}
}
}
))
# Add Finding Report Object
with open(file_path, 'r') as file:
data = file.read().replace('--finding_report--', body)
f = open(file_path, "w")
f.write(data)
f.close()
def writeObjectNewContent(self, object: dict, type: SecurityContentType) -> None:
if type == SecurityContentType.detections:
file_path = os.path.join(self.input_path, 'detections', object['source'], self.convertNameToFileName(object['name'],object['tags']['product']))
object.pop('source')
elif type == SecurityContentType.stories:
file_path = os.path.join(self.input_path, 'stories', self.convertNameToFileName(object['name'],object['tags']['product']))
else:
raise(Exception(f"Object Must be Story or Detection, but is not: {object}"))
YmlWriter.writeYmlFile(file_path, object)
def convertNameToFileName(self, name: str, product: list):
file_name = name \
.replace(' ', '_') \
.replace('-','_') \
.replace('.','_') \
.replace('/','_') \
.lower()
if 'Splunk Behavioral Analytics' in product:
file_name = 'ssa___' + file_name + '.yml'
else:
file_name = file_name + '.yml'
return file_name
def convertNameToTestFileName(self, name: str, product: list):
file_name = name \
.replace(' ', '_') \
.replace('-','_') \
.replace('.','_') \
.replace('/','_') \
.lower()
if 'Splunk Behavioral Analytics' in product:
file_name = 'ssa___' + file_name + '.test.yml'
else:
file_name = file_name + '.test.yml'
return file_name
def isComplexBARule(self, search):
return re.findall("stats|first_time_event|adaptive_threshold", search)
@@ -1,22 +0,0 @@
### DETECTIONS ###
{% for detection in objects %}
{% if (detection.type == 'TTP' or detection.type == 'Anomaly' or detection.type == 'Hunting' or detection.type == 'Correlation') %}
[savedsearch://ESCU - {{ detection.name }} - Rule]
type = detection
asset_type = {{ detection.tags.asset_type }}
confidence = medium
explanation = {{ detection.description }}
{% if detection.how_to_implement is defined %}
how_to_implement = {{ detection.how_to_implement }}
{% else %}
how_to_implement = none
{% endif %}
annotations = {{ detection.mappings | tojson }}
known_false_positives = {{ detection.known_false_positives }}
providing_technologies = {{ detection.providing_technologies | tojson }}
{% endif %}
{% endfor %}
### END DETECTIONS ###
@@ -1,21 +0,0 @@
### RESPONSE TASKS ###
{% for detection in objects %}
{% if (detection.type == 'Investigation') %}
[savedsearch://ESCU - {{ detection.name }} - Response Task]
type = investigation
explanation = none
{% if detection.how_to_implement is defined %}
how_to_implement = {{ detection.how_to_implement }}
{% else %}
how_to_implement = none
{% endif %}
known_false_positives = not defined
earliest_time_offset = 14400
latest_time_offset = 0
{% endif %}
{% endfor %}
### END RESPONSE TASKS ###
@@ -1,21 +0,0 @@
### STORIES ###
{% for story in objects %}
[analytic_story://{{ story.name }}]
category = {{ story.tags.category[0] }}
last_updated = {{ story.date }}
version = {{ story.version }}
references = {{ story.references | tojson }}
maintainers = [{"company": "{{ story.author_company }}", "email": "-", "name": "{{ story.author_name }}"}]
spec_version = 3
searches = {{ (story.detection_names + story.investigation_names) | tojson }}
description = {{ story.description }}
{% if story.narrative is defined %}
narrative = {{ story.narrative }}
{% endif %}
{% endfor %}
### END STORIES ###
@@ -1,9 +0,0 @@
{% for lookup in objects %}
{% if lookup.collection is defined and lookup.collection != None %}
[{{ lookup.name }}]
enforceTypes = false
replicate = false
{% endif %}
{% endfor %}
@@ -1,18 +0,0 @@
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" width="105" height="20">
<linearGradient id="a" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
<stop offset="2" stop-opacity=".1"/>
</linearGradient>
<rect rx="3" width="65" height="20" fill="#555"/> <!-- Comment -->
<rect rx="3" x="65" width="40" height="20" fill="#4c1"/>
<path fill="#4c1" d="M63 0h4v20h-4z"/>
<rect rx="3" width="105" height="20" fill="url(#a)"/>
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
<text x="30" y="14">detections</text>
<text x="83" y="14">{{ object.count }}</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 670 B

@@ -1,18 +0,0 @@
<?xml version="1.0"?>
<svg xmlns="http://www.w3.org/2000/svg" width="100" height="20">
<linearGradient id="a" x2="0" y2="100%">
<stop offset="0" stop-color="#bbb" stop-opacity=".1"/>
<stop offset="2" stop-opacity=".1"/>
</linearGradient>
<rect rx="3" width="60" height="20" fill="#555"/> <!-- Comment -->
<rect rx="3" x="60" width="40" height="20" fill="#4c1"/>
<path fill="#4c1" d="M58 0h4v20h-4z"/>
<rect rx="3" width="100" height="20" fill="url(#a)"/>
<g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11">
<text x="30" y="14">coverage</text>
<text x="80" y="14">{{ object.coverage }}</text>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 671 B

Some files were not shown because too many files have changed in this diff Show More