"""File upload validation utilities""" import os from werkzeug.utils import secure_filename import clamd from flask import current_app # Allowed file extensions for attachments ALLOWED_EXTENSIONS = { 'txt', 'log', 'pdf', 'png', 'jpg', 'jpeg', 'gif', 'doc', 'docx', 'xls', 'xlsx', 'csv' } # Maximum file size (10MB) MAX_FILE_SIZE = 10 * 1024 * 1024 def allowed_file(filename): """Check if file extension is allowed Args: filename: Name of the uploaded file Returns: bool: True if extension is allowed, False otherwise """ if not filename: return False return '.' in filename and \ filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS def validate_file_size(file_stream): """Check if file size is within limits Args: file_stream: File stream object Returns: bool: True if size is acceptable, False otherwise """ # Seek to end to get file size file_stream.seek(0, os.SEEK_END) size = file_stream.tell() # Reset to beginning file_stream.seek(0) return size <= MAX_FILE_SIZE def get_safe_filename(filename): """Get secure version of filename Args: filename: Original filename Returns: str: Secure filename safe for filesystem storage """ return secure_filename(filename) def validate_file(file): """Validate uploaded file Args: file: Werkzeug FileStorage object Returns: tuple: (is_valid, error_message) is_valid: bool indicating if file is valid error_message: str with error description or None """ if not file: return False, "No file provided" if file.filename == '': return False, "No file selected" if not allowed_file(file.filename): return False, f"File type not allowed. Allowed types: {', '.join(ALLOWED_EXTENSIONS)}" if not validate_file_size(file.stream): return False, f"File size exceeds maximum of {MAX_FILE_SIZE / (1024 * 1024):.0f}MB" return True, None def scan_file_for_viruses(file): """Scan file for viruses using ClamAV Args: file: Werkzeug FileStorage object Returns: tuple: (is_clean, error_message) is_clean: bool indicating if file is clean (True) or infected (False) error_message: str with error description or None """ try: # Connect to ClamAV daemon clamd_socket = current_app.config.get('CLAMD_SOCKET') if not clamd_socket: # ClamAV not configured, skip scanning current_app.logger.warning("ClamAV socket not configured, skipping virus scan") return True, None cd = clamd.ClamdUnixSocket(clamd_socket) # Ping to check if ClamAV is available try: cd.ping() except Exception as e: current_app.logger.warning(f"ClamAV not available: {e}, skipping virus scan") return True, None # Read file content file.stream.seek(0) file_data = file.stream.read() file.stream.seek(0) # Reset for later use # Scan file scan_result = cd.instream(file_data) # Check result if scan_result and 'stream' in scan_result: status, virus_name = scan_result['stream'] if status == 'OK': return True, None elif status == 'FOUND': return False, f"Virus detected: {virus_name}" else: return False, f"Scan error: {status}" return True, None except Exception as e: current_app.logger.error(f"ClamAV scanning error: {e}") # On error, we'll allow the file but log the error # In production, you might want to reject files if scanning fails return True, None