mirror of
https://github.com/splunk/security_content
synced 2026-06-08 17:32:49 +00:00
Branch was auto-updated.
This commit is contained in:
@@ -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.
|
||||
@@ -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())
|
||||
-268
@@ -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)
|
||||
|
||||
-25
@@ -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
|
||||
-88
@@ -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
|
||||
-88
@@ -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
|
||||
-35
@@ -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'
|
||||
-68
@@ -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
|
||||
-88
@@ -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
|
||||
-5
@@ -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
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
definition: summariesonly=false allow_old_summaries=true
|
||||
description: search data model's summaries only
|
||||
name: security_content_summariesonly
|
||||
-4
@@ -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
|
||||
-48
@@ -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
|
||||
-37
@@ -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—gathering credentials from a target system, often
|
||||
hashed or encrypted—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
|
||||
-20
@@ -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
|
||||
-23
@@ -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
|
||||
-88
@@ -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
|
||||
-89
@@ -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
|
||||
-116
@@ -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(">",">")
|
||||
investigation.search = investigation.search.replace("<","<")
|
||||
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)
|
||||
|
||||
|
||||
-22
@@ -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 ###
|
||||
-21
@@ -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 ###
|
||||
-21
@@ -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 |
-18
@@ -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
Reference in New Issue
Block a user