From 9471ef16acaa82f14f4e20fc8e4e0afdefe1f17d Mon Sep 17 00:00:00 2001 From: pyth0n1c <87383215+pyth0n1c@users.noreply.github.com> Date: Thu, 27 Jul 2023 10:54:05 -0700 Subject: [PATCH 1/4] Begin to support abstract types. start with the securitycontentobject and detection object --- .../DetectionTestingInfrastructure.py | 2 +- contentctl/contentctl.py | 1 + .../detection_abstract.py | 157 ++++++++++++++++++ .../security_content_object_abstract.py | 60 +++++++ contentctl/objects/app.py | 3 +- contentctl/objects/constants.py | 2 +- contentctl/objects/detection.py | 149 ++--------------- contentctl/objects/security_content_object.py | 57 +------ 8 files changed, 237 insertions(+), 194 deletions(-) create mode 100644 contentctl/objects/abstract_security_content_objects/detection_abstract.py create mode 100644 contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py diff --git a/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py b/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py index 1bb6a079..e3c60ae6 100644 --- a/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py +++ b/contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py @@ -58,7 +58,7 @@ class DetectionTestingManagerOutputDto: start_time: Union[datetime.datetime, None] = None replay_index: str = "CONTENTCTL_TESTING_INDEX" replay_host: str = "CONTENTCTL_HOST" - timeout_seconds: int = 15 + timeout_seconds: int = 60 terminate: bool = False diff --git a/contentctl/contentctl.py b/contentctl/contentctl.py index c4788bec..f4162525 100644 --- a/contentctl/contentctl.py +++ b/contentctl/contentctl.py @@ -170,6 +170,7 @@ def test(args: argparse.Namespace): local_path=str(pathlib.Path(config.build.path_root)/f"{config.build.name}.tar.gz"), description=config.build.description, splunkbase_path=None, + force_local=True ) # We need to do this instead of appending to retrigger validation. diff --git a/contentctl/objects/abstract_security_content_objects/detection_abstract.py b/contentctl/objects/abstract_security_content_objects/detection_abstract.py new file mode 100644 index 00000000..4b0059ad --- /dev/null +++ b/contentctl/objects/abstract_security_content_objects/detection_abstract.py @@ -0,0 +1,157 @@ +import uuid +import string +import requests +import time +import sys + +from pydantic import BaseModel, validator, root_validator, Extra +from dataclasses import dataclass +from typing import Union +from datetime import datetime, timedelta + + +from contentctl.objects.security_content_object import SecurityContentObject +from contentctl.objects.enums import AnalyticsType +from contentctl.objects.enums import DataModel +from contentctl.objects.enums import DetectionStatus +from contentctl.objects.detection_tags import DetectionTags +from contentctl.objects.config import ConfigDetectionConfiguration +from contentctl.objects.unit_test import UnitTest +from contentctl.objects.macro import Macro +from contentctl.objects.lookup import Lookup +from contentctl.objects.baseline import Baseline +from contentctl.objects.playbook import Playbook +from contentctl.helper.link_validator import LinkValidator +from contentctl.objects.enums import SecurityContentType + + +class Detection_Abstract(SecurityContentObject): + contentType: SecurityContentType = SecurityContentType.detections + type: str + status: DetectionStatus + data_source: list[str] + search: Union[str, dict] + how_to_implement: str + known_false_positives: str + check_references: bool = False + references: list + tags: DetectionTags + tests: list[UnitTest] = [] + + # enrichments + datamodel: list = None + deprecated: bool = None + experimental: bool = None + deployment: ConfigDetectionConfiguration = None + annotations: dict = None + risk: list = None + playbooks: list[Playbook] = None + baselines: list[Baseline] = None + mappings: dict = None + macros: list[Macro] = None + lookups: list[Lookup] = None + cve_enrichment: list = None + splunk_app_enrichment: list = None + file_path: str = None + source: str = None + nes_fields: str = None + providing_technologies: list = None + runtime: str = None + + class Config: + use_enum_values = True + + @validator("type") + def type_valid(cls, v, values): + if v.lower() not in [el.name.lower() for el in AnalyticsType]: + raise ValueError("not valid analytics type: " + values["name"]) + return v + + @validator('how_to_implement') + def encode_error(cls, v, values, field): + return SecurityContentObject.free_text_field_valid(cls,v,values,field) + + # @root_validator + # def search_validation(cls, values): + # if 'ssa_' not in values['file_path']: + # if not '_filter' in values['search']: + # raise ValueError('filter macro missing in: ' + values["name"]) + # if any(x in values['search'] for x in ['eventtype=', 'sourcetype=', ' source=', 'index=']): + # if not 'index=_internal' in values['search']: + # raise ValueError('Use source macro instead of eventtype, sourcetype, source or index in detection: ' + values["name"]) + # return values + + # disable it because of performance reasons + # @validator('references') + # def references_check(cls, v, values): + # return LinkValidator.check_references(v, values["name"]) + # return v + + + @validator("search") + def search_validate(cls, v, values): + # write search validator + return v + + @validator("tests") + def tests_validate(cls, v, values): + if values.get("status","") != DetectionStatus.production and not v: + raise ValueError( + "tests value is needed for production detection: " + values["name"] + ) + return v + + @validator("experimental", always=True) + def experimental_validate(cls, v, values): + if DetectionStatus(values.get("status","")) == DetectionStatus.experimental: + return True + return False + + @validator("deprecated", always=True) + def deprecated_validate(cls, v, values): + if DetectionStatus(values.get("status","")) == DetectionStatus.deprecated: + return True + return False + + @validator("datamodel") + def datamodel_valid(cls, v, values): + for datamodel in v: + if datamodel not in [el.name for el in DataModel]: + raise ValueError("not valid data model: " + values["name"]) + return v + + def all_tests_successful(self) -> bool: + if len(self.tests) == 0: + return False + for test in self.tests: + if test.result is None or test.result.success == False: + return False + return True + + def get_summary( + self, + detection_fields: list[str] = ["name", "search"], + test_model_fields: list[str] = ["success", "message"], + test_job_fields: list[str] = ["resultCount", "runDuration"], + ) -> dict: + summary_dict = {} + for field in detection_fields: + summary_dict[field] = getattr(self, field) + summary_dict["success"] = self.all_tests_successful() + summary_dict["tests"] = [] + for test in self.tests: + result: dict[str, Union[str, bool]] = {"name": test.name} + if test.result is not None: + result.update( + test.result.get_summary_dict( + model_fields=test_model_fields, + job_fields=test_job_fields, + ) + ) + else: + result["success"] = False + result["message"] = "RESULT WAS NONE" + + summary_dict["tests"].append(result) + + return summary_dict diff --git a/contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py b/contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py new file mode 100644 index 00000000..4ae242cf --- /dev/null +++ b/contentctl/objects/abstract_security_content_objects/security_content_object_abstract.py @@ -0,0 +1,60 @@ +import abc +import string +import uuid +from datetime import datetime +from pydantic import BaseModel, validator, ValidationError +from contentctl.objects.enums import SecurityContentType + + +class SecurityContentObject_Abstract(BaseModel, abc.ABC): + contentType: SecurityContentType + name: str + author: str = "UNKNOWN_AUTHOR" + date: str = "1990-01-01" + version: int = 99999 + id: str = None + description: str = "UNKNOWN_DESCRIPTION" + + @validator('name') + def name_max_length(cls, v): + if len(v) > 67: + print("LENGTH ERROR!") + raise ValueError('name is longer then 67 chars: ' + v) + return v + + @validator('name') + def name_invalid_chars(cls, v): + invalidChars = set(string.punctuation.replace("-", "")) + if any(char in invalidChars for char in v): + raise ValueError('invalid chars used in name: ' + v) + return v + + @validator('id',always=True) + def id_check(cls, v, values): + try: + uuid.UUID(str(v)) + except: + #print(f"Generating missing uuid for {values['name']}") + return str(uuid.uuid4()) + raise ValueError('uuid is not valid: ' + values["name"]) + return v + + @validator('date') + def date_valid(cls, v, values): + try: + datetime.strptime(v, "%Y-%m-%d") + except: + raise ValueError('date is not in format YYYY-MM-DD: ' + values["name"]) + return v + + @staticmethod + def free_text_field_valid(input_cls, v, values, field): + try: + v.encode('ascii') + except UnicodeEncodeError: + raise ValueError('encoding error in ' + field.name + ': ' + values["name"]) + return v + + @validator('description') + def description_valid(cls, v, values, field): + return SecurityContentObject_Abstract.free_text_field_valid(cls,v,values,field) diff --git a/contentctl/objects/app.py b/contentctl/objects/app.py index e9a322e1..db7f7194 100644 --- a/contentctl/objects/app.py +++ b/contentctl/objects/app.py @@ -44,6 +44,7 @@ class App(BaseModel, extra=Extra.forbid): # This will be set via a function call and should not be provided in the YML # Note that this is the path relative to the container mount environment_path: str = ENVIRONMENT_PATH_NOT_SET + force_local:bool = False def configure_app_source_for_container( self, @@ -57,7 +58,7 @@ def configure_app_source_for_container( splunkbase_username is not None and splunkbase_password is not None ) - if splunkbase_creds_provided and self.splunkbase_path is not None: + if splunkbase_creds_provided and self.splunkbase_path is not None and not self.force_local: self.environment_path = self.splunkbase_path elif self.local_path is not None: diff --git a/contentctl/objects/constants.py b/contentctl/objects/constants.py index a275a1da..288d32b4 100644 --- a/contentctl/objects/constants.py +++ b/contentctl/objects/constants.py @@ -103,7 +103,7 @@ "File Name": 7, "File Hash": 8, "Process Name": 9, - "Ressource UID": 10, + "Resource UID": 10, "Endpoint": 20, "User": 21, "Email": 22, diff --git a/contentctl/objects/detection.py b/contentctl/objects/detection.py index f0e89469..90501bda 100644 --- a/contentctl/objects/detection.py +++ b/contentctl/objects/detection.py @@ -10,7 +10,7 @@ from datetime import datetime, timedelta -from contentctl.objects.security_content_object import SecurityContentObject +from contentctl.objects.abstract_security_content_objects.detection_abstract import Detection_Abstract from contentctl.objects.enums import AnalyticsType from contentctl.objects.enums import DataModel from contentctl.objects.enums import DetectionStatus @@ -25,140 +25,15 @@ from contentctl.objects.enums import SecurityContentType -class Detection(SecurityContentObject): - # detection spec - #name: str - #id: str - #version: int - #date: str - #author: str - contentType: SecurityContentType = SecurityContentType.detections - type: str - status: DetectionStatus - #description: str - data_source: list[str] - search: Union[str, dict] - how_to_implement: str - known_false_positives: str - check_references: bool = False - references: list - tags: DetectionTags - tests: list[UnitTest] = [] +class Detection(Detection_Abstract): + # Customization to the Detection Class go here. + # You may add fields and/or validations - # enrichments - datamodel: list = None - deprecated: bool = None - experimental: bool = None - deployment: ConfigDetectionConfiguration = None - annotations: dict = None - risk: list = None - playbooks: list[Playbook] = None - baselines: list[Baseline] = None - mappings: dict = None - macros: list[Macro] = None - lookups: list[Lookup] = None - cve_enrichment: list = None - splunk_app_enrichment: list = None - file_path: str = None - source: str = None - nes_fields: str = None - providing_technologies: list = None - runtime: str = None - - class Config: - use_enum_values = True - - @validator("type") - def type_valid(cls, v, values): - if v.lower() not in [el.name.lower() for el in AnalyticsType]: - raise ValueError("not valid analytics type: " + values["name"]) - return v - - @validator('how_to_implement') - def encode_error(cls, v, values, field): - return SecurityContentObject.free_text_field_valid(cls,v,values,field) - - # @root_validator - # def search_validation(cls, values): - # if 'ssa_' not in values['file_path']: - # if not '_filter' in values['search']: - # raise ValueError('filter macro missing in: ' + values["name"]) - # if any(x in values['search'] for x in ['eventtype=', 'sourcetype=', ' source=', 'index=']): - # if not 'index=_internal' in values['search']: - # raise ValueError('Use source macro instead of eventtype, sourcetype, source or index in detection: ' + values["name"]) - # return values - - # disable it because of performance reasons - # @validator('references') - # def references_check(cls, v, values): - # return LinkValidator.check_references(v, values["name"]) - # return v - - - @validator("search") - def search_validate(cls, v, values): - # write search validator - return v - - @validator("tests") - def tests_validate(cls, v, values): - if values.get("status","") != DetectionStatus.production and not v: - raise ValueError( - "tests value is needed for production detection: " + values["name"] - ) - return v - - @validator("experimental", always=True) - def experimental_validate(cls, v, values): - if DetectionStatus(values.get("status","")) == DetectionStatus.experimental: - return True - return False - - @validator("deprecated", always=True) - def deprecated_validate(cls, v, values): - if DetectionStatus(values.get("status","")) == DetectionStatus.deprecated: - return True - return False - - @validator("datamodel") - def datamodel_valid(cls, v, values): - for datamodel in v: - if datamodel not in [el.name for el in DataModel]: - raise ValueError("not valid data model: " + values["name"]) - return v - - def all_tests_successful(self) -> bool: - if len(self.tests) == 0: - return False - for test in self.tests: - if test.result is None or test.result.success == False: - return False - return True - - def get_summary( - self, - detection_fields: list[str] = ["name", "search"], - test_model_fields: list[str] = ["success", "message"], - test_job_fields: list[str] = ["resultCount", "runDuration"], - ) -> dict: - summary_dict = {} - for field in detection_fields: - summary_dict[field] = getattr(self, field) - summary_dict["success"] = self.all_tests_successful() - summary_dict["tests"] = [] - for test in self.tests: - result: dict[str, Union[str, bool]] = {"name": test.name} - if test.result is not None: - result.update( - test.result.get_summary_dict( - model_fields=test_model_fields, - job_fields=test_job_fields, - ) - ) - else: - result["success"] = False - result["message"] = "RESULT WAS NONE" - - summary_dict["tests"].append(result) - - return summary_dict + # You may also experiment with removing fields + # and/or validations, or chagning validation(s). + # Please be aware that many defaults field(s) + # or validation(s) are required and removing or + # them or modifying their behavior may cause + # undefined issues with the contentctl tooling + # or output of the tooling. + pass \ No newline at end of file diff --git a/contentctl/objects/security_content_object.py b/contentctl/objects/security_content_object.py index 1f603af5..611e0f62 100644 --- a/contentctl/objects/security_content_object.py +++ b/contentctl/objects/security_content_object.py @@ -3,58 +3,7 @@ import uuid from datetime import datetime from pydantic import BaseModel, validator, ValidationError -from contentctl.objects.enums import SecurityContentType +from contentctl.objects.abstract_security_content_objects.security_content_object_abstract import SecurityContentObject_Abstract - -class SecurityContentObject(BaseModel, abc.ABC): - contentType: SecurityContentType - name: str - author: str = "UNKNOWN_AUTHOR" - date: str = "1990-01-01" - version: int = 99999 - id: str = None - description: str = "UNKNOWN_DESCRIPTION" - - @validator('name') - def name_max_length(cls, v): - if len(v) > 67: - print("LENGTH ERROR!") - raise ValueError('name is longer then 67 chars: ' + v) - return v - - @validator('name') - def name_invalid_chars(cls, v): - invalidChars = set(string.punctuation.replace("-", "")) - if any(char in invalidChars for char in v): - raise ValueError('invalid chars used in name: ' + v) - return v - - @validator('id',always=True) - def id_check(cls, v, values): - try: - uuid.UUID(str(v)) - except: - #print(f"Generating missing uuid for {values['name']}") - return str(uuid.uuid4()) - raise ValueError('uuid is not valid: ' + values["name"]) - return v - - @validator('date') - def date_valid(cls, v, values): - try: - datetime.strptime(v, "%Y-%m-%d") - except: - raise ValueError('date is not in format YYYY-MM-DD: ' + values["name"]) - return v - - @staticmethod - def free_text_field_valid(input_cls, v, values, field): - try: - v.encode('ascii') - except UnicodeEncodeError: - raise ValueError('encoding error in ' + field.name + ': ' + values["name"]) - return v - - @validator('description') - def description_valid(cls, v, values, field): - return SecurityContentObject.free_text_field_valid(cls,v,values,field) +class SecurityContentObject(SecurityContentObject_Abstract): + pass \ No newline at end of file From 61ef92b74285c1304c9bf934a64478649147264f Mon Sep 17 00:00:00 2001 From: pyth0n1c <87383215+pyth0n1c@users.noreply.github.com> Date: Thu, 27 Jul 2023 17:09:20 -0700 Subject: [PATCH 2/4] Resolve pyyaml issue by removing appinspect support --- contentctl/output/conf_output.py | 23 +++++++++++++++++++---- pyproject.toml | 4 ++-- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/contentctl/output/conf_output.py b/contentctl/output/conf_output.py index 10e213f9..4c2282dd 100644 --- a/contentctl/output/conf_output.py +++ b/contentctl/output/conf_output.py @@ -7,10 +7,7 @@ from typing import Union from pathlib import Path import pathlib -from splunk_appinspect.main import ( - validate, MODE_OPTION, APP_PACKAGE_ARGUMENT, OUTPUT_FILE_OPTION, - LOG_FILE_OPTION, INCLUDED_TAGS_OPTION, EXCLUDED_TAGS_OPTION, - PRECERT_MODE, TEST_MODE) + import shutil from contentctl.output.conf_writer import ConfWriter from contentctl.objects.enums import SecurityContentType @@ -175,6 +172,24 @@ def inspectApp(self)-> None: name_without_version = pathlib.Path(self.config.build.path_root)/f"{self.config.build.name}.tar.gz" shutil.copy2(output_app_expected_name, name_without_version, follow_symlinks=False) + try: + from splunk_appinspect.main import ( + validate, MODE_OPTION, APP_PACKAGE_ARGUMENT, OUTPUT_FILE_OPTION, + LOG_FILE_OPTION, INCLUDED_TAGS_OPTION, EXCLUDED_TAGS_OPTION, + PRECERT_MODE, TEST_MODE) + except Exception as e: + import sys + print("******WARNING******") + if sys.version_info.major == 3 and sys.version_info.minor == 9: + print("The package splunk-appinspect was not installed due to a current issue with the library on Python3.10+. " + "Please use the following commands to set up a virtualenvironment in a different folder so you may run appinspect manually:" + f"\n\tpython3.9 -m venv .venv; source .venv/bin/activate; python3 -m pip install splunk-appinspect; splunk-appinspect inspect {name_without_version} --mode precert") + + else: + print("splunk-appinspect is only compatable with Python3.9 at this time. Please see the following open issue here: https://github.com/splunk/contentctl/issues/28") + print("******WARNING******") + return + # Note that all tags are available and described here: # https://dev.splunk.com/enterprise/reference/appinspect/appinspecttagreference/ # By default, precert mode will run ALL checks. Explicitly included or excluding tags will diff --git a/pyproject.toml b/pyproject.toml index c2a24342..8ae1c599 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ contentctl = 'contentctl.contentctl:main' [tool.poetry.dependencies] python = "^3.9" pydantic = "^1.10.11" -PyYAML = "<6.0" +PyYAML = "^6.0" requests = "^2.28.1" pycvesearch = "^1.2" xmltodict = "^0.13.0" @@ -26,7 +26,7 @@ validators = "^0.20.0" semantic-version = "^2.10.0" bottle = "^0.12.23" tqdm = "^4.65.0" -splunk-appinspect = "^2.36.0" +#splunk-appinspect = "^2.36.0" splunk-packaging-toolkit = "^1.0.1" [tool.poetry.dev-dependencies] From f2be199cb229b2051b3d6819ef2b72a7a30fd2ea Mon Sep 17 00:00:00 2001 From: pyth0n1c <87383215+pyth0n1c@users.noreply.github.com> Date: Thu, 27 Jul 2023 17:15:38 -0700 Subject: [PATCH 3/4] Fixes to temporarily remove splunk-appinspect --- contentctl/actions/inspect.py | 42 -------------------------------- contentctl/contentctl.py | 19 +-------------- contentctl/output/conf_output.py | 1 - 3 files changed, 1 insertion(+), 61 deletions(-) delete mode 100644 contentctl/actions/inspect.py diff --git a/contentctl/actions/inspect.py b/contentctl/actions/inspect.py deleted file mode 100644 index cc21a305..00000000 --- a/contentctl/actions/inspect.py +++ /dev/null @@ -1,42 +0,0 @@ -import os - -from dataclasses import dataclass -import pathlib -import splunk_appinspect - -@dataclass(frozen=True) -class InspectInputDto: - path: pathlib.Path - - -class Inspect: - - def execute(self, input_dto: InspectInputDto) -> None: - ''' - director_output_dto = DirectorOutputDto([],[],[],[],[],[],[],[]) - director = Director(director_output_dto) - director.execute(input_dto.director_input_dto) - - #svg_output = SvgOutput() - #svg_output.writeObjects(director_output_dto.detections, input_dto.output_path) - - attack_nav_output = AttackNavOutput() - attack_nav_output.writeObjects( - director_output_dto.detections, - os.path.join(input_dto.director_input_dto.input_path, "reporting") - ) - ''' - if not input_dto.path.is_file(): - raise Exception(f'Error inspecting {input_dto.path}: The file does not exist') - - - import subprocess - my_args = ["splunk-appinspect", "inspect", "dist/ESCU.tar.gz"] - print(my_args) - subprocess.run(args=my_args) - - - try: - print(f'Inspection of {input_dto.path} successful') - except Exception as e: - raise Exception(f'Error inspecting {input_dto.path}: {str(e)}') \ No newline at end of file diff --git a/contentctl/contentctl.py b/contentctl/contentctl.py index f4162525..183c27ff 100644 --- a/contentctl/contentctl.py +++ b/contentctl/contentctl.py @@ -17,7 +17,6 @@ from contentctl.actions.new_content import NewContentInputDto, NewContent from contentctl.actions.doc_gen import DocGenInputDto, DocGen from contentctl.actions.initialize import Initialize, InitializeInputDto -from contentctl.actions.inspect import InspectInputDto, Inspect from contentctl.actions.api_deploy import API_Deploy, API_DeployInputDto from contentctl.input.director import DirectorInputDto @@ -116,14 +115,6 @@ def build(args, config:Union[Config,None]=None) -> DirectorOutputDto: return generate.execute(generate_input_dto) -def inspect(args) -> None: - config=start(args) - app_path = pathlib.Path(config.build.path_root)/f"{config.build.name}.tar.gz" - input_dto = InspectInputDto(path=app_path) - i = Inspect() - i.execute(input_dto=input_dto) - - def api_deploy(args) -> None: config = start(args) deploy_input_dto = API_DeployInputDto(path=pathlib.Path(args.path), config=config) @@ -339,15 +330,7 @@ def main(): reporting_parser.set_defaults(func=reporting) - inspect_parser.add_argument( - "-ap", - "--app_path", - required=False, - type=str, - default=None, - help="path to the Splunk app to be inspected", - ) - inspect_parser.set_defaults(func=inspect) + api_deploy_parser.set_defaults(func=api_deploy) diff --git a/contentctl/output/conf_output.py b/contentctl/output/conf_output.py index 4c2282dd..aaac72d3 100644 --- a/contentctl/output/conf_output.py +++ b/contentctl/output/conf_output.py @@ -178,7 +178,6 @@ def inspectApp(self)-> None: LOG_FILE_OPTION, INCLUDED_TAGS_OPTION, EXCLUDED_TAGS_OPTION, PRECERT_MODE, TEST_MODE) except Exception as e: - import sys print("******WARNING******") if sys.version_info.major == 3 and sys.version_info.minor == 9: print("The package splunk-appinspect was not installed due to a current issue with the library on Python3.10+. " From 891afe46d3a4b4aea6f87612de05ac0bcd3cc732 Mon Sep 17 00:00:00 2001 From: pyth0n1c <87383215+pyth0n1c@users.noreply.github.com> Date: Thu, 27 Jul 2023 17:21:42 -0700 Subject: [PATCH 4/4] Bump version with PyYAML splunk-appinspect fix --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 8ae1c599..86c6ae31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "contentctl" -version = "2.0.0" +version = "2.0.1" description = "Splunk Content Control Tool" authors = ["STRT "] license = "Apache 2.0"