#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Security Utilities Module Provides security-related utility functions for Noma. """ import re import json import os from pathlib import Path from typing import Any, Optional def read_json_safe(file_path: Path, default: Any = None) -> Any: """ Safely read a JSON file, returning default on error. Args: file_path: Path to the JSON file default: Value to return if file doesn't exist or is invalid JSON Returns: Parsed JSON data or default value """ try: path = Path(file_path) if not path.exists(): return default with open(path, "r", encoding="utf-8") as f: return json.load(f) except (json.JSONDecodeError, IOError): return default def sanitize_commit_message(message: str) -> str: """ Sanitize a commit message to prevent injection attacks. Args: message: Raw commit message Returns: Sanitized commit message safe for git """ if not message: return "" # Remove any control characters message = re.sub(r'[\x00-\x1f\x7f-\x9f]', '', message) # Limit length if len(message) > 500: message = message[:500] + "..." return message.strip() def atomic_write_json(file_path: Path, data: Any, encoding: str = "utf-8", use_lock: bool = False, backup: bool = False) -> bool: """ Atomically write JSON data to a file. Uses a temporary file and atomic rename to ensure the file is never partially written. Args: file_path: Target file path data: Data to write (must be JSON-serializable) encoding: File encoding use_lock: Whether to use file locking (not implemented) backup: Whether to create backup before writing (not implemented) Returns: True if successful, False otherwise """ import tempfile file_path = Path(file_path) temp_fd = None temp_path = None try: # Create temp file in same directory (for atomic rename) temp_fd, temp_path = tempfile.mkstemp( dir=file_path.parent, prefix=f".{file_path.name}.", suffix=".tmp" ) # Write data to temp file with os.fdopen(temp_fd, 'w', encoding=encoding) as f: json.dump(data, f, ensure_ascii=False, indent=2) f.flush() os.fsync(f.fileno()) temp_fd = None # File is now closed # Atomic rename os.replace(temp_path, file_path) return True except Exception: # Clean up temp file if it exists if temp_fd is not None: os.close(temp_fd) if temp_path is not None and os.path.exists(temp_path): os.unlink(temp_path) return False def is_git_available() -> bool: """ Check if git is available in the system PATH. Returns: True if git command is available, False otherwise """ import shutil return shutil.which("git") is not None def validate_file_path(file_path: str, base_dir: Optional[Path] = None) -> bool: """ Validate that a file path is safe (no path traversal). Args: file_path: The file path to validate base_dir: Optional base directory to check against Returns: True if the path is safe, False otherwise """ try: path = Path(file_path).resolve() if base_dir is not None: base_dir = Path(base_dir).resolve() # Check if path is within base_dir try: path.relative_to(base_dir) return True except ValueError: return False # Check for path traversal patterns if ".." in file_path: return False return True except Exception: return False def create_secure_directory(dir_path: Path, mode: int = 0o755) -> bool: """ Create a directory with secure permissions. Args: dir_path: Directory path to create mode: Directory permissions (default: 0o755) Returns: True if successful, False otherwise """ try: dir_path.mkdir(parents=True, exist_ok=True, mode=mode) return True except Exception: return False def is_git_repo(path: Path) -> bool: """ Check if a directory is a git repository. Args: path: Directory path to check Returns: True if it's a git repository, False otherwise """ if not is_git_available(): return False import subprocess try: result = subprocess.run( ["git", "rev-parse", "--is-inside-work-tree"], cwd=str(path), capture_output=True, text=True ) return result.returncode == 0 and result.stdout.strip() == "true" except Exception: return False def git_graceful_operation(path: Path, operation: str, *args, **kwargs): """ Perform a git operation gracefully. Args: path: Repository path operation: Git command to run *args: Arguments for the git command **kwargs: Keyword arguments for subprocess.run Returns: subprocess.CompletedProcess result """ if not is_git_available(): raise RuntimeError("Git is not available") import subprocess cmd = ["git", operation] + list(args) return subprocess.run(cmd, cwd=str(path), **kwargs)