""" Docker Models for DockMon Pydantic models for Docker hosts, containers, and configurations """ import re import uuid import logging from datetime import datetime from typing import Optional from pydantic import BaseModel, Field, validator, model_validator logger = logging.getLogger(__name__) class DockerHostConfig(BaseModel): """Configuration for a Docker host""" name: str = Field(..., min_length=1, max_length=100, pattern=r'^[a-zA-Z0-9][a-zA-Z0-9 ._-]*$') url: str = Field(..., min_length=1, max_length=500) tls_cert: Optional[str] = Field(None, max_length=10000) tls_key: Optional[str] = Field(None, max_length=10000) tls_ca: Optional[str] = Field(None, max_length=10000) @validator('name') def validate_name(cls, v): """Validate host name for security""" if not v or not v.strip(): raise ValueError('Host name cannot be empty') # Prevent XSS and injection sanitized = re.sub(r'[<>"\']', '', v.strip()) if len(sanitized) != len(v.strip()): raise ValueError('Host name contains invalid characters') return sanitized @validator('url') def validate_url(cls, v): """Validate Docker URL for security - prevent SSRF attacks""" if not v or not v.strip(): raise ValueError('URL cannot be empty') v = v.strip() # Only allow specific protocols allowed_protocols = ['tcp://', 'unix://', 'http://', 'https://'] if not any(v.startswith(proto) for proto in allowed_protocols): raise ValueError('URL must use tcp://, unix://, http:// or https:// protocol') # Block ONLY the most dangerous SSRF targets (cloud metadata & loopback) # Allow private networks (10.*, 172.16-31.*, 192.168.*) for legitimate Docker hosts extremely_dangerous_patterns = [ r'169\.254\.169\.254', # AWS/GCP metadata (specific) r'169\.254\.', # Link-local range (broader) r'metadata\.google\.internal', # GCP metadata r'metadata\.goog', # GCP metadata alternative r'100\.100\.100\.200', # Alibaba Cloud metadata r'fd00:ec2::254', # AWS IPv6 metadata r'0\.0\.0\.0', # All interfaces binding r'::1', # IPv6 localhost r'localhost(?!\:|$)', # Localhost variations but allow localhost:port r'127\.0\.0\.(?!1$)', # 127.x.x.x but allow 127.0.0.1 ] # Check for extremely dangerous metadata service targets for pattern in extremely_dangerous_patterns: if re.search(pattern, v, re.IGNORECASE): # Special handling for localhost - allow localhost:port but block bare localhost if 'localhost' in pattern.lower() and ':' in v: continue # Allow localhost:2376 etc raise ValueError('URL targets cloud metadata service or dangerous internal endpoint') # Additional validation: warn about but allow private networks private_network_patterns = [ r'10\.', # 10.0.0.0/8 r'172\.(1[6-9]|2[0-9]|3[01])\.', # 172.16.0.0/12 r'192\.168\.', # 192.168.0.0/16 ] # Log private network usage for monitoring (but don't block) for pattern in private_network_patterns: if re.search(pattern, v, re.IGNORECASE): logger.info(f"Docker host configured on private network: {v[:50]}...") break return v @validator('tls_cert', 'tls_key', 'tls_ca') def validate_certificate(cls, v): """Validate TLS certificate data""" if v is None: return v v = v.strip() if not v: return None # Basic PEM format validation with helpful error messages if '-----BEGIN' not in v and '-----END' not in v: raise ValueError('Certificate is incomplete. PEM certificates must start with "-----BEGIN" and end with "-----END". Please copy the entire certificate including both lines.') elif '-----BEGIN' not in v: raise ValueError('Certificate is missing the "-----BEGIN" header line. Make sure you copied the complete certificate starting from the "-----BEGIN" line.') elif '-----END' not in v: raise ValueError('Certificate is missing the "-----END" footer line. Make sure you copied the complete certificate including the "-----END" line.') # Block potential code injection dangerous_patterns = ['