from typing import List, Optional, Annotated, Union from enum import Enum from pydantic import BaseModel, Field, validator import ipaddress class Protocol(str, Enum): """ Enum для сетевых протоколов UFW. """ TCP = "tcp" UDP = "udp" BOTH = "both" @property def protocols(self) -> List[str]: """Возвращает список протоколов для использования в команде UFW.""" return ["tcp", "udp"] if self == Protocol.BOTH else [self.value] class Action(str, Enum): """ Enum для действий UFW. Attributes: ALLOW: Разрешить трафик DENY: Запретить трафик без уведомления REJECT: Запретить трафик с уведомлением отправителю """ ALLOW = "allow" DENY = "deny" REJECT = "reject" class Direction(str, Enum): """ Enum для направления трафика UFW. Attributes: IN: Входящий трафик OUT: Исходящий трафик """ IN = "in" OUT = "out" class UFWRule(BaseModel): """Модель правила UFW""" action: Action direction: Direction from_ip: str to_ip: str port: Union[int, str] protocol: Protocol comment: Optional[str] = None @validator("port") def validate_port(cls, v): """ Проверяет корректность порта или диапазона портов. Args: v: Порт (число) или диапазон портов (строка вида "3000:3999") Returns: Union[int, str]: Валидированное значение порта Raises: ValueError: Если порт или диапазон портов некорректен """ if isinstance(v, int): if not (1 <= v <= 65535): raise ValueError(f"Порт должен быть в диапазоне 1-65535, получено: {v}") return v elif isinstance(v, str): try: start, end = map(int, v.split(':')) if not (1 <= start <= end <= 65535): raise ValueError return v except (ValueError, TypeError): raise ValueError(f"Некорректный диапазон портов: {v}. Формат: start:end, где 1 <= start <= end <= 65535") else: raise ValueError(f"Порт должен быть числом или строкой с диапазоном, получено: {type(v)}") @validator("from_ip", "to_ip") def validate_ip(cls, v): """Проверяет корректность IP адреса""" try: # Пробуем создать объект IP-сети, что автоматически проверит корректность адреса ipaddress.ip_network(v, strict=False) return v except ValueError: raise ValueError(f"Некорректный IP адрес или подсеть: {v}") @validator("to_ip") def validate_ip_version_compatibility(cls, v, values): """Проверяет совместимость версий IP адресов""" if "from_ip" not in values: return v # Определяем версии IP from_ip = ipaddress.ip_network(values["from_ip"], strict=False) to_ip = ipaddress.ip_network(v, strict=False) if isinstance(from_ip, ipaddress.IPv6Network) != isinstance(to_ip, ipaddress.IPv6Network): raise ValueError("Несовместимые версии IP: нельзя смешивать IPv4 и IPv6 в одном правиле") return v @validator("action", pre=True) def validate_action(cls, v): """Приводит значение action к нижнему регистру перед валидацией""" if isinstance(v, str): return v.lower() return v @validator("protocol", pre=True) def validate_protocol(cls, v): """Приводит значение protocol к нижнему регистру перед валидацией""" if isinstance(v, str): return v.lower() return v def to_ufw_command(self) -> str: """Преобразует правило в команду UFW.""" cmd_parts = ["ufw"] # Добавляем действие cmd_parts.append(self.action.value) # Добавляем направление cmd_parts.append(self.direction.value) # Добавляем from с IP cmd_parts.extend(["from", self.from_ip]) # Добавляем to с IP cmd_parts.extend(["to", self.to_ip]) # Добавляем порт и протокол port_str = str(self.port) # Работает как с числами, так и с диапазонами cmd_parts.extend(["port", port_str]) # Добавляем протокол proto = self.protocol.value if proto == "both": cmd_parts.append("proto tcp/udp") else: cmd_parts.extend(["proto", proto]) # Добавляем комментарий если есть if self.comment: cmd_parts.extend(["comment", f"'{self.comment}'"]) return " ".join(cmd_parts) @classmethod def from_ufw_output(cls, line: str) -> "UFWRule": """ Создает экземпляр UFWRule из строки вывода команды 'ufw status numbered'. Args: line: Строка вывода UFW (например, "[1] 80/tcp ALLOW IN 192.168.1.0/24 # Web Server") Returns: UFWRule: Новый экземпляр правила """ import re # Регулярное выражение для разбора строки правила UFW pattern = r"""^\s* \[\s*(?P\d+)\s*\]\s* (?P\d+(?::\d+)?)/(?Ptcp(?:/udp)?|udp)\s+ (?PALLOW|DENY|REJECT)\s+(?PIN|OUT)\s+ (?P[\w\d\./]+|\bAnywhere\b) (?:\s+\(v6\))? (?:\s+\((?:out|in)\))? \s* (?:\#\s*(?P.*?))? \s*$ """ match = re.match(pattern, line, re.VERBOSE | re.IGNORECASE) if not match: raise ValueError(f"Невозможно разобрать строку правила UFW: {line}") groups = match.groupdict() # Преобразуем протокол в enum proto_str = groups['protocol'].lower() protocol = Protocol.BOTH if proto_str == "tcp/udp" else \ Protocol.UDP if proto_str == "udp" else Protocol.TCP # Преобразуем действие в enum (игнорируем регистр) action_str = groups['action'].lower() action = Action.ALLOW if action_str == "allow" else \ Action.DENY if action_str == "deny" else Action.REJECT # Преобразуем направление в enum direction_str = groups['direction'].lower() direction = Direction.IN if direction_str == "in" else Direction.OUT # Обрабатываем IP адреса from_ip = groups['from_ip'].strip() if from_ip.lower() == "anywhere": from_ip = "0.0.0.0/0" # Для исходящих правил меняем местами from и to to_ip = "0.0.0.0/0" # По умолчанию anywhere # Обрабатываем порт port = groups['port'] if ':' in port: # Оставляем как строку для диапазона port = port.strip() else: # Преобразуем в число для одиночного порта port = int(port) # Обрабатываем комментарий comment = groups.get('comment', '').strip() if groups.get('comment') else None return cls( action=action, direction=direction, from_ip=from_ip, to_ip=to_ip, port=port, protocol=protocol, comment=comment ) class UFWRulesResponse(BaseModel): """ Модель для представления списка правил UFW. Attributes: enabled: Включен ли UFW rules: Список правил UFW total_rules: Общее количество правил """ enabled: bool = Field(..., description="Включен ли UFW") rules: List[UFWRule] = Field(default_factory=list, description="Список правил UFW") @property def total_rules(self) -> int: """Возвращает общее количество правил.""" return len(self.rules) class UFWStatus(BaseModel): """ Модель для представления статуса UFW. Attributes: enabled: Включен ли UFW message: Сообщение о статусе last_checked: Время последней проверки статуса """ enabled: bool = Field(..., description="Включен ли UFW") message: str = Field(..., description="Сообщение о статусе")