def validate_dictionary(dict_, dict_key): if dict_key in dict_: if not dict_[dict_key]: return for key, value in dict_[dict_key].items(): if "name" in value: raise ValidationException( f"Deprecated 'name' key '{value}' found when 'title' should be used instead." ) if "title" in value: try: TitleValidator.validate_title(value["title"], plugin_title=False) except Exception as e: raise ValidationException( f"{dict_key} key '{key}' error.", e)
def compare_encodings(key, value): unicode_string = str(value) decoded_string = unicode_string.encode("ascii", "ignore").decode("utf-8") if unicode_string != decoded_string: raise ValidationException( f"A forbidden character was found in the '{key}' field of the workflow.spec.yaml file: {value}" )
def validate_version(version): if re.match("[1-9]+.[0-9]+.[0-9]+$", version) is None: raise ValidationException( "Version does not match required semver format. " "Version should be in form X.Y.Z with X, Y, and Z " "being numbers. No special characters or spaces allowed. " "Versions start at 1.0.0, see https://semver.org/ for more information." )
def validate(self, plugin_spec): cloud_ready = plugin_spec.spec_dictionary().get("cloud_ready") connection = plugin_spec.spec_dictionary().get("connection") if cloud_ready and connection: for key in connection: if connection[key].get("type") == "credential_token": raise ValidationException( "Cloud plugin connection does not support credential_token yet." )
def check_new_inputs_outputs(self, remote: dict, local: dict, input_output: str): # remote, local are dictionaries of action or trigger inputs or outputs if input_output in local: for inner_key in local[input_output]: if inner_key not in remote[input_output]: raise ValidationException( f"New {input_output} added without incrementing minor version." f"{self.MINOR_INSTRUCTIONS_STRING}")
def validate_help_exists(spec): """ Checks that 'help' is not in .yaml In old code help was included in the yaml. It should no longer be there """ if "help" in spec: raise ValidationException( "Help section should exist in help.md and not in the workflow.spec.yaml file." )
def validate_version_quotes(spec): """Requires raw spec to see the quotes""" for line in spec.splitlines(): if line.startswith("version:"): val = line[line.find(" ") + 1:] if "'" in val or '"' in val: raise ValidationException( "Vendor is surrounded by or contains quotes when it should not." )
def check_icon_less_than_equal_70kb(plugin_spec): directory = plugin_spec.directory icon_file = directory + "/" + "icon.png" info = os.stat(icon_file) if info.st_size >= 70000: raise ValidationException( f"Included icon ({info.st_size}) file exceeds maximum size limitation of 70Kb." )
def _hash_python_setup(self) -> MD5: setup_file: str = os.path.join(self.plugin_directory, self._SETUP_PY) try: with open(file=setup_file, mode="rb") as sf: return md5(sf.read()).hexdigest() except FileNotFoundError: raise ValidationException(f"Fatal: No {self._SETUP_PY} found in Python plugin.")
def validate_print(section): print_found = False for f in section: if "print(" in f: print_found = True if print_found: raise ValidationException("One or more files use print statements, update to use self.logger.")
def check_extension_image_file_is_nonzero_size(plugin_spec): directory = plugin_spec.directory extension_image_file = f"{directory}/extension.png" image_file = os.stat(extension_image_file) if not image_file.st_size > 0: raise ValidationException( "Extension image file is size zero. Please include a color PNG image of a logo for this vendor or product." )
def _hash_python_spec(self) -> MD5: spec_file: str = os.path.join(self.plugin_directory, "plugin.spec.yaml") try: with open(file=spec_file, mode="rb") as sf: return md5(sf.read()).hexdigest() except FileNotFoundError: raise ValidationException("Fatal: No plugin spec found in Python plugin.")
def _hash_python_manifest(self) -> MD5: manifest_directory: str = os.path.join(self.plugin_directory, "bin") try: manifest_file: str = os.path.join(manifest_directory, os.listdir(manifest_directory)[0]) with open(file=manifest_file, mode="rb") as mf: return md5(mf.read()).hexdigest() except (FileNotFoundError, IndexError): raise ValidationException("Fatal: No binfile found in Python plugin.")
def check_if_extension_image_file_exists(plugin_spec): directory = plugin_spec.directory extension_image_file = f"{directory}/extension.png" file_item = Path(extension_image_file) if not file_item.is_file(): raise ValidationException( "extension.png file not included in plugin. Please include a color PNG image of a logo for this vendor or product." )
def get_versions(help_content): raw_versions = re.findall(r"Version History\n\n.*?\n\n", help_content, re.DOTALL) if not raw_versions: raise ValidationException("Incorrect Version History in help.md.") versions_history = raw_versions[0].replace("Version History\n\n", "").replace("\n\n", "").split("\n") return versions_history
def validate(self, spec): workflow_file = WorkflowHelpPluginUtilizationValidator.load_workflow_file( spec) workflow = WorkflowHelpPluginUtilizationValidator.extract_workflow( workflow_file) plugins_used = WorkflowHelpPluginUtilizationValidator.extract_plugins_used( workflow) plugin_in_help = WorkflowHelpPluginUtilizationValidator.extract_plugins_in_help( spec.raw_help()) for plugin in plugins_used: if plugin not in plugin_in_help: raise ValidationException( "The following plugin was found in the .icon file," f" but not in the help file {plugin}") for plugin in plugin_in_help: if plugin not in plugins_used: raise ValidationException( "The following plugin was found in the help file," f" but not in the .icon file {plugin}")
def validate(self, spec): """ Checks that .icon file names do not contain spaces """ d = spec.directory for file_name in os.listdir(d): if file_name.endswith(".icon"): if " " in file_name: raise ValidationException(f"The .icon file name was '{file_name}'.\n " ".icon file may not contain spaces use a '_' instead.")
def validate_no_input_new_or_required(self, remote: dict, local: dict): # operates on dictionary of individual action/trigger/task for input_key, input_value in local[SpecConstants.INPUT].items(): if input_value.get(SpecConstants.REQUIRED): if input_key not in remote[SpecConstants.INPUT] or \ not remote[SpecConstants.INPUT][input_key].get(SpecConstants.REQUIRED, False): raise ValidationException( f"Input has been added or changed to required in {input_key} without" f" a major version increment." f"{self.MAJOR_INSTRUCTIONS_STRING}")
def validate(self, spec): """ Checks that the name key in .yaml is the same as the name of the .icon file without the .icon part """ d = spec.directory icon_file = "" for file_name in listdir(d): if file_name.endswith(".icon"): icon_file = file_name[:-5] if "name" not in spec.spec_dictionary(): raise ValidationException( "The workflow name is missing in workflow.spec.yaml.") if not isinstance(spec.spec_dictionary()["name"], str): raise ValidationException( "The workflow name does not contain a string.") name = spec.spec_dictionary()["name"] if not name == icon_file: raise ValidationException( f"The workflow name in workflow.spec.yaml ({name}) and the name of the " f".icon workflow file ({icon_file}) do not match.")
def validate_duplicate_headings(help_raw: str): header_errors = [] for header in HelpValidator.HELP_HEADERS_LIST: normalize_header = header.strip(" #") pattern = re.compile(f"#[ ]*{normalize_header}") if len(pattern.findall(help_raw)) > 1: header_errors.append(f"Please check {header} headings and remove duplicates.") if header_errors: joined_errors = "\n".join(header_errors) raise ValidationException(f"More than one headings in type was found. \n{joined_errors}")
def validate(self, spec): """ Check that the extension.png file is the correct file """ d = spec.directory hasher = sha256() try: with open(f"{d}/extension.png", "rb") as file: temp = file.read() hasher.update(temp) hash_ = hasher.hexdigest() if not hash_ == self._GOOD_HASH: raise ValidationException( "The extension.png file in the workflow directory is incorrect." " The file should have a SHA256 hash of" f" {self._GOOD_HASH}." f" The hash of the provided file was {hash_}") except FileNotFoundError: raise ValidationException( "Extension.png is missing from the workflow directory!")
def get_icon_steps(spec): icon_file = spec.directory for file_name in os.listdir(icon_file): if file_name.endswith(".icon"): data = dict() with open(f"{icon_file}/{file_name}") as json_file: try: data = json.load(json_file) except json.JSONDecodeError: raise ValidationException( "ICON file is not in JSON format try exporting the .icon file again" ) workflow_versions = data.get("kom", {}).get("workflowVersions", []) if workflow_versions: return workflow_versions[0].get("steps") raise ValidationException( "ICON file is not in JSON format try exporting the .icon file again" )
def validate(self, spec: KomandPluginSpec): ConfidentialValidator.validate_help(spec.directory) # ConfidentialValidator.validate_code(spec.directory) ConfidentialValidator.validate_tests(spec.directory) if len(ConfidentialValidator.violations): for violation in ConfidentialValidator.violations: print(f"violation: {violation}") raise ValidationException( f"Please use '*****@*****.**' when including emails. The above items violated this." )
def validate_title(title, file_type): if not isinstance(title, str): raise ValidationException( f"Title must not be blank in {file_type} file.") if title == "": raise ValidationException( f"Title must not be blank in {file_type} file.") if title.endswith("."): raise ValidationException( f"Title ends with period when it should not in {file_type} file." ) if title[0].islower(): # This plugin title is OK: minFraud # This plugin title is OK: ifconfig.co raise ValidationException( f"Title should not start with a lower case letter in {file_type} file." ) if title[0].isspace(): raise ValidationException( f"Title should not start with a whitespace character in {file_type} file." ) for word in title.split(): if not title.startswith(word): if word in title_validation_list: raise ValidationException( f"Title contains a capitalized '{word}' when it should not in {file_type} file." ) elif "By" == word and not title.endswith("By"): # This is OK: Order By # This is NOT OK: Search By String raise ValidationException( f"Title contains a capitalized 'By' when it should not in {file_type} file." ) elif "Of" == word and not title.endswith("Of"): # This is OK: Member Of # This is NOT OK: Type Of String raise ValidationException( f"Title contains a capitalized 'Of' when it should not in {file_type} file." ) elif not word[0].isupper() and not word.capitalize( ) in title_validation_list: if not word.isnumeric(): if not word.lower() == "by" or word.lower() == "of": raise ValidationException( f"Title contains a lowercase '{word}' when it should not in {file_type} file." )
def validate(self, spec): tests_dir = os.path.join(spec.directory, "tests") for path, _, files in os.walk(tests_dir): for name in files: if name.endswith(".json"): with open(os.path.join(tests_dir, name)) as test: try: json.load(test) except json.decoder.JSONDecodeError: JSONValidator.invalid_files.append(name) if len(JSONValidator.invalid_files) > 0: raise ValidationException(f"The following test files are not in proper JSON format: {JSONValidator.invalid_files}")
def validate_support_quotes(spec): """ Check for quotes around the support. """ # Requires raw spec to see the quotes for line in spec.splitlines(): if line.startswith("support:"): val = line[line.find(" ") + 1:] if '"' in val or "'" in val: raise ValidationException( "Support is surrounded by or contains quotes when it should not." )
def validate_support(support): """ Check that support is not komand. Check that support does not end with a . Check that support does not start with a capital letter. Check that support does not contain spaces. """ lsupport = support.lower() if lsupport == "komand": raise ValidationException( "Support 'komand' not allowed. It's likely you meant 'rapid7'." ) if support.endswith("."): raise ValidationException( "Support ends with period when it should not.") if not support[0].islower(): raise ValidationException( "Support starts with a capital letter when it should not.") if " " in support: raise ValidationException( "Support should be separated by underscores, not spaces.")
def validate(self, plugin_spec): connection = plugin_spec.spec_dictionary().get("connection") if connection is None: return else: for key in connection: for sub_key in connection[key]: if sub_key == "type": if connection[key][sub_key] == "password": raise ValidationException( "Remove credentials using type 'password' in plugin spec connection." " Use 'credentials' types instead.")
def validate(self, spec): DescriptionValidator.validate_plugin_description(spec) DescriptionValidator.validate_actions(spec.spec_dictionary(), "actions") DescriptionValidator.validate_actions(spec.spec_dictionary(), "triggers") DescriptionValidator.validate_actions(spec.spec_dictionary(), "tasks") if DescriptionValidator.errors: errors = DescriptionValidator.errors DescriptionValidator.errors = [] raise ValidationException("\n\t\t" + "\n\t\t".join(errors))
def _get_python_main_directory(self) -> str: """ Parses a Python plugin and returns the main directory holding all the actions/triggers/tasks/connection/etc ie. 'komand_base64'. Note: This does NOT return the directory path - JUST the directory name! :return: Main Python plugin directory, ie. 'komand_base64' """ all_ = os.listdir(self.plugin_directory) for a in all_: if os.path.isdir(a) and self.plugin_name in a: return a raise ValidationException("Fatal: Python plugin missing main directory.")