База киберугроз для LLM-агентов: STIX/TAXII + векторный поиск 2026 | AiManual
AiManual Logo Ai / Manual.
06 Июл 2026 Гайд

Строим базу киберугроз для LLM-агентов: от прототипа до production с векторным поиском и STIX/TAXII

Пошаговое руководство по построению production-ready базы знаний для LLM-агентов в кибербезопасности: STIX/TAXII, векторный поиск, RAG, временная релевантность.

Поток угроз захлестывает, а ИИ-агент путает CVE из 2010 с вчерашней атакой?

Ты сидишь в SOC, перед тобой десятки фидов STIX/TAXII, PDF-ки от vendor-ов, RSS-ленты. Данных море. А LLM-агент, которого ты гордо внедрил для помощи аналитикам, на вопрос «Какие актуальные индикаторы компрометации для APT28?» выдает CVE десятилетней давности и IP-шники из спам-листа 2015 года. Знакомая боль? Как Senior DevOps, переживший не одну попытку построить полезного безопасного бота, скажу: проблема не в LLM, а в базе знаний. Без нормального пайплайна сбора, нормализации и поиска threat intelligence ты просто кормишь нейронку мусором.

Вот вам тезис, который я буду доказывать кодом: для LLM-агента в security разница между RAG и RAG с метаданными — это разница между подсказкой из «Википедии» и докладом разведки.

Почему обычный RAG не вывозит threat intelligence

Большинство RAG-решений сегодня работают по принципу «забросал все документы в векторную БД — и молись». Для генеральных вопросов об истории это сойдет. Но для кибербезопасности есть три убийственных нюанса, которые превращают такой RAG в тыкву:

  1. Временной гниль: Индикатор компрометации, актуальный неделю назад, сегодня может быть легитимным. Без поля valid_until LLM выдаст опасную рекомендацию.
  2. Шум и качество: 90% фидов — это мусор: сканеры, боты, устаревшие хэши. RAG, который ищет по смыслу, найдет похожий по тексту, но не по сути устаревший объект.
  3. Стандартизация: Одна угроза может быть описана как STIX Indicator, другая — как PDF под грифом «для служебного пользования». LLM нужно дать единую схему.

Я уже писал о том, почему поиск в RAG — это на самом деле фильтрация. В threat intelligence эта идея выходит на первый план. Мы не просто ищем похожие строки — мы должны отсекать всё, что потеряло свежесть, не относится к интересующей нас группировке или имеет низкую достоверность.

Архитектура: не просто векторный поиск, а контекстный движок

Наша целевая система — это не «еще один RAG». Это контекстно-осведомленная база киберугроз, которая умеет отвечать LLM-агенту с учетом временных меток, типов объектов и доверенных источников. Вдохновение черпаем из концепции RAG 2026: от гибридного поиска до production — там как раз разложен правильный путь.

КомпонентТехнологияЗачем
Сбор и нормализацияSTIX 2.1 / TAXII, python-stix2, cabby, Apache TikaПриводим разношерстные данные к единой схеме
Хранение и векторизацияQdrant + BGE-M3 (или OpenAI ada-002)Быстрый поиск с фильтрами по метаданным
Оркестрация агентаLangChain / LlamaIndex + кастомный ретриверУчет дат, статусов, типов при запросе
Мониторинг и обновлениеKubernetes CronJob + Kafka / NATSЖивые фиды без ручного дампа

Главное отличие от «наивного RAG»: каждый индикатор, каждая атака, каждая уязвимость хранятся не просто как вектор + текст, а с обязательными метаданными: type, valid_from, valid_until, confidence, source, labels. Без этих полей любая векторизация — пустая трата GPU.

💡 Кстати, когда SQL и векторный поиск дерутся за ваши данные — отличная статья про то, как совместить фильтрацию по метаданным с семантическим поиском. Мы пойдем по тому же пути.

Шаг 1: Жадно глотаем STIX-фиды, но с фильтром

