mirror of
https://github.com/splunk/security_content
synced 2026-06-08 17:32:49 +00:00
416 lines
21 KiB
Python
416 lines
21 KiB
Python
import sys
|
|
import argparse
|
|
import os
|
|
|
|
from bin.contentctl_project.contentctl_core.domain.entities.link_validator import LinkValidator
|
|
|
|
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), 'bin/contentctl_project')))
|
|
|
|
from bin.contentctl_project.contentctl_core.application.use_cases.content_changer import ContentChanger, ContentChangerInputDto
|
|
from bin.contentctl_project.contentctl_core.application.use_cases.generate import GenerateInputDto, Generate
|
|
from bin.contentctl_project.contentctl_core.application.use_cases.validate import ValidateInputDto, Validate
|
|
from bin.contentctl_project.contentctl_core.application.use_cases.doc_gen import DocGenInputDto, DocGen
|
|
from bin.contentctl_project.contentctl_core.application.use_cases.new_content import NewContentInputDto, NewContent, NewAttackDataContent
|
|
from bin.contentctl_project.contentctl_core.application.use_cases.reporting import ReportingInputDto, Reporting
|
|
from bin.contentctl_project.contentctl_core.application.use_cases.initialize import Initialize
|
|
from bin.contentctl_project.contentctl_core.application.use_cases.deploy import Deploy
|
|
from bin.contentctl_project.contentctl_core.application.use_cases.build import Build
|
|
from bin.contentctl_project.contentctl_core.application.use_cases.inspect import Inspect
|
|
from bin.contentctl_project.contentctl_core.application.factory.factory import FactoryInputDto
|
|
from bin.contentctl_project.contentctl_core.application.factory.ba_factory import BAFactoryInputDto
|
|
from bin.contentctl_project.contentctl_core.application.factory.new_content_factory import NewContentFactoryInputDto
|
|
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.adapter.obj_to_json_adapter import ObjToJsonAdapter
|
|
from bin.contentctl_project.contentctl_infrastructure.builder.security_content_story_builder import SecurityContentStoryBuilder
|
|
from bin.contentctl_project.contentctl_infrastructure.builder.security_content_detection_builder import SecurityContentDetectionBuilder
|
|
from bin.contentctl_project.contentctl_infrastructure.builder.security_content_basic_builder import SecurityContentBasicBuilder
|
|
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.security_content_playbook_builder import SecurityContentPlaybookBuilder
|
|
from bin.contentctl_project.contentctl_core.domain.entities.enums.enums import SecurityContentProduct
|
|
from bin.contentctl_project.contentctl_infrastructure.adapter.obj_to_conf_adapter import ObjToConfAdapter
|
|
from bin.contentctl_project.contentctl_infrastructure.adapter.obj_to_md_adapter import ObjToMdAdapter
|
|
from bin.contentctl_project.contentctl_infrastructure.adapter.obj_to_svg_adapter import ObjToSvgAdapter
|
|
from bin.contentctl_project.contentctl_infrastructure.adapter.obj_to_attack_nav_adapter import ObjToAttackNavAdapter
|
|
from bin.contentctl_project.contentctl_infrastructure.builder.attack_enrichment import AttackEnrichment
|
|
from bin.contentctl_project.contentctl_core.domain.entities.enums.enums import SecurityContentType
|
|
from bin.contentctl_project.contentctl_infrastructure.adapter.obj_to_attackdata_yml_adapter import ObjToAttackDataYmlAdapter
|
|
|
|
def init():
|
|
|
|
print("""
|
|
Running Splunk Security Content Control Tool (contentctl)
|
|
starting program loaded for TIE Fighter...
|
|
_ _
|
|
T T T T
|
|
| | | |
|
|
| | | |
|
|
| | | |
|
|
| | | |
|
|
| | | |
|
|
| | | |
|
|
| | ____ | |
|
|
| | ___.r-"`--'"-r.____ | |
|
|
| |.-._,.,---~"_/_/ .----. \_\_"~---,.,_,-.| |
|
|
| ]|.[_]_ T~T[_.-Y / \ / \ Y-._]T~T _[_].|| |
|
|
[|-+[ ___]| [__ |-=[--()--]=-| __] |[___ ]+-|]
|
|
| ]|"[_] l_j[_"-l \ / \ / !-"_]l_j [_]~|| |
|
|
| |`-' "~"---.,_\\"\ "o--o" /"/_,.---"~" `-'| |
|
|
| | ~~"^-.____.-^"~~ | |
|
|
| | | |
|
|
| | | |
|
|
| | | |
|
|
| | | |
|
|
| | | |
|
|
| | | |
|
|
| | | |
|
|
l_i l_j -Row
|
|
|
|
""")
|
|
|
|
|
|
def content_changer(args) -> None:
|
|
factory_input_dto = ObjectFactoryInputDto(
|
|
os.path.abspath(args.path),
|
|
SecurityContentObjectBuilder(),
|
|
SecurityContentDirector()
|
|
)
|
|
|
|
input_dto = ContentChangerInputDto(
|
|
ObjToYmlAdapter(args.path),
|
|
factory_input_dto,
|
|
args.change_function
|
|
)
|
|
|
|
content_changer = ContentChanger()
|
|
content_changer.execute(input_dto)
|
|
|
|
|
|
def generate(args) -> None:
|
|
if not args.product:
|
|
print("ERROR: missing parameter -p/--product .")
|
|
sys.exit(1)
|
|
|
|
#For now, the custom product is treated just like ESCU
|
|
if args.product == 'CUSTOM':
|
|
args.product = 'ESCU'
|
|
|
|
if args.product not in ['ESCU', 'SSA', 'API']:
|
|
print("ERROR: invalid product. valid products are ESCU, SSA or API. If you are building a custom app, use CUSTOM.")
|
|
sys.exit(1)
|
|
|
|
|
|
if args.cached_and_offline:
|
|
LinkValidator.initialize_cache(args.cached_and_offline)
|
|
|
|
#Save runtime by only generating the required factory inputs
|
|
factory_input_dto = None
|
|
ba_factory_input_dto = None
|
|
if args.product in ["ESCU", "API"]:
|
|
factory_input_dto = FactoryInputDto(
|
|
os.path.abspath(args.path),
|
|
SecurityContentBasicBuilder(),
|
|
SecurityContentDetectionBuilder(force_cached_or_offline=args.cached_and_offline, skip_enrichment=args.skip_enrichment),
|
|
SecurityContentStoryBuilder(),
|
|
SecurityContentBaselineBuilder(),
|
|
SecurityContentInvestigationBuilder(),
|
|
SecurityContentPlaybookBuilder(input_path=args.path),
|
|
SecurityContentDirector(),
|
|
AttackEnrichment.get_attack_lookup(args.path, force_cached_or_offline=args.cached_and_offline, skip_enrichment=args.skip_enrichment)
|
|
)
|
|
if args.product in ["SSA", "API"]:
|
|
ba_factory_input_dto = BAFactoryInputDto(
|
|
os.path.abspath(args.path),
|
|
SecurityContentBasicBuilder(),
|
|
SecurityContentDetectionBuilder(force_cached_or_offline = args.cached_and_offline, skip_enrichment=args.skip_enrichment),
|
|
SecurityContentDirector()
|
|
)
|
|
|
|
|
|
if args.product == "ESCU":
|
|
generate_input_dto = GenerateInputDto(
|
|
os.path.abspath(args.output),
|
|
factory_input_dto,
|
|
ba_factory_input_dto,
|
|
ObjToConfAdapter(args.path),
|
|
SecurityContentProduct.ESCU
|
|
)
|
|
elif args.product == "API":
|
|
generate_input_dto = GenerateInputDto(
|
|
os.path.abspath(args.output),
|
|
factory_input_dto,
|
|
ba_factory_input_dto,
|
|
ObjToJsonAdapter(),
|
|
SecurityContentProduct.API
|
|
)
|
|
else:
|
|
print("making dto")
|
|
generate_input_dto = GenerateInputDto(
|
|
os.path.abspath(args.output),
|
|
factory_input_dto,
|
|
ba_factory_input_dto,
|
|
ObjToYmlAdapter(args.path),
|
|
SecurityContentProduct.SSA
|
|
)
|
|
generate = Generate()
|
|
generate.execute(generate_input_dto)
|
|
|
|
if args.cached_and_offline:
|
|
LinkValidator.close_cache()
|
|
|
|
def validate(args) -> None:
|
|
if not args.product:
|
|
print("ERROR: missing parameter -p/--product .")
|
|
sys.exit(1)
|
|
|
|
#For now, the custom product is treated just like ESCU
|
|
if args.product == 'CUSTOM':
|
|
args.product = 'ESCU'
|
|
|
|
if args.product not in ['ESCU', 'SSA', 'all']:
|
|
print("ERROR: invalid product. valid products are all, ESCU or SSA. If you are building a custom app, use CUSTOM.")
|
|
sys.exit(1)
|
|
|
|
|
|
if args.cached_and_offline:
|
|
LinkValidator.initialize_cache(args.cached_and_offline)
|
|
|
|
#Save runtime by only generating the required factory inputs
|
|
factory_input_dto = None
|
|
ba_factory_input_dto = None
|
|
if args.product in ["ESCU", "all"]:
|
|
factory_input_dto = FactoryInputDto(
|
|
os.path.abspath(args.path),
|
|
SecurityContentBasicBuilder(),
|
|
SecurityContentDetectionBuilder(force_cached_or_offline=args.cached_and_offline, check_references=args.check_references, skip_enrichment=args.skip_enrichment),
|
|
SecurityContentStoryBuilder(check_references=args.check_references),
|
|
SecurityContentBaselineBuilder(check_references=args.check_references),
|
|
SecurityContentInvestigationBuilder(check_references=args.check_references),
|
|
SecurityContentPlaybookBuilder(input_path=args.path, check_references=args.check_references),
|
|
SecurityContentDirector(),
|
|
AttackEnrichment.get_attack_lookup(args.path, force_cached_or_offline=args.cached_and_offline, skip_enrichment=args.skip_enrichment)
|
|
)
|
|
if args.product in ["SSA", "all"]:
|
|
ba_factory_input_dto = BAFactoryInputDto(
|
|
os.path.abspath(args.path),
|
|
SecurityContentBasicBuilder(),
|
|
SecurityContentDetectionBuilder(force_cached_or_offline = args.cached_and_offline, check_references=args.check_references, skip_enrichment=args.skip_enrichment),
|
|
SecurityContentDirector()
|
|
)
|
|
|
|
if args.product == "ESCU" or args.product == "all":
|
|
validate_input_dto = ValidateInputDto(
|
|
factory_input_dto,
|
|
ba_factory_input_dto,
|
|
SecurityContentProduct.ESCU
|
|
)
|
|
validate = Validate()
|
|
validate.execute(validate_input_dto)
|
|
|
|
if args.product == "SSA" or args.product == "all":
|
|
validate_input_dto = ValidateInputDto(
|
|
factory_input_dto,
|
|
ba_factory_input_dto,
|
|
SecurityContentProduct.SSA
|
|
)
|
|
validate = Validate()
|
|
validate.execute(validate_input_dto)
|
|
|
|
if args.cached_and_offline:
|
|
LinkValidator.close_cache()
|
|
|
|
|
|
def doc_gen(args) -> None:
|
|
factory_input_dto = FactoryInputDto(
|
|
os.path.abspath(args.path),
|
|
SecurityContentBasicBuilder(),
|
|
SecurityContentDetectionBuilder(force_cached_or_offline=args.cached_and_offline, skip_enrichment=args.skip_enrichment),
|
|
SecurityContentStoryBuilder(),
|
|
SecurityContentBaselineBuilder(),
|
|
SecurityContentInvestigationBuilder(),
|
|
SecurityContentPlaybookBuilder(input_path=args.path),
|
|
SecurityContentDirector(),
|
|
AttackEnrichment.get_attack_lookup(args.path, force_cached_or_offline=args.cached_and_offline, skip_enrichment=args.skip_enrichment)
|
|
)
|
|
|
|
doc_gen_input_dto = DocGenInputDto(
|
|
os.path.abspath(args.output),
|
|
factory_input_dto,
|
|
ObjToMdAdapter()
|
|
)
|
|
|
|
doc_gen = DocGen()
|
|
doc_gen.execute(doc_gen_input_dto)
|
|
|
|
|
|
def new_content(args) -> None:
|
|
if args.type == 'detection':
|
|
contentType = SecurityContentType.detections
|
|
elif args.type == 'story':
|
|
contentType = SecurityContentType.stories
|
|
elif args.type == 'attack_data':
|
|
contentType = SecurityContentType.attack_data
|
|
else:
|
|
print("ERROR: type " + args.type + " not supported")
|
|
sys.exit(1)
|
|
|
|
|
|
new_content_factory_input_dto = NewContentFactoryInputDto(contentType)
|
|
if args.type == 'attack_data':
|
|
new_content_input_dto = NewContentInputDto(new_content_factory_input_dto, ObjToAttackDataYmlAdapter())
|
|
new_content = NewAttackDataContent()
|
|
else:
|
|
new_content_input_dto = NewContentInputDto(new_content_factory_input_dto, ObjToYmlAdapter(args.path))
|
|
new_content = NewContent()
|
|
new_content.execute(new_content_input_dto)
|
|
|
|
|
|
def reporting(args) -> None:
|
|
factory_input_dto = FactoryInputDto(
|
|
os.path.abspath(args.path),
|
|
SecurityContentBasicBuilder(),
|
|
SecurityContentDetectionBuilder(force_cached_or_offline=args.cached_and_offline, skip_enrichment=args.skip_enrichment),
|
|
SecurityContentStoryBuilder(),
|
|
SecurityContentBaselineBuilder(),
|
|
SecurityContentInvestigationBuilder(),
|
|
SecurityContentPlaybookBuilder(input_path=args.path),
|
|
SecurityContentDirector(),
|
|
AttackEnrichment.get_attack_lookup(args.path, force_cached_or_offline=args.cached_and_offline, skip_enrichment=args.skip_enrichment)
|
|
)
|
|
|
|
reporting_input_dto = ReportingInputDto(
|
|
factory_input_dto,
|
|
ObjToSvgAdapter(),
|
|
ObjToAttackNavAdapter()
|
|
)
|
|
|
|
reporting = Reporting()
|
|
reporting.execute(reporting_input_dto)
|
|
|
|
|
|
def initialize(args) -> None:
|
|
Initialize(args)
|
|
|
|
|
|
def build(args) -> None:
|
|
Build(args)
|
|
|
|
def inspect(args) -> None:
|
|
Inspect(args)
|
|
|
|
def cloud_deploy(args) -> None:
|
|
Deploy(args)
|
|
|
|
def main(args):
|
|
|
|
init()
|
|
|
|
# grab arguments
|
|
parser = argparse.ArgumentParser(
|
|
description="Use `contentctl.py action -h` to get help with any Splunk Security Content action")
|
|
parser.add_argument("-p", "--path", required=True,
|
|
help="path to the Splunk Security Content folder",)
|
|
parser.add_argument("--cached_and_offline", action=argparse.BooleanOptionalAction,
|
|
help="Force cached/offline resources. While this makes execution much faster, it may result in enrichment which is out of date. This is suitable for use only in development or disconnected environments.")
|
|
parser.add_argument("--skip_enrichment", action=argparse.BooleanOptionalAction,
|
|
help="Skip enrichment of CVEs. This can significantly decrease the amount of time needed to run content_ctl.")
|
|
|
|
parser.set_defaults(cached_and_offline=False, func=lambda _: parser.print_help())
|
|
|
|
actions_parser = parser.add_subparsers(title="Splunk Security Content actions", dest="action")
|
|
#new_parser = actions_parser.add_parser("new", help="Create new content (detection, story, baseline)")
|
|
init_parser = actions_parser.add_parser("init", help="Initialize a repo with scaffolding in place to build a custom app."
|
|
"This allows a user to easily add their own content and, eventually, "
|
|
"build a custom application consisting of their custom content.")
|
|
new_content_parser = actions_parser.add_parser("new_content", help="Create new security content object")
|
|
content_changer_parser = actions_parser.add_parser("content_changer", help="Change Security Content based on defined rules")
|
|
validate_parser = actions_parser.add_parser("validate", help="Validates written content")
|
|
generate_parser = actions_parser.add_parser("generate", help="Generates a deployment package for different platforms (splunk_app)")
|
|
docgen_parser = actions_parser.add_parser("docgen", help="Generates documentation")
|
|
|
|
reporting_parser = actions_parser.add_parser("reporting", help="Create security content reporting")
|
|
|
|
|
|
build_parser = actions_parser.add_parser("build", help="Build an application suitable for deployment to a search head")
|
|
inspect_parser = actions_parser.add_parser("inspect", help="Run appinspect to ensure that an app meets minimum requirements for deployment.")
|
|
cloud_deploy_parser = actions_parser.add_parser("cloud_deploy", help="Install an application on a target Splunk Cloud Instance.")
|
|
|
|
|
|
# # new arguments
|
|
# new_parser.add_argument("-t", "--type", required=False, type=str, default="detection",
|
|
# help="Type of new content to create, please choose between `detection`, `baseline` or `story`. Defaults to `detection`")
|
|
# new_parser.add_argument("-x", "--example_only", required=False, action='store_true',
|
|
# help="Generates an example content UPDATE on the fields that need updating. Use `git status` to see what specific files are added. Skips new content wizard prompts.")
|
|
# new_parser.set_defaults(func=new)
|
|
|
|
validate_parser.add_argument("-pr", "--product", required=True, type=str, default='all',
|
|
help="Type of package to create, choose between all, `ESCU` or `SSA`.")
|
|
validate_parser.add_argument('--check_references', action=argparse.BooleanOptionalAction, help="The number of threads to use to resolve references. "
|
|
"Larger numbers will result in faster resolution, but will be more likely to hit rate limits or use a large amount of "
|
|
"bandwidth. A larger number of threads is particularly useful on high-bandwidth connections, but does not improve "
|
|
"performance on slow connections.")
|
|
|
|
validate_parser.set_defaults(func=validate, check_references=False, epilog="""
|
|
Validates security manifest for correctness, adhering to spec and other common items.""")
|
|
|
|
generate_parser.add_argument("-o", "--output", required=True, type=str,
|
|
help="Path where to store the deployment package")
|
|
generate_parser.add_argument("-pr", "--product", required=True, type=str,
|
|
help="Type of package to create, choose between `ESCU`, `SSA` or `API`.")
|
|
generate_parser.set_defaults(func=generate)
|
|
|
|
content_changer_choices = ContentChanger.enumerate_content_changer_functions()
|
|
content_changer_parser.add_argument("-cf", "--change_function", required=True, metavar='{ ' + ', '.join(content_changer_choices) +' }' , type=str, choices=content_changer_choices,
|
|
help= "Choose from the functions above defined in \nbin/contentctl_core/contentctl/application/use_cases/content_changer.py")
|
|
|
|
content_changer_parser.set_defaults(func=content_changer)
|
|
|
|
docgen_parser.add_argument("-o", "--output", required=True, type=str,
|
|
help="Path where to store the documentation")
|
|
docgen_parser.set_defaults(func=doc_gen)
|
|
|
|
new_content_parser.add_argument("-t", "--type", required=True, type=str,
|
|
help="Type of security content object, choose between `detection`, `story`")
|
|
new_content_parser.set_defaults(func=new_content)
|
|
|
|
reporting_parser.set_defaults(func=reporting)
|
|
|
|
|
|
init_parser.add_argument("-t", "--title", type=str, required=True, help="The title of the application to be built.")
|
|
init_parser.add_argument("-n", "--name", type=str, required=True, help="The name of the application to be built.")
|
|
init_parser.add_argument("-v", "--version", type=str, required=True, help="The version of the application to be built. It should be in MAJOR.MINOR.PATCH format.")
|
|
init_parser.add_argument("-a", "--author_name", type=str, required=True, help="The name of the application author.")
|
|
init_parser.add_argument("-e", "--author_email", type=str, required=True, help="The email of the application author.")
|
|
init_parser.add_argument("-c", "--author_company", type=str, required=True, help="The company of the application author.")
|
|
init_parser.add_argument("-d", "--description", type=str, required=True, help="A brief description of the app.")
|
|
init_parser.set_defaults(func=initialize)
|
|
|
|
build_parser.add_argument("-o", "--output_dir", required=False, default="build", type=str, help="Directory to output the built package to (default is 'build')")
|
|
build_parser.add_argument("-pr", "--product", required=True, type=str, help="Name of the product to build. This is the name you created during init. To find the name of your app, look for the name of the folder created in the ./dist folder.")
|
|
build_parser.set_defaults(func=build)
|
|
|
|
|
|
inspect_parser.add_argument("-p", "--package_path", required=True, type=str, help="Path to the package to be inspected")
|
|
inspect_parser.set_defaults(func=inspect)
|
|
|
|
|
|
cloud_deploy_parser.add_argument("--app-package", required=True, type=str, help="Path to the package you wish to deploy")
|
|
cloud_deploy_parser.add_argument("--acs-legal-ack", required=True, type=str, help="specify '--acs-legal-ack=Y' to acknowledge your acceptance of any risks (required)")
|
|
cloud_deploy_parser.add_argument("--username", required=True, type=str, help="splunk.com username")
|
|
cloud_deploy_parser.add_argument("--password", required=True, type=str, help="splunk.com password")
|
|
cloud_deploy_parser.add_argument("--server", required=False, default="https://admin.splunk.com", type=str, help="Override server URL (default 'https://admin.splunk.com')")
|
|
cloud_deploy_parser.set_defaults(func=cloud_deploy)
|
|
|
|
# # parse them
|
|
args = parser.parse_args()
|
|
try:
|
|
return args.func(args)
|
|
except Exception as e:
|
|
print(f"Error for function [{args.func.__name__}]: {str(e)}")
|
|
|
|
if __name__ == "__main__":
|
|
main(sys.argv[1:])
|