Coverage for src / marimushka / validators.py: 100%

34 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-02-28 17:41 +0000

1"""Input validation for paths, templates, and configuration. 

2 

3This module provides validation functions for template files and other inputs 

4used in the export process. It ensures security by checking for path traversal, 

5file existence, and size limits. 

6""" 

7 

8import stat 

9from pathlib import Path 

10 

11from loguru import logger 

12 

13from .audit import AuditLogger 

14from .exceptions import TemplateInvalidError, TemplateNotFoundError 

15from .security import sanitize_error_message, validate_file_size, validate_path_traversal 

16 

17 

18def validate_template(template_path: Path, audit_logger: AuditLogger) -> None: 

19 """Validate the template file exists and has correct extension. 

20 

21 Args: 

22 template_path: Path to the template file. 

23 audit_logger: Logger for audit events. 

24 

25 Raises: 

26 TemplateNotFoundError: If the template file does not exist. 

27 TemplateInvalidError: If the template path is not a file. 

28 

29 """ 

30 # Validate path traversal 

31 try: 

32 validate_path_traversal(template_path) 

33 audit_logger.log_path_validation(template_path, "traversal", True) 

34 except ValueError as e: 

35 audit_logger.log_path_validation(template_path, "traversal", False, str(e)) 

36 sanitized_msg = sanitize_error_message(str(e)) 

37 raise TemplateInvalidError(template_path, reason=f"path traversal detected: {sanitized_msg}") from e 

38 

39 # Check existence (avoid TOCTOU by using stat) 

40 try: 

41 stat_result = template_path.stat() 

42 except FileNotFoundError: 

43 audit_logger.log_path_validation(template_path, "existence", False, "file not found") 

44 raise TemplateNotFoundError(template_path) from None 

45 except OSError as e: 

46 audit_logger.log_path_validation(template_path, "existence", False, str(e)) 

47 raise TemplateInvalidError(template_path, reason=f"cannot access file: {e}") from e 

48 

49 # Check if it's a regular file 

50 if not stat.S_ISREG(stat_result.st_mode): 

51 audit_logger.log_path_validation(template_path, "file_type", False, "not a regular file") 

52 raise TemplateInvalidError(template_path, reason="path is not a file") 

53 

54 # Check file size to prevent DoS 

55 try: 

56 validate_file_size(template_path, max_size_bytes=10 * 1024 * 1024) # 10MB limit 

57 except ValueError as e: 

58 audit_logger.log_path_validation(template_path, "size", False, str(e)) 

59 sanitized_msg = sanitize_error_message(str(e)) 

60 raise TemplateInvalidError(template_path, reason=f"file size limit exceeded: {sanitized_msg}") from e 

61 

62 if template_path.suffix not in (".j2", ".jinja2"): 

63 logger.warning(f"Template file '{template_path}' does not have .j2 or .jinja2 extension") 

64 

65 audit_logger.log_path_validation(template_path, "complete", True)