Первый шаг — это не запись в БД, а настройка критического фильтра. Прежде чем что-то индексировать, реши: какие данные ты вообще хочешь видеть? Если твой SOC занимается APT-атаками, тебе не нужны сотни тысяч IP из спам-листов. Используй TAXII-коллекции с флагом confidence > 60 или labels, содержащие apt, malware, cve.

from stix2 import TAXIICollectionSource, MemoryStore
from cabby import TAXIIClient

client = TAXIIClient('taxii.server.local')
# получаем коллекцию, где публикуют только подтвержденные APT-индикаторы
collection = client.get_collection('apt_high_confidence')
source = TAXIICollectionSource(collection)

for indicator in source.query(query_expression='SELECT * FROM Indicator'):
    if indicator.get('confidence', {}).get('value', 0) < 60:
        continue
    if indicator.get('valid_until') < datetime.utcnow():
        continue
    # теперь можно векторизовать

1Парсим PDF и RSS — да, больно, но нужно

Не всё приходит по TAXII. Многие вендоры кидают отчеты в PDF. Бери Apache Tika или Unstructured.io для вытаскивания текста. Но не вздумай весь PDF закинуть одним вектором. Разбивай в соответствии со STIX-объектами: если в PDF описаны три разные уязвимости — создай три отдельных документа с метаданными (CVE ID, дата, severity). Иначе ретривер словит шум.

2Векторизация: BGE-M3 vs Ada-002

На текущий момент (июль 2026) лучший выбор для локальной инсталляции — BGE-M3 от BAAI. Он понимает кириллицу, даёт эмбеддинги размерности 1024 и поддерживает sparse-поиск. Если можешь позволить себе платить — ada-002 делает векторы неплохо, но привязка к облаку. Для security иногда приходится работать в изолированном контуре, так что BGE-M3 — выбор прагматика.

from sentence_transformers import SentenceTransformer

model = SentenceTransformer('BAAI/bge-m3')
text = "CVE-2026-1234: Remote code execution in Apache HTTP Server …"
embedding = model.encode(text)  # shape: (1024,)
💡
Совет: перед эмбеддингом нормализуй текст — удали лишние пробелы, приведи к нижнему регистру, но не удаляй идентификаторы (CVE, IP, хэши). LLM-агент будет искать по ним точное совпадение, а векторная БД может не увидеть разницы между похожими хэшами.

3Qdrant: коллекция с фильтрами — наше всё

Создаем коллекцию. Обязательно включаем payload_schema для полей valid_from, valid_until, cve_id, type, source. Это позволит ретриверу делать пред-фильтр до поиска по вектору. Без этого — будете получать старые данные, как в плохом фильме ужасов.

from qdrant_client import QdrantClient, models

client = QdrantClient('localhost', port=6333)
client.create_collection(
    collection_name="threat_intel",
    vectors_config=models.VectorParams(size=1024, distance=models.Distance.COSINE),
)
client.create_payload_index(
    collection_name="threat_intel",
    field_name="valid_until",
    field_schema=models.PayloadSchemaType.DATETIME,
)
client.create_payload_index(
    collection_name="threat_intel",
    field_name="type",
    field_schema=models.PayloadSchemaType.KEYWORD,
)

4LLM-агент с кастомным ретривером: делаем умный filter

Теперь самое интересное: соединяем всё с LLM. Берем LangChain (на июль 2026 уже стабильная версия 0.3.x) и пишем кастомный ретривер, который перед поиском добавляет фильтр по актуальности и типу угрозы. Пример:

from langchain.retrievers import QdrantRetriever
from qdrant_client.http import models

class FreshnessAwareRetriever(QdrantRetriever):
    def get_relevant_documents(self, query: str) -> List[Document]:
        # Извлекаем из запроса параметры (можно через LLM extraction)
        now = datetime.utcnow()
        filter_ = models.Filter(
            must=[
                models.FieldCondition(key="valid_until", range=models.Range(gte=now)),
                models.FieldCondition(key="valid_from", range=models.Range(lte=now)),
            ]
        )
        return self._qdrant_client.scroll(
            collection_name=self.collection_name,
            scroll_filter=filter_,
            with_payload=True,
        )[0]

