mirror of
https://github.com/ivuorinen/actions.git
synced 2026-01-26 03:23:59 +00:00
343 lines
11 KiB
Python
Executable File
343 lines
11 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""Custom validator for docker-build action.
|
|
|
|
This validator handles complex Docker build validation including:
|
|
- Dockerfile path validation
|
|
- Build context validation
|
|
- Platform validation (linux/amd64, linux/arm64, etc.)
|
|
- Build argument format validation
|
|
- Tag format validation
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
import sys
|
|
|
|
# Add validate-inputs directory to path to import validators
|
|
validate_inputs_path = Path(__file__).parent.parent / "validate-inputs"
|
|
sys.path.insert(0, str(validate_inputs_path))
|
|
|
|
from validators.base import BaseValidator
|
|
from validators.boolean import BooleanValidator
|
|
from validators.docker import DockerValidator
|
|
from validators.file import FileValidator
|
|
from validators.numeric import NumericValidator
|
|
from validators.version import VersionValidator
|
|
|
|
|
|
class CustomValidator(BaseValidator):
|
|
"""Custom validator for docker-build action.
|
|
|
|
Validates Docker build-specific inputs with complex rules.
|
|
"""
|
|
|
|
def __init__(self, action_type: str = "docker-build") -> None:
|
|
"""Initialize the docker-build validator."""
|
|
super().__init__(action_type)
|
|
self.docker_validator = DockerValidator(action_type)
|
|
self.file_validator = FileValidator(action_type)
|
|
self.boolean_validator = BooleanValidator(action_type)
|
|
self.numeric_validator = NumericValidator(action_type)
|
|
self.version_validator = VersionValidator(action_type)
|
|
|
|
def validate_inputs(self, inputs: dict[str, str]) -> bool:
|
|
"""Validate docker-build specific inputs.
|
|
|
|
Args:
|
|
inputs: Dictionary of input names to values
|
|
|
|
Returns:
|
|
True if all validations pass, False otherwise
|
|
"""
|
|
valid = True
|
|
|
|
# Validate required inputs
|
|
valid &= self.validate_required_inputs(inputs)
|
|
|
|
# Validate dockerfile path
|
|
if inputs.get("dockerfile"):
|
|
valid &= self.validate_dockerfile(inputs["dockerfile"])
|
|
|
|
# Validate context path
|
|
if inputs.get("context"):
|
|
valid &= self.validate_context(inputs["context"])
|
|
|
|
# Validate image name
|
|
if inputs.get("image-name"):
|
|
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"):
|
|
valid &= self.validate_with(
|
|
self.docker_validator, "validate_docker_tag", inputs["tag"], "tag"
|
|
)
|
|
|
|
# Validate architectures/platforms
|
|
if inputs.get("architectures"):
|
|
valid &= self.validate_with(
|
|
self.docker_validator,
|
|
"validate_architectures",
|
|
inputs["architectures"],
|
|
"architectures",
|
|
)
|
|
|
|
# Validate build arguments
|
|
if inputs.get("build-args"):
|
|
valid &= self.validate_build_args(inputs["build-args"])
|
|
|
|
# Validate push flag
|
|
if inputs.get("push"):
|
|
valid &= self.validate_with(
|
|
self.boolean_validator, "validate_optional_boolean", inputs["push"], "push"
|
|
)
|
|
|
|
# Validate cache settings
|
|
if inputs.get("cache-from"):
|
|
valid &= self.validate_cache_from(inputs["cache-from"])
|
|
|
|
if inputs.get("cache-to"):
|
|
valid &= self.validate_cache_to(inputs["cache-to"])
|
|
|
|
# Validate cache-mode
|
|
if inputs.get("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"):
|
|
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"):
|
|
valid &= self.validate_with(
|
|
self.numeric_validator,
|
|
"validate_numeric_range",
|
|
inputs["parallel-builds"],
|
|
min_val=0,
|
|
max_val=16,
|
|
name="parallel-builds",
|
|
)
|
|
|
|
# Validate boolean flags
|
|
for bool_input in [
|
|
"dry-run",
|
|
"verbose",
|
|
"platform-fallback",
|
|
"scan-image",
|
|
"sign-image",
|
|
"auto-detect-platforms",
|
|
]:
|
|
if inputs.get(bool_input):
|
|
valid &= self.validate_with(
|
|
self.boolean_validator,
|
|
"validate_optional_boolean",
|
|
inputs[bool_input],
|
|
bool_input,
|
|
)
|
|
|
|
# Validate sbom-format
|
|
if inputs.get("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"):
|
|
valid &= self.validate_with(
|
|
self.numeric_validator,
|
|
"validate_numeric_range",
|
|
inputs["max-retries"],
|
|
min_val=0,
|
|
max_val=10,
|
|
name="max-retries",
|
|
)
|
|
|
|
return valid
|
|
|
|
def get_required_inputs(self) -> list[str]:
|
|
"""Get list of required inputs for docker-build.
|
|
|
|
Returns:
|
|
List of required input names
|
|
"""
|
|
# Tag is the only required input according to action.yml
|
|
return ["tag"]
|
|
|
|
def get_validation_rules(self) -> dict:
|
|
"""Get validation rules for docker-build.
|
|
|
|
Returns:
|
|
Dictionary of validation rules
|
|
"""
|
|
return {
|
|
"dockerfile": "Path to Dockerfile (default: ./Dockerfile)",
|
|
"context": "Build context path (default: .)",
|
|
"tag": "Docker image tag (required)",
|
|
"architectures": "Comma-separated list of platforms",
|
|
"build-args": "Build arguments in KEY=value format",
|
|
"push": "Whether to push the image (true/false)",
|
|
"cache-from": "Cache sources",
|
|
"cache-to": "Cache destinations",
|
|
"cache-mode": "Cache mode (min, max, or inline)",
|
|
"buildx-version": "Docker Buildx version",
|
|
"sbom-format": "SBOM format (spdx-json, cyclonedx-json, or syft-json)",
|
|
"parallel-builds": "Number of parallel builds (0-16)",
|
|
}
|
|
|
|
def validate_dockerfile(self, dockerfile: str) -> bool:
|
|
"""Validate Dockerfile path.
|
|
|
|
Args:
|
|
dockerfile: Path to Dockerfile
|
|
|
|
Returns:
|
|
True if valid, False otherwise
|
|
"""
|
|
if self.is_github_expression(dockerfile):
|
|
return True
|
|
return self.validate_with(
|
|
self.file_validator, "validate_file_path", dockerfile, "dockerfile"
|
|
)
|
|
|
|
def validate_context(self, context: str) -> bool:
|
|
"""Validate build context path.
|
|
|
|
Args:
|
|
context: Build context path
|
|
|
|
Returns:
|
|
True if valid, False otherwise
|
|
"""
|
|
# Allow GitHub Actions expressions
|
|
if self.is_github_expression(context):
|
|
return True
|
|
|
|
# Allow current directory
|
|
if context in [".", "./", ""]:
|
|
return True
|
|
|
|
# Note: The test says "accepts path traversal in context (no validation in action)"
|
|
# This means we should NOT validate for path traversal in context
|
|
# We allow path traversal for context as Docker needs to access parent directories
|
|
# Only check for command injection patterns like ; | ` $()
|
|
dangerous_chars = [";", "|", "`", "$(", "&&", "||"]
|
|
if any(char in context for char in dangerous_chars):
|
|
self.add_error(f"Command injection detected in context: {context}")
|
|
return False
|
|
|
|
return True
|
|
|
|
def validate_platforms(self, platforms: str) -> bool:
|
|
"""Validate platform list.
|
|
|
|
Args:
|
|
platforms: Comma-separated platform list
|
|
|
|
Returns:
|
|
True if valid, False otherwise
|
|
"""
|
|
return self.validate_with(
|
|
self.docker_validator, "validate_architectures", platforms, "platforms"
|
|
)
|
|
|
|
def validate_build_args(self, build_args: str) -> bool:
|
|
"""Validate build arguments.
|
|
|
|
Args:
|
|
build_args: Build arguments in KEY=value format
|
|
|
|
Returns:
|
|
True if valid, False otherwise
|
|
"""
|
|
# Allow GitHub Actions expressions
|
|
if self.is_github_expression(build_args):
|
|
return True
|
|
|
|
# Build args can be comma-separated or newline-separated
|
|
# Split by both
|
|
args = build_args.replace(",", "\n").strip().split("\n")
|
|
|
|
for arg in args:
|
|
arg = arg.strip()
|
|
if not arg:
|
|
continue
|
|
|
|
# Check for KEY=value format
|
|
if "=" not in arg:
|
|
self.add_error(f"Build argument must be in KEY=value format: {arg}")
|
|
return False
|
|
|
|
key, value = arg.split("=", 1)
|
|
|
|
# Validate key format
|
|
if not key:
|
|
self.add_error("Build argument key cannot be empty")
|
|
return False
|
|
|
|
# Check for security issues in values
|
|
if not self.validate_security_patterns(value, f"build-arg {key}"):
|
|
return False
|
|
|
|
return True
|
|
|
|
def validate_cache_from(self, cache_from: str) -> bool:
|
|
"""Validate cache-from sources.
|
|
|
|
Args:
|
|
cache_from: Cache sources
|
|
|
|
Returns:
|
|
True if valid, False otherwise
|
|
"""
|
|
# Allow GitHub Actions expressions
|
|
if self.is_github_expression(cache_from):
|
|
return True
|
|
|
|
# Basic format validation for cache sources
|
|
# Format: type=registry,ref=user/app:cache
|
|
if "type=" not in cache_from:
|
|
self.add_error("cache-from must specify type (e.g., type=registry,ref=...)")
|
|
return False
|
|
|
|
# Check for security issues
|
|
return self.validate_security_patterns(cache_from, "cache-from")
|
|
|
|
def validate_cache_to(self, cache_to: str) -> bool:
|
|
"""Validate cache-to destinations.
|
|
|
|
Args:
|
|
cache_to: Cache destinations
|
|
|
|
Returns:
|
|
True if valid, False otherwise
|
|
"""
|
|
# Allow GitHub Actions expressions
|
|
if self.is_github_expression(cache_to):
|
|
return True
|
|
|
|
# Basic format validation for cache destinations
|
|
if "type=" not in cache_to:
|
|
self.add_error("cache-to must specify type (e.g., type=registry,ref=...)")
|
|
return False
|
|
|
|
# Check for security issues
|
|
return self.validate_security_patterns(cache_to, "cache-to")
|