diff --git a/_tests/framework/validation.py b/_tests/framework/validation.py index 667ca11..637ca56 100755 --- a/_tests/framework/validation.py +++ b/_tests/framework/validation.py @@ -21,6 +21,9 @@ import sys import yaml # pylint: disable=import-error +# Default value for unknown action names (matches shared.validation_core.DEFAULT_UNKNOWN) +_DEFAULT_UNKNOWN = "Unknown" + class ActionValidator: """Handles validation of GitHub Action inputs using Python regex engine.""" @@ -86,7 +89,7 @@ class ActionValidator: return True, "" # Check for environment variable reference (e.g., $GITHUB_TOKEN) - if re.match(r"^\$[A-Za-z_][A-Za-z0-9_]*$", token): + if re.match(r"^\$[A-Za-z_]\w*$", token, re.ASCII): return True, "" # Check against all known token patterns @@ -330,16 +333,16 @@ def get_action_name(action_file: str) -> str: action_file: Path to the action.yml file Returns: - Action name or "Unknown" if not found + Action name or _DEFAULT_UNKNOWN if not found """ try: with Path(action_file).open(encoding="utf-8") as f: data = yaml.safe_load(f) - return data.get("name", "Unknown") + return data.get("name", _DEFAULT_UNKNOWN) except Exception: - return "Unknown" + return _DEFAULT_UNKNOWN def _show_usage(): diff --git a/_tests/shared/validation_core.py b/_tests/shared/validation_core.py index 84228a4..f2ac771 100755 --- a/_tests/shared/validation_core.py +++ b/_tests/shared/validation_core.py @@ -25,6 +25,9 @@ from typing import Any import yaml # pylint: disable=import-error +# Default value for unknown items (used by ActionFileParser) +DEFAULT_UNKNOWN = "Unknown" + class ValidationCore: """Core validation functionality with standardized patterns and functions.""" @@ -497,9 +500,9 @@ class ActionFileParser: """Get the action name from an action.yml file.""" try: data = ActionFileParser.load_action_file(action_file) - return data.get("name", "Unknown") + return data.get("name", DEFAULT_UNKNOWN) except (OSError, ValueError, yaml.YAMLError, AttributeError): - return "Unknown" + return DEFAULT_UNKNOWN @staticmethod def get_action_inputs(action_file: str) -> list[str]: diff --git a/codeql-analysis/CustomValidator.py b/codeql-analysis/CustomValidator.py index 0055ef9..d6f4a06 100755 --- a/codeql-analysis/CustomValidator.py +++ b/codeql-analysis/CustomValidator.py @@ -81,21 +81,13 @@ class CustomValidator(BaseValidator): # Validate threads if inputs.get("threads"): - result = self.codeql_validator.validate_threads(inputs["threads"]) - for error in self.codeql_validator.errors: - if error not in self.errors: - self.add_error(error) - self.codeql_validator.clear_errors() - valid &= result + valid &= self.validate_with( + self.codeql_validator, "validate_threads", inputs["threads"] + ) # Validate RAM if inputs.get("ram"): - result = self.codeql_validator.validate_ram(inputs["ram"]) - for error in self.codeql_validator.errors: - if error not in self.errors: - self.add_error(error) - self.codeql_validator.clear_errors() - valid &= result + valid &= self.validate_with(self.codeql_validator, "validate_ram", inputs["ram"]) # Validate debug mode if inputs.get("debug"): @@ -226,19 +218,10 @@ class CustomValidator(BaseValidator): Returns: True if valid, False otherwise """ - # Check for empty queries first if not queries or not queries.strip(): self.add_error("CodeQL queries cannot be empty") return False - - # Use the CodeQL validator - result = self.codeql_validator.validate_codeql_queries(queries) - # Copy any errors from codeql validator - for error in self.codeql_validator.errors: - if error not in self.errors: - self.add_error(error) - self.codeql_validator.clear_errors() - return result + return self.validate_with(self.codeql_validator, "validate_codeql_queries", queries) def validate_categories(self, categories: str) -> bool: """Validate CodeQL categories. @@ -249,14 +232,7 @@ class CustomValidator(BaseValidator): Returns: True if valid, False otherwise """ - # Use the CodeQL validator - result = self.codeql_validator.validate_category_format(categories) - # Copy any errors from codeql validator - for error in self.codeql_validator.errors: - if error not in self.errors: - self.add_error(error) - self.codeql_validator.clear_errors() - return result + return self.validate_with(self.codeql_validator, "validate_category_format", categories) def validate_category(self, category: str) -> bool: """Validate CodeQL category (singular). @@ -267,14 +243,7 @@ class CustomValidator(BaseValidator): Returns: True if valid, False otherwise """ - # Use the CodeQL validator - result = self.codeql_validator.validate_category_format(category) - # Copy any errors from codeql validator - for error in self.codeql_validator.errors: - if error not in self.errors: - self.add_error(error) - self.codeql_validator.clear_errors() - return result + return self.validate_with(self.codeql_validator, "validate_category_format", category) def validate_config_file(self, config_file: str) -> bool: """Validate CodeQL configuration file path. @@ -287,21 +256,11 @@ class CustomValidator(BaseValidator): """ if not config_file or not config_file.strip(): return True - - # Allow GitHub Actions expressions if self.is_github_expression(config_file): return True - - # Use FileValidator for yaml file validation - result = self.file_validator.validate_yaml_file(config_file, "config-file") - - # Copy any errors from file validator - for error in self.file_validator.errors: - if error not in self.errors: - self.add_error(error) - self.file_validator.clear_errors() - - return result + return self.validate_with( + self.file_validator, "validate_yaml_file", config_file, "config-file" + ) def validate_database(self, database: str) -> bool: """Validate CodeQL database path. @@ -312,25 +271,13 @@ class CustomValidator(BaseValidator): Returns: True if valid, False otherwise """ - # Allow GitHub Actions expressions if self.is_github_expression(database): return True - - # Use FileValidator for path validation - result = self.file_validator.validate_file_path(database, "database") - - # Copy any errors from file validator - for error in self.file_validator.errors: - if error not in self.errors: - self.add_error(error) - self.file_validator.clear_errors() - + result = self.validate_with(self.file_validator, "validate_file_path", database, "database") # Database paths often contain the language # e.g., "codeql-database/javascript" or "/tmp/codeql_databases/python" - # Just validate it's a reasonable path after basic validation if result and database.startswith("/tmp/"): # noqa: S108 return True - return result def validate_debug(self, debug: str) -> bool: @@ -342,20 +289,9 @@ class CustomValidator(BaseValidator): Returns: True if valid, False otherwise """ - # Allow GitHub Actions expressions if self.is_github_expression(debug): return True - - # Use BooleanValidator - result = self.boolean_validator.validate_boolean(debug, "debug") - - # Copy any errors from boolean validator - for error in self.boolean_validator.errors: - if error not in self.errors: - self.add_error(error) - self.boolean_validator.clear_errors() - - return result + return self.validate_with(self.boolean_validator, "validate_boolean", debug, "debug") def validate_upload_database(self, upload: str) -> bool: """Validate upload-database setting. @@ -366,20 +302,11 @@ class CustomValidator(BaseValidator): Returns: True if valid, False otherwise """ - # Allow GitHub Actions expressions if self.is_github_expression(upload): return True - - # Use BooleanValidator - result = self.boolean_validator.validate_boolean(upload, "upload-database") - - # Copy any errors from boolean validator - for error in self.boolean_validator.errors: - if error not in self.errors: - self.add_error(error) - self.boolean_validator.clear_errors() - - return result + return self.validate_with( + self.boolean_validator, "validate_boolean", upload, "upload-database" + ) def validate_upload_sarif(self, upload: str) -> bool: """Validate upload-sarif setting. @@ -390,20 +317,11 @@ class CustomValidator(BaseValidator): Returns: True if valid, False otherwise """ - # Allow GitHub Actions expressions if self.is_github_expression(upload): return True - - # Use BooleanValidator - result = self.boolean_validator.validate_boolean(upload, "upload-sarif") - - # Copy any errors from boolean validator - for error in self.boolean_validator.errors: - if error not in self.errors: - self.add_error(error) - self.boolean_validator.clear_errors() - - return result + return self.validate_with( + self.boolean_validator, "validate_boolean", upload, "upload-sarif" + ) def validate_packs(self, packs: str) -> bool: """Validate CodeQL packs. @@ -487,16 +405,9 @@ class CustomValidator(BaseValidator): Returns: True if valid, False otherwise """ - # Use the TokenValidator for proper validation - result = self.token_validator.validate_github_token(token, required=False) - - # Copy any errors from token validator - for error in self.token_validator.errors: - if error not in self.errors: - self.add_error(error) - self.token_validator.clear_errors() - - return result + return self.validate_with( + self.token_validator, "validate_github_token", token, required=False + ) def validate_token(self, token: str) -> bool: """Validate GitHub token. @@ -507,21 +418,12 @@ class CustomValidator(BaseValidator): Returns: True if valid, False otherwise """ - # Check for empty token if not token or not token.strip(): self.add_error("Input 'token' is missing or empty") return False - - # Use the TokenValidator for proper validation - result = self.token_validator.validate_github_token(token, required=True) - - # Copy any errors from token validator - for error in self.token_validator.errors: - if error not in self.errors: - self.add_error(error) - self.token_validator.clear_errors() - - return result + return self.validate_with( + self.token_validator, "validate_github_token", token, required=True + ) def validate_working_directory(self, directory: str) -> bool: """Validate working directory path. @@ -532,20 +434,11 @@ class CustomValidator(BaseValidator): Returns: True if valid, False otherwise """ - # Allow GitHub Actions expressions if self.is_github_expression(directory): return True - - # Use FileValidator for path validation - result = self.file_validator.validate_file_path(directory, "working-directory") - - # Copy any errors from file validator - for error in self.file_validator.errors: - if error not in self.errors: - self.add_error(error) - self.file_validator.clear_errors() - - return result + return self.validate_with( + self.file_validator, "validate_file_path", directory, "working-directory" + ) def validate_upload_results(self, value: str) -> bool: """Validate upload-results boolean value. @@ -556,27 +449,14 @@ class CustomValidator(BaseValidator): Returns: True if valid, False otherwise """ - # Check for empty if not value or not value.strip(): self.add_error("upload-results cannot be empty") return False - - # Allow GitHub Actions expressions if self.is_github_expression(value): return True - - # Check for uppercase TRUE/FALSE first if value in ["TRUE", "FALSE"]: self.add_error("Must be lowercase 'true' or 'false'") return False - - # Use BooleanValidator for normal validation - result = self.boolean_validator.validate_boolean(value, "upload-results") - - # Copy any errors from boolean validator - for error in self.boolean_validator.errors: - if error not in self.errors: - self.add_error(error) - self.boolean_validator.clear_errors() - - return result + return self.validate_with( + self.boolean_validator, "validate_boolean", value, "upload-results" + ) diff --git a/compress-images/CustomValidator.py b/compress-images/CustomValidator.py index b2c771a..5edd8ad 100755 --- a/compress-images/CustomValidator.py +++ b/compress-images/CustomValidator.py @@ -36,47 +36,35 @@ class CustomValidator(BaseValidator): # Validate optional inputs if inputs.get("image-quality"): - result = self.numeric_validator.validate_numeric_range( - inputs["image-quality"], min_val=0, max_val=100 + valid &= self.validate_with( + self.numeric_validator, + "validate_numeric_range", + inputs["image-quality"], + min_val=0, + max_val=100, ) - for error in self.numeric_validator.errors: - if error not in self.errors: - self.add_error(error) - self.numeric_validator.clear_errors() - if not result: - valid = False if inputs.get("png-quality"): - result = self.numeric_validator.validate_numeric_range( - inputs["png-quality"], min_val=0, max_val=100 + valid &= self.validate_with( + self.numeric_validator, + "validate_numeric_range", + inputs["png-quality"], + min_val=0, + max_val=100, ) - for error in self.numeric_validator.errors: - if error not in self.errors: - self.add_error(error) - self.numeric_validator.clear_errors() - if not result: - valid = False if inputs.get("directory"): - result = self.file_validator.validate_file_path(inputs["directory"], "directory") - for error in self.file_validator.errors: - if error not in self.errors: - self.add_error(error) - self.file_validator.clear_errors() - if not result: - valid = False + valid &= self.validate_with( + self.file_validator, "validate_file_path", inputs["directory"], "directory" + ) if inputs.get("ignore-paths"): - # Validate for injection - result = self.security_validator.validate_no_injection( - inputs["ignore-paths"], "ignore-paths" + valid &= self.validate_with( + self.security_validator, + "validate_no_injection", + inputs["ignore-paths"], + "ignore-paths", ) - for error in self.security_validator.errors: - if error not in self.errors: - self.add_error(error) - self.security_validator.clear_errors() - if not result: - valid = False return valid diff --git a/docker-build/CustomValidator.py b/docker-build/CustomValidator.py index e6ed7f8..56ffd1c 100755 --- a/docker-build/CustomValidator.py +++ b/docker-build/CustomValidator.py @@ -65,35 +65,24 @@ class CustomValidator(BaseValidator): # Validate image name if inputs.get("image-name"): - result = self.docker_validator.validate_image_name(inputs["image-name"], "image-name") - # Propagate errors from docker validator - for error in self.docker_validator.errors: - if error not in self.errors: - self.add_error(error) - self.docker_validator.clear_errors() - valid &= result + valid &= self.validate_with( + self.docker_validator, "validate_image_name", inputs["image-name"], "image-name" + ) # Validate tag (singular - as per action.yml) if inputs.get("tag"): - result = self.docker_validator.validate_docker_tag(inputs["tag"], "tag") - # Propagate errors - for error in self.docker_validator.errors: - if error not in self.errors: - self.add_error(error) - self.docker_validator.clear_errors() - valid &= result + valid &= self.validate_with( + self.docker_validator, "validate_docker_tag", inputs["tag"], "tag" + ) # Validate architectures/platforms if inputs.get("architectures"): - result = self.docker_validator.validate_architectures( - inputs["architectures"], "architectures" + valid &= self.validate_with( + self.docker_validator, + "validate_architectures", + inputs["architectures"], + "architectures", ) - # Propagate errors - for error in self.docker_validator.errors: - if error not in self.errors: - self.add_error(error) - self.docker_validator.clear_errors() - valid &= result # Validate build arguments if inputs.get("build-args"): @@ -101,12 +90,9 @@ class CustomValidator(BaseValidator): # Validate push flag if inputs.get("push"): - result = self.boolean_validator.validate_optional_boolean(inputs["push"], "push") - for error in self.boolean_validator.errors: - if error not in self.errors: - self.add_error(error) - self.boolean_validator.clear_errors() - valid &= result + valid &= self.validate_with( + self.boolean_validator, "validate_optional_boolean", inputs["push"], "push" + ) # Validate cache settings if inputs.get("cache-from"): @@ -117,22 +103,35 @@ class CustomValidator(BaseValidator): # Validate cache-mode if inputs.get("cache-mode"): - valid &= self.validate_cache_mode(inputs["cache-mode"]) + valid &= self.validate_enum( + inputs["cache-mode"], + "cache-mode", + ["min", "max", "inline"], + case_sensitive=True, + ) # Validate buildx-version if inputs.get("buildx-version"): - valid &= self.validate_buildx_version(inputs["buildx-version"]) + version = inputs["buildx-version"] + # Allow 'latest' as special value + if version != "latest" and not self.is_github_expression(version): + valid &= self.validate_with( + self.version_validator, + "validate_semantic_version", + version, + "buildx-version", + ) # Validate parallel-builds if inputs.get("parallel-builds"): - result = self.numeric_validator.validate_numeric_range( - inputs["parallel-builds"], min_val=0, max_val=16, name="parallel-builds" + valid &= self.validate_with( + self.numeric_validator, + "validate_numeric_range", + inputs["parallel-builds"], + min_val=0, + max_val=16, + name="parallel-builds", ) - for error in self.numeric_validator.errors: - if error not in self.errors: - self.add_error(error) - self.numeric_validator.clear_errors() - valid &= result # Validate boolean flags for bool_input in [ @@ -144,29 +143,32 @@ class CustomValidator(BaseValidator): "auto-detect-platforms", ]: if inputs.get(bool_input): - result = self.boolean_validator.validate_optional_boolean( - inputs[bool_input], bool_input + valid &= self.validate_with( + self.boolean_validator, + "validate_optional_boolean", + inputs[bool_input], + bool_input, ) - for error in self.boolean_validator.errors: - if error not in self.errors: - self.add_error(error) - self.boolean_validator.clear_errors() - valid &= result # Validate sbom-format if inputs.get("sbom-format"): - valid &= self.validate_sbom_format(inputs["sbom-format"]) + valid &= self.validate_enum( + inputs["sbom-format"], + "sbom-format", + ["spdx-json", "cyclonedx-json", "syft-json"], + case_sensitive=True, + ) # Validate max-retries if inputs.get("max-retries"): - result = self.numeric_validator.validate_numeric_range( - inputs["max-retries"], min_val=0, max_val=10, name="max-retries" + valid &= self.validate_with( + self.numeric_validator, + "validate_numeric_range", + inputs["max-retries"], + min_val=0, + max_val=10, + name="max-retries", ) - for error in self.numeric_validator.errors: - if error not in self.errors: - self.add_error(error) - self.numeric_validator.clear_errors() - valid &= result return valid @@ -209,19 +211,11 @@ class CustomValidator(BaseValidator): Returns: True if valid, False otherwise """ - # Allow GitHub Actions expressions if self.is_github_expression(dockerfile): return True - - # Use file validator for path validation - result = self.file_validator.validate_file_path(dockerfile, "dockerfile") - # Propagate errors - for error in self.file_validator.errors: - if error not in self.errors: - self.add_error(error) - self.file_validator.clear_errors() - - return result + return self.validate_with( + self.file_validator, "validate_file_path", dockerfile, "dockerfile" + ) def validate_context(self, context: str) -> bool: """Validate build context path. @@ -245,10 +239,9 @@ class CustomValidator(BaseValidator): # We allow path traversal for context as Docker needs to access parent directories # Only check for command injection patterns like ; | ` $() dangerous_chars = [";", "|", "`", "$(", "&&", "||"] - for char in dangerous_chars: - if char in context: - self.add_error(f"Command injection detected in context: {context}") - return False + if any(char in context for char in dangerous_chars): + self.add_error(f"Command injection detected in context: {context}") + return False return True @@ -261,15 +254,9 @@ class CustomValidator(BaseValidator): Returns: True if valid, False otherwise """ - # Use docker validator for architectures - result = self.docker_validator.validate_architectures(platforms, "platforms") - # Propagate errors - for error in self.docker_validator.errors: - if error not in self.errors: - self.add_error(error) - self.docker_validator.clear_errors() - - return result + return self.validate_with( + self.docker_validator, "validate_architectures", platforms, "platforms" + ) def validate_build_args(self, build_args: str) -> bool: """Validate build arguments. @@ -353,78 +340,3 @@ class CustomValidator(BaseValidator): # Check for security issues return self.validate_security_patterns(cache_to, "cache-to") - - def validate_cache_mode(self, cache_mode: str) -> bool: - """Validate cache mode. - - Args: - cache_mode: Cache mode value - - Returns: - True if valid, False otherwise - """ - # Allow GitHub Actions expressions - if self.is_github_expression(cache_mode): - return True - - # Valid cache modes - valid_modes = ["min", "max", "inline"] - if cache_mode.lower() not in valid_modes: - self.add_error(f"Invalid cache-mode: {cache_mode}. Must be one of: min, max, inline") - return False - - return True - - def validate_buildx_version(self, version: str) -> bool: - """Validate buildx version. - - Args: - version: Buildx version - - Returns: - True if valid, False otherwise - """ - # Allow GitHub Actions expressions - if self.is_github_expression(version): - return True - - # Allow 'latest' - if version == "latest": - return True - - # Check for security issues (semicolon injection etc) - if not self.validate_security_patterns(version, "buildx-version"): - return False - - # Basic version format validation (e.g., 0.12.0, v0.12.0) - import re - - if not re.match(r"^v?\d+\.\d+(\.\d+)?$", version): - self.add_error(f"Invalid buildx-version format: {version}") - return False - - return True - - def validate_sbom_format(self, sbom_format: str) -> bool: - """Validate SBOM format. - - Args: - sbom_format: SBOM format value - - Returns: - True if valid, False otherwise - """ - # Allow GitHub Actions expressions - if self.is_github_expression(sbom_format): - return True - - # Valid SBOM formats - valid_formats = ["spdx-json", "cyclonedx-json", "syft-json"] - if sbom_format.lower() not in valid_formats: - self.add_error( - f"Invalid sbom-format: {sbom_format}. " - "Must be one of: spdx-json, cyclonedx-json, syft-json" - ) - return False - - return True diff --git a/docker-publish/CustomValidator.py b/docker-publish/CustomValidator.py index afbfd34..4f5fc8b 100755 --- a/docker-publish/CustomValidator.py +++ b/docker-publish/CustomValidator.py @@ -11,6 +11,7 @@ This validator handles Docker publish-specific validation including: from __future__ import annotations from pathlib import Path +import re import sys # Add validate-inputs directory to path to import validators @@ -58,12 +59,9 @@ class CustomValidator(BaseValidator): # Validate platforms if inputs.get("platforms"): - result = self.docker_validator.validate_architectures(inputs["platforms"], "platforms") - for error in self.docker_validator.errors: - if error not in self.errors: - self.add_error(error) - self.docker_validator.clear_errors() - valid &= result + valid &= self.validate_with( + self.docker_validator, "validate_architectures", inputs["platforms"], "platforms" + ) # Validate boolean flags for bool_input in [ @@ -74,18 +72,18 @@ class CustomValidator(BaseValidator): "verbose", ]: if inputs.get(bool_input): - result = self.boolean_validator.validate_optional_boolean( - inputs[bool_input], bool_input + valid &= self.validate_with( + self.boolean_validator, + "validate_optional_boolean", + inputs[bool_input], + bool_input, ) - for error in self.boolean_validator.errors: - if error not in self.errors: - self.add_error(error) - self.boolean_validator.clear_errors() - valid &= result # Validate cache-mode if inputs.get("cache-mode"): - valid &= self.validate_cache_mode(inputs["cache-mode"]) + valid &= self.validate_enum( + inputs["cache-mode"], "cache-mode", ["min", "max", "inline"] + ) # Validate buildx-version if inputs.get("buildx-version"): @@ -96,24 +94,18 @@ class CustomValidator(BaseValidator): valid &= self.validate_username(inputs["dockerhub-username"]) if inputs.get("dockerhub-password"): - # Use token validator for password/token - result = self.token_validator.validate_docker_token( - inputs["dockerhub-password"], "dockerhub-password" + valid &= self.validate_with( + self.token_validator, + "validate_docker_token", + inputs["dockerhub-password"], + "dockerhub-password", ) - for error in self.token_validator.errors: - if error not in self.errors: - self.add_error(error) - self.token_validator.clear_errors() - valid &= result # Validate github-token if inputs.get("github-token"): - result = self.token_validator.validate_github_token(inputs["github-token"]) - for error in self.token_validator.errors: - if error not in self.errors: - self.add_error(error) - self.token_validator.clear_errors() - valid &= result + valid &= self.validate_with( + self.token_validator, "validate_github_token", inputs["github-token"] + ) return valid @@ -156,40 +148,7 @@ class CustomValidator(BaseValidator): Returns: True if valid, False otherwise """ - # Allow GitHub Actions expressions - if self.is_github_expression(registry): - return True - - # Valid registry values according to action description - valid_registries = ["dockerhub", "github", "both"] - if registry.lower() not in valid_registries: - self.add_error( - f"Invalid registry: {registry}. Must be one of: dockerhub, github, or both" - ) - return False - - return True - - def validate_cache_mode(self, cache_mode: str) -> bool: - """Validate cache mode. - - Args: - cache_mode: Cache mode value - - Returns: - True if valid, False otherwise - """ - # Allow GitHub Actions expressions - if self.is_github_expression(cache_mode): - return True - - # Valid cache modes - valid_modes = ["min", "max", "inline"] - if cache_mode.lower() not in valid_modes: - self.add_error(f"Invalid cache-mode: {cache_mode}. Must be one of: min, max, inline") - return False - - return True + return self.validate_enum(registry, "registry", ["dockerhub", "github", "both"]) def validate_buildx_version(self, version: str) -> bool: """Validate buildx version. @@ -213,8 +172,6 @@ class CustomValidator(BaseValidator): return False # Basic version format validation - import re - if not re.match(r"^v?\d+\.\d+(\.\d+)?$", version): self.add_error(f"Invalid buildx-version format: {version}") return False @@ -244,8 +201,6 @@ class CustomValidator(BaseValidator): return False # Docker Hub username rules: lowercase letters, digits, periods, hyphens, underscores - import re - if not re.match(r"^[a-z0-9._-]+$", username.lower()): self.add_error(f"Invalid Docker Hub username format: {username}") return False diff --git a/go-lint/CustomValidator.py b/go-lint/CustomValidator.py index 0fcb000..15a6aad 100755 --- a/go-lint/CustomValidator.py +++ b/go-lint/CustomValidator.py @@ -37,105 +37,78 @@ class CustomValidator(BaseValidator): # Validate working-directory if provided if inputs.get("working-directory"): - result = self.file_validator.validate_file_path( - inputs["working-directory"], "working-directory" + valid &= self.validate_with( + self.file_validator, + "validate_file_path", + inputs["working-directory"], + "working-directory", ) - for error in self.file_validator.errors: - if error not in self.errors: - self.add_error(error) - self.file_validator.clear_errors() - if not result: - valid = False # Validate golangci-lint-version if provided if inputs.get("golangci-lint-version"): value = inputs["golangci-lint-version"] - # Accept 'latest' or version format if value != "latest" and not self.is_github_expression(value): - result = self.version_validator.validate_semantic_version( - value, "golangci-lint-version" + valid &= self.validate_with( + self.version_validator, + "validate_semantic_version", + value, + "golangci-lint-version", ) - for error in self.version_validator.errors: - if error not in self.errors: - self.add_error(error) - self.version_validator.clear_errors() - if not result: - valid = False # Validate go-version if provided if inputs.get("go-version"): value = inputs["go-version"] - # Accept 'stable', 'oldstable' or version format if value not in ["stable", "oldstable"] and not self.is_github_expression(value): - result = self.version_validator.validate_go_version(value, "go-version") - for error in self.version_validator.errors: - if error not in self.errors: - self.add_error(error) - self.version_validator.clear_errors() - if not result: - valid = False + valid &= self.validate_with( + self.version_validator, "validate_go_version", value, "go-version" + ) # Validate config-file if provided if inputs.get("config-file"): - result = self.file_validator.validate_file_path(inputs["config-file"], "config-file") - for error in self.file_validator.errors: - if error not in self.errors: - self.add_error(error) - self.file_validator.clear_errors() - if not result: - valid = False + valid &= self.validate_with( + self.file_validator, "validate_file_path", inputs["config-file"], "config-file" + ) # Validate timeout if provided if inputs.get("timeout"): value = inputs["timeout"] - # Validate timeout format (e.g., 5m, 1h, 30s) - if not self.is_github_expression(value): - timeout_pattern = r"^\d+[smh]$" - if not re.match(timeout_pattern, value): - self.add_error( - f"Invalid timeout format: {value}. Expected format like '5m', '1h', '30s'" - ) - valid = False + if not self.is_github_expression(value) and not re.match(r"^\d+[smh]$", value): + self.add_error( + f"Invalid timeout format: {value}. Expected format like '5m', '1h', '30s'" + ) + valid = False # Validate boolean inputs for field in ["cache", "fail-on-error", "only-new-issues", "disable-all"]: if inputs.get(field): - result = self.boolean_validator.validate_boolean(inputs[field], field) - for error in self.boolean_validator.errors: - if error not in self.errors: - self.add_error(error) - self.boolean_validator.clear_errors() - if not result: - valid = False + valid &= self.validate_with( + self.boolean_validator, "validate_boolean", inputs[field], field + ) # Validate report-format if inputs.get("report-format"): - value = inputs["report-format"] - valid_formats = ["json", "sarif", "github-actions", "colored-line-number", "tab"] - if value not in valid_formats and not self.is_github_expression(value): - self.add_error( - f"Invalid report format: {value}. Must be one of: {', '.join(valid_formats)}" - ) - valid = False + valid &= self.validate_enum( + inputs["report-format"], + "report-format", + ["json", "sarif", "github-actions", "colored-line-number", "tab"], + case_sensitive=True, + ) # Validate max-retries if inputs.get("max-retries"): - result = self.numeric_validator.validate_numeric_range( - inputs["max-retries"], min_val=1, max_val=10, name="max-retries" + valid &= self.validate_with( + self.numeric_validator, + "validate_numeric_range", + inputs["max-retries"], + min_val=1, + max_val=10, + name="max-retries", ) - for error in self.numeric_validator.errors: - if error not in self.errors: - self.add_error(error) - self.numeric_validator.clear_errors() - if not result: - valid = False # Validate enable-linters and disable-linters for field in ["enable-linters", "disable-linters"]: if inputs.get(field): value = inputs[field] - - # First check format - must be comma-separated without spaces if not self.is_github_expression(value): if " " in value: self.add_error(f"Invalid {field} format: spaces not allowed in linter list") @@ -145,15 +118,9 @@ class CustomValidator(BaseValidator): f"Invalid {field} format: must be comma-separated list of linters" ) valid = False - - # Then check for injection - result = self.security_validator.validate_no_injection(value, field) - for error in self.security_validator.errors: - if error not in self.errors: - self.add_error(error) - self.security_validator.clear_errors() - if not result: - valid = False + valid &= self.validate_with( + self.security_validator, "validate_no_injection", value, field + ) return valid diff --git a/npm-publish/CustomValidator.py b/npm-publish/CustomValidator.py index 8200c4f..8a920ac 100755 --- a/npm-publish/CustomValidator.py +++ b/npm-publish/CustomValidator.py @@ -42,109 +42,40 @@ class CustomValidator(BaseValidator): self.add_error("Input 'npm_token' is required") valid = False elif inputs["npm_token"]: - token = inputs["npm_token"] - # Check for NPM classic token format first - if token.startswith("npm_"): - # NPM classic token format: npm_ followed by 36+ alphanumeric characters - if not re.match(r"^npm_[a-zA-Z0-9]{36,}$", token): - self.add_error("Invalid NPM token format") - valid = False - # Also check for injection - result = self.security_validator.validate_no_injection(token, "npm_token") - for error in self.security_validator.errors: - if error not in self.errors: - self.add_error(error) - self.security_validator.clear_errors() - if not result: - valid = False - else: - # Otherwise validate as GitHub token - result = self.token_validator.validate_github_token(token, required=True) - for error in self.token_validator.errors: - if error not in self.errors: - self.add_error(error) - self.token_validator.clear_errors() - if not result: - valid = False + valid &= self._validate_npm_token(inputs["npm_token"]) # Validate registry-url if inputs.get("registry-url"): - url = inputs["registry-url"] - if not self.is_github_expression(url): - # Must be http or https URL - if not url.startswith(("http://", "https://")): - self.add_error("Registry URL must use http or https protocol") - valid = False - else: - # Validate URL format - result = self.network_validator.validate_url(url, "registry-url") - for error in self.network_validator.errors: - if error not in self.errors: - self.add_error(error) - self.network_validator.clear_errors() - if not result: - valid = False + valid &= self._validate_registry_url(inputs["registry-url"]) # Validate scope if inputs.get("scope"): - scope = inputs["scope"] - if not self.is_github_expression(scope): - # Scope must start with @ and contain only valid characters - if not scope.startswith("@"): - self.add_error("Scope must start with @ symbol") - valid = False - elif not re.match(r"^@[a-z0-9][a-z0-9\-_.]*$", scope): - self.add_error( - "Invalid scope format: must be @org-name with lowercase " - "letters, numbers, hyphens, dots, and underscores" - ) - valid = False - - # Check for injection - result = self.security_validator.validate_no_injection(scope, "scope") - for error in self.security_validator.errors: - if error not in self.errors: - self.add_error(error) - self.security_validator.clear_errors() - if not result: - valid = False + valid &= self._validate_scope(inputs["scope"]) # Validate access if inputs.get("access"): - access = inputs["access"] - if not self.is_github_expression(access): - valid_access = ["public", "restricted", "private"] - if access and access not in valid_access: - self.add_error( - f"Invalid access level: {access}. Must be one of: {', '.join(valid_access)}" - ) - valid = False + valid &= self.validate_enum( + inputs["access"], "access", ["public", "restricted", "private"] + ) # Validate boolean inputs (only always-auth and include-merged-tags are strict) for field in ["always-auth", "include-merged-tags"]: if inputs.get(field): - result = self.boolean_validator.validate_boolean(inputs[field], field) - for error in self.boolean_validator.errors: - if error not in self.errors: - self.add_error(error) - self.boolean_validator.clear_errors() - if not result: - valid = False + valid &= self.validate_with( + self.boolean_validator, "validate_boolean", inputs[field], field + ) # provenance and dry-run accept any value (npm handles them) # No validation needed for these # Validate package-version if inputs.get("package-version"): - result = self.version_validator.validate_semantic_version( - inputs["package-version"], "package-version" + valid &= self.validate_with( + self.version_validator, + "validate_semantic_version", + inputs["package-version"], + "package-version", ) - for error in self.version_validator.errors: - if error not in self.errors: - self.add_error(error) - self.version_validator.clear_errors() - if not result: - valid = False # Validate tag if inputs.get("tag"): @@ -161,16 +92,57 @@ class CustomValidator(BaseValidator): # Validate working-directory and ignore-scripts as file paths for field in ["working-directory", "ignore-scripts"]: if inputs.get(field): - result = self.file_validator.validate_path(inputs[field], field) - for error in self.file_validator.errors: - if error not in self.errors: - self.add_error(error) - self.file_validator.clear_errors() - if not result: - valid = False + valid &= self.validate_with( + self.file_validator, "validate_path", inputs[field], field + ) return valid + def _validate_npm_token(self, token: str) -> bool: + """Validate NPM token format.""" + # Check for NPM classic token format first + if token.startswith("npm_"): + # NPM classic token format: npm_ followed by 36+ alphanumeric characters + if not re.match(r"^npm_[a-zA-Z0-9]{36,}$", token): + self.add_error("Invalid NPM token format") + return False + # Also check for injection + return self.validate_with( + self.security_validator, "validate_no_injection", token, "npm_token" + ) + # Otherwise validate as GitHub token + return self.validate_with( + self.token_validator, "validate_github_token", token, required=True + ) + + def _validate_registry_url(self, url: str) -> bool: + """Validate registry URL format.""" + if self.is_github_expression(url): + return True + # Must be http or https URL + if not url.startswith(("http://", "https://")): + self.add_error("Registry URL must use http or https protocol") + return False + # Validate URL format + return self.validate_with(self.network_validator, "validate_url", url, "registry-url") + + def _validate_scope(self, scope: str) -> bool: + """Validate NPM scope format.""" + if self.is_github_expression(scope): + return True + # Scope must start with @ and contain only valid characters + if not scope.startswith("@"): + self.add_error("Scope must start with @ symbol") + return False + if not re.match(r"^@[a-z0-9][a-z0-9\-_.]*$", scope): + self.add_error( + "Invalid scope format: must be @org-name with lowercase " + "letters, numbers, hyphens, dots, and underscores" + ) + return False + # Check for injection + return self.validate_with(self.security_validator, "validate_no_injection", scope, "scope") + def get_required_inputs(self) -> list[str]: """Get list of required inputs.""" return ["npm_token"] diff --git a/php-tests/CustomValidator.py b/php-tests/CustomValidator.py index 57bbf99..3983636 100755 --- a/php-tests/CustomValidator.py +++ b/php-tests/CustomValidator.py @@ -33,59 +33,31 @@ class CustomValidator(BaseValidator): # Validate token (optional) if inputs.get("token"): token = inputs["token"] - result = self.token_validator.validate_github_token(token) - for error in self.token_validator.errors: - if error not in self.errors: - self.add_error(error) - self.token_validator.clear_errors() - if not result: - valid = False - + valid &= self.validate_with(self.token_validator, "validate_github_token", token) # Also check for variable expansion if not self.is_github_expression(token): - result = self.security_validator.validate_no_injection(token, "token") - for error in self.security_validator.errors: - if error not in self.errors: - self.add_error(error) - self.security_validator.clear_errors() - if not result: - valid = False + valid &= self.validate_with( + self.security_validator, "validate_no_injection", token, "token" + ) # Validate email (optional, empty means use default) - if "email" in inputs and inputs["email"] and inputs["email"] != "": + if inputs.get("email"): email = inputs["email"] - result = self.network_validator.validate_email(email, "email") - for error in self.network_validator.errors: - if error not in self.errors: - self.add_error(error) - self.network_validator.clear_errors() - if not result: - valid = False - + valid &= self.validate_with(self.network_validator, "validate_email", email, "email") # Also check for shell metacharacters (but allow @ and .) if not self.is_github_expression(email): - # Only check for dangerous shell metacharacters, not @ or . dangerous_chars = [";", "&", "|", "`", "$", "(", ")", "<", ">", "\n", "\r"] - for char in dangerous_chars: - if char in email: - self.add_error(f"email: Contains dangerous character '{char}'") - valid = False - break + if any(char in email for char in dangerous_chars): + self.add_error("email: Contains dangerous shell metacharacter") + valid = False # Validate username (optional) if inputs.get("username"): username = inputs["username"] if not self.is_github_expression(username): - # Check for injection - result = self.security_validator.validate_no_injection(username, "username") - for error in self.security_validator.errors: - if error not in self.errors: - self.add_error(error) - self.security_validator.clear_errors() - if not result: - valid = False - - # Check username length (GitHub usernames are max 39 characters) + valid &= self.validate_with( + self.security_validator, "validate_no_injection", username, "username" + ) if len(username) > 39: self.add_error("Username is too long (max 39 characters)") valid = False diff --git a/pre-commit/CustomValidator.py b/pre-commit/CustomValidator.py index 7dc8375..208df93 100755 --- a/pre-commit/CustomValidator.py +++ b/pre-commit/CustomValidator.py @@ -34,74 +34,45 @@ class CustomValidator(BaseValidator): # Validate pre-commit-config if provided if "pre-commit-config" in inputs: - result = self.file_validator.validate_file_path( - inputs["pre-commit-config"], "pre-commit-config" + valid &= self.validate_with( + self.file_validator, + "validate_file_path", + inputs["pre-commit-config"], + "pre-commit-config", ) - for error in self.file_validator.errors: - if error not in self.errors: - self.add_error(error) - self.file_validator.clear_errors() - if not result: - valid = False # Validate base-branch if provided (just check for injection) if inputs.get("base-branch"): - # Check for dangerous characters that could cause shell injection - result = self.security_validator.validate_no_injection( - inputs["base-branch"], "base-branch" + valid &= self.validate_with( + self.security_validator, + "validate_no_injection", + inputs["base-branch"], + "base-branch", ) - for error in self.security_validator.errors: - if error not in self.errors: - self.add_error(error) - self.security_validator.clear_errors() - if not result: - valid = False # Validate token if provided if inputs.get("token"): - result = self.token_validator.validate_github_token(inputs["token"]) - for error in self.token_validator.errors: - if error not in self.errors: - self.add_error(error) - self.token_validator.clear_errors() - if not result: - valid = False + valid &= self.validate_with( + self.token_validator, "validate_github_token", inputs["token"] + ) # Validate commit_user if provided (allow spaces for Git usernames) - # Check both underscore and hyphen versions since inputs can have either - commit_user_key = ( - "commit_user" - if "commit_user" in inputs - else "commit-user" - if "commit-user" in inputs - else None - ) + commit_user_key = self.get_key_variant(inputs, "commit_user", "commit-user") if commit_user_key and inputs[commit_user_key]: - # Check for dangerous injection patterns value = inputs[commit_user_key] - if any(char in value for char in [";", "&", "|", "`", "$", "(", ")", "\n", "\r"]): + if any(c in value for c in [";", "&", "|", "`", "$", "(", ")", "\n", "\r"]): self.add_error(f"{commit_user_key}: Contains potentially dangerous characters") valid = False # Validate commit_email if provided - # Check both underscore and hyphen versions - commit_email_key = ( - "commit_email" - if "commit_email" in inputs - else "commit-email" - if "commit-email" in inputs - else None - ) + commit_email_key = self.get_key_variant(inputs, "commit_email", "commit-email") if commit_email_key and inputs[commit_email_key]: - result = self.network_validator.validate_email( - inputs[commit_email_key], commit_email_key + valid &= self.validate_with( + self.network_validator, + "validate_email", + inputs[commit_email_key], + commit_email_key, ) - for error in self.network_validator.errors: - if error not in self.errors: - self.add_error(error) - self.network_validator.clear_errors() - if not result: - valid = False return valid diff --git a/python-lint-fix/CustomValidator.py b/python-lint-fix/CustomValidator.py index f54ffb3..cff3615 100755 --- a/python-lint-fix/CustomValidator.py +++ b/python-lint-fix/CustomValidator.py @@ -31,68 +31,42 @@ class CustomValidator(BaseValidator): valid = True # Validate python-version if provided - if "python-version" in inputs or "python_version" in inputs: - key = "python-version" if "python-version" in inputs else "python_version" - value = inputs[key] - - # Empty string should fail validation - if value == "": + version_key = self.get_key_variant(inputs, "python-version", "python_version") + if version_key: + value = inputs[version_key] + if not value: self.add_error("Python version cannot be empty") valid = False - elif value: - result = self.version_validator.validate_python_version(value, key) - - # Propagate errors from the version validator - for error in self.version_validator.errors: - if error not in self.errors: - self.add_error(error) - - self.version_validator.clear_errors() - - if not result: - valid = False + else: + valid &= self.validate_with( + self.version_validator, "validate_python_version", value, version_key + ) # Validate username - if "username" in inputs: + if inputs.get("username"): username = inputs["username"] - if username: - # Check username length (GitHub usernames are max 39 characters) - if len(username) > 39: - self.add_error("Username is too long (max 39 characters)") - valid = False - # Check for command injection patterns - if ";" in username or "`" in username or "$" in username: - self.add_error("Username contains potentially dangerous characters") - valid = False + if len(username) > 39: + self.add_error("Username is too long (max 39 characters)") + valid = False + if ";" in username or "`" in username or "$" in username: + self.add_error("Username contains potentially dangerous characters") + valid = False # Validate email - if "email" in inputs: - email = inputs["email"] - if email: - result = self.network_validator.validate_email(email, "email") - for error in self.network_validator.errors: - if error not in self.errors: - self.add_error(error) - self.network_validator.clear_errors() - if not result: - valid = False + if inputs.get("email"): + valid &= self.validate_with( + self.network_validator, "validate_email", inputs["email"], "email" + ) # Validate token - if "token" in inputs: + if inputs.get("token"): token = inputs["token"] - if token: - # Check for variable expansion (but allow GitHub Actions expressions) - if "${" in token and not token.startswith("${{ ") and not token.endswith(" }}"): - self.add_error("Token contains potentially dangerous variable expansion") - valid = False - else: - result = self.token_validator.validate_github_token(token) - for error in self.token_validator.errors: - if error not in self.errors: - self.add_error(error) - self.token_validator.clear_errors() - if not result: - valid = False + # Check for variable expansion (but allow GitHub Actions expressions) + if "${" in token and not token.startswith("${{ ") and not token.endswith(" }}"): + self.add_error("Token contains potentially dangerous variable expansion") + valid = False + else: + valid &= self.validate_with(self.token_validator, "validate_github_token", token) return valid diff --git a/sync-labels/CustomValidator.py b/sync-labels/CustomValidator.py index b46f21d..5ce4338 100755 --- a/sync-labels/CustomValidator.py +++ b/sync-labels/CustomValidator.py @@ -78,16 +78,9 @@ class CustomValidator(BaseValidator): # Validate token if provided if "token" in inputs: - token_valid = self.token_validator.validate_github_token( - inputs["token"], - required=False, # Token is optional, defaults to ${{ github.token }} + valid &= self.validate_with( + self.token_validator, "validate_github_token", inputs["token"], required=False ) - # Copy any errors from token validator - for error in self.token_validator.errors: - if error not in self.errors: - self.add_error(error) - self.token_validator.clear_errors() - valid &= token_valid return valid @@ -100,27 +93,15 @@ class CustomValidator(BaseValidator): Returns: True if valid, False otherwise """ - # Allow GitHub Actions expressions if self.is_github_expression(path): return True - # First check basic file path security - result = self.file_validator.validate_file_path(path, "labels") - # Copy any errors from file validator - for error in self.file_validator.errors: - if error not in self.errors: - self.add_error(error) - self.file_validator.clear_errors() - + result = self.validate_with(self.file_validator, "validate_file_path", path, "labels") if not result: return False - # Check file extension if not (path.endswith(".yml") or path.endswith(".yaml")): self.add_error(f'Invalid labels file: "{path}". Must be a .yml or .yaml file') return False - # Additional custom validation could go here - # For example, checking if the file exists, validating YAML structure, etc. - return True diff --git a/terraform-lint-fix/CustomValidator.py b/terraform-lint-fix/CustomValidator.py index 6d5e9b4..e7cc877 100755 --- a/terraform-lint-fix/CustomValidator.py +++ b/terraform-lint-fix/CustomValidator.py @@ -30,54 +30,32 @@ class CustomValidator(BaseValidator): """Validate terraform-lint-fix action inputs.""" valid = True - # Validate terraform-version if provided - if "terraform-version" in inputs: - value = inputs["terraform-version"] + # Validate terraform-version if provided (empty is OK - uses default) + if inputs.get("terraform-version"): + valid &= self.validate_with( + self.version_validator, + "validate_terraform_version", + inputs["terraform-version"], + "terraform-version", + ) - # Empty string is OK - uses default - if value == "": - pass # Allow empty, will use default - elif value: - result = self.version_validator.validate_terraform_version( - value, "terraform-version" - ) - - # Propagate errors from the version validator - for error in self.version_validator.errors: - if error not in self.errors: - self.add_error(error) - - self.version_validator.clear_errors() - - if not result: - valid = False - - # Validate token if provided - if "token" in inputs: - value = inputs["token"] - if value == "": - # Empty token is OK - uses default - pass - elif value: - result = self.token_validator.validate_github_token(value, required=False) - for error in self.token_validator.errors: - if error not in self.errors: - self.add_error(error) - self.token_validator.clear_errors() - if not result: - valid = False + # Validate token if provided (empty is OK - uses default) + if inputs.get("token"): + valid &= self.validate_with( + self.token_validator, + "validate_github_token", + inputs["token"], + required=False, + ) # Validate working-directory if provided - if "working-directory" in inputs: - value = inputs["working-directory"] - if value: - result = self.file_validator.validate_file_path(value, "working-directory") - for error in self.file_validator.errors: - if error not in self.errors: - self.add_error(error) - self.file_validator.clear_errors() - if not result: - valid = False + if inputs.get("working-directory"): + valid &= self.validate_with( + self.file_validator, + "validate_file_path", + inputs["working-directory"], + "working-directory", + ) return valid diff --git a/validate-inputs/CustomValidator.py b/validate-inputs/CustomValidator.py index e3dfd59..44d288b 100755 --- a/validate-inputs/CustomValidator.py +++ b/validate-inputs/CustomValidator.py @@ -27,57 +27,45 @@ class CustomValidator(BaseValidator): self.boolean_validator = BooleanValidator() self.file_validator = FileValidator() - def validate_inputs(self, inputs: dict[str, str]) -> bool: # pylint: disable=too-many-branches + def validate_inputs(self, inputs: dict[str, str]) -> bool: """Validate validate-inputs action inputs.""" valid = True # Validate action/action-type input - if "action" in inputs or "action-type" in inputs: - action_input = inputs.get("action") or inputs.get("action-type", "") - # Check for empty action + action_key = self.get_key_variant(inputs, "action", "action-type") + if action_key: + action_input = inputs[action_key] if action_input == "": self.add_error("Action name cannot be empty") valid = False - # Allow GitHub expressions - elif action_input.startswith("${{") and action_input.endswith("}}"): - pass # GitHub expressions are valid - # Check for dangerous characters - elif any( - char in action_input - for char in [";", "`", "$", "&", "|", ">", "<", "\n", "\r", "/"] - ): - self.add_error(f"Invalid characters in action name: {action_input}") - valid = False - # Validate action name format (should be lowercase with hyphens or underscores) - elif action_input and not re.match(r"^[a-z][a-z0-9_-]*[a-z0-9]$", action_input): - self.add_error(f"Invalid action name format: {action_input}") - valid = False + elif not self.is_github_expression(action_input): + # Only validate non-GitHub expressions + if any( + char in action_input + for char in [";", "`", "$", "&", "|", ">", "<", "\n", "\r", "/"] + ): + self.add_error(f"Invalid characters in action name: {action_input}") + valid = False + elif action_input and not re.match(r"^[a-z][a-z0-9_-]*[a-z0-9]$", action_input): + self.add_error(f"Invalid action name format: {action_input}") + valid = False # Validate rules-file if provided if inputs.get("rules-file"): - result = self.file_validator.validate_file_path(inputs["rules-file"], "rules-file") - for error in self.file_validator.errors: - if error not in self.errors: - self.add_error(error) - self.file_validator.clear_errors() - if not result: - valid = False + valid &= self.validate_with( + self.file_validator, "validate_file_path", inputs["rules-file"], "rules-file" + ) # Validate fail-on-error boolean if "fail-on-error" in inputs: value = inputs["fail-on-error"] - # Reject empty string if value == "": self.add_error("fail-on-error cannot be empty") valid = False elif value: - result = self.boolean_validator.validate_boolean(value, "fail-on-error") - for error in self.boolean_validator.errors: - if error not in self.errors: - self.add_error(error) - self.boolean_validator.clear_errors() - if not result: - valid = False + valid &= self.validate_with( + self.boolean_validator, "validate_boolean", value, "fail-on-error" + ) return valid diff --git a/validate-inputs/tests/test_conventions.py b/validate-inputs/tests/test_conventions.py index c1941f9..c34f530 100644 --- a/validate-inputs/tests/test_conventions.py +++ b/validate-inputs/tests/test_conventions.py @@ -895,7 +895,7 @@ optional_inputs: self.validator._validate_multi_value_enum("test", "input", valid_values=["only_one"]) raise AssertionError("Should raise ValueError for single value") except ValueError as e: - assert "at least 2 valid values" in str(e) + assert ">= 2 values" in str(e) # Should raise ValueError if more than max_values try: @@ -906,7 +906,7 @@ optional_inputs: ) raise AssertionError("Should raise ValueError for 11 values") except ValueError as e: - assert "at most 10 valid values" in str(e) + assert "<= 10 values" in str(e) def test_validate_exit_code_list_valid(self): """Test exit code list validation with valid values.""" diff --git a/validate-inputs/validators/base.py b/validate-inputs/validators/base.py index 775493b..f20ed4a 100644 --- a/validate-inputs/validators/base.py +++ b/validate-inputs/validators/base.py @@ -227,3 +227,82 @@ class BaseValidator(ABC): or ("${{" in value and "}}" in value) or (value.strip().startswith("${{") and value.strip().endswith("}}")) ) + + def propagate_errors(self, validator: BaseValidator, result: bool) -> bool: + """Copy errors from another validator and return result. + + Args: + validator: The validator to copy errors from + result: The validation result to return + + Returns: + The result parameter unchanged + """ + for error in validator.errors: + if error not in self.errors: + self.add_error(error) + validator.clear_errors() + return result + + def validate_with( + self, validator: BaseValidator, method: str, *args: Any, **kwargs: Any + ) -> bool: + """Call validator method and propagate errors. + + Args: + validator: The validator instance to use + method: The method name to call on the validator + *args: Positional arguments to pass to the method + **kwargs: Keyword arguments to pass to the method + + Returns: + The validation result + """ + result = getattr(validator, method)(*args, **kwargs) + return self.propagate_errors(validator, result) + + def validate_enum( + self, + value: str, + name: str, + valid_values: list[str], + *, + case_sensitive: bool = False, + ) -> bool: + """Validate value is one of allowed options. + + Args: + value: The value to validate + name: The name of the input for error messages + valid_values: List of allowed values + case_sensitive: Whether comparison should be case sensitive + + Returns: + True if value is valid or empty/GitHub expression, False otherwise + """ + if not value or self.is_github_expression(value): + return True + check = value if case_sensitive else value.lower() + allowed = valid_values if case_sensitive else [v.lower() for v in valid_values] + if check not in allowed: + self.add_error(f"Invalid {name}: {value}. Must be one of: {', '.join(valid_values)}") + return False + return True + + @staticmethod + def get_key_variant(inputs: dict[str, str], *variants: str) -> str | None: + """Get first matching key variant from inputs. + + Useful for inputs that may use underscore or hyphen variants. + + Args: + inputs: Dictionary of inputs to check + *variants: Key variants to search for in order + + Returns: + The first matching key, or None if no match + """ + for key in variants: + if key in inputs: + return key + return None diff --git a/validate-inputs/validators/conventions.py b/validate-inputs/validators/conventions.py index 3d87df8..899af95 100644 --- a/validate-inputs/validators/conventions.py +++ b/validate-inputs/validators/conventions.py @@ -5,6 +5,7 @@ This validator automatically applies validation based on input naming convention from __future__ import annotations +import re from pathlib import Path from typing import Any @@ -424,7 +425,10 @@ class ConventionBasedValidator(BaseValidator): if error not in self.errors: self.add_error(error) # Clear the module's errors after copying - validator_module.errors = [] + if hasattr(validator_module, "clear_errors"): + validator_module.clear_errors() + else: + validator_module.errors = [] return result # Method not found, skip validation @@ -629,7 +633,8 @@ class ConventionBasedValidator(BaseValidator): Args: value: The comma-separated list value input_name: The input name for error messages - item_pattern: Regex pattern each item must match (default: alphanumeric+hyphens+underscores) + item_pattern: Regex pattern each item must match + (default: alphanumeric+hyphens+underscores) valid_items: Optional list of valid items for enum-style validation check_injection: Whether to check for shell injection patterns item_name: Descriptive name for items in error messages (e.g., "linter", "extension") @@ -654,8 +659,6 @@ class ConventionBasedValidator(BaseValidator): ... ) True """ - import re - if not value or value.strip() == "": return True # Optional @@ -895,14 +898,12 @@ class ConventionBasedValidator(BaseValidator): # Validate valid_values count if len(valid_values) < min_values: - raise ValueError( - f"Multi-value enum requires at least {min_values} valid values, got {len(valid_values)}" - ) + msg = f"Multi-value enum needs >= {min_values} values, got {len(valid_values)}" + raise ValueError(msg) if len(valid_values) > max_values: - raise ValueError( - f"Multi-value enum supports at most {max_values} valid values, got {len(valid_values)}" - ) + msg = f"Multi-value enum allows <= {max_values} values, got {len(valid_values)}" + raise ValueError(msg) if not value or value.strip() == "": return True # Optional @@ -1024,8 +1025,6 @@ class ConventionBasedValidator(BaseValidator): Returns: True if valid, False otherwise """ - import re - if not value or value.strip() == "": return True # Optional @@ -1123,8 +1122,6 @@ class ConventionBasedValidator(BaseValidator): Valid: "0", "0,1,2", "5,10,15", "0,130", "" Invalid: "256", "0,256", "-1", "0,abc", "0,,1" """ - import re - if not value or value.strip() == "": return True # Optional @@ -1169,8 +1166,10 @@ class ConventionBasedValidator(BaseValidator): Args: value: The key-value list value (comma-separated KEY=VALUE pairs) input_name: The input name for error messages - key_pattern: Regex pattern for key validation (default: alphanumeric+underscores+hyphens) - check_injection: Whether to check for shell injection patterns in values (default: True) + key_pattern: Regex pattern for key validation + (default: alphanumeric+underscores+hyphens) + check_injection: Whether to check for shell injection patterns + in values (default: True) Returns: True if valid, False otherwise @@ -1179,7 +1178,6 @@ class ConventionBasedValidator(BaseValidator): Valid: "KEY=value", "KEY1=value1,KEY2=value2", "BUILD_ARG=hello", "" Invalid: "KEY", "=value", "KEY=", "KEY=value,", "KEY=val;whoami" """ - import re if not value or value.strip() == "": return True # Optional @@ -1260,8 +1258,6 @@ class ConventionBasedValidator(BaseValidator): Returns: bool: True if valid, False otherwise """ - import re - if not value or value.strip() == "": return True # Optional @@ -1412,8 +1408,6 @@ class ConventionBasedValidator(BaseValidator): Returns: bool: True if valid, False otherwise """ - import re - if not value or value.strip() == "": return True # Optional diff --git a/validate-inputs/validators/token.py b/validate-inputs/validators/token.py index a3f9670..240b9ee 100644 --- a/validate-inputs/validators/token.py +++ b/validate-inputs/validators/token.py @@ -12,7 +12,8 @@ class TokenValidator(BaseValidator): """Validator for various authentication tokens.""" # Token patterns for different token types (based on official GitHub documentation) - # https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/about-authentication-to-github#githubs-token-formats + # See: https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/ + # about-authentication-to-github#githubs-token-formats # Note: The lengths include the prefix TOKEN_PATTERNS: ClassVar[dict[str, str]] = { # Personal access token (classic):