Добавляем этот ретривер в цепочку LangChain. Теперь агент на вопрос «Свежие индикаторы по группе Lazarus» вернет только те записи, которые ещё не истекли. И обрати внимание: без такой фильтрации LLM может «увидеть» старый IP и начать блокировать легитимный сервис. Цена ошибки — недоступность продакшена.

Кстати, если тебе нужен пример более продвинутой оркестрации с учетом контекста, посмотри контекстная инженерия для локальных LLM. Там отлично показано, как средняя модель становится надежной за счет контекста.

Production: обновление в реальном времени и мониторинг

Прототип собрали? Теперь думаем о production. Самая частая ошибка — статический дамп данных. Угрозы появляются каждый час, а некоторые IOC живут минуты. Нужна стриминговая архитектура:

  • Слушаем TAXII-канал на предмет новых публикаций (или дергаем по расписанию через CronJob);
  • Кладем новые объекты в Kafka-топик;
  • Consumer читает, векторизует, пишет в Qdrant;
  • Старые записи с истекшим valid_until не удаляем сразу — помечаем status: expired и в ретривере фильтруем по этому полю. Это нужно для forensic-анализа.
apiVersion: batch/v1
kind: CronJob
metadata:
  name: threat-intel-updater
spec:
  schedule: "*/15 * * * *"
  jobTemplate:
    spec:
      template:
        spec:
          containers:
          - name: updater
            image: registry.local/threat-updater:2026.07.06
            env:
            - name: TAXII_ENDPOINT
              value: "https://taxii.internal.example.com"

Топ-3 ошибки, которые превращают базу угроз в тыкву

Ошибка №1: Индексировать всё без фильтра качества. Я видел системы, где 80% векторов — это мусорные хэши из открытых списков. В итоге агент находит «похожий» индикатор, который ни о чем не говорит. Решение: черный список источников низкого качества, порог confidence.

Ошибка №2: Не учитывать тип объекта STIX. Indicator, Campaign, ThreatActor, Vulnerability — у каждого свои поля. Если ты запихнешь всё в один вектор с текстом «группа APT28 использует...», ретривер не сможет отличить описание атаки от ее индикатора. Лучше создать отдельные коллекции для разных типов или добавить жесткую фильтрацию по type.

Ошибка №3: Не тестировать временную релевантность. Проверь, что твой ретривер не возвращает данные с просроченным valid_until. Я бы советовал добавить юнит-тесты, которые на дату «2026-07-06» не подтягивают записи, у которых valid_until = «2026-06-01». Иначе в один прекрасный понедельник агент предложит заблокировать IP, который уже принадлежит госсайту.

Альтернативный взгляд: GraphRAG для связей между угрозами

Векторный поиск хорош для индикаторов, но плохо передает связи: кто на кого атакует, какие техники MITRE ATT&CK использует группа, как связаны CVE. Если тебе нужно такое — смотри в сторону GraphRAG против слепоты векторного поиска. Комбинация графов и векторов — имба, но порог входа выше. Для начала сделай нормальную векторную историю, а потом накрути граф.

Неочевидный совет: зачем хранить expired-записи отдельно

Когда индикатор теряет актуальность, не удаляй его из основной коллекции — просто добавь флаг status: expired и обнови valid_until в прошлом. Во-первых, это позволяет forensic-аналитикам восстанавливать картину атаки на определенную дату. Во-вторых, если ты случайно удалил запись, а потом потребовалось разобраться, что произошло три месяца назад — данных уже нет. Держи две коллекции: threat_intel_active (только актуальные) и threat_intel_archive (все исторические с тэгом). Ретривер для агента по умолчанию использует активную, а для исследовательских запросов — архивную.

Итог: ты построил базу, которая не просто складирует угрозы, а живет с ними, понимает, что вчера было опасно, а сегодня — нет. LLM-агент перестает быть «болталкой», а становится реальным помощником в SOC. Уже к 2027 году без такой инфраструктуры любая автоматизация будет лишь профанацией. Не будь тем парнем, чей ИИ предлагает блокировать Google DNS.

Подписаться на канал