From 8cc82a9d0d0d0b9284fd5bdf32804cf22ff8e3bd Mon Sep 17 00:00:00 2001 From: pyth0n1c <87383215+pyth0n1c@users.noreply.github.com> Date: Fri, 22 Apr 2022 11:59:10 -0700 Subject: [PATCH 01/31] Added modes and stub functions to the contentctl.py as an entry point for cleaning and deploying. --- contentctl.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/contentctl.py b/contentctl.py index f0e4026e8f..0a1be4e4af 100644 --- a/contentctl.py +++ b/contentctl.py @@ -249,6 +249,12 @@ def reporting(args) -> None: reporting.execute(reporting_input_dto) +def clean(args) -> None: + pass + +def deploy(args) -> None: + pass + def main(args): init() @@ -268,6 +274,10 @@ def main(args): docgen_parser = actions_parser.add_parser("docgen", help="Generates documentation") new_content_parser = actions_parser.add_parser("new_content", help="Create new security content object") reporting_parser = actions_parser.add_parser("reporting", help="Create security content reporting") + clean_parser = actions_parser.add_parser("clean", help="Remove all content from Security Content. " + "This allows a user to easily add their own content and, eventually, " + "build a custom application consisting of their custom content.") + deploy_parser = actions_parser.add_parser("deploy", help="Install an application on a target Splunk Search Head.") # # new arguments # new_parser.add_argument("-t", "--type", required=False, type=str, default="detection", @@ -301,6 +311,13 @@ def main(args): reporting_parser.set_defaults(func=reporting) + clean_parser.set_defaults(func=clean) + + deploy_parser.add_argument("-a", "--search_head_address", required=True, type=str, help="The address of the Splunk Search Head to deploy the application to.") + deploy_parser.add_argument("-u", "--username", required=True, type=str, help="Username for Splunk Search Head. Note that this user MUST be able to install applications.") + deploy_parser.add_argument("-p", "--password", required=True, type=str, help="Password for Splunk Search Head.") + deploy_parser.set_defaults(func=deploy) + # # parse them args = parser.parse_args() return args.func(args) From 804123257e9326975d10913ca00c7709ac570021 Mon Sep 17 00:00:00 2001 From: pyth0n1c <87383215+pyth0n1c@users.noreply.github.com> Date: Fri, 22 Apr 2022 12:04:43 -0700 Subject: [PATCH 02/31] Small change - renamed a caraible from type to content_type. It is better not to overwrite python builtins. --- contentctl.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/contentctl.py b/contentctl.py index 0a1be4e4af..4e6be2b4a0 100644 --- a/contentctl.py +++ b/contentctl.py @@ -213,14 +213,14 @@ def doc_gen(args) -> None: def new_content(args) -> None: if args.type == 'detection': - type = SecurityContentType.detections + content_type = SecurityContentType.detections elif args.type == 'story': - type = SecurityContentType.stories + content_type = SecurityContentType.stories else: print("ERROR: type " + args.type + " not supported") sys.exit(1) - new_content_factory_input_dto = NewContentFactoryInputDto(type) + new_content_factory_input_dto = NewContentFactoryInputDto(content_type) new_content_input_dto = NewContentInputDto(new_content_factory_input_dto, ObjToYmlAdapter()) new_content = NewContent() new_content.execute(new_content_input_dto) From 760242e45bd5b24dd116c8065e29d151ba6aefe9 Mon Sep 17 00:00:00 2001 From: pyth0n1c <87383215+pyth0n1c@users.noreply.github.com> Date: Fri, 22 Apr 2022 13:25:53 -0700 Subject: [PATCH 03/31] Included basic implementation of clean, which is yet to be tested. --- .../application/use_cases/clean.py | 97 +++++++++++++++++++ .../application/use_cases/deploy.py | 4 + contentctl.py | 2 + 3 files changed, 103 insertions(+) create mode 100644 bin/contentctl_project/contentctl_core/application/use_cases/clean.py create mode 100644 bin/contentctl_project/contentctl_core/application/use_cases/deploy.py diff --git a/bin/contentctl_project/contentctl_core/application/use_cases/clean.py b/bin/contentctl_project/contentctl_core/application/use_cases/clean.py new file mode 100644 index 0000000000..e648fc4ce4 --- /dev/null +++ b/bin/contentctl_project/contentctl_core/application/use_cases/clean.py @@ -0,0 +1,97 @@ +import re +import glob +import os + +class Clean: + def __init__(self, args): + pass + + def remove_all_content(self)-> bool: + errors = [] + + steps = [(self.remove_detections,"Removing Detections"), + (self.remove_investigations,"Removing Investigations"), + (self.remove_lookups,"Removing Lookups"), + (self.remove_macros,"Removing Macros"), + (self.remove_notebooks,"Removing Notebooks"), + (self.remove_playbooks,"Removing Playbooks"), + (self.remove_stories,"Removing Stores"), + (self.remove_tests,"Removing Tests")] + + 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:\n\t{'\n\t'.join(errors)}") + return False + + 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"], 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]=["tests/**/*.yml"], 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) + for filename in matched_filenames: + 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}") + 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) + return True + except Exception as e: + print(f"Error deleting file {filename}: {str(e)}") + return False + + + diff --git a/bin/contentctl_project/contentctl_core/application/use_cases/deploy.py b/bin/contentctl_project/contentctl_core/application/use_cases/deploy.py new file mode 100644 index 0000000000..20040e29ef --- /dev/null +++ b/bin/contentctl_project/contentctl_core/application/use_cases/deploy.py @@ -0,0 +1,4 @@ +class Deploy: + def __init__(self, args): + pass + \ No newline at end of file diff --git a/contentctl.py b/contentctl.py index 4e6be2b4a0..f0f3018ee9 100644 --- a/contentctl.py +++ b/contentctl.py @@ -10,6 +10,8 @@ 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 from bin.contentctl_project.contentctl_core.application.use_cases.reporting import ReportingInputDto, Reporting +from bin.contentctl_project.contentctl_core.application.use_cases.clean import Clean +from bin.contentctl_project.contentctl_core.application.use_cases.deploy import Deploy 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 d6e09d3c1b1965ce6e78181d6824ea6269d53714 Mon Sep 17 00:00:00 2001 From: pyth0n1c <87383215+pyth0n1c@users.noreply.github.com> Date: Fri, 22 Apr 2022 13:39:09 -0700 Subject: [PATCH 04/31] Actually calling clean now instead of just passing over it. --- .../application/use_cases/clean.py | 32 +++++++++++++++++-- contentctl.py | 3 +- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/bin/contentctl_project/contentctl_core/application/use_cases/clean.py b/bin/contentctl_project/contentctl_core/application/use_cases/clean.py index e648fc4ce4..661e356477 100644 --- a/bin/contentctl_project/contentctl_core/application/use_cases/clean.py +++ b/bin/contentctl_project/contentctl_core/application/use_cases/clean.py @@ -2,9 +2,31 @@ import glob import os +#f-strings cannot include a backslash, so we include this as a constant +NEWLINE_INDENT = "\n\t" class Clean: def __init__(self, args): - pass + self.items_scanned = [] + self.items_deleted = [] + self.items_kept = [] + self.items_deleted_failed = [] + self.success = self.remove_all_content() + + self.print_results_summary() + return self.success + + def print_results_summary(self): + if self.success is True: + print("security content has been cleaned successfully!\n" + "Ready for your custom constent!") + else: + print("**Failure(s) cleaning security content - check log for details**") + print(f"\n\tSummary" + 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 = [] @@ -32,7 +54,7 @@ def remove_all_content(self)-> bool: if len(errors) == 0: return True else: - print(f"Clean failed on the following steps:\n\t{'\n\t'.join(errors)}") + print(f"Clean failed on the following steps:{NEWLINE_INDENT}{NEWLINE_INDENT.join(errors)}") return False def remove_detections(self, glob_patterns:list[str]=["detections/**/*.yml"], keep:list[str]=[]) -> bool: @@ -55,7 +77,7 @@ def remove_playbooks(self, glob_patterns:list[str]=["playbooks/**/*"], keep:list 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) @@ -69,6 +91,7 @@ def remove_by_glob_pattern(self, glob_pattern:str, keep:list[str]) -> bool: try: matched_filenames = glob.glob(glob_pattern) for filename in matched_filenames: + self.items_scanned.append(filename) success &= self.remove_file(filename, keep) return success except Exception as e: @@ -81,6 +104,7 @@ 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 @@ -88,9 +112,11 @@ def remove_file(self, filename:str, keep:list[str]) -> bool: #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 diff --git a/contentctl.py b/contentctl.py index f0f3018ee9..ff3be63f65 100644 --- a/contentctl.py +++ b/contentctl.py @@ -252,7 +252,8 @@ def reporting(args) -> None: def clean(args) -> None: - pass + Clean(args) + def deploy(args) -> None: pass From 814a13c522f5b74b21b6bb5debc1c5743c1520d5 Mon Sep 17 00:00:00 2001 From: pyth0n1c <87383215+pyth0n1c@users.noreply.github.com> Date: Sat, 23 Apr 2022 06:44:18 -0700 Subject: [PATCH 05/31] Stubs for building an application with slim and inspecting with command line version of appinspect. --- .../contentctl_core/application/use_cases/build.py | 0 .../contentctl_core/application/use_cases/inspect.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 bin/contentctl_project/contentctl_core/application/use_cases/build.py create mode 100644 bin/contentctl_project/contentctl_core/application/use_cases/inspect.py diff --git a/bin/contentctl_project/contentctl_core/application/use_cases/build.py b/bin/contentctl_project/contentctl_core/application/use_cases/build.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/bin/contentctl_project/contentctl_core/application/use_cases/inspect.py b/bin/contentctl_project/contentctl_core/application/use_cases/inspect.py new file mode 100644 index 0000000000..e69de29bb2 From aecd70e74d0dd0ca561a0976eefee0cd852a2d45 Mon Sep 17 00:00:00 2001 From: pyth0n1c <87383215+pyth0n1c@users.noreply.github.com> Date: Sat, 23 Apr 2022 07:39:31 -0700 Subject: [PATCH 06/31] Now support building the app. Added requirements to support CLI-based appinspect. --- .../application/use_cases/build.py | 22 ++++++++++ .../application/use_cases/clean.py | 4 +- .../application/use_cases/deploy.py | 40 ++++++++++++++++++- .../application/use_cases/inspect.py | 8 ++++ contentctl.py | 19 +++++++-- requirements.txt | 3 ++ 6 files changed, 90 insertions(+), 6 deletions(-) diff --git a/bin/contentctl_project/contentctl_core/application/use_cases/build.py b/bin/contentctl_project/contentctl_core/application/use_cases/build.py index e69de29bb2..ef1f70a452 100644 --- a/bin/contentctl_project/contentctl_core/application/use_cases/build.py +++ b/bin/contentctl_project/contentctl_core/application/use_cases/build.py @@ -0,0 +1,22 @@ +import slim +import sys + + +class Build: + def __init__(self, args, source:str = "", output_dir:str = ""): + try: + print("Validating Splunkbase App...", end='') + sys.stdout.flush() + slim.validate(source=source) + print("done") + + print("Building Splunkbase App...", end='') + sys.stdout.flush() + slim.package(source=source, output_dir=output_dir) + print("done") + + + except Exception as e: + raise(Exception(f"Error building Splunk App: {str(e)}")) + + \ No newline at end of file diff --git a/bin/contentctl_project/contentctl_core/application/use_cases/clean.py b/bin/contentctl_project/contentctl_core/application/use_cases/clean.py index 661e356477..66d5a4f38d 100644 --- a/bin/contentctl_project/contentctl_core/application/use_cases/clean.py +++ b/bin/contentctl_project/contentctl_core/application/use_cases/clean.py @@ -13,7 +13,7 @@ def __init__(self, args): self.success = self.remove_all_content() self.print_results_summary() - return self.success + def print_results_summary(self): if self.success is True: @@ -21,7 +21,7 @@ def print_results_summary(self): "Ready for your custom constent!") else: print("**Failure(s) cleaning security content - check log for details**") - print(f"\n\tSummary" + 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)}" diff --git a/bin/contentctl_project/contentctl_core/application/use_cases/deploy.py b/bin/contentctl_project/contentctl_core/application/use_cases/deploy.py index 20040e29ef..12f185bfbd 100644 --- a/bin/contentctl_project/contentctl_core/application/use_cases/deploy.py +++ b/bin/contentctl_project/contentctl_core/application/use_cases/deploy.py @@ -1,4 +1,42 @@ +from distutils.command.install_data import install_data +import splunklib.client as client + + class Deploy: def __init__(self, args): - pass + self.username = args.username + self.password = args.password + self.host = args.host + self.api_port = args.api_port + self.path = args.path + self.overwrite_app = args.overwrite_app + self.install_app() + + def install_app(self) -> bool: + #Connect to the service + 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: + print(f"Failure connecting the the Splunk Search Head: {str(e)}") + return False + + #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}") + return True + + \ No newline at end of file diff --git a/bin/contentctl_project/contentctl_core/application/use_cases/inspect.py b/bin/contentctl_project/contentctl_core/application/use_cases/inspect.py index e69de29bb2..d2586ee47a 100644 --- a/bin/contentctl_project/contentctl_core/application/use_cases/inspect.py +++ b/bin/contentctl_project/contentctl_core/application/use_cases/inspect.py @@ -0,0 +1,8 @@ +class Inspect: + def __init__(self, args): + try: + import magic + 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)")) + import splunk_appinspect diff --git a/contentctl.py b/contentctl.py index ff3be63f65..f5d90050a2 100644 --- a/contentctl.py +++ b/contentctl.py @@ -253,10 +253,10 @@ def reporting(args) -> None: def clean(args) -> None: Clean(args) - + def deploy(args) -> None: - pass + Deploy(args) def main(args): @@ -280,6 +280,10 @@ def main(args): clean_parser = actions_parser.add_parser("clean", help="Remove all content from Security Content. " "This allows a user to easily add their own content and, eventually, " "build a custom application consisting of their custom content.") + + 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.") + deploy_parser = actions_parser.add_parser("deploy", help="Install an application on a target Splunk Search Head.") # # new arguments @@ -316,9 +320,18 @@ def main(args): clean_parser.set_defaults(func=clean) - deploy_parser.add_argument("-a", "--search_head_address", required=True, type=str, help="The address of the Splunk Search Head to deploy the application to.") + build_parser.set_defaults(func=build) + + inspect_parser.set_defaults(func=inspect) + + + + deploy_parser.add_argument("-h", "--search_head_address", required=True, type=str, help="The address of the Splunk Search Head to deploy the application to.") deploy_parser.add_argument("-u", "--username", required=True, type=str, help="Username for Splunk Search Head. Note that this user MUST be able to install applications.") deploy_parser.add_argument("-p", "--password", required=True, type=str, help="Password for Splunk Search Head.") + deploy_parser.add_argument("-a", "--api_port", required=False, type=int, default=8089, help="Port serving the Splunk API (you probably have not changed this).") + deploy_parser.add_argument("--overwrite_app", required=True, action=argparse.BooleanOptionalAction, help="If an app with the same name already exists, should it be overwritten?") + deploy_parser.set_defaults(overwrite_app=False) deploy_parser.set_defaults(func=deploy) # # parse them diff --git a/requirements.txt b/requirements.txt index db54162106..dae96ec7ae 100644 --- a/requirements.txt +++ b/requirements.txt @@ -8,3 +8,6 @@ PyYAML questionary requests xmltodict +splunk-sdk +https://download.splunk.com/misc/packaging-toolkit/splunk-packaging-toolkit-1.0.1.tar.gz +splunk-appinspect From 0db105e00cb0312f875e530d4aeed5f23173a1a3 Mon Sep 17 00:00:00 2001 From: pyth0n1c <87383215+pyth0n1c@users.noreply.github.com> Date: Sat, 23 Apr 2022 09:27:41 -0700 Subject: [PATCH 07/31] More progress toward build and inspect of the app. --- .../application/use_cases/build.py | 45 +++++++++++++++---- .../application/use_cases/inspect.py | 25 ++++++++++- contentctl.py | 8 ++++ 3 files changed, 67 insertions(+), 11 deletions(-) diff --git a/bin/contentctl_project/contentctl_core/application/use_cases/build.py b/bin/contentctl_project/contentctl_core/application/use_cases/build.py index ef1f70a452..5fef3fbd16 100644 --- a/bin/contentctl_project/contentctl_core/application/use_cases/build.py +++ b/bin/contentctl_project/contentctl_core/application/use_cases/build.py @@ -1,22 +1,49 @@ import slim import sys - - +import tarfile +import os class Build: - def __init__(self, args, source:str = "", output_dir:str = ""): - try: + def __init__(self, args): + self.source = args.path + self.output_dir = args.output_dir + self.output_package = self.output_dir+'.tar.gz' + + + self.validate_splunk_app() + self.build_splunk_app() + self.archive_splunk_app() + + def validate_splunk_app(self): + try: print("Validating Splunkbase App...", end='') sys.stdout.flush() - slim.validate(source=source) + slim.validate(source=self.source) print("done") - + except Exception as e: + print("error") + raise(Exception(f"Error validating Splunk App: {str(e)}")) + + def build_splunk_app(self): + try: print("Building Splunkbase App...", end='') sys.stdout.flush() - slim.package(source=source, output_dir=output_dir) + slim.package(source=self.source, output_dir=self.output_dir) print("done") - - 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, arcname=os.path.basename(self.output_dir)) + print("done") + except Exception as e: + print("error") + raise(Exception(f"Error creating {self.output_package}: {str(e)}")) + \ No newline at end of file diff --git a/bin/contentctl_project/contentctl_core/application/use_cases/inspect.py b/bin/contentctl_project/contentctl_core/application/use_cases/inspect.py index d2586ee47a..d9ff520738 100644 --- a/bin/contentctl_project/contentctl_core/application/use_cases/inspect.py +++ b/bin/contentctl_project/contentctl_core/application/use_cases/inspect.py @@ -1,8 +1,29 @@ +import subprocess class Inspect: def __init__(self, args): try: - import magic + 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)")) - import splunk_appinspect + + #Splunk appinspect does not have a documented python API... so we run it + #used the Command Line interface + self.path = args.path + + proc = "no output produced..." + try: + proc = subprocess.check_output(["splunk-appinspect", "inspect", self.path], stderr=subprocess.STDOUT) + except Exception as e: + print(f"Appinspect failed with output: \n{proc}") + raise(Exception(f"Error running appinspect on {self.path}: {str(e)}")) + + print(f"Appinspect on {self.path} was successful!") + + + + + + + + diff --git a/contentctl.py b/contentctl.py index f5d90050a2..2eaa674b0f 100644 --- a/contentctl.py +++ b/contentctl.py @@ -12,6 +12,8 @@ from bin.contentctl_project.contentctl_core.application.use_cases.reporting import ReportingInputDto, Reporting from bin.contentctl_project.contentctl_core.application.use_cases.clean import Clean 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 @@ -255,6 +257,12 @@ def clean(args) -> None: Clean(args) +def build(args) -> None: + Build(args) + +def inspect(args) -> None: + Inspect(args) + def deploy(args) -> None: Deploy(args) From 6694a5f659a0f1c65369e0e66876a1d52a351eb5 Mon Sep 17 00:00:00 2001 From: pyth0n1c <87383215+pyth0n1c@users.noreply.github.com> Date: Sat, 23 Apr 2022 10:22:56 -0700 Subject: [PATCH 08/31] building and appinspecting of the package are now working. removed an errorneous comma from lookups/attack_tools.csv that caused errors during appinspect and resulted in a badly cormatted csv. --- .../application/use_cases/build.py | 41 +++++++++++++------ contentctl.py | 4 +- lookups/attacker_tools.csv | 2 +- 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/bin/contentctl_project/contentctl_core/application/use_cases/build.py b/bin/contentctl_project/contentctl_core/application/use_cases/build.py index 5fef3fbd16..aa447e2575 100644 --- a/bin/contentctl_project/contentctl_core/application/use_cases/build.py +++ b/bin/contentctl_project/contentctl_core/application/use_cases/build.py @@ -1,33 +1,50 @@ -import slim +import subprocess import sys import tarfile import os class Build: def __init__(self, args): self.source = args.path - self.output_dir = args.output_dir - self.output_package = self.output_dir+'.tar.gz' - + self.app_name = args.app_name + self.output_dir_base = args.output_dir + self.output_dir_source = os.path.join(self.output_dir_base, "source", self.app_name) + self.output_dir_build = os.path.join(self.output_dir_base, "build", self.app_name) + self.output_package = os.path.join(self.output_dir_base, "build", self.app_name+'.tar.gz') + self.copy_app_source() self.validate_splunk_app() self.build_splunk_app() - self.archive_splunk_app() + #self.archive_splunk_app() + def copy_app_source(self): + import shutil + try: + 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): - try: - print("Validating Splunkbase App...", end='') + proc = "nothing..." + try: + print("Validating Splunk App...", end='') sys.stdout.flush() - slim.validate(source=self.source) + nothing = subprocess.check_output(["slim", "package", "-o", self.output_dir_build, self.output_dir_source], stderr=sys.stderr) print("done") except Exception as e: print("error") - raise(Exception(f"Error validating Splunk App: {str(e)}")) + raise(Exception(f"Error building Splunk App: {str(e)}")) + def build_splunk_app(self): + proc = "nothing..." try: - print("Building Splunkbase App...", end='') + print("Building Splunk App...", end='') sys.stdout.flush() - slim.package(source=self.source, output_dir=self.output_dir) + nothing = subprocess.check_output(["slim", "package", "-o", self.output_dir_build, self.output_dir_source], stderr=sys.stderr) print("done") except Exception as e: print("error") @@ -39,7 +56,7 @@ def archive_splunk_app(self): 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, arcname=os.path.basename(self.output_dir)) + tar.add(self.output_dir_build, arcname=os.path.basename(self.output_dir_build)) print("done") except Exception as e: print("error") diff --git a/contentctl.py b/contentctl.py index 2eaa674b0f..d8d4c67036 100644 --- a/contentctl.py +++ b/contentctl.py @@ -328,13 +328,15 @@ def main(args): clean_parser.set_defaults(func=clean) + build_parser.add_argument("-o", "--output_dir", required=True, type=str, help="Directory to output the built package to.") + build_parser.add_argument("-n", "--app_name", required=True, type=str, help="Name of the application.") build_parser.set_defaults(func=build) inspect_parser.set_defaults(func=inspect) - deploy_parser.add_argument("-h", "--search_head_address", required=True, type=str, help="The address of the Splunk Search Head to deploy the application to.") + deploy_parser.add_argument("-s", "--search_head_address", required=True, type=str, help="The address of the Splunk Search Head to deploy the application to.") deploy_parser.add_argument("-u", "--username", required=True, type=str, help="Username for Splunk Search Head. Note that this user MUST be able to install applications.") deploy_parser.add_argument("-p", "--password", required=True, type=str, help="Password for Splunk Search Head.") deploy_parser.add_argument("-a", "--api_port", required=False, type=int, default=8089, help="Port serving the Splunk API (you probably have not changed this).") diff --git a/lookups/attacker_tools.csv b/lookups/attacker_tools.csv index 2f95dfb054..38d33513d5 100644 --- a/lookups/attacker_tools.csv +++ b/lookups/attacker_tools.csv @@ -24,4 +24,4 @@ KPortScan3.exe,This executable was delivered in the XMRig Crypto Miner and is co NLAChecker.exe,A scanner tool that checks for Windows hosts for Network Level Authentication. This tool allows attackers to detect Windows Servers with RDP without NLA enabled which facilitates the use of brute force non microsoft rdp tools or exploits ns.exe,A commonly used tool used by attackers to scan and map file shares SilverBullet.exe,Malware was discovered in our monitoring of honey pots that abuses this open source software for scanning and connecting to hosts. -kportscan3.exe, KPortScan 3.0 is a widely used port scanning tool on Hacking Forums, to perform network scanning on the internal networks. \ No newline at end of file +kportscan3.exe, KPortScan 3.0 is a widely used port scanning tool on Hacking Forums to perform network scanning on the internal networks. From 4aeb43540a0c8f2617240d4203e6eef584f28378 Mon Sep 17 00:00:00 2001 From: pyth0n1c <87383215+pyth0n1c@users.noreply.github.com> Date: Mon, 25 Apr 2022 16:30:21 -0700 Subject: [PATCH 09/31] Non working... yet... deploy function. --- .../application/use_cases/deploy.py | 46 +++++++++++++++++-- 1 file changed, 41 insertions(+), 5 deletions(-) diff --git a/bin/contentctl_project/contentctl_core/application/use_cases/deploy.py b/bin/contentctl_project/contentctl_core/application/use_cases/deploy.py index 12f185bfbd..2b359af45b 100644 --- a/bin/contentctl_project/contentctl_core/application/use_cases/deploy.py +++ b/bin/contentctl_project/contentctl_core/application/use_cases/deploy.py @@ -1,27 +1,60 @@ -from distutils.command.install_data import install_data -import splunklib.client as client +import splunklib.client as client +import multiprocessing +import http.server +import time class Deploy: def __init__(self, args): self.username = args.username self.password = args.password - self.host = args.host + self.host = args.search_head_address self.api_port = args.api_port self.path = args.path self.overwrite_app = args.overwrite_app + self.server_app_path=f"http://192.168.0.187:9998/args.path" + + self.http_process = self.start_http_server() + self.install_app() + def start_http_server(self, http_address:str ='', http_listen_port:int=9998) -> multiprocessing.Process: + httpd = http.server.HTTPServer((http_address, http_listen_port), http.server.BaseHTTPRequestHandler) + m = multiprocessing.Process(target=httpd.serve_forever) + m.start() + return m + + + def install_app(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: - print(f"Failure connecting the the Splunk Search Head: {str(e)}") - return False + raise(Exception(f"Failure installing the app {self.server_app_path}: {str(e)}")) + #Query and list all of the installed apps try: @@ -36,6 +69,9 @@ def install_app(self) -> bool: print(f"Installing app {self.path}") + + self.http_process.terminate() + return True From 29894c6d3eeb3e097d1c6cbc3883999ac0985955 Mon Sep 17 00:00:00 2001 From: pyth0n1c <87383215+pyth0n1c@users.noreply.github.com> Date: Fri, 13 May 2022 09:31:54 -0400 Subject: [PATCH 10/31] Fixed broken 'clean' paths for multiple content folders. --- .../contentctl_core/application/use_cases/clean.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/contentctl_project/contentctl_core/application/use_cases/clean.py b/bin/contentctl_project/contentctl_core/application/use_cases/clean.py index 66d5a4f38d..e6691fc096 100644 --- a/bin/contentctl_project/contentctl_core/application/use_cases/clean.py +++ b/bin/contentctl_project/contentctl_core/application/use_cases/clean.py @@ -72,7 +72,7 @@ def remove_macros(self,glob_patterns:list[str]=["macros/**/*.yml"], keep:list[st 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: + 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: @@ -81,7 +81,7 @@ def remove_stories(self, glob_patterns:list[str]=["stories/**/*.yml"], keep:list 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]=["tests/**/*.yml"], keep:list[str]=[]) -> bool: + 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) @@ -89,7 +89,7 @@ def remove_by_glob_patterns(self, glob_patterns:list[str]=["tests/**/*.yml"], ke def remove_by_glob_pattern(self, glob_pattern:str, keep:list[str]) -> bool: success = True try: - matched_filenames = glob.glob(glob_pattern) + matched_filenames = glob.glob(glob_pattern, recursive=True) for filename in matched_filenames: self.items_scanned.append(filename) success &= self.remove_file(filename, keep) From 83c6448e6fdabc5cb6059dcdc36c32154fcc52ad Mon Sep 17 00:00:00 2001 From: pyth0n1c <87383215+pyth0n1c@users.noreply.github.com> Date: Fri, 13 May 2022 11:42:01 -0400 Subject: [PATCH 11/31] Fixing some paths and command line arguments for the contentctl build option. --- .../application/use_cases/build.py | 44 +++++++++++++------ contentctl.py | 4 +- 2 files changed, 33 insertions(+), 15 deletions(-) diff --git a/bin/contentctl_project/contentctl_core/application/use_cases/build.py b/bin/contentctl_project/contentctl_core/application/use_cases/build.py index aa447e2575..e6332edb3c 100644 --- a/bin/contentctl_project/contentctl_core/application/use_cases/build.py +++ b/bin/contentctl_project/contentctl_core/application/use_cases/build.py @@ -2,14 +2,30 @@ import sys import tarfile import os +from typing import TextIO class Build: def __init__(self, args): - self.source = args.path - self.app_name = args.app_name + 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, "source", self.app_name) - self.output_dir_build = os.path.join(self.output_dir_base, "build", self.app_name) - self.output_package = os.path.join(self.output_dir_base, "build", self.app_name+'.tar.gz') + + + 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() @@ -30,26 +46,28 @@ def copy_app_source(self): def validate_splunk_app(self): proc = "nothing..." try: - print("Validating Splunk App...", end='') + print("Validating Splunk App...") sys.stdout.flush() - nothing = subprocess.check_output(["slim", "package", "-o", self.output_dir_build, self.output_dir_source], stderr=sys.stderr) - print("done") + nothing = subprocess.check_output(["slim", "validate", self.output_dir_source]) + + print("Package Validation Complete") except Exception as e: - print("error") + 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...", end='') + print("Building Splunk App...") sys.stdout.flush() - nothing = subprocess.check_output(["slim", "package", "-o", self.output_dir_build, self.output_dir_source], stderr=sys.stderr) - print("done") + 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: @@ -61,6 +79,6 @@ def archive_splunk_app(self): except Exception as e: print("error") raise(Exception(f"Error creating {self.output_package}: {str(e)}")) - + ''' \ No newline at end of file diff --git a/contentctl.py b/contentctl.py index d8d4c67036..e0912045c3 100644 --- a/contentctl.py +++ b/contentctl.py @@ -328,8 +328,8 @@ def main(args): clean_parser.set_defaults(func=clean) - build_parser.add_argument("-o", "--output_dir", required=True, type=str, help="Directory to output the built package to.") - build_parser.add_argument("-n", "--app_name", required=True, type=str, help="Name of the application.") + build_parser.add_argument("-o", "--output_dir", required=True, default="build", type=str, help="Directory to output the built package to.") + build_parser.add_argument("-pr", "--product", required=True, type=str, help="Name of the application") build_parser.set_defaults(func=build) inspect_parser.set_defaults(func=inspect) From 3ef15420009b14678ac8b248036a59e2ead325d9 Mon Sep 17 00:00:00 2001 From: pyth0n1c <87383215+pyth0n1c@users.noreply.github.com> Date: Fri, 13 May 2022 12:58:53 -0400 Subject: [PATCH 12/31] Added another folder to clean. Updated command line arguments for build. Improved implmentation of inspect. --- .../contentctl_core/application/use_cases/build.py | 9 +++++++++ .../contentctl_core/application/use_cases/clean.py | 10 +++++++--- .../contentctl_core/application/use_cases/inspect.py | 12 +++++++----- contentctl.py | 6 ++++-- 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/bin/contentctl_project/contentctl_core/application/use_cases/build.py b/bin/contentctl_project/contentctl_core/application/use_cases/build.py index e6332edb3c..bce78cfc02 100644 --- a/bin/contentctl_project/contentctl_core/application/use_cases/build.py +++ b/bin/contentctl_project/contentctl_core/application/use_cases/build.py @@ -34,7 +34,16 @@ def __init__(self, args): 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) diff --git a/bin/contentctl_project/contentctl_core/application/use_cases/clean.py b/bin/contentctl_project/contentctl_core/application/use_cases/clean.py index e6691fc096..053734a168 100644 --- a/bin/contentctl_project/contentctl_core/application/use_cases/clean.py +++ b/bin/contentctl_project/contentctl_core/application/use_cases/clean.py @@ -38,7 +38,8 @@ def remove_all_content(self)-> bool: (self.remove_notebooks,"Removing Notebooks"), (self.remove_playbooks,"Removing Playbooks"), (self.remove_stories,"Removing Stores"), - (self.remove_tests,"Removing Tests")] + (self.remove_tests,"Removing Tests"), + (self.remove_dist_lookups,"Removing Dist Lookups")] for func, text in steps: print(f"{text}...",end='') @@ -56,14 +57,17 @@ def remove_all_content(self)-> bool: else: print(f"Clean failed on the following steps:{NEWLINE_INDENT}{NEWLINE_INDENT.join(errors)}") return False - + + 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"], keep:list[str]=[]) -> bool: + 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: diff --git a/bin/contentctl_project/contentctl_core/application/use_cases/inspect.py b/bin/contentctl_project/contentctl_core/application/use_cases/inspect.py index d9ff520738..acb4814b4f 100644 --- a/bin/contentctl_project/contentctl_core/application/use_cases/inspect.py +++ b/bin/contentctl_project/contentctl_core/application/use_cases/inspect.py @@ -1,4 +1,5 @@ import subprocess +import os class Inspect: def __init__(self, args): try: @@ -7,18 +8,19 @@ def __init__(self, args): 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 - #used the Command Line interface - self.path = args.path + #using the Command Line interface + self.package_path = args.package_path proc = "no output produced..." try: - proc = subprocess.check_output(["splunk-appinspect", "inspect", self.path], stderr=subprocess.STDOUT) + proc = subprocess.check_output(["splunk-appinspect", "inspect", self.package_path]) except Exception as e: print(f"Appinspect failed with output: \n{proc}") - raise(Exception(f"Error running appinspect on {self.path}: {str(e)}")) + raise(Exception(f"Error running appinspect on {self.package_path}: {str(e)}")) - print(f"Appinspect on {self.path} was successful!") + print(f"Appinspect on {self.package_path} was successful!") diff --git a/contentctl.py b/contentctl.py index e0912045c3..6a4021b46d 100644 --- a/contentctl.py +++ b/contentctl.py @@ -328,10 +328,12 @@ def main(args): clean_parser.set_defaults(func=clean) - build_parser.add_argument("-o", "--output_dir", required=True, default="build", type=str, help="Directory to output the built package to.") - build_parser.add_argument("-pr", "--product", required=True, type=str, help="Name of the application") + build_parser.add_argument("-o", "--output_dir", required=False, default="build", type=str, help="Directory to output the built package to.") + build_parser.add_argument("-pr", "--product", required=True, type=str, help="Name of the product to build.") 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) From 751736f5e30478dc43fdea29d93935c7887a7d59 Mon Sep 17 00:00:00 2001 From: pyth0n1c <87383215+pyth0n1c@users.noreply.github.com> Date: Fri, 13 May 2022 13:53:49 -0400 Subject: [PATCH 13/31] Better error handling for inspect --- .../contentctl_core/application/use_cases/inspect.py | 5 +++-- contentctl.py | 6 ++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/bin/contentctl_project/contentctl_core/application/use_cases/inspect.py b/bin/contentctl_project/contentctl_core/application/use_cases/inspect.py index acb4814b4f..f970ab586b 100644 --- a/bin/contentctl_project/contentctl_core/application/use_cases/inspect.py +++ b/bin/contentctl_project/contentctl_core/application/use_cases/inspect.py @@ -15,9 +15,10 @@ def __init__(self, args): proc = "no output produced..." try: - proc = subprocess.check_output(["splunk-appinspect", "inspect", self.package_path]) + 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: - print(f"Appinspect failed with output: \n{proc}") raise(Exception(f"Error running appinspect on {self.package_path}: {str(e)}")) print(f"Appinspect on {self.package_path} was successful!") diff --git a/contentctl.py b/contentctl.py index 6a4021b46d..f7e72dea1d 100644 --- a/contentctl.py +++ b/contentctl.py @@ -348,8 +348,10 @@ def main(args): # # parse them args = parser.parse_args() - return args.func(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:]) \ No newline at end of file From 4a86d06212a9ac11bf8ddec76bdee54bfc11f31a Mon Sep 17 00:00:00 2001 From: pyth0n1c <87383215+pyth0n1c@users.noreply.github.com> Date: Fri, 13 May 2022 14:50:13 -0400 Subject: [PATCH 14/31] Remove overwrite app as a default argument for deploy. --- contentctl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contentctl.py b/contentctl.py index f7e72dea1d..2707722791 100644 --- a/contentctl.py +++ b/contentctl.py @@ -342,7 +342,7 @@ def main(args): deploy_parser.add_argument("-u", "--username", required=True, type=str, help="Username for Splunk Search Head. Note that this user MUST be able to install applications.") deploy_parser.add_argument("-p", "--password", required=True, type=str, help="Password for Splunk Search Head.") deploy_parser.add_argument("-a", "--api_port", required=False, type=int, default=8089, help="Port serving the Splunk API (you probably have not changed this).") - deploy_parser.add_argument("--overwrite_app", required=True, action=argparse.BooleanOptionalAction, help="If an app with the same name already exists, should it be overwritten?") + deploy_parser.add_argument("--overwrite_app", required=False, action=argparse.BooleanOptionalAction, help="If an app with the same name already exists, should it be overwritten?") deploy_parser.set_defaults(overwrite_app=False) deploy_parser.set_defaults(func=deploy) From e751727b5b9f8b1853b832fca2de4a68e9f9cb71 Mon Sep 17 00:00:00 2001 From: pyth0n1c <87383215+pyth0n1c@users.noreply.github.com> Date: Tue, 17 May 2022 12:45:03 -0700 Subject: [PATCH 15/31] Changed all of the clean terminology to init. This is in preparation for distributing the tool standalone in a separate repo as opposed to a part of the security_content repo. --- .../contentctl_core/application/use_cases/clean.py | 6 +++--- contentctl.py | 12 +++++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/bin/contentctl_project/contentctl_core/application/use_cases/clean.py b/bin/contentctl_project/contentctl_core/application/use_cases/clean.py index 053734a168..93d141e03f 100644 --- a/bin/contentctl_project/contentctl_core/application/use_cases/clean.py +++ b/bin/contentctl_project/contentctl_core/application/use_cases/clean.py @@ -4,7 +4,7 @@ #f-strings cannot include a backslash, so we include this as a constant NEWLINE_INDENT = "\n\t" -class Clean: +class Init: def __init__(self, args): self.items_scanned = [] self.items_deleted = [] @@ -17,10 +17,10 @@ def __init__(self, args): def print_results_summary(self): if self.success is True: - print("security content has been cleaned successfully!\n" + print("repo has been initialized successfully!\n" "Ready for your custom constent!") else: - print("**Failure(s) cleaning security content - check log for details**") + 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)}" diff --git a/contentctl.py b/contentctl.py index 2707722791..947bd19c87 100644 --- a/contentctl.py +++ b/contentctl.py @@ -10,7 +10,7 @@ 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 from bin.contentctl_project.contentctl_core.application.use_cases.reporting import ReportingInputDto, Reporting -from bin.contentctl_project.contentctl_core.application.use_cases.clean import Clean +from bin.contentctl_project.contentctl_core.application.use_cases.clean import Init 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 @@ -253,8 +253,8 @@ def reporting(args) -> None: reporting.execute(reporting_input_dto) -def clean(args) -> None: - Clean(args) +def init(args) -> None: + Init(args) def build(args) -> None: @@ -285,7 +285,7 @@ def main(args): docgen_parser = actions_parser.add_parser("docgen", help="Generates documentation") new_content_parser = actions_parser.add_parser("new_content", help="Create new security content object") reporting_parser = actions_parser.add_parser("reporting", help="Create security content reporting") - clean_parser = actions_parser.add_parser("clean", help="Remove all content from Security Content. " + 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.") @@ -326,7 +326,9 @@ def main(args): reporting_parser.set_defaults(func=reporting) - clean_parser.set_defaults(func=clean) + 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.set_defaults(func=init) build_parser.add_argument("-o", "--output_dir", required=False, default="build", type=str, help="Directory to output the built package to.") build_parser.add_argument("-pr", "--product", required=True, type=str, help="Name of the product to build.") From 6d6feb950890793504b73469fa503757c7f8f377 Mon Sep 17 00:00:00 2001 From: pyth0n1c <87383215+pyth0n1c@users.noreply.github.com> Date: Tue, 17 May 2022 12:47:54 -0700 Subject: [PATCH 16/31] Name conflict on init... fixed --- .../contentctl_core/application/use_cases/clean.py | 2 +- contentctl.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bin/contentctl_project/contentctl_core/application/use_cases/clean.py b/bin/contentctl_project/contentctl_core/application/use_cases/clean.py index 93d141e03f..67bd3860e0 100644 --- a/bin/contentctl_project/contentctl_core/application/use_cases/clean.py +++ b/bin/contentctl_project/contentctl_core/application/use_cases/clean.py @@ -4,7 +4,7 @@ #f-strings cannot include a backslash, so we include this as a constant NEWLINE_INDENT = "\n\t" -class Init: +class Initialize: def __init__(self, args): self.items_scanned = [] self.items_deleted = [] diff --git a/contentctl.py b/contentctl.py index 947bd19c87..9f2b9743d1 100644 --- a/contentctl.py +++ b/contentctl.py @@ -10,7 +10,7 @@ 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 from bin.contentctl_project.contentctl_core.application.use_cases.reporting import ReportingInputDto, Reporting -from bin.contentctl_project.contentctl_core.application.use_cases.clean import Init +from bin.contentctl_project.contentctl_core.application.use_cases.clean 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 @@ -253,8 +253,8 @@ def reporting(args) -> None: reporting.execute(reporting_input_dto) -def init(args) -> None: - Init(args) +def initialize(args) -> None: + Initialize(args) def build(args) -> None: @@ -328,7 +328,7 @@ def main(args): 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.set_defaults(func=init) + 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.") build_parser.add_argument("-pr", "--product", required=True, type=str, help="Name of the product to build.") From 123600d2f2f20768a0e9de34a1a14f4040235f79 Mon Sep 17 00:00:00 2001 From: pyth0n1c <87383215+pyth0n1c@users.noreply.github.com> Date: Tue, 17 May 2022 13:49:29 -0700 Subject: [PATCH 17/31] Added generation of an app.manifest file based on command line arguments. --- .../application/use_cases/clean.py | 127 ------------------ contentctl.py | 7 +- 2 files changed, 6 insertions(+), 128 deletions(-) delete mode 100644 bin/contentctl_project/contentctl_core/application/use_cases/clean.py diff --git a/bin/contentctl_project/contentctl_core/application/use_cases/clean.py b/bin/contentctl_project/contentctl_core/application/use_cases/clean.py deleted file mode 100644 index 67bd3860e0..0000000000 --- a/bin/contentctl_project/contentctl_core/application/use_cases/clean.py +++ /dev/null @@ -1,127 +0,0 @@ -import re -import glob -import os - -#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 = [] - self.success = self.remove_all_content() - - self.print_results_summary() - - - def print_results_summary(self): - if self.success is True: - print("repo has been initialized successfully!\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 = [] - - steps = [(self.remove_detections,"Removing Detections"), - (self.remove_investigations,"Removing Investigations"), - (self.remove_lookups,"Removing Lookups"), - (self.remove_macros,"Removing Macros"), - (self.remove_notebooks,"Removing Notebooks"), - (self.remove_playbooks,"Removing Playbooks"), - (self.remove_stories,"Removing Stores"), - (self.remove_tests,"Removing Tests"), - (self.remove_dist_lookups,"Removing Dist Lookups")] - - 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_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 - - - diff --git a/contentctl.py b/contentctl.py index 9f2b9743d1..e4e1af0696 100644 --- a/contentctl.py +++ b/contentctl.py @@ -10,7 +10,7 @@ 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 from bin.contentctl_project.contentctl_core.application.use_cases.reporting import ReportingInputDto, Reporting -from bin.contentctl_project.contentctl_core.application.use_cases.clean import Initialize +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 @@ -326,8 +326,13 @@ def main(args): 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.") From 004710a350a8d95f8833957c37078f170e1cda5a Mon Sep 17 00:00:00 2001 From: pyth0n1c <87383215+pyth0n1c@users.noreply.github.com> Date: Tue, 17 May 2022 14:36:29 -0700 Subject: [PATCH 18/31] Renamed add generation of app configuration file as well as manifest. Successfully built, then manually deployed an app to a Splunk Cloud instance using the ACS command line. The next steps will be to integrate the command line functionality into this app via the deploy option. --- .../application/use_cases/initialize.py | 276 ++++++++++++++++++ 1 file changed, 276 insertions(+) create mode 100644 bin/contentctl_project/contentctl_core/application/use_cases/initialize.py diff --git a/bin/contentctl_project/contentctl_core/application/use_cases/initialize.py b/bin/contentctl_project/contentctl_core/application/use_cases/initialize.py new file mode 100644 index 0000000000..56713902f5 --- /dev/null +++ b/bin/contentctl_project/contentctl_core/application/use_cases/initialize.py @@ -0,0 +1,276 @@ +import re +import glob +import os +import copy +import json + +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 = [] + + self.path = args.path + + #Information that will be used for generation of a custom manifest + self.app_title = args.title + self.app_name = args.name + 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.generate_custom_manifest() + self.generate_app_configuration_file() + + self.success = self.remove_all_content() + + self.print_results_summary() + + + + 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) + print("format done") + app_configuration_file_path = os.path.join(self.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.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("repo has been initialized successfully!\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 = [] + + steps = [(self.remove_detections,"Removing Detections"), + (self.remove_investigations,"Removing Investigations"), + (self.remove_lookups,"Removing Lookups"), + (self.remove_macros,"Removing Macros"), + (self.remove_notebooks,"Removing Notebooks"), + (self.remove_playbooks,"Removing Playbooks"), + (self.remove_stories,"Removing Stores"), + (self.remove_tests,"Removing Tests"), + (self.remove_dist_lookups,"Removing Dist Lookups")] + + 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_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 + + + From c9552fcf891fc3910a1afd737e3f9b95a9cee314 Mon Sep 17 00:00:00 2001 From: pyth0n1c <87383215+pyth0n1c@users.noreply.github.com> Date: Tue, 17 May 2022 15:26:34 -0700 Subject: [PATCH 19/31] Updates to deploy towards working with the ACS application for deployment of apps to Splunk Cloud using Automated Private App Vetting / APAV --- .../application/use_cases/deploy.py | 45 +++++++++++++++---- contentctl.py | 15 +++++-- 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/bin/contentctl_project/contentctl_core/application/use_cases/deploy.py b/bin/contentctl_project/contentctl_core/application/use_cases/deploy.py index 2b359af45b..8ef28052e5 100644 --- a/bin/contentctl_project/contentctl_core/application/use_cases/deploy.py +++ b/bin/contentctl_project/contentctl_core/application/use_cases/deploy.py @@ -1,11 +1,33 @@ +from ssl import _PasswordType 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.username = args.username self.password = args.password self.host = args.search_head_address @@ -13,20 +35,27 @@ def __init__(self, args): self.path = args.path self.overwrite_app = args.overwrite_app self.server_app_path=f"http://192.168.0.187:9998/args.path" + ''' + sys.exit(0) self.http_process = self.start_http_server() self.install_app() - def start_http_server(self, http_address:str ='', http_listen_port:int=9998) -> multiprocessing.Process: - httpd = http.server.HTTPServer((http_address, http_listen_port), http.server.BaseHTTPRequestHandler) - m = multiprocessing.Process(target=httpd.serve_forever) - m.start() - return m - + + def deploy_to_splunk_cloud(self): + try: + 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}" + print(commandline.split(' ')) + subprocess.run(args = commandline.split(' '), ) + + except Exception as e: + raise(Exception(f"Error deplying to Splunk Cloud Instance: {str(e)}")) - def install_app(self) -> bool: + def install_app_local(self) -> bool: #Connect to the service time.sleep(1) #self.http_process.start() diff --git a/contentctl.py b/contentctl.py index e4e1af0696..b0f4d9b265 100644 --- a/contentctl.py +++ b/contentctl.py @@ -263,7 +263,7 @@ def build(args) -> None: def inspect(args) -> None: Inspect(args) -def deploy(args) -> None: +def cloud_deploy(args) -> None: Deploy(args) def main(args): @@ -292,7 +292,7 @@ def main(args): 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.") - deploy_parser = actions_parser.add_parser("deploy", help="Install an application on a target Splunk Search Head.") + 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", @@ -344,7 +344,14 @@ def main(args): inspect_parser.set_defaults(func=inspect) - + cloud_deploy_parser.add_argument("--app-package", required=True, type=bool, 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=bool, help="splunk.com username") + cloud_deploy_parser.add_argument("--password", required=True, type=bool, 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) + + ''' deploy_parser.add_argument("-s", "--search_head_address", required=True, type=str, help="The address of the Splunk Search Head to deploy the application to.") deploy_parser.add_argument("-u", "--username", required=True, type=str, help="Username for Splunk Search Head. Note that this user MUST be able to install applications.") deploy_parser.add_argument("-p", "--password", required=True, type=str, help="Password for Splunk Search Head.") @@ -352,7 +359,7 @@ def main(args): deploy_parser.add_argument("--overwrite_app", required=False, action=argparse.BooleanOptionalAction, help="If an app with the same name already exists, should it be overwritten?") deploy_parser.set_defaults(overwrite_app=False) deploy_parser.set_defaults(func=deploy) - + ''' # # parse them args = parser.parse_args() try: From a9b118a6ced450c37a07b7f53a8e9952bb158031 Mon Sep 17 00:00:00 2001 From: pyth0n1c <87383215+pyth0n1c@users.noreply.github.com> Date: Tue, 17 May 2022 16:15:45 -0700 Subject: [PATCH 20/31] Deployment using acs is working. It is currently difficult to tell whether the acs command has failed since even an ACS failure gives a return code of 0. I have raised this issue with the ACS team and am waiting on a reponse and guidance. --- .../application/use_cases/deploy.py | 35 +++++++++---------- contentctl.py | 15 ++------ 2 files changed, 20 insertions(+), 30 deletions(-) diff --git a/bin/contentctl_project/contentctl_core/application/use_cases/deploy.py b/bin/contentctl_project/contentctl_core/application/use_cases/deploy.py index 8ef28052e5..1d6e4136da 100644 --- a/bin/contentctl_project/contentctl_core/application/use_cases/deploy.py +++ b/bin/contentctl_project/contentctl_core/application/use_cases/deploy.py @@ -1,5 +1,3 @@ - -from ssl import _PasswordType import splunklib.client as client import multiprocessing import http.server @@ -24,9 +22,7 @@ def __init__(self, args): self.password = args.password self.server = args.server - - - + ''' self.username = args.username self.password = args.password @@ -36,25 +32,28 @@ def __init__(self, args): self.overwrite_app = args.overwrite_app self.server_app_path=f"http://192.168.0.187:9998/args.path" ''' + self.deploy_to_splunk_cloud() + #self.http_process = self.start_http_server() - sys.exit(0) - self.http_process = self.start_http_server() - - self.install_app() + #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}" + print(commandline) + try: - 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}" - print(commandline.split(' ')) - subprocess.run(args = commandline.split(' '), ) - + res = subprocess.run(args = commandline.split(' '), ) except Exception as e: - raise(Exception(f"Error deplying to Splunk Cloud Instance: {str(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) @@ -102,6 +101,6 @@ def install_app_local(self) -> bool: self.http_process.terminate() return True - + ''' \ No newline at end of file diff --git a/contentctl.py b/contentctl.py index b0f4d9b265..37ca5e70b6 100644 --- a/contentctl.py +++ b/contentctl.py @@ -344,22 +344,13 @@ def main(args): inspect_parser.set_defaults(func=inspect) - cloud_deploy_parser.add_argument("--app-package", required=True, type=bool, help="Path to the package you wish to deploy") + 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=bool, help="splunk.com username") - cloud_deploy_parser.add_argument("--password", required=True, type=bool, help="splunk.com password") + 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) - ''' - deploy_parser.add_argument("-s", "--search_head_address", required=True, type=str, help="The address of the Splunk Search Head to deploy the application to.") - deploy_parser.add_argument("-u", "--username", required=True, type=str, help="Username for Splunk Search Head. Note that this user MUST be able to install applications.") - deploy_parser.add_argument("-p", "--password", required=True, type=str, help="Password for Splunk Search Head.") - deploy_parser.add_argument("-a", "--api_port", required=False, type=int, default=8089, help="Port serving the Splunk API (you probably have not changed this).") - deploy_parser.add_argument("--overwrite_app", required=False, action=argparse.BooleanOptionalAction, help="If an app with the same name already exists, should it be overwritten?") - deploy_parser.set_defaults(overwrite_app=False) - deploy_parser.set_defaults(func=deploy) - ''' # # parse them args = parser.parse_args() try: From b17b224a66b5e53c07ed42261401da99080d3b08 Mon Sep 17 00:00:00 2001 From: pyth0n1c <87383215+pyth0n1c@users.noreply.github.com> Date: Tue, 17 May 2022 19:59:40 -0700 Subject: [PATCH 21/31] Added removal of baselines during initialize. --- .../contentctl_core/application/use_cases/initialize.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/bin/contentctl_project/contentctl_core/application/use_cases/initialize.py b/bin/contentctl_project/contentctl_core/application/use_cases/initialize.py index 56713902f5..dae0d5eacf 100644 --- a/bin/contentctl_project/contentctl_core/application/use_cases/initialize.py +++ b/bin/contentctl_project/contentctl_core/application/use_cases/initialize.py @@ -180,7 +180,9 @@ def print_results_summary(self): def remove_all_content(self)-> bool: errors = [] + #Sort the steps so they are performced alphabetically steps = [(self.remove_detections,"Removing Detections"), + (self.remove_baselines,"Removing Baselines"), (self.remove_investigations,"Removing Investigations"), (self.remove_lookups,"Removing Lookups"), (self.remove_macros,"Removing Macros"), @@ -188,7 +190,7 @@ def remove_all_content(self)-> bool: (self.remove_playbooks,"Removing Playbooks"), (self.remove_stories,"Removing Stores"), (self.remove_tests,"Removing Tests"), - (self.remove_dist_lookups,"Removing Dist Lookups")] + (self.remove_dist_lookups,"Removing Dist Lookups")].sort(key=lambda name: name[1]) for func, text in steps: print(f"{text}...",end='') @@ -207,6 +209,9 @@ def remove_all_content(self)-> bool: 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) From e47765f09fc625fde0a831e410c08901f1234fb9 Mon Sep 17 00:00:00 2001 From: pyth0n1c <87383215+pyth0n1c@users.noreply.github.com> Date: Tue, 17 May 2022 20:01:45 -0700 Subject: [PATCH 22/31] Fixing error introduced when sorting steps in initialize. --- .../contentctl_core/application/use_cases/initialize.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bin/contentctl_project/contentctl_core/application/use_cases/initialize.py b/bin/contentctl_project/contentctl_core/application/use_cases/initialize.py index dae0d5eacf..7929fb7787 100644 --- a/bin/contentctl_project/contentctl_core/application/use_cases/initialize.py +++ b/bin/contentctl_project/contentctl_core/application/use_cases/initialize.py @@ -180,7 +180,7 @@ def print_results_summary(self): def remove_all_content(self)-> bool: errors = [] - #Sort the steps so they are performced alphabetically + #List out all the steps we will have to take steps = [(self.remove_detections,"Removing Detections"), (self.remove_baselines,"Removing Baselines"), (self.remove_investigations,"Removing Investigations"), @@ -190,7 +190,9 @@ def remove_all_content(self)-> bool: (self.remove_playbooks,"Removing Playbooks"), (self.remove_stories,"Removing Stores"), (self.remove_tests,"Removing Tests"), - (self.remove_dist_lookups,"Removing Dist Lookups")].sort(key=lambda name: name[1]) + (self.remove_dist_lookups,"Removing 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='') From e3459eaeb3c379f6e2607e7f9c1e260f6e7da31d Mon Sep 17 00:00:00 2001 From: pyth0n1c <87383215+pyth0n1c@users.noreply.github.com> Date: Tue, 17 May 2022 23:13:53 -0700 Subject: [PATCH 23/31] Updates to contentctl, deploy, enums, and initialize to support building the app scaffold and removing all the content that needs to be removed. --- .../application/use_cases/deploy.py | 12 +---- .../application/use_cases/initialize.py | 53 +++++++++++++------ .../domain/entities/enums/enums.py | 3 +- contentctl.py | 9 ++++ 4 files changed, 49 insertions(+), 28 deletions(-) diff --git a/bin/contentctl_project/contentctl_core/application/use_cases/deploy.py b/bin/contentctl_project/contentctl_core/application/use_cases/deploy.py index 1d6e4136da..fcffff972c 100644 --- a/bin/contentctl_project/contentctl_core/application/use_cases/deploy.py +++ b/bin/contentctl_project/contentctl_core/application/use_cases/deploy.py @@ -23,15 +23,7 @@ def __init__(self, args): self.server = args.server - ''' - self.username = args.username - self.password = args.password - self.host = args.search_head_address - self.api_port = args.api_port - self.path = args.path - self.overwrite_app = args.overwrite_app - self.server_app_path=f"http://192.168.0.187:9998/args.path" - ''' + self.deploy_to_splunk_cloud() #self.http_process = self.start_http_server() @@ -43,7 +35,7 @@ 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}" - print(commandline) + try: res = subprocess.run(args = commandline.split(' '), ) diff --git a/bin/contentctl_project/contentctl_core/application/use_cases/initialize.py b/bin/contentctl_project/contentctl_core/application/use_cases/initialize.py index 7929fb7787..2abb6481fa 100644 --- a/bin/contentctl_project/contentctl_core/application/use_cases/initialize.py +++ b/bin/contentctl_project/contentctl_core/application/use_cases/initialize.py @@ -105,24 +105,41 @@ def __init__(self, args): self.app_author_email = args.author_email self.app_author_company = args.author_company self.app_description = args.description - self.generate_custom_manifest() - self.generate_app_configuration_file() - + self.success = self.remove_all_content() - + self.generate_files_and_directories() self.print_results_summary() + def generate_files_and_directories(self): + #Generate files + self.generate_custom_manifest() + self.generate_app_configuration_file() + self.generate_readme() + + #Generate directories? + + def generate_readme(self): + readme_file_path = os.path.join(self.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) - print("format done") app_configuration_file_path = os.path.join(self.path, "default", "app.conf") try: if not os.path.exists(os.path.dirname(app_configuration_file_path)): @@ -166,31 +183,33 @@ def generate_custom_manifest(self): def print_results_summary(self): if self.success is True: - print("repo has been initialized successfully!\n" + print(f"repo has been initialized successfully for app [{self.app_name}]!\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,"Removing Detections"), - (self.remove_baselines,"Removing Baselines"), - (self.remove_investigations,"Removing Investigations"), - (self.remove_lookups,"Removing Lookups"), - (self.remove_macros,"Removing Macros"), - (self.remove_notebooks,"Removing Notebooks"), - (self.remove_playbooks,"Removing Playbooks"), - (self.remove_stories,"Removing Stores"), - (self.remove_tests,"Removing Tests"), - (self.remove_dist_lookups,"Removing Dist Lookups")] + 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]) diff --git a/bin/contentctl_project/contentctl_core/domain/entities/enums/enums.py b/bin/contentctl_project/contentctl_core/domain/entities/enums/enums.py index 848e8f176d..8b26df0fe5 100644 --- a/bin/contentctl_project/contentctl_core/domain/entities/enums/enums.py +++ b/bin/contentctl_project/contentctl_core/domain/entities/enums/enums.py @@ -39,4 +39,5 @@ class SecurityContentType(enum.Enum): class SecurityContentProduct(enum.Enum): ESCU = 1 SSA = 2 - API = 3 \ No newline at end of file + API = 3 + CUSTOM = 4 diff --git a/contentctl.py b/contentctl.py index 37ca5e70b6..c608d29d56 100644 --- a/contentctl.py +++ b/contentctl.py @@ -92,6 +92,10 @@ def generate(args) -> None: 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.") sys.exit(1) @@ -149,9 +153,14 @@ def validate(args) -> None: 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.") sys.exit(1) + factory_input_dto = FactoryInputDto( os.path.abspath(args.path), From ef6ef782817e1a670a2d376f46f0e94e19915407 Mon Sep 17 00:00:00 2001 From: pyth0n1c <87383215+pyth0n1c@users.noreply.github.com> Date: Tue, 17 May 2022 23:28:52 -0700 Subject: [PATCH 24/31] Fixed output dir naming and path for init. --- .../contentctl_core/application/use_cases/initialize.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/bin/contentctl_project/contentctl_core/application/use_cases/initialize.py b/bin/contentctl_project/contentctl_core/application/use_cases/initialize.py index 2abb6481fa..90b6c2e0b8 100644 --- a/bin/contentctl_project/contentctl_core/application/use_cases/initialize.py +++ b/bin/contentctl_project/contentctl_core/application/use_cases/initialize.py @@ -94,7 +94,7 @@ def __init__(self, args): self.items_kept = [] self.items_deleted_failed = [] - self.path = args.path + #Information that will be used for generation of a custom manifest self.app_title = args.title @@ -105,6 +105,8 @@ def __init__(self, args): self.app_author_email = args.author_email self.app_author_company = args.author_company self.app_description = args.description + self.path = os.path.join(args.path, self.app_name) + self.success = self.remove_all_content() self.generate_files_and_directories() @@ -183,7 +185,7 @@ def generate_custom_manifest(self): def print_results_summary(self): if self.success is True: - print(f"repo has been initialized successfully for app [{self.app_name}]!\n" + print(f"repo has been initialized successfully for app [{self.app_name} with output [{self.path}]]!\n" "Ready for your custom constent!") else: print("**Failure(s) initializing repo - check log for details**") From f6790d12c31690bdffae035d8f32296a525f4c7d Mon Sep 17 00:00:00 2001 From: pyth0n1c <87383215+pyth0n1c@users.noreply.github.com> Date: Wed, 18 May 2022 01:50:49 -0700 Subject: [PATCH 25/31] Updated the initialize output path and documentation in contentctl help. --- .../contentctl_core/application/use_cases/initialize.py | 4 ++-- contentctl.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/contentctl_project/contentctl_core/application/use_cases/initialize.py b/bin/contentctl_project/contentctl_core/application/use_cases/initialize.py index 90b6c2e0b8..c9908be5a6 100644 --- a/bin/contentctl_project/contentctl_core/application/use_cases/initialize.py +++ b/bin/contentctl_project/contentctl_core/application/use_cases/initialize.py @@ -105,7 +105,7 @@ def __init__(self, args): self.app_author_email = args.author_email self.app_author_company = args.author_company self.app_description = args.description - self.path = os.path.join(args.path, self.app_name) + self.path = os.path.join(args.path, "dist", self.app_name) self.success = self.remove_all_content() @@ -185,7 +185,7 @@ def generate_custom_manifest(self): def print_results_summary(self): if self.success is True: - print(f"repo has been initialized successfully for app [{self.app_name} with output [{self.path}]]!\n" + print(f"Repo has been initialized successfully for app [{self.app_name}] at path [{self.path}]!\n" "Ready for your custom constent!") else: print("**Failure(s) initializing repo - check log for details**") diff --git a/contentctl.py b/contentctl.py index c608d29d56..0c43719fbd 100644 --- a/contentctl.py +++ b/contentctl.py @@ -344,7 +344,7 @@ def main(args): 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.") + 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.") build_parser.set_defaults(func=build) From 14b16fa87b314269481584611dbc67e6e8ff4149 Mon Sep 17 00:00:00 2001 From: pyth0n1c <87383215+pyth0n1c@users.noreply.github.com> Date: Thu, 11 Aug 2022 12:13:18 -0700 Subject: [PATCH 26/31] Updated some of the documentation in contentctl.py and updated the README file with usage documentation. --- README.md | 21 ++++++++++++--------- contentctl.py | 6 +++--- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 58039bb5e8..4079caef1f 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@

