Files
actions/validate-inputs/validators/conventions.py
Ismo Vuorinen ab371bdebf feat: simplify actions (#353)
* feat: first pass simplification

* refactor: simplify actions repository structure

Major simplification reducing actions from 44 to 30:

Consolidations:
- Merge biome-check + biome-fix → biome-lint (mode: check/fix)
- Merge eslint-check + eslint-fix → eslint-lint (mode: check/fix)
- Merge prettier-check + prettier-fix → prettier-lint (mode: check/fix)
- Merge 5 version-detect actions → language-version-detect (language param)

Removals:
- common-file-check, common-retry (better served by external tools)
- docker-publish-gh, docker-publish-hub (consolidated into docker-publish)
- github-release (redundant with existing tooling)
- set-git-config (no longer needed)
- version-validator (functionality moved to language-version-detect)

Fixes:
- Rewrite docker-publish to use official Docker actions directly
- Update validate-inputs example (eslint-fix → eslint-lint)
- Update tests and documentation for new structure

Result: ~6,000 lines removed, cleaner action catalog, maintained functionality.

* refactor: complete action simplification and cleanup

Remove deprecated actions and update remaining actions:

Removed:
- common-file-check, common-retry: utility actions
- docker-publish-gh, docker-publish-hub: replaced by docker-publish wrapper
- github-release, version-validator, set-git-config: no longer needed
- Various version-detect actions: replaced by language-version-detect

Updated:
- docker-publish: rewrite as simple wrapper using official Docker actions
- validate-inputs: update example (eslint-fix → eslint-lint)
- Multiple actions: update configurations and remove deprecated dependencies
- Tests: update integration/unit tests for new structure
- Documentation: update README, remove test for deleted actions

Configuration updates:
- Linter configs, ignore files for new structure
- Makefile, pyproject.toml updates

* fix: enforce POSIX compliance in GitHub workflows

Convert all workflow shell scripts to POSIX-compliant sh:

Critical fixes:
- Replace bash with sh in all shell declarations
- Replace [[ with [ for test conditions
- Replace == with = for string comparisons
- Replace set -euo pipefail with set -eu
- Split compound AND conditions into separate [ ] tests

Files updated:
- .github/workflows/test-actions.yml (7 shell declarations, 10 test operators)
- .github/workflows/security-suite.yml (set -eu)
- .github/workflows/action-security.yml (2 shell declarations)
- .github/workflows/pr-lint.yml (3 shell declarations)
- .github/workflows/issue-stats.yml (1 shell declaration)

Ensures compatibility with minimal sh implementations and aligns with
CLAUDE.md standards requiring POSIX shell compliance across all scripts.

All tests pass: 764 pytest tests, 100% coverage.

* fix: add missing permissions for private repository support

Add critical permissions to pr-lint workflow for private repositories:

Workflow-level permissions:
+ packages: read - Access private npm/PyPI/Composer packages

Job-level permissions:
+ packages: read - Access private packages during dependency installation
+ checks: write - Create and update check runs

Fixes failures when:
- Installing private npm packages from GitHub Packages
- Installing private Composer dependencies
- Installing private Python packages
- Creating status checks with github-script

Valid permission scopes per actionlint:
actions, attestations, checks, contents, deployments, discussions,
id-token, issues, models, packages, pages, pull-requests,
repository-projects, security-events, statuses

Note: "workflows" and "metadata" are NOT valid permission scopes
(they are PAT-only scopes or auto-granted respectively).

* docs: update readmes

* fix: replace bash-specific 'source' with POSIX '.' command

Replace all occurrences of 'source' with '.' (dot) for POSIX compliance:

Changes in python-lint-fix/action.yml:
- Line 165: source .venv/bin/activate → . .venv/bin/activate
- Line 179: source .venv/bin/activate → . .venv/bin/activate
- Line 211: source .venv/bin/activate → . .venv/bin/activate

Also fixed bash-specific test operator:
- Line 192: [[ "$FAIL_ON_ERROR" == "true" ]] → [ "$FAIL_ON_ERROR" = "true" ]

The 'source' command is bash-specific. POSIX sh uses '.' (dot) to source files.
Both commands have identical functionality but '.' is portable across all
POSIX-compliant shells.

* security: fix code injection vulnerability in docker-publish

Fix CodeQL code injection warning (CWE-094, CWE-095, CWE-116):

Issue: inputs.context was used directly in GitHub Actions expression
without sanitization at line 194, allowing potential code injection
by external users.

Fix: Use environment variable indirection to prevent expression injection:
- Added env.BUILD_CONTEXT to capture inputs.context
- Changed context parameter to use ${{ env.BUILD_CONTEXT }}

Environment variables are evaluated after expression compilation,
preventing malicious code execution during workflow parsing.

Security Impact: Medium severity (CVSS 5.0)
Identified by: GitHub Advanced Security (CodeQL)
Reference: https://github.com/ivuorinen/actions/pull/353#pullrequestreview-3481935924

* security: prevent credential persistence in pr-lint checkout

Add persist-credentials: false to checkout step to mitigate untrusted
checkout vulnerability. This prevents GITHUB_TOKEN from being accessible
to potentially malicious PR code.

Fixes: CodeQL finding CWE-829 (untrusted checkout on privileged workflow)

* fix: prevent security bot from overwriting unrelated comments

Replace broad string matching with unique HTML comment marker for
identifying bot-generated comments. Previously, any comment containing
'Security Analysis' or '🔐 GitHub Actions Permissions' would be
overwritten, causing data loss.

Changes:
- Add unique marker: <!-- security-analysis-bot-comment -->
- Prepend marker to generated comment body
- Update comment identification to use marker only
- Add defensive null check for comment.body

This fixes critical data loss bug where user comments could be
permanently overwritten by the security analysis bot.

Follows same proven pattern as test-actions.yml coverage comments.

* improve: show concise permissions diff instead of full blocks

Replace verbose full-block permissions diff with line-by-line changes.
Now shows only added/removed permissions, making output much more
readable.

Changes:
- Parse permissions into individual lines
- Compare old vs new to identify actual changes
- Show only removed (-) and added (+) lines in diff
- Collapse unchanged permissions into details section (≤3 items)
- Show count summary for many unchanged permissions (>3 items)

Example output:
  Before: 30+ lines showing entire permissions block
  After: 3-5 lines showing only what changed

This addresses user feedback that permissions changes were too verbose.

* security: add input validation and trust model documentation

Add comprehensive security validation for docker-publish action to prevent
code injection attacks (CWE-094, CWE-116).

Changes:
- Add validation for context input (reject absolute paths, warn on URLs)
- Add validation for dockerfile input (reject absolute/URL paths)
- Document security trust model in README
- Add best practices for secure usage
- Explain validation rules and threat model

Prevents malicious actors from:
- Building from arbitrary file system locations
- Fetching Dockerfiles from untrusted remote sources
- Executing code injection through build context manipulation

Addresses: CodeRabbit review comments #2541434325, #2541549615
Fixes: GitHub Advanced Security code injection findings

* security: replace unmaintained nick-fields/retry with step-security/retry

Replace nick-fields/retry with step-security/retry across all 4 actions:
- csharp-build/action.yml
- php-composer/action.yml
- go-build/action.yml
- ansible-lint-fix/action.yml

The nick-fields/retry action has security vulnerabilities and low maintenance.
step-security/retry is a drop-in replacement with full API compatibility.

All inputs (timeout_minutes, max_attempts, command, retry_wait_seconds) are
compatible. Using SHA-pinned version for security.

Addresses CodeRabbit review comment #2541549598

* test: add is_input_required() helper function

Add helper function to check if an action input is required, reducing
duplication across test suites.

The function:
- Takes action_file and input_name as parameters
- Uses validation_core.py to query the 'required' property
- Returns 0 (success) if input is required
- Returns 1 (failure) if input is optional

This DRY improvement addresses CodeRabbit review comment #2541549572

* feat: add mode validation convention mapping

Add "mode" to the validation conventions mapping for lint actions
(eslint-lint, biome-lint, prettier-lint).

Note: The update-validators script doesn't currently recognize "string"
as a validator type, so mode validation coverage remains at 93%. The
actions already have inline validation for mode (check|fix), so this is
primarily for improving coverage metrics.

Addresses part of CodeRabbit review comment #2541549570
(validation coverage improvement)

* docs: fix CLAUDE.md action counts and add missing action

- Update action count from 31 to 29 (line 42)
- Add missing 'action-versioning' to Utilities category (line 74)

Addresses CodeRabbit review comments #2541553130 and #2541553110

* docs: add security considerations to docker-publish

Add security documentation to both action.yml header and README.md:
- Trust model explanation
- Input validation details for context and dockerfile
- Attack prevention information
- Best practices for secure usage

The documentation was previously removed when README was autogenerated.
Now documented in both places to ensure it persists.

* fix: correct step ID reference in docker-build

Fix incorrect step ID reference in platforms output:
- Changed steps.platforms.outputs.built to steps.detect-platforms.outputs.platforms
- The step is actually named 'detect-platforms' not 'platforms'
- Ensures output correctly references the detect-platforms step defined at line 188

* fix: ensure docker-build platforms output is always available

Make detect-platforms step unconditional to fix broken output contract.

The platforms output (line 123) references steps.detect-platforms.outputs.platforms,
but the step only ran when auto-detect-platforms was true (default: false).
This caused undefined output in most cases.

Changes:
- Remove 'if' condition from detect-platforms step
- Step now always runs and always produces platforms output
- When auto-detect is false: outputs configured architectures
- When auto-detect is true: outputs detected platforms or falls back to architectures
- Add '|| true' to grep to prevent errors when no platforms detected

Fixes CodeRabbit review comment #2541824904

* security: remove env var indirection in docker-publish BUILD_CONTEXT

Remove BUILD_CONTEXT env var indirection to address GitHub Advanced Security alert.

The inputs.context is validated at lines 137-147 (rejects absolute paths, warns on URLs)
before being used, so the env var indirection is unnecessary and triggers false positive
code injection warnings.

Changes:
- Remove BUILD_CONTEXT env var (line 254)
- Use inputs.context directly (line 256 → 254)
- Input validation remains in place (lines 137-147)

Fixes GitHub Advanced Security code injection alerts (comments #2541405269, #2541522320)

* feat: implement mode_enum validator for lint actions

Add mode_enum validator to validate mode inputs in linting actions.

Changes to conventions.py:
- Add 'mode_enum' to exact_matches mapping (line 215)
- Add 'mode_enum' to PHP-specific validators list (line 560)
- Implement _validate_mode_enum() method (lines 642-660)
  - Validates mode values against ['check', 'fix']
  - Returns clear error messages for invalid values

Updated rules.yml files:
- biome-lint: Add mode: mode_enum convention
- eslint-lint: Add mode: mode_enum convention
- prettier-lint: Add mode: mode_enum convention
- All rules.yml: Fix YAML formatting with yamlfmt

This addresses PR #353 comment #2541522326 which reported that mode validation
was being skipped due to unrecognized 'string' type, reducing coverage to 93%.

Tested with biome-lint action - correctly rejects invalid values and accepts
valid 'check' and 'fix' values.

* docs: update action count from 29 to 30 in CLAUDE.md

Update two references to action count in CLAUDE.md:
- Line 42: repository_overview memory description
- Line 74: Repository Structure section header

The repository has 30 actions total (29 listed + validate-inputs).

Addresses PR #353 comment #2541549588.

* docs: use pinned version ref in language-version-detect README

Change usage example from @main to @v2025 for security best practices.

Using pinned version refs (instead of @main) ensures:
- Predictable behavior across workflow runs
- Protection against breaking changes
- Better security through immutable references

Follows repository convention documented in main README and CLAUDE.md.

Addresses PR #353 comment #2541549588.

* refactor: remove deprecated add-snippets input from codeql-analysis

Remove add-snippets input which has been deprecated by GitHub's CodeQL action
and no longer has any effect.

Changes:
- Remove add-snippets input definition (lines 93-96)
- Remove reference in init step (line 129)
- Remove reference in analyze step (line 211)
- Regenerate README and rules.yml

This is a non-breaking change since:
- Default was 'false' (minimal usage expected)
- GitHub's action already ignores this parameter
- Aligns with recent repository simplification efforts

* feat: add mode_enum validator and update rules

Add mode_enum validator support for lint actions and regenerate all validation rules:

Validator Changes:
- Add mode_enum to action_overrides for biome-lint, eslint-lint, prettier-lint
- Remove deprecated add-snippets from codeql-analysis overrides

Rules Updates:
- All 29 action rules.yml files regenerated with consistent YAML formatting
- biome-lint, eslint-lint, prettier-lint now validate mode input (check/fix)
- Improved coverage for lint actions (79% → 83% for biome, 93% for eslint, 79% for prettier)

Documentation:
- Fix language-version-detect README to use @v2025 (not @main)
- Remove outdated docker-publish security docs (now handled by official actions)

This completes PR #353 review feedback implementation.

* fix: replace bash-specific $'\n' with POSIX-compliant printf

Replace non-POSIX $'\n' syntax in tag building loop with printf-based
approach that works in any POSIX shell.

Changed:
- Line 216: tags="${tags}"$'\n'"${image}:${tag}"
+ Line 216: tags="$(printf '%s\n%s' "$tags" "${image}:${tag}")"

This ensures docker-publish/action.yml runs correctly on systems using
/bin/sh instead of bash.
2025-11-19 15:42:06 +02:00

661 lines
23 KiB
Python

"""Convention-based validator that uses naming patterns to determine validation rules.
This validator automatically applies validation based on input naming conventions.
"""
from __future__ import annotations
from pathlib import Path
from typing import Any
import yaml # pylint: disable=import-error
from .base import BaseValidator
from .convention_mapper import ConventionMapper
TOKEN_TYPES = {
"github": "github_token",
"npm": "npm_token",
"docker": "docker_token",
}
VERSION_MAPPINGS = {
"python": "python_version",
"node": "node_version",
"go": "go_version",
"php": "php_version",
"terraform": "terraform_version",
"dotnet": "dotnet_version",
"net": "dotnet_version",
}
FILE_TYPES = {
"yaml": "yaml_file",
"yml": "yaml_file",
"json": "json_file",
}
class ConventionBasedValidator(BaseValidator):
"""Validator that applies validation based on naming conventions.
Automatically detects validation requirements based on input names
and applies appropriate validators.
"""
def __init__(self, action_type: str) -> None:
"""Initialize the convention-based validator.
Args:
action_type: The type of GitHub Action being validated
"""
super().__init__(action_type)
self._rules = self.load_rules()
self._validator_modules: dict[str, Any] = {}
self._convention_mapper = ConventionMapper() # Use the ConventionMapper
self._load_validator_modules()
def _load_validator_modules(self) -> None:
"""Lazy-load validator modules as needed."""
# These will be imported as needed to avoid circular imports
def load_rules(self, rules_path: Path | None = None) -> dict[str, Any]:
"""Load validation rules from YAML file.
Args:
rules_path: Optional path to the rules YAML file
Returns:
Dictionary of validation rules
"""
if rules_path and rules_path.exists():
rules_file = rules_path
else:
# Find the rules file for this action in the action folder
# Convert underscores back to dashes for the folder name
action_name = self.action_type.replace("_", "-")
project_root = Path(__file__).parent.parent.parent
rules_file = project_root / action_name / "rules.yml"
if not rules_file.exists():
# Return default empty rules if no rules file exists
return {
"action_type": self.action_type,
"required_inputs": [],
"optional_inputs": [],
"conventions": {},
"overrides": {},
}
try:
with Path(rules_file).open() as f:
rules = yaml.safe_load(f) or {}
# Ensure all expected keys exist
rules.setdefault("required_inputs", [])
rules.setdefault("optional_inputs", [])
rules.setdefault("conventions", {})
rules.setdefault("overrides", {})
# Build conventions from optional_inputs if not explicitly set
if not rules["conventions"] and rules["optional_inputs"]:
conventions = {}
optional_inputs = rules["optional_inputs"]
# Handle both list and dict formats for optional_inputs
if isinstance(optional_inputs, list):
# List format: just input names
for input_name in optional_inputs:
conventions[input_name] = self._infer_validator_type(input_name, {})
elif isinstance(optional_inputs, dict):
# Dict format: input names with config
for input_name, input_config in optional_inputs.items():
conventions[input_name] = self._infer_validator_type(
input_name, input_config
)
rules["conventions"] = conventions
return rules
except Exception:
return {
"action_type": self.action_type,
"required_inputs": [],
"optional_inputs": [],
"conventions": {},
"overrides": {},
}
def _infer_validator_type(self, input_name: str, input_config: dict[str, Any]) -> str | None:
"""Infer the validator type from input name and configuration.
Args:
input_name: The name of the input
input_config: The input configuration from rules
Returns:
The inferred validator type or None
"""
# Check for explicit validator type in config
if isinstance(input_config, dict) and "validator" in input_config:
return input_config["validator"]
# Infer based on name patterns
name_lower = input_name.lower().replace("-", "_")
# Try to determine validator type
validator_type = self._check_exact_matches(name_lower)
if validator_type is None:
validator_type = self._check_pattern_based_matches(name_lower)
return validator_type
def _check_exact_matches(self, name_lower: str) -> str | None:
"""Check for exact pattern matches."""
exact_matches = {
# Docker patterns
"platforms": "docker_architectures",
"architectures": "docker_architectures",
"cache_from": "cache_mode",
"cache_to": "cache_mode",
"sbom": "sbom_format",
"registry": "registry_url",
"registry_url": "registry_url",
"tags": "docker_tags",
# File patterns
"file": "file_path",
"path": "file_path",
"file_path": "file_path",
"config_file": "file_path",
"dockerfile": "file_path",
"branch": "branch_name",
"branch_name": "branch_name",
"ref": "branch_name",
# Network patterns
"email": "email",
"url": "url",
"endpoint": "url",
"webhook": "url",
"repository_url": "repository_url",
"repo_url": "repository_url",
"scope": "scope",
"username": "username",
"user": "username",
# Boolean patterns
"dry_run": "boolean",
"draft": "boolean",
"prerelease": "boolean",
"push": "boolean",
"delete": "boolean",
"all_files": "boolean",
"force": "boolean",
"skip": "boolean",
"enabled": "boolean",
"disabled": "boolean",
"verbose": "boolean",
"debug": "boolean",
# Numeric patterns
"retries": "retries",
"retry": "retries",
"attempts": "retries",
"timeout": "timeout",
"timeout_ms": "timeout",
"timeout_seconds": "timeout",
"threads": "threads",
"workers": "threads",
"concurrency": "threads",
# Other patterns
"category": "category_format",
"cache": "package_manager_enum",
"package_manager": "package_manager_enum",
"format": "report_format",
"output_format": "report_format",
"report_format": "report_format",
"mode": "mode_enum",
}
return exact_matches.get(name_lower)
def _check_pattern_based_matches(self, name_lower: str) -> str | None: # noqa: PLR0912
"""Check for pattern-based matches."""
result = None
# Token patterns
if "token" in name_lower:
token_types = TOKEN_TYPES
for key, value in token_types.items():
if key in name_lower:
result = value
break
if result is None:
result = "github_token" # Default token type
# Docker patterns
elif name_lower.startswith("docker_"):
result = f"docker_{name_lower[7:]}"
# Version patterns
elif "version" in name_lower:
version_mappings = VERSION_MAPPINGS
for key, value in version_mappings.items():
if key in name_lower:
result = value
break
if result is None:
result = "flexible_version" # Default to flexible version
# File suffix patterns
elif name_lower.endswith("_file") and name_lower != "config_file":
file_types = FILE_TYPES
for key, value in file_types.items():
if key in name_lower:
result = value
break
if result is None:
result = "file_path"
# CodeQL patterns
elif name_lower.startswith("codeql_"):
result = name_lower
# Cache-related check (special case for returning None)
elif "cache" in name_lower and name_lower != "cache":
result = None # cache-related but not numeric
return result
def get_required_inputs(self) -> list[str]:
"""Get the list of required input names from rules.
Returns:
List of required input names
"""
return self._rules.get("required_inputs", [])
def get_validation_rules(self) -> dict[str, Any]:
"""Get the validation rules.
Returns:
Dictionary of validation rules
"""
return self._rules
def validate_inputs(self, inputs: dict[str, str]) -> bool:
"""Validate inputs based on conventions and rules.
Args:
inputs: Dictionary of input names to values
Returns:
True if all inputs are valid, False otherwise
"""
valid = True
# First validate required inputs
valid &= self.validate_required_inputs(inputs)
# Get conventions and overrides from rules
conventions = self._rules.get("conventions", {})
overrides = self._rules.get("overrides", {})
optional_inputs = self._rules.get("optional_inputs", [])
required_inputs = self.get_required_inputs()
# Validate each input
for input_name, value in inputs.items():
# Skip if explicitly overridden to null
if input_name in overrides and overrides[input_name] is None:
continue
# Check if input is defined in the action's rules
is_defined_input = (
input_name in required_inputs
or input_name in optional_inputs
or input_name in conventions
or input_name in overrides
)
# Skip validation for undefined inputs with empty values
# This prevents auto-validation of irrelevant inputs from the
# validate-inputs action's own input list
if not is_defined_input and (
not value or (isinstance(value, str) and not value.strip())
):
continue
# Get validator type from overrides or conventions
validator_type = self._get_validator_type(input_name, conventions, overrides)
if validator_type:
# Check if this is a required input
is_required = input_name in required_inputs
valid &= self._apply_validator(
input_name, value, validator_type, is_required=is_required
)
return valid
def _get_validator_type(
self,
input_name: str,
conventions: dict[str, str],
overrides: dict[str, str],
) -> str | None:
"""Determine the validator type for an input.
Args:
input_name: The name of the input
conventions: Convention mappings
overrides: Override mappings
Returns:
The validator type or None if no validator found
"""
# Check overrides first
if input_name in overrides:
return overrides[input_name]
# Check exact convention match
if input_name in conventions:
return conventions[input_name]
# Check with dash/underscore conversion
if "_" in input_name:
dash_version = input_name.replace("_", "-")
if dash_version in overrides:
return overrides[dash_version]
if dash_version in conventions:
return conventions[dash_version]
elif "-" in input_name:
underscore_version = input_name.replace("-", "_")
if underscore_version in overrides:
return overrides[underscore_version]
if underscore_version in conventions:
return conventions[underscore_version]
# Fall back to convention mapper for pattern-based detection
return self._convention_mapper.get_validator_type(input_name)
def _apply_validator(
self,
input_name: str,
value: str,
validator_type: str,
*,
is_required: bool,
) -> bool:
"""Apply the appropriate validator to an input value.
Args:
input_name: The name of the input
value: The value to validate
validator_type: The type of validator to apply
is_required: Whether the input is required
Returns:
True if valid, False otherwise
"""
# Get the validator module and method
validator_module, method_name = self._get_validator_method(validator_type)
if not validator_module:
# Unknown validator type, skip validation
return True
try:
# Call the validation method
if hasattr(validator_module, method_name):
method = getattr(validator_module, method_name)
# Some validators need additional parameters
if validator_type == "github_token" and method_name == "validate_github_token":
result = method(value, required=is_required)
elif "numeric_range" in validator_type:
# Parse range from validator type
min_val, max_val = self._parse_numeric_range(validator_type)
result = method(value, min_val, max_val, input_name)
else:
# Standard validation call
result = method(value, input_name)
# Copy errors from the validator module to this validator
# Skip if validator_module is self (for internal validators)
if validator_module is not self and hasattr(validator_module, "errors"):
for error in validator_module.errors:
if error not in self.errors:
self.add_error(error)
# Clear the module's errors after copying
validator_module.errors = []
return result
# Method not found, skip validation
return True
except Exception as e:
self.add_error(f"Validation error for {input_name}: {e}")
return False
def _get_validator_method(self, validator_type: str) -> tuple[Any, str]: # noqa: C901, PLR0912
"""Get the validator module and method name for a validator type.
Args:
validator_type: The validator type string
Returns:
Tuple of (validator_module, method_name)
"""
# Lazy import validators to avoid circular dependencies
# Token validators
if validator_type in [
"github_token",
"npm_token",
"docker_token",
"namespace_with_lookahead",
]:
if "token" not in self._validator_modules:
from . import token
self._validator_modules["token"] = token.TokenValidator()
return self._validator_modules["token"], f"validate_{validator_type}"
# Docker validators
if validator_type.startswith("docker_") or validator_type in [
"cache_mode",
"sbom_format",
"registry_enum",
]:
if "docker" not in self._validator_modules:
from . import docker
self._validator_modules["docker"] = docker.DockerValidator()
if validator_type.startswith("docker_"):
method = f"validate_{validator_type[7:]}" # Remove "docker_" prefix
elif validator_type == "registry_enum":
method = "validate_registry"
else:
method = f"validate_{validator_type}"
return self._validator_modules["docker"], method
# Version validators
if "version" in validator_type or validator_type in ["calver", "semantic", "flexible"]:
if "version" not in self._validator_modules:
from . import version
self._validator_modules["version"] = version.VersionValidator()
return self._validator_modules["version"], f"validate_{validator_type}"
# File validators
if validator_type in [
"file_path",
"branch_name",
"file_extensions",
"yaml_file",
"json_file",
"config_file",
]:
if "file" not in self._validator_modules:
from . import file
self._validator_modules["file"] = file.FileValidator()
return self._validator_modules["file"], f"validate_{validator_type}"
# Network validators
if validator_type in [
"email",
"url",
"scope",
"username",
"registry_url",
"repository_url",
]:
if "network" not in self._validator_modules:
from . import network
self._validator_modules["network"] = network.NetworkValidator()
return self._validator_modules["network"], f"validate_{validator_type}"
# Boolean validator
if validator_type == "boolean":
if "boolean" not in self._validator_modules:
from . import boolean
self._validator_modules["boolean"] = boolean.BooleanValidator()
return self._validator_modules["boolean"], "validate_boolean"
# Numeric validators
if validator_type.startswith("numeric_range") or validator_type in [
"retries",
"timeout",
"threads",
]:
if "numeric" not in self._validator_modules:
from . import numeric
self._validator_modules["numeric"] = numeric.NumericValidator()
if validator_type.startswith("numeric_range"):
return self._validator_modules["numeric"], "validate_range"
return self._validator_modules["numeric"], f"validate_{validator_type}"
# Security validators
if validator_type in ["security_patterns", "injection_patterns", "prefix", "regex_pattern"]:
if "security" not in self._validator_modules:
from . import security
self._validator_modules["security"] = security.SecurityValidator()
if validator_type == "prefix":
# Use no_injection for prefix - checks for injection patterns
# without character restrictions
return self._validator_modules["security"], "validate_no_injection"
return self._validator_modules["security"], f"validate_{validator_type}"
# CodeQL validators
if validator_type.startswith("codeql_") or validator_type in ["category_format"]:
if "codeql" not in self._validator_modules:
from . import codeql
self._validator_modules["codeql"] = codeql.CodeQLValidator()
return self._validator_modules["codeql"], f"validate_{validator_type}"
# PHP-specific validators
if validator_type in ["php_extensions", "coverage_driver", "mode_enum"]:
# Return self for PHP-specific validation methods
return self, f"_validate_{validator_type}"
# Package manager and report format validators
if validator_type in ["package_manager_enum", "report_format"]:
# These could be in a separate module, but for now we'll put them in file validator
if "file" not in self._validator_modules:
from . import file
self._validator_modules["file"] = file.FileValidator()
# These methods need to be added to file validator or a new module
return None, ""
# Default: no validator
return None, ""
def _parse_numeric_range(self, validator_type: str) -> tuple[int, int]:
"""Parse min and max values from a numeric_range validator type.
Args:
validator_type: String like "numeric_range_1_100"
Returns:
Tuple of (min_value, max_value)
"""
parts = validator_type.split("_")
if len(parts) >= 4:
try:
return int(parts[2]), int(parts[3])
except ValueError:
pass
# Default range
return 0, 100
def _validate_php_extensions(self, value: str, input_name: str) -> bool:
"""Validate PHP extensions format.
Args:
value: The extensions value (comma-separated list)
input_name: The input name for error messages
Returns:
True if valid, False otherwise
"""
import re
if not value:
return True
# Check for injection patterns
if re.search(r"[;&|`$()@#]", value):
self.add_error(f"Potential injection detected in {input_name}: {value}")
return False
# Check format - should be alphanumeric, underscores, commas, spaces only
if not re.match(r"^[a-zA-Z0-9_,\s]+$", value):
self.add_error(f"Invalid format for {input_name}: {value}")
return False
return True
def _validate_coverage_driver(self, value: str, input_name: str) -> bool:
"""Validate coverage driver enum.
Args:
value: The coverage driver value
input_name: The input name for error messages
Returns:
True if valid, False otherwise
"""
valid_drivers = ["none", "xdebug", "pcov", "xdebug3"]
if value and value not in valid_drivers:
self.add_error(
f"Invalid {input_name}: {value}. Must be one of: {', '.join(valid_drivers)}"
)
return False
return True
def _validate_mode_enum(self, value: str, input_name: str) -> bool:
"""Validate mode enum for linting actions.
Args:
value: The mode value
input_name: The input name for error messages
Returns:
True if valid, False otherwise
"""
valid_modes = ["check", "fix"]
if value and value not in valid_modes:
self.add_error(
f"Invalid {input_name}: {value}. Must be one of: {', '.join(valid_modes)}"
)
return False
return True