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 @@