- + # Splunk Security Content ![security_content](docs/static/logo.png) ===== @@ -43,12 +43,16 @@ curl -s https://content.splunkresearch.com | jq ``` # Usage 🧰 -### contentctl.py -The Content Control tool allows you to manipulate Splunk Security Content via the following actions: +### contentctl.py +The Content Control tool allows you to manipulate Splunk Security Content via the following actions: +0. **init** - Initilialize a new repo from scratch so you can easily add your own content to a custom application. Note that this requires a large number of command line arguments, so use python _contentctl.py init --help_ for documentation around those arguments. 1. **new_content** - Creates new content (detection, story, baseline) 2. **validate** - Validates written content 3. **generate** - Generates a deployment package for different platforms (splunk_app) +4. **build** - Builds an application suitable for deployment on a search head using Slim, the Splunk Packaging Toolkit +5. **inspect** - Uses a local version of appinspect to ensure that the app you built meets basic quality standards. +6. **cloud_deploy** - Using ACS, deploy your custom app to a running Splunk Cloud Instance. ### pre-requisites Make sure you use python version 3.9. @@ -64,16 +68,16 @@ pip install -r requirements.txt ### Architecture details for the tooling - [WIKI](https://github.com/splunk/security_content/wiki/Security-Content-Code) -### create a new detection -`python contentctl.py -p . new_content -t detection` +### create a new detection +`python contentctl.py -p . new_content -t detection` for a more indepth write up on how to write content see our [guide](https://github.com/splunk/security_content/wiki/Developing-Content). -### validate security content -`python contentctl.py -p . validate -pr ESCU` +### validate security content +`python contentctl.py -p . validate -pr ESCU` ### generate a splunk app from current content -`python contentctl.py -p . generate -o dist/escu -pr ESCU` +`python contentctl.py -p . generate -o dist/escu -pr ESCU` # MITRE ATT&CK ⚔️ ### Detection Coverage @@ -129,4 +133,3 @@ 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. - diff --git a/contentctl.py b/contentctl.py index fa6278912c..815a1d6a82 100644 --- a/contentctl.py +++ b/contentctl.py @@ -99,7 +99,7 @@ def generate(args) -> None: args.product = 'ESCU' if args.product not in ['ESCU', 'SSA', 'API']: - print("ERROR: invalid product. valid products are ESCU, SSA or API.") + print("ERROR: invalid product. valid products are ESCU, SSA or API. If you are building a custom app, use CUSTOM.") sys.exit(1) @@ -171,7 +171,7 @@ def validate(args) -> None: args.product = 'ESCU' if args.product not in ['ESCU', 'SSA', 'all']: - print("ERROR: invalid product. valid products are all, ESCU or SSA.") + print("ERROR: invalid product. valid products are all, ESCU or SSA. If you are building a custom app, use CUSTOM.") sys.exit(1) @@ -388,7 +388,7 @@ def main(args): 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.") + 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) From f8e3fb6d4a1d0ab851aa15cd8f3a93131f293580 Mon Sep 17 00:00:00 2001 From: pyth0n1c <87383215+pyth0n1c@users.noreply.github.com> Date: Thu, 11 Aug 2022 15:14:17 -0700 Subject: [PATCH 27/31] code to update templates files that must change for an app with different name to work --- .../application/use_cases/initialize.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/bin/contentctl_project/contentctl_core/application/use_cases/initialize.py b/bin/contentctl_project/contentctl_core/application/use_cases/initialize.py index c9908be5a6..25e008a0f9 100644 --- a/bin/contentctl_project/contentctl_core/application/use_cases/initialize.py +++ b/bin/contentctl_project/contentctl_core/application/use_cases/initialize.py @@ -1,8 +1,10 @@ +from logging import shutdown import re import glob import os import copy import json +import shutil APP_CONFIGURATION_FILE = ''' ## Splunk app configuration file @@ -106,19 +108,52 @@ def __init__(self, args): self.app_author_company = args.author_company self.app_description = args.description self.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.path, dirs_exist_ok=True) + 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() + + raw = '''| rest /services/configs/conf-analyticstories splunk_server=local count=0 |search eai:acl.app = "{app_name}"''' + original = raw.format(app_name="DA-ESS-ContentUpdate") + updated = raw.format(app_name=self.app_name) + filename = os.path.join(self.path,"default","data","ui","views","escu_summary") + 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 = "bin/contentctl_project/contentctl_infrastructure/adapter/templates/" + for fname in ["savedsearches_investigations.j2", "savedsearches_detections.j2", "analyticstores_investigations.j2", "analyticstories_detections.j2", "savedsearches_baselines.j2"]: + full_path = os.path.join(filename_root, fname) + self.simple_replace_line(full_path, original, updated) + self.simple_replace_line(filename, original, updated) #Generate directories? def generate_readme(self): From f63281372044c3cab105f4285481d51d4dece688 Mon Sep 17 00:00:00 2001 From: pyth0n1c <87383215+pyth0n1c@users.noreply.github.com> Date: Thu, 11 Aug 2022 16:54:05 -0700 Subject: [PATCH 28/31] Changes to a number of files to make them suitable for use in an app by a different name. --- .../application/use_cases/initialize.py | 49 ++++++++++++++----- .../templates/savedsearches_baselines.j2 | 2 +- .../templates/savedsearches_detections.j2 | 2 +- .../templates/savedsearches_investigations.j2 | 2 +- 4 files changed, 41 insertions(+), 14 deletions(-) diff --git a/bin/contentctl_project/contentctl_core/application/use_cases/initialize.py b/bin/contentctl_project/contentctl_core/application/use_cases/initialize.py index 25e008a0f9..60c970c4bd 100644 --- a/bin/contentctl_project/contentctl_core/application/use_cases/initialize.py +++ b/bin/contentctl_project/contentctl_core/application/use_cases/initialize.py @@ -6,6 +6,11 @@ import json import shutil +CONTENT_VERSION_FILE = ''' +[content-version] +version = {version} +''' + APP_CONFIGURATION_FILE = ''' ## Splunk app configuration file @@ -107,7 +112,8 @@ def __init__(self, args): self.app_author_email = args.author_email self.app_author_company = args.author_company self.app_description = args.description - self.path = os.path.join(args.path, "dist", self.app_name) + 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") @@ -119,7 +125,7 @@ def __init__(self, args): 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.path, dirs_exist_ok=True) + shutil.copytree(self.escu_path, self.dist_app_path, dirs_exist_ok=True) print("done") def simple_replace_line(self, filename:str, original:str,updated:str): @@ -137,27 +143,48 @@ def generate_files_and_directories(self): self.generate_custom_manifest() self.generate_app_configuration_file() self.generate_readme() + self.generate_content_version_file() - raw = '''| rest /services/configs/conf-analyticstories splunk_server=local count=0 |search eai:acl.app = "{app_name}"''' + raw = '''{app_name}''' original = raw.format(app_name="DA-ESS-ContentUpdate") updated = raw.format(app_name=self.app_name) - filename = os.path.join(self.path,"default","data","ui","views","escu_summary") + 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 = "bin/contentctl_project/contentctl_infrastructure/adapter/templates/" - for fname in ["savedsearches_investigations.j2", "savedsearches_detections.j2", "analyticstores_investigations.j2", "analyticstories_detections.j2", "savedsearches_baselines.j2"]: + 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) - self.simple_replace_line(filename, original, updated) #Generate directories? + 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.path, "README.md") + 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)): @@ -177,7 +204,7 @@ def generate_app_configuration_file(self): description=self.app_description, label=self.app_title, id=self.app_name) - app_configuration_file_path = os.path.join(self.path, "default", "app.conf") + 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) @@ -204,7 +231,7 @@ def generate_custom_manifest(self): raise(Exception(f"Failure setting field to generate custom manifest: {str(e)}")) #Output the new manifest file - manifest_path = os.path.join(self.path, "app.manifest") + manifest_path = os.path.join(self.dist_app_path, "app.manifest") try: if not os.path.exists(os.path.dirname(manifest_path)): @@ -220,7 +247,7 @@ def generate_custom_manifest(self): 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.path}]!\n" + 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**") diff --git a/bin/contentctl_project/contentctl_infrastructure/adapter/templates/savedsearches_baselines.j2 b/bin/contentctl_project/contentctl_infrastructure/adapter/templates/savedsearches_baselines.j2 index aa7d1a408b..8cf9692fe7 100644 --- a/bin/contentctl_project/contentctl_infrastructure/adapter/templates/savedsearches_baselines.j2 +++ b/bin/contentctl_project/contentctl_infrastructure/adapter/templates/savedsearches_baselines.j2 @@ -4,7 +4,7 @@ {% for detection in objects %} {% if (detection.type == 'Baseline') %} -[ESCU - {{ detection.name }}] +[MY_NAME - {{ detection.name }}] action.escu = 0 action.escu.enabled = 1 action.escu.search_type = support diff --git a/bin/contentctl_project/contentctl_infrastructure/adapter/templates/savedsearches_detections.j2 b/bin/contentctl_project/contentctl_infrastructure/adapter/templates/savedsearches_detections.j2 index c4155ab1a3..945c2de061 100644 --- a/bin/contentctl_project/contentctl_infrastructure/adapter/templates/savedsearches_detections.j2 +++ b/bin/contentctl_project/contentctl_infrastructure/adapter/templates/savedsearches_detections.j2 @@ -2,7 +2,7 @@ {% for detection in objects %} {% if (detection.type == 'TTP' or detection.type == 'Anomaly' or detection.type == 'Hunting' or detection.type == 'Correlation') %} -[ESCU - {{ detection.name }} - Rule] +[MY_NAME - {{ detection.name }} - Rule] action.escu = 0 action.escu.enabled = 1 {% if detection.deprecated %} diff --git a/bin/contentctl_project/contentctl_infrastructure/adapter/templates/savedsearches_investigations.j2 b/bin/contentctl_project/contentctl_infrastructure/adapter/templates/savedsearches_investigations.j2 index 5b674e6b79..91d6b9fe86 100644 --- a/bin/contentctl_project/contentctl_infrastructure/adapter/templates/savedsearches_investigations.j2 +++ b/bin/contentctl_project/contentctl_infrastructure/adapter/templates/savedsearches_investigations.j2 @@ -5,7 +5,7 @@ {% for detection in objects %} {% if (detection.type == 'Investigation') %} {% if detection.search is defined %} -[ESCU - {{ detection.name }} - Response Task] +[MY_NAME - {{ detection.name }} - Response Task] action.escu = 0 action.escu.enabled = 1 action.escu.search_type = investigative From a17b44daf714fed3184df2123b2a600bbabfd49e Mon Sep 17 00:00:00 2001 From: pyth0n1c <87383215+pyth0n1c@users.noreply.github.com> Date: Fri, 12 Aug 2022 12:09:17 -0700 Subject: [PATCH 29/31] Remove lookups from copied ESCU directory. --- .../contentctl_core/application/use_cases/initialize.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/bin/contentctl_project/contentctl_core/application/use_cases/initialize.py b/bin/contentctl_project/contentctl_core/application/use_cases/initialize.py index 60c970c4bd..6c3779e6f3 100644 --- a/bin/contentctl_project/contentctl_core/application/use_cases/initialize.py +++ b/bin/contentctl_project/contentctl_core/application/use_cases/initialize.py @@ -126,6 +126,11 @@ def __init__(self, args): 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): From 1a02a53f5d872861aa9f884b45eaf3e9a25e25c5 Mon Sep 17 00:00:00 2001 From: pyth0n1c <87383215+pyth0n1c@users.noreply.github.com> Date: Fri, 12 Aug 2022 12:59:53 -0700 Subject: [PATCH 30/31] Updated the order of the options in the help menu for contentctl. This way, the options flow logically from creation to validation to building to deployment. --- contentctl.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/contentctl.py b/contentctl.py index 815a1d6a82..b0ca086944 100644 --- a/contentctl.py +++ b/contentctl.py @@ -322,19 +322,20 @@ def main(args): 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)") - content_changer_parser = actions_parser.add_parser("content_changer", help="Change Security Content based on defined rules") docgen_parser = actions_parser.add_parser("docgen", help="Generates documentation") - new_content_parser = actions_parser.add_parser("new_content", help="Create new security content object") + reporting_parser = actions_parser.add_parser("reporting", help="Create security content reporting") - 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.") + 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.") From 4f6c1f20d3b5d1eb7fdba08638716ca739d467ed Mon Sep 17 00:00:00 2001 From: pyth0n1c <87383215+pyth0n1c@users.noreply.github.com> Date: Fri, 12 Aug 2022 13:15:41 -0700 Subject: [PATCH 31/31] Fixed template files that were modified in error --- .../adapter/templates/savedsearches_baselines.j2 | 2 +- .../adapter/templates/savedsearches_detections.j2 | 2 +- .../adapter/templates/savedsearches_investigations.j2 | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bin/contentctl_project/contentctl_infrastructure/adapter/templates/savedsearches_baselines.j2 b/bin/contentctl_project/contentctl_infrastructure/adapter/templates/savedsearches_baselines.j2 index 8cf9692fe7..aa7d1a408b 100644 --- a/bin/contentctl_project/contentctl_infrastructure/adapter/templates/savedsearches_baselines.j2 +++ b/bin/contentctl_project/contentctl_infrastructure/adapter/templates/savedsearches_baselines.j2 @@ -4,7 +4,7 @@ {% for detection in objects %} {% if (detection.type == 'Baseline') %} -[MY_NAME - {{ detection.name }}] +[ESCU - {{ detection.name }}] action.escu = 0 action.escu.enabled = 1 action.escu.search_type = support diff --git a/bin/contentctl_project/contentctl_infrastructure/adapter/templates/savedsearches_detections.j2 b/bin/contentctl_project/contentctl_infrastructure/adapter/templates/savedsearches_detections.j2 index 945c2de061..c4155ab1a3 100644 --- a/bin/contentctl_project/contentctl_infrastructure/adapter/templates/savedsearches_detections.j2 +++ b/bin/contentctl_project/contentctl_infrastructure/adapter/templates/savedsearches_detections.j2 @@ -2,7 +2,7 @@ {% for detection in objects %} {% if (detection.type == 'TTP' or detection.type == 'Anomaly' or detection.type == 'Hunting' or detection.type == 'Correlation') %} -[MY_NAME - {{ detection.name }} - Rule] +[ESCU - {{ detection.name }} - Rule] action.escu = 0 action.escu.enabled = 1 {% if detection.deprecated %} diff --git a/bin/contentctl_project/contentctl_infrastructure/adapter/templates/savedsearches_investigations.j2 b/bin/contentctl_project/contentctl_infrastructure/adapter/templates/savedsearches_investigations.j2 index 91d6b9fe86..c213e706de 100644 --- a/bin/contentctl_project/contentctl_infrastructure/adapter/templates/savedsearches_investigations.j2 +++ b/bin/contentctl_project/contentctl_infrastructure/adapter/templates/savedsearches_investigations.j2 @@ -5,7 +5,7 @@ {% for detection in objects %} {% if (detection.type == 'Investigation') %} {% if detection.search is defined %} -[MY_NAME - {{ detection.name }} - Response Task] +[ESCU - {{ detection.name }} - Response Task] action.escu = 0 action.escu.enabled = 1 action.escu.search_type = investigative @@ -35,4 +35,4 @@ search = {{ detection.search }} {% endfor %} -### END ESCU RESPONSE TASKS ### \ No newline at end of file +### END ESCU RESPONSE TASKS ###