commit cb55eaef0102100b9f273c1fe3ac5d13c0417fb5 Author: KuzarinM Date: Sat May 2 18:33:38 2026 +0300 Первый запуск diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/LogsPatternExtractor.iml b/.idea/LogsPatternExtractor.iml new file mode 100644 index 0000000..2c80e12 --- /dev/null +++ b/.idea/LogsPatternExtractor.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..06bb031 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..02c3c16 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..ce10670 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Generator/Enums/RandomType.py b/Generator/Enums/RandomType.py new file mode 100644 index 0000000..5bdce7e --- /dev/null +++ b/Generator/Enums/RandomType.py @@ -0,0 +1,13 @@ +from enum import Enum, auto + + +class RandomType(Enum): + IP = auto() + DATE = auto() + EMAIL = auto() + STATUS_CODE = auto() + PATH = auto() + USERNAME = auto() + INT = auto() + VERSION = auto() + ID = auto() \ No newline at end of file diff --git a/Generator/LogGenerator.py b/Generator/LogGenerator.py new file mode 100644 index 0000000..beb2070 --- /dev/null +++ b/Generator/LogGenerator.py @@ -0,0 +1,178 @@ +import random +import re + +from sentence_transformers import InputExample + +from Generator.Enums.RandomType import RandomType +from Generator.Models.ConstLiteral import ConstLiteral +from Generator.Models.Term import Term +from Generator.Models.VariableLiteral import VariableLiteral +from Generator.UniversalRandomizer import UniversalRandomizer + + +class LogGenerator: + def __init__(self): + # Обертки для переменных: id=..., [ip], 'user' + self.wrappers = [("", ""), ("", ""), ("id=", ""), ("user:", ""), ("[", "]"), ("'", "'")] + + # Словарь для констант (имитация логов) + self.log_keywords = [ + # Уровни логирования + "INFO", "ERROR", "WARN", "DEBUG", "TRACE", "CRITICAL", "FATAL", "NOTICE", + + # Действия (Verbs) + "started", "stopped", "failed", "completed", "aborted", "retrying", + "connecting", "disconnected", "listening", "resolving", "binding", + "parsing", "rendering", "authenticating", "authorizing", "validated", + "rejected", "accepted", "dropped", "created", "deleted", "updated", + "fetching", "sending", "receiving", "waiting", "killing", "spawning", + + # Сущности (Nouns) + "System", "Kernel", "Thread", "Process", "Worker", "Daemon", "Job", + "Connection", "Session", "User", "Client", "Server", "Proxy", "Gateway", + "Database", "Table", "Index", "Query", "Transaction", "Commit", "Rollback", + "Cache", "Buffer", "Heap", "Stack", "Memory", "Disk", "Volume", + "Network", "Port", "Socket", "Interface", "Protocol", "Packet", + "Request", "Response", "Header", "Body", "Payload", "Token", "Key", + "File", "Directory", "Path", "Config", "Module", "Plugin", "Component", + "Exception", "Error", "Timeout", "Latency", "HealthCheck", "Status", + + # HTTP и Web + "GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD", + "HTTP/1.1", "HTTP/2", "API", "Endpoint", "Route", "URI", "URL", + "JSON", "XML", "YAML", "HTML", "CSS", "JS", + + # Предлоги и связки + "at", "in", "on", "to", "from", "by", "with", "for", "via", "through", + + # Прилагательные и состояния + "successful", "failed", "denied", "allowed", "active", "inactive", + "pending", "queued", "blocked", "locked", "corrupted", "invalid", + "missing", "found", "available", "unavailable", "busy", "idle", + "secure", "insecure", "public", "private", "local", "remote" + ] + + def generate(self, min_literals=15, max_literals=25) -> Term: + count = random.randint(min_literals, max_literals) + literals = [] + + for i in range(count): + # 60% Константа, 40% Переменная + if random.random() < 0.6: + # Либо слово из словаря, либо случайное слово + txt = random.choice(self.log_keywords) if random.random() < 0.8 else UniversalRandomizer.fake.text.word() + literals.append(ConstLiteral(text=txt)) + else: + r_type = random.choice(list(RandomType)) + pref, post = random.choice(self.wrappers) + literals.append(VariableLiteral(name=f"v{i}", type=r_type, prefix=pref, postfix=post)) + + return Term(literals=literals, separator=random.choice([" ", ";", "|"])) + + def generate_training_data(self, count=100): + train_examples = [] + + for _ in range(count): + anchor_term = self.generate() + + anchor_text = anchor_term.render().text + + # 2. Генерируем Positive (Позитивный пример) + positive_text = anchor_term.render().text + + # 3. Генерируем Hard Negative + literals_copy = anchor_term.literals[:] + random.shuffle(literals_copy) + + negative_hard_text = anchor_term.separator.join([lit.render().text for lit in literals_copy]) + + # 4. Генерируем Easy Negative (Совсем другой шаблон) + random_other_term = self.generate() + negative_easy_text = random_other_term.render().text + + # 3. Генерируем Very Hard Negative + + bad_sep = random.choice([" ", ";", "|", " "]) + negative_very_hard_text = bad_sep.join([lit.render().text for lit in literals_copy]) + + # 5. Упаковываем для Sentence Transformers + + # Перемешивание, но с сохранением разделителя + train_examples.append(InputExample(texts=[ + self.mask_log_structure(anchor_text), + self.mask_log_structure(positive_text), + self.mask_log_structure(negative_hard_text) + ])) + + # Другой лог + train_examples.append(InputExample(texts=[ + self.mask_log_structure(anchor_text), + self.mask_log_structure(positive_text), + self.mask_log_structure(negative_easy_text) + ])) + + # Перемешивание + случайный разделитель + train_examples.append(InputExample(texts=[ + self.mask_log_structure(anchor_text), + self.mask_log_structure(positive_text), + self.mask_log_structure(negative_very_hard_text) + ])) + + return train_examples + + def mask_log_structure(self, text: str) -> str: + # 1. GUID / UUID (строгий паттерн) + # Пример: 123e4567-e89b-12d3-a456-426614174000 + text = re.sub(r'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}', '', text) + + # 2. IP-адреса (IPv4) + # Пример: 192.168.0.1 + # Важно делать ДО флоатов, иначе 192.168 определится как Float + text = re.sub(r'\d{1,3}(?:\.\d{1,3}){3}', '', text) + + # 3. Числа с плавающей точкой (Floats) + # Пример: 0.05, 123.45, -3.14 + # (?', text) + + # 4. Целые числа (Integers) + # Пример: 404, 500, -1 + text = re.sub(r'-?\d+', '', text) + + # 5. (Опционально) Hex-строки (адреса памяти, хеши) + # Пример: 0x7fff5fbff + text = re.sub(r'0x[0-9a-fA-F]+', '', text) + + return text + + + +if __name__ == "__main__": + gen = LogGenerator() + gen.generate_training_data(count=1) + + print("Пример генерации датасета:\n") + + # Генерируем 5 примеров + for i in range(10): + # 1. Получаем объект Term + term = gen.generate() + + # 3. Используем данные (например, сохраняем в JSON для обучения) + print(f"--- Sample {i + 1} ---") + result = term.render() + print(f"{term.structure().text}") + + for j in range(5): + # 2. Рендерим его в строку и метаданные + result = term.render() + + print(f"Positive {j}: {result.text}") + + for j in range(5): + # 2. Рендерим его в строку и метаданные + random.shuffle(term.literals) + term.separator = random.choice([" ", ";", "|"]) + result = term.render() + + print(f"Negative {j}: {result.text}") diff --git a/Generator/Models/ConstLiteral.py b/Generator/Models/ConstLiteral.py new file mode 100644 index 0000000..0cbdab0 --- /dev/null +++ b/Generator/Models/ConstLiteral.py @@ -0,0 +1,15 @@ +from dataclasses import dataclass + +from Generator.Models.Literal import Literal +from Generator.Models.RenderResult import RenderResult + + +@dataclass +class ConstLiteral(Literal): + text: str + + def render(self, chanse: float = 1) -> RenderResult: + return RenderResult(self.text, []) + + def structure(self) -> RenderResult: + return self.render() \ No newline at end of file diff --git a/Generator/Models/Literal.py b/Generator/Models/Literal.py new file mode 100644 index 0000000..5f43f75 --- /dev/null +++ b/Generator/Models/Literal.py @@ -0,0 +1,12 @@ +from dataclasses import dataclass + +from Generator.Models.RenderResult import RenderResult + + +@dataclass +class Literal: + def render(self, chanse: float = 1) -> RenderResult: + return RenderResult("", []) + + def structure(self) -> RenderResult: + return RenderResult("", []) diff --git a/Generator/Models/RenderResult.py b/Generator/Models/RenderResult.py new file mode 100644 index 0000000..6e4a7ff --- /dev/null +++ b/Generator/Models/RenderResult.py @@ -0,0 +1,8 @@ +from dataclasses import dataclass +from typing import List, Tuple + + +@dataclass +class RenderResult: + text: str + spans: List[Tuple[int, int, str]] \ No newline at end of file diff --git a/Generator/Models/Term.py b/Generator/Models/Term.py new file mode 100644 index 0000000..cfef37d --- /dev/null +++ b/Generator/Models/Term.py @@ -0,0 +1,53 @@ +from dataclasses import dataclass +from typing import List + +from Generator.Models.ConstLiteral import ConstLiteral +from Generator.Models.Literal import Literal +from Generator.Models.RenderResult import RenderResult +from Generator.Models.VariableLiteral import VariableLiteral + + +@dataclass +class Term: + literals: List[Literal] + separator: str = " " + + def render(self, chanse: float = 1) -> RenderResult: + final_text = "" + final_spans = [] + + for i, literal in enumerate(self.literals): + res = literal.render(chanse) + + current_offset = len(final_text) + final_text += res.text + + # Сдвигаем координаты с учетом позиции слова в строке + for (start, end, label) in res.spans: + final_spans.append((current_offset + start, current_offset + end, label)) + + # Добавляем разделитель, если это не последнее слово + if i < len(self.literals) - 1: + final_text += self.separator + + return RenderResult(final_text, final_spans) + + def structure(self) -> RenderResult: + final_text = "" + final_spans = [] + + for i, literal in enumerate(self.literals): + res = literal.structure() + + current_offset = len(final_text) + final_text += res.text + + # Сдвигаем координаты с учетом позиции слова в строке + for (start, end, label) in res.spans: + final_spans.append((current_offset + start, current_offset + end, label)) + + # Добавляем разделитель, если это не последнее слово + if i < len(self.literals) - 1: + final_text += self.separator + + return RenderResult(final_text, final_spans) \ No newline at end of file diff --git a/Generator/Models/VariableLiteral.py b/Generator/Models/VariableLiteral.py new file mode 100644 index 0000000..0bd7d95 --- /dev/null +++ b/Generator/Models/VariableLiteral.py @@ -0,0 +1,45 @@ +import random +from dataclasses import dataclass + +from Generator.Enums.RandomType import RandomType +from Generator.Models.Literal import Literal +from Generator.Models.RenderResult import RenderResult +from Generator.UniversalRandomizer import UniversalRandomizer + + +@dataclass +class VariableLiteral(Literal): + name: str + type: RandomType + prefix: str = "" + postfix: str = "" + last_value: str | None = None + + def render(self, chanse: float = 1) -> RenderResult: + if self.last_value is None or random.random() <= chanse: + # Генерируем значение + val = str(UniversalRandomizer().get_random(self.type)) + self.last_value = val + else: + val = self.last_value + + # Формируем строку: префикс + значение + постфикс + full_text = f"{self.prefix}{val}{self.postfix}" + + # Вычисляем координаты ЧИСТОГО значения (без префикса) + start = len(self.prefix) + end = start + len(val) + + return RenderResult(full_text, [(start, end, self.type.name)]) + + def structure(self) -> RenderResult: + val = f"<{self.type.name}>" + + # Формируем строку: префикс + значение + постфикс + full_text = f"{self.prefix}{val}{self.postfix}" + + # Вычисляем координаты ЧИСТОГО значения (без префикса) + start = len(self.prefix) + end = start + len(val) + + return RenderResult(full_text, [(start, end, self.type.name)]) \ No newline at end of file diff --git a/Generator/UniversalRandomizer.py b/Generator/UniversalRandomizer.py new file mode 100644 index 0000000..aa0929b --- /dev/null +++ b/Generator/UniversalRandomizer.py @@ -0,0 +1,31 @@ +import random +from typing import Any + +from Generator.Enums.RandomType import RandomType +from mimesis import Generic +from mimesis.locales import Locale + + +class UniversalRandomizer: + fake = Generic(locale=Locale.EN) + + def get_random(self, r_type: RandomType) -> Any: + if r_type == RandomType.IP: + return self.fake.internet.ip_v4() + if r_type == RandomType.DATE: + return self.fake.datetime.date().isoformat() + if r_type == RandomType.EMAIL: + return self.fake.person.email() + if r_type == RandomType.STATUS_CODE: + return self.fake.internet.http_status_code() + if r_type == RandomType.PATH: + return f"/var/log/{self.fake.file.file_name()}" + if r_type == RandomType.USERNAME: + return self.fake.person.username() + if r_type == RandomType.INT: + return random.randint(1, 9999) + if r_type == RandomType.VERSION: + return self.fake.development.version() + if r_type == RandomType.ID: + return self.fake.cryptographic.uuid().split('-')[0] + return "UNKNOWN" diff --git a/Infrostructure/ProtocolCoder/BitReader.py b/Infrostructure/ProtocolCoder/BitReader.py new file mode 100644 index 0000000..7cdfd19 --- /dev/null +++ b/Infrostructure/ProtocolCoder/BitReader.py @@ -0,0 +1,36 @@ +class BitReader: + """ + Класс для чтение битов из байтовой строки (bytes). + """ + + def __init__(self, data): + self.data = data + self.bit_pos = 0 + self.total_bits = len(data) * 8 + + def read_bits(self, length): + """ + Считывает length бит и возвращает их как целое число. + """ + if self.bit_pos + length > self.total_bits: + raise ValueError(f"Недостаточно данных: запрошено {length}, осталось {self.remaining()}") + + value = 0 + # Читаем побитово (можно оптимизировать, но так надежнее для понимания) + for _ in range(length): + byte_index = self.bit_pos // 8 + # В байте биты идут слева направо (7..0), где 7 - старший + bit_offset = 7 - (self.bit_pos % 8) + + bit = (self.data[byte_index] >> bit_offset) & 1 + value = (value << 1) | bit + + self.bit_pos += 1 + return value + + def has_bits(self, length): + """Проверяет, осталось ли достаточно бит для чтения.""" + return self.bit_pos + length <= self.total_bits + + def remaining(self): + return self.total_bits - self.bit_pos \ No newline at end of file diff --git a/Infrostructure/ProtocolCoder/BitWriter.py b/Infrostructure/ProtocolCoder/BitWriter.py new file mode 100644 index 0000000..7872d72 --- /dev/null +++ b/Infrostructure/ProtocolCoder/BitWriter.py @@ -0,0 +1,34 @@ +class BitWriter: + """ + Класс для накопления бит и их конвертации в байтовую строку. + """ + + def __init__(self): + self.value = 0 + self.bit_count = 0 + + def add_bits(self, val, length): + """ + Добавляет length бит из числа val в поток. + """ + # Сдвигаем текущее накопленное значение влево на length + self.value = (self.value << length) | (val & ((1 << length) - 1)) + self.bit_count += length + + def get_bytes(self): + """ + Возвращает накопленные биты в виде объекта bytes. + Если количество бит не кратно 8, дополняет нулями справа (до полного байта). + """ + if self.bit_count == 0: + return b'' + + # Вычисляем количество необходимых байт + num_bytes = (self.bit_count + 7) // 8 + + # Сдвигаем значение влево, чтобы заполнить последний байт, если он не полон + # Например, если есть 4 бита 1010, нам нужно получить байт 10100000 (0xA0) + shift_remainder = (num_bytes * 8) - self.bit_count + final_value = self.value << shift_remainder + + return final_value.to_bytes(num_bytes, byteorder='big') \ No newline at end of file diff --git a/Infrostructure/ProtocolCoder/MessageEncoder.py b/Infrostructure/ProtocolCoder/MessageEncoder.py new file mode 100644 index 0000000..116f3ec --- /dev/null +++ b/Infrostructure/ProtocolCoder/MessageEncoder.py @@ -0,0 +1,218 @@ +import time + +from Infrostructure.ProtocolCoder.BitReader import BitReader +from Infrostructure.ProtocolCoder.BitWriter import BitWriter + + +class MessageEncoder: + def __init__(self): + pass + + def encode_protocol(self, template_id, variables, section_power=3): + + # --- 1. Секция заголовков --- + writer = BitWriter() + + # Поле 1: Размер секции (1 байт) + # Здесь указываем саму степень (например, 3) + writer.add_bits(section_power, 8) + + # Вычисляем размер одной секции в битах (S) + section_size_bits = 1 << section_power + # Максимальное число, которое можно записать в поле, описывающее длину (например, для 8 бит это 255) + max_len_per_section = (1 << section_size_bits) - 1 + + # Поле 2: Зарезервированная область (4 секции) + # 4 секции * section_size_bits + writer.add_bits(0, 4 * section_size_bits) + + # --- 2. Секция шаблона --- + + # Определяем битовую длину ID шаблона + # Если ID=0, нужно хотя бы 1 бит, но bit_length() вернет 0, обрабатываем это + tn = template_id.bit_length() if template_id > 0 else 1 + + # Поле 3: Размер следующей секции (tn) в секциях (размер поля = 1 секция) + # Внимание: в ТЗ написано "1 секция – размер следующей секции ... в битах". + writer.add_bits(tn, section_size_bits) + + # Поле 4: Идентификатор шаблона (tn бит) + writer.add_bits(template_id, tn) + + # --- 3. Секции данных --- + + for var_id, var_val in variables: + # Подготовка значения переменной + if isinstance(var_val, str): + # Если строка, берем код первого символа (для примера 'A' -> 65) + # Для полноценных строк нужно кодировать в байты, здесь упрощение под "числовые переменные" + if len(var_val) == 1: + val_int = ord(var_val) + else: + # Если пришла длинная строка, кодируем как большое число + val_bytes = var_val.encode('utf-8') + val_int = int.from_bytes(val_bytes, byteorder='big') + else: + val_int = var_val + + # Определяем необходимые биты для значения и ID + # Используем bit_length для максимальной компактности + # Однако, в примере ID=1 (1 бит) записан в 4 бита. + # Алгоритм: берем минимально необходимый размер, либо выравниваем, если требуется. + # ТЗ: "вписываются в максимально компактном виде". Значит, берем реальный bit_length. + + # Биты для значения + val_total_bits = val_int.bit_length() if val_int > 0 else 1 + # Биты для ID + id_bits = var_id.bit_length() if var_id > 0 else 1 + + # Логика разбиения на секции, если значение не влезает в одну секцию описания размера. + # Поле размера (xn) само имеет размер 1 секцию (например, 8 бит). + # Значит, максимальная длина блока данных = 255 бит. + # Если val_total_bits > 255, нужно разбивать на несколько секций данных. + + bits_left = val_total_bits + + # Для корректной нарезки битов большого числа нам удобно преобразовать его в строку или срезать маской + # Но проще математически брать куски от старших бит к младшим или наоборот. + # Порядок записи битов: обычно Big Endian. + + while bits_left > 0: + # Определяем, сколько бит значения запишем в этот блок + # Либо всё что осталось, либо максимум, который можно описать одним числом в поле размера + chunk_size = min(bits_left, max_len_per_section) + + # Вырезаем нужный кусок (chunk) из числа val_int + # Нам нужны старшие биты из оставшихся. + # Пример: всего 10 бит, берем 8. Нужно сдвинуть (10-8)=2 раза вправо. + shift = bits_left - chunk_size + chunk_val = (val_int >> shift) & ((1 << chunk_size) - 1) + + # Поле 5: Размер ID в битах (n) - занимает 1 секцию + writer.add_bits(id_bits, section_size_bits) + + # Поле 6: Размер блока значения в битах (xn) - занимает 1 секцию + writer.add_bits(chunk_size, section_size_bits) + + # Поле 7: Идентификатор (n бит) + writer.add_bits(var_id, id_bits) + + # Поле 8: Блок значения (xn бит) + writer.add_bits(chunk_val, chunk_size) + + bits_left -= chunk_size + + return writer.get_bytes() + + def decode_protocol(self, data): + """ + Декодирует бинарные данные обратно в ID шаблона и список переменных. + + :param data: bytes объект + :return: кортеж (template_id, list_of_variables) + где list_of_variables это список кортежей (var_id, value) + """ + reader = BitReader(data) + + # --- 1. Секция заголовков --- + if not reader.has_bits(8): + raise ValueError("Пустые данные или некорректный заголовок") + + # 1. Размер секции (степень двойки) + section_power = reader.read_bits(8) + section_size = 1 << section_power # 2^power + + # 2. Пропускаем зарезервированную область (4 секции) + reader.read_bits(4 * section_size) + + # --- 2. Секция шаблона --- + + # 3. Размер ID шаблона (1 секция) + tn = reader.read_bits(section_size) + + # 4. Идентификатор шаблона (tn бит) + template_id = reader.read_bits(tn) + + # --- 3. Секции данных --- + + variables = [] + last_var_id = None + + # Читаем, пока есть данные. + # Минимальный блок данных требует 2 секции заголовков (размер ID и размер значения) + while reader.has_bits(2 * section_size): + # 5. Размер ID переменной (1 секция) + n = reader.read_bits(section_size) + + # 6. Размер значения переменной (1 секция) + xn = reader.read_bits(section_size) + + # Проверяем, хватает ли бит на само тело данных + # (Это может случиться, если в конце файла "мусорные" нули для выравнивания байта) + if not reader.has_bits(n + xn): + break + + # 7. Идентификатор переменной + var_id = reader.read_bits(n) + + # 8. Значение переменной (часть значения) + chunk_value = reader.read_bits(xn) + + # Логика склеивания (Reassembly): + # Если ID текущей переменной совпадает с ID последней добавленной, + # значит это продолжение большого числа, которое было разбито на секции. + # Энкодер писал старшие части первыми (Big Endian logic в чанках), + # поэтому мы сдвигаем старое значение и добавляем новый кусок. + if last_var_id is not None and var_id == last_var_id: + # Получаем предыдущее значение + _, prev_val = variables.pop() + # Сдвигаем его влево на размер нового куска и добавляем новый кусок + new_val = (prev_val << xn) | chunk_value + variables.append((var_id, new_val)) + else: + # Новая переменная + variables.append((var_id, chunk_value)) + last_var_id = var_id + + return template_id, variables + + def get_hex(self, data): + return " ".join(f"{b:02X}" for b in data) + + def from_hex(self, hex_str): + return bytes.fromhex(hex_str) + + def int_to_str(self, number): + if number == 0: + return "" + # 1. Вычисляем, сколько байт занимает число + # (bit_length() + 7) // 8 — это округление вверх до целого байта + num_bytes = (number.bit_length() + 7) // 8 + + # 2. Превращаем число в байты + # Важно использовать byteorder='big', так как энкодер записывал старшие байты первыми + bytes_data = number.to_bytes(num_bytes, byteorder='big') + + # 3. Декодируем байты в строку + try: + return bytes_data.decode('utf-8') + except UnicodeDecodeError: + # Если число не является валидной utf-8 строкой, возвращаем как есть или hex + return f"" + + +if __name__ == '__main__': + me = MessageEncoder() + hex = "03 00 00 00 00 01 81 27 59 18 19 1A 96 98 19 16 98 19 00 8F F9 37 B7 BA 00" + + # Генерируем + binary_data = me.from_hex(hex) + + t = time.time() + for i in range(1000): + data = me.decode_protocol(binary_data) + print((time.time() - t )*1000) + + tmp = [(i[0], me.int_to_str(i[1])) if i[1] > 100000 else i for i in data[1]] + + print(data[0], tmp) diff --git a/Infrostructure/RabbitMQ/RabbitMQMessenger.py b/Infrostructure/RabbitMQ/RabbitMQMessenger.py new file mode 100644 index 0000000..e494d6a --- /dev/null +++ b/Infrostructure/RabbitMQ/RabbitMQMessenger.py @@ -0,0 +1,98 @@ +import pika +import sys + + +class RabbitMQMessenger: + def __init__(self, host='k8s.worker', username='rabbit', password='rabbit', port=32294): + """ + Инициализация подключения к RabbitMQ. + """ + self.credentials = pika.PlainCredentials(username, password) + self.parameters = pika.ConnectionParameters( + host=host, + port=port, + credentials=self.credentials, + # heartbeat нужен, чтобы соединение не рвалось при долгом ожидании + heartbeat=600 + ) + self.connection = None + self.channel = None + self._connect() + + def _connect(self): + """Создаем соединение и канал.""" + try: + self.connection = pika.BlockingConnection(self.parameters) + self.channel = self.connection.channel() + except Exception as e: + print(f"Ошибка подключения к RabbitMQ: {e}") + sys.exit(1) + + def send_message(self, queue_name: str, message: str): + """ + Отправка сообщения в очередь. + :param queue_name: Имя очереди, куда отправляем данные. + :param message: Данные (текст). + """ + # Объявляем очередь (durable=True значит, что очередь переживет перезагрузку RabbitMQ) + self.channel.queue_declare(queue=queue_name, durable=True) + + self.channel.basic_publish( + exchange='', + routing_key=queue_name, + body=message.encode('utf-8'), # Превращаем строку в байты + properties=pika.BasicProperties( + delivery_mode=2, # Сделать сообщение персистентным (сохранить на диске) + )) + print(f"[x] Отправлено в '{queue_name}': {message}") + + def send_binary_message(self, queue_name: str,message: bytes): + # Объявляем очередь (durable=True значит, что очередь переживет перезагрузку RabbitMQ) + self.channel.queue_declare(queue=queue_name, durable=True) + + self.channel.basic_publish( + exchange='', + routing_key=queue_name, + body=message, # Превращаем строку в байты + properties=pika.BasicProperties( + delivery_mode=2, # Сделать сообщение персистентным (сохранить на диске) + )) + print(f"[x] Отправлено в '{queue_name}': {message}") + + def start_listening(self, queue_name: str, callback_function): + """ + Запуск прослушивания очереди (блокирует выполнение скрипта). + :param queue_name: Имя очереди, которую слушаем (ответы). + :param callback_function: Функция, которая будет вызвана при получении сообщения. + Должна принимать один аргумент (текст сообщения). + """ + self.channel.queue_declare(queue=queue_name, durable=True) + + # prefetch_count=1 говорит RabbitMQ не давать работнику больше 1 сообщения за раз, + # пока он не обработает предыдущее. + self.channel.basic_qos(prefetch_count=1) + + # Внутренняя обертка, чтобы декодировать байты в текст перед передачей в ваш callback + def internal_callback(ch, method, properties, body): + text_data = body.decode('utf-8') + print(f"[v] Получено из '{queue_name}'") + + # Вызываем вашу логику обработки + callback_function(text_data) + + # Подтверждаем выполнение (ACK), чтобы сообщение удалилось из очереди + ch.basic_ack(delivery_tag=method.delivery_tag) + + self.channel.basic_consume(queue=queue_name, on_message_callback=internal_callback) + + print(f"[*] Ожидание сообщений в очереди '{queue_name}'. Нажмите CTRL+C для выхода.") + try: + self.channel.start_consuming() + except KeyboardInterrupt: + self.close() + + def close(self): + """Закрытие соединения.""" + if self.connection and not self.connection.is_closed: + self.connection.close() + print("\n[!] Соединение закрыто") \ No newline at end of file diff --git a/LogProcessingWorker.py b/LogProcessingWorker.py new file mode 100644 index 0000000..716ba06 --- /dev/null +++ b/LogProcessingWorker.py @@ -0,0 +1,116 @@ +import os + +from Infrostructure.ProtocolCoder.MessageEncoder import MessageEncoder +from Infrostructure.RabbitMQ.RabbitMQMessenger import RabbitMQMessenger +from Processor.StreamingLogCluster import StreamingLogCluster + + +class LogProcessingWorker: + def __init__(self, + model_path: str, + db_path: str, + input_queue: str = 'logs_input', + output_queue: str = 'logs_output', + output_debug_queue: str = 'logs_debug_output',): + + if os.path.exists(db_path): + os.remove(db_path) + + self.output_queue = output_queue + self.output_debug_queue = output_debug_queue + + print("--- ЗАПУСК основоного алгоритма ---") + self.clusterer = StreamingLogCluster(model_path, db_path=db_path) + + print("--- ЗАПУСК системы кодирования ---") + self.encoder = MessageEncoder() + + print("--- ЗАПУСК системы приёма/отправки сообщений ---") + self.messenger = RabbitMQMessenger() + + print("--- ЗАПУСК системы чтения сообщений ---") + self.messenger.start_listening( + queue_name=input_queue, + callback_function=self._process_log_callback + ) + + def _process_log_callback(self, log_text: str): + try: + log_text = log_text.strip() + if not log_text: + return + + print(f" [>] Обработка: {log_text[:50]}...") + + # А. Кластеризация + # process() возвращает dict, который полностью готов к JSON + analysis_result = self.clusterer.process(log_text) + + me = MessageEncoder() + + data = me.encode_protocol(analysis_result['template_id'], + [(i['uid'], i['value']) for i in analysis_result['variables']] + ) + + # Г. Отправка результата в Output очередь + # Messenger сам переподключится, если связь мигнула + self.messenger.send_binary_message(self.output_queue, data ) + self.messenger.send_message(self.output_debug_queue, str(analysis_result)) + + except Exception as e: + print(f" [!] Ошибка внутри логики обработки: {e}") + + +def local_test(): + MODEL_PATH = './Resources/model' + DB_FILE = "logs.db" + TEST_FILE = "./Resources/test/container-qfdpbp.log" + + if os.path.exists(DB_FILE): + os.remove(DB_FILE) + + print("--- ЗАПУСК основоного алгоритма ---") + clusterer = StreamingLogCluster(MODEL_PATH, db_path=DB_FILE) + + print("--- ЗАПУСК системы кодирования ---") + encoder = MessageEncoder() + + me = MessageEncoder() + + new_len = 0 + + dict = {} + + with open(TEST_FILE, 'r', errors='ignore') as f: + while True: + log_text = f.readline() + + if log_text == "": + break + analysis_result = clusterer.process(log_text) + + data = me.encode_protocol(analysis_result['template_id'], + [(i['uid'], i['value']) for i in analysis_result['variables']] + ) + new_len += len(data) + + if analysis_result['template_id'] in dict: + dict[analysis_result['template_id']] +=1 + else: + dict[analysis_result['template_id']] = 1 + print(f"[{len(data)}]->({analysis_result['template_id']})",data) + + print(new_len / 1024) + print(dict,sep="\n") + +if __name__ == '__main__': + local_test() + # MODEL_PATH = './Resources/model' + # DB_FILE = "logs.db" + # INPUT_QUEUE = "input" + # OUTPUT_QUEUE = "output" + # OUTPUT_DEBUG_QUEUE = "debug_output" + # + # processor = LogProcessingWorker(MODEL_PATH, DB_FILE, INPUT_QUEUE, OUTPUT_QUEUE, OUTPUT_DEBUG_QUEUE) + + diff --git a/Processor/Models/LogTemplate.py b/Processor/Models/LogTemplate.py new file mode 100644 index 0000000..95479d0 --- /dev/null +++ b/Processor/Models/LogTemplate.py @@ -0,0 +1,24 @@ +from typing import List, Union + +from Processor.Models.LogVariable import LogVariable + + +class LogTemplate: + def __init__(self, uid: int, tokens: List[Union[str, LogVariable]], representative_log: str): + self.uid = uid + self.tokens = tokens + self.representative_log = representative_log + self.embedding = None + self.hits = 1 + self.local_var_counter = 1 + + def get_tokens_as_str_list(self) -> List[str]: + return [str(t) if isinstance(t, LogVariable) else t for t in self.tokens] + + def render(self) -> str: + return "".join(str(t) for t in self.tokens) + + def get_next_var_id(self) -> int: + vid = self.local_var_counter + self.local_var_counter += 1 + return vid \ No newline at end of file diff --git a/Processor/Models/LogVariable.py b/Processor/Models/LogVariable.py new file mode 100644 index 0000000..33704ec --- /dev/null +++ b/Processor/Models/LogVariable.py @@ -0,0 +1,12 @@ +class LogVariable: + def __init__(self, uid: int, initial_value: str = "", var_type: str = "VAR"): + self.uid = uid + self.initial_value = initial_value + self.var_type = var_type + + def __str__(self): + return f"<{self.var_type}_{self.uid}>" + + def __repr__(self): + return str(self) + diff --git a/Processor/StreamingLogCluster.py b/Processor/StreamingLogCluster.py new file mode 100644 index 0000000..c239132 --- /dev/null +++ b/Processor/StreamingLogCluster.py @@ -0,0 +1,417 @@ +import difflib +import os +import re +import time +from typing import List, Dict, Any, Union, Optional + +import numpy as np +from sentence_transformers import SentenceTransformer, util + +from Processor.Models.LogTemplate import LogTemplate +from Processor.Models.LogVariable import LogVariable +from Processor.TemplateDatabase import TemplateDatabase + + +class StreamingLogCluster: + # --- Константы класса для удобства настройки --- + THRESHOLD_CREATE_NEW = 0.7 #0.70 + SCORE_EXACT_MATCH = 0.85 + SCORE_PARTIAL_MATCH = 0.6 + MAX_VAR_LEN = 32 + + HARD_DELIMITERS = {'|', ';', ','} + SOFT_DELIMITERS = {'=', ':', '-', '>', '<', '[', ']', '(', ')', '{', '}', '"', "'"} + + def __init__(self, model_path: str, db_path: str = "logs_knowledge.db"): + self.model = SentenceTransformer(model_path) + self.db = TemplateDatabase(db_path) + + # Компилируем регулярные выражения один раз + self.mask_regex = { + 'guid': re.compile(r'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-...'), + 'ip': re.compile(r'\d{1,3}(?:\.\d{1,3}){3}'), + 'ver': re.compile(r'\d{1,3}(?:\.\d{1,3}){2}'), + 'num': re.compile(r'-?\d+(\.\d+)?'), + 'base64': re.compile(r'(?\d{4}-\d{2}-\d{2}|\d{2}\.\d{2}\.\d{4}|\d{2}/\d{2}/\d{4})', + r'(?P