Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ dist/*
apps*
test_results*
attack_data*

security_content/

# Byte-compiled / optimized / DLL files
__pycache__/
Expand Down
1 change: 1 addition & 0 deletions contentctl/actions/detection_testing/GitHubService.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ def get_all_content(self, director: DirectorOutputDto) -> DirectorOutputDto:
self.get_macros(director),
self.get_lookups(director),
[],
[]
)

def get_stories(self, director: DirectorOutputDto) -> list[Story]:
Expand Down
36 changes: 20 additions & 16 deletions contentctl/actions/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class GenerateInputDto:
class Generate:

def execute(self, input_dto: GenerateInputDto) -> DirectorOutputDto:
director_output_dto = DirectorOutputDto([],[],[],[],[],[],[],[])
director_output_dto = DirectorOutputDto([],[],[],[],[],[],[],[],[])
director = Director(director_output_dto)
director.execute(input_dto.director_input_dto)

Expand All @@ -34,25 +34,29 @@ def execute(self, input_dto: GenerateInputDto) -> DirectorOutputDto:
conf_output.writeObjects(director_output_dto.macros, SecurityContentType.macros)
conf_output.writeAppConf()
conf_output.packageApp()
conf_output.inspectApp()
#conf_output.inspectApp()

print(f'Generate of security content successful to {conf_output.output_path}')
return director_output_dto

elif input_dto.director_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/')
shutil.rmtree(input_dto.director_input_dto.config.build_ssa.output_path + '/srs/', ignore_errors=True)
shutil.rmtree(input_dto.director_input_dto.config.build_ssa.output_path + '/complex/', ignore_errors=True)
os.makedirs(input_dto.director_input_dto.config.build_ssa.output_path + '/complex/')
os.makedirs(input_dto.director_input_dto.config.build_ssa.output_path + '/srs/')
ba_yml_output = BAYmlOutput()
ba_yml_output.writeObjects(director_output_dto.detections, input_dto.output_path)
ba_yml_output.writeObjects(director_output_dto.ssa_detections, input_dto.director_input_dto.config.build_ssa.output_path)

elif input_dto.director_input_dto.product == SecurityContentProduct.API:
shutil.rmtree(input_dto.director_input_dto.config.build_api.output_path, ignore_errors=True)
os.makedirs(input_dto.director_input_dto.config.build_api.output_path)
api_json_output = ApiJsonOutput()
api_json_output.writeObjects(director_output_dto.detections, input_dto.output_path, SecurityContentType.detections)
api_json_output.writeObjects(director_output_dto.stories, input_dto.output_path, SecurityContentType.stories)
api_json_output.writeObjects(director_output_dto.baselines, input_dto.output_path, SecurityContentType.baselines)
api_json_output.writeObjects(director_output_dto.investigations, input_dto.output_path, SecurityContentType.investigations)
api_json_output.writeObjects(director_output_dto.lookups, input_dto.output_path, SecurityContentType.lookups)
api_json_output.writeObjects(director_output_dto.macros, input_dto.output_path, SecurityContentType.macros)
api_json_output.writeObjects(director_output_dto.deployments, input_dto.output_path, SecurityContentType.deployments)

print(f'Generate of security content successful to {conf_output.output_path}')
api_json_output.writeObjects(director_output_dto.detections, input_dto.director_input_dto.config.build_api.output_path, SecurityContentType.detections)
api_json_output.writeObjects(director_output_dto.stories, input_dto.director_input_dto.config.build_api.output_path, SecurityContentType.stories)
api_json_output.writeObjects(director_output_dto.baselines, input_dto.director_input_dto.config.build_api.output_path, SecurityContentType.baselines)
api_json_output.writeObjects(director_output_dto.investigations, input_dto.director_input_dto.config.build_api.output_path, SecurityContentType.investigations)
api_json_output.writeObjects(director_output_dto.lookups, input_dto.director_input_dto.config.build_api.output_path, SecurityContentType.lookups)
api_json_output.writeObjects(director_output_dto.macros, input_dto.director_input_dto.config.build_api.output_path, SecurityContentType.macros)
api_json_output.writeObjects(director_output_dto.deployments, input_dto.director_input_dto.config.build_api.output_path, SecurityContentType.deployments)

return director_output_dto
7 changes: 6 additions & 1 deletion contentctl/actions/initialize.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def execute(self, input_dto: InitializeInputDto) -> None:
YmlWriter.writeYmlFile(os.path.join(input_dto.path, 'contentctl_test.yml'), dict(obj))


folders = ['detections', 'stories', 'lookups', 'macros', 'baselines', 'dist', 'docs', 'reporting']
folders = ['detections', 'stories', 'lookups', 'macros', 'baselines', 'dist', 'docs', 'reporting', 'investigations']
for folder in folders:
os.makedirs(os.path.join(input_dto.path, folder))

Expand All @@ -53,6 +53,11 @@ def execute(self, input_dto: InitializeInputDto) -> None:
dest_path/detection_name)


shutil.copytree(
os.path.join(os.path.dirname(__file__), '../templates/deployments'),
os.path.join(input_dto.path, 'deployments')
)

shutil.copyfile(
os.path.join(os.path.dirname(__file__), '../templates/stories/cobalt_strike.yml'),
os.path.join(input_dto.path, 'stories', 'cobalt_strike.yml')
Expand Down
29 changes: 12 additions & 17 deletions contentctl/actions/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class ValidateInputDto:

class Validate:
def execute(self, input_dto: ValidateInputDto) -> None:
director_output_dto = DirectorOutputDto([], [], [], [], [], [], [], [])
director_output_dto = DirectorOutputDto([], [], [], [], [], [], [], [], [])
director = Director(director_output_dto)
director.execute(input_dto.director_input_dto)

Expand All @@ -35,11 +35,6 @@ def execute(self, input_dto: ValidateInputDto) -> None:
)
self.validate_duplicate_uuids(security_content_objects)

# validate tests
self.validate_detection_exist_for_test(
director_output_dto.tests, director_output_dto.detections
)

except ValueError as e:
print(e)
sys.exit(1)
Expand Down Expand Up @@ -73,14 +68,14 @@ def validate_duplicate_uuids(self, security_content_objects):
+ "\n".join([obj.name for obj in content_with_duplicate_uuid])
)

def validate_detection_exist_for_test(self, tests: list, detections: list):
for test in tests:
found_detection = False
for detection in detections:
if test.tests[0].file in detection.file_path:
found_detection = True

if not found_detection:
raise ValueError(
"ERROR: detection doesn't exist for test file: " + test.name
)
# def validate_detection_exist_for_test(self, tests: list, detections: list):
# for test in tests:
# found_detection = False
# for detection in detections:
# if test.tests[0].file in detection.file_path:
# found_detection = True

# if not found_detection:
# raise ValueError(
# "ERROR: detection doesn't exist for test file: " + test.name
# )
78 changes: 61 additions & 17 deletions contentctl/contentctl.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,19 @@ def initialize(args) -> None:
def build(args, config:Union[Config,None]=None) -> DirectorOutputDto:
if config == None:
config = start(args)
product_type = SecurityContentProduct.SPLUNK_APP
if args.type == "app":
product_type = SecurityContentProduct.SPLUNK_APP
elif args.type == "ssa":
product_type = SecurityContentProduct.SSA
elif args.type == "api":
product_type = SecurityContentProduct.API
else:
print(f"Invalid build type. Valid options app, ssa or api")
sys.exit(1)
director_input_dto = DirectorInputDto(
input_path=os.path.abspath(args.path), product=product_type, config=config
input_path=os.path.abspath(args.path),
product=product_type,
config=config
)
generate_input_dto = GenerateInputDto(director_input_dto)

Expand Down Expand Up @@ -158,7 +168,7 @@ def test(args: argparse.Namespace):
title=config.build.name,
release=config.build.version,
http_path=None,
local_path=str(pathlib.Path(config.build.path_root)/f"{config.build.name}.tar.gz"),
local_path=str(pathlib.Path(config.build.path_root)/f"{config.build.name}-{config.build.version}.tar.gz"),
description=config.build.description,
splunkbase_path=None,
force_local=True
Expand All @@ -180,27 +190,37 @@ def test(args: argparse.Namespace):

test = Test()

try:
#try:

result = test.execute(test_input_dto)
# This return code is important. Even if testing
# fully completes, if everything does not pass then
# we want to return a nonzero status code
if result:
sys.exit(0)
else:
sys.exit(1)

except Exception as e:
print(f"Error running contentctl test: {str(e)}")
result = test.execute(test_input_dto)
# This return code is important. Even if testing
# fully completes, if everything does not pass then
# we want to return a nonzero status code
if result:
sys.exit(0)
else:
sys.exit(1)

# except Exception as e:
# print(f"Error running contentctl test: {str(e)}")
# sys.exit(1)


def validate(args) -> None:
config = start(args)
product_type = SecurityContentProduct.SPLUNK_APP
if args.type == "app":
product_type = SecurityContentProduct.SPLUNK_APP
elif args.type == "ssa":
product_type = SecurityContentProduct.SSA
elif args.type == "api":
product_type = SecurityContentProduct.API
else:
print(f"Invalid build type. Valid options app, ssa or api")
sys.exit(1)
director_input_dto = DirectorInputDto(
input_path=pathlib.Path(args.path), product=product_type, config=config
input_path=pathlib.Path(args.path),
product=product_type,
config=config
)
validate_input_dto = ValidateInputDto(director_input_dto=director_input_dto)
validate = Validate()
Expand Down Expand Up @@ -313,8 +333,24 @@ def main():
"and on detection that will fail 'contentctl test'. This is useful "
"for demonstrating contentctl functionality.")

validate_parser.add_argument(
"-t",
"--type",
required=False,
type=str,
default="app",
help="Type of package: app, ssa or api"
)
validate_parser.set_defaults(func=validate)

build_parser.add_argument(
"-t",
"--type",
required=False,
type=str,
default="app",
help="Type of package: app, ssa or api"
)
build_parser.set_defaults(func=build)

docs_parser.set_defaults(func=doc_gen)
Expand All @@ -334,6 +370,14 @@ def main():

api_deploy_parser.set_defaults(func=api_deploy)

test_parser.add_argument(
"-t",
"--type",
required=False,
type=str,
default="app",
help="Type of package: app, ssa or api"
)
test_parser.add_argument(
"--mode",
required=False,
Expand Down
134 changes: 134 additions & 0 deletions contentctl/helper/constants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
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"
}
Loading