Поток угроз захлестывает, а ИИ-агент путает 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 в тыкву:
- Временной гниль: Индикатор компрометации, актуальный неделю назад, сегодня может быть легитимным. Без поля valid_until LLM выдаст опасную рекомендацию.
- Шум и качество: 90% фидов — это мусор: сканеры, боты, устаревшие хэши. RAG, который ищет по смыслу, найдет похожий по тексту, но не по сути устаревший объект.
- Стандартизация: Одна угроза может быть описана как 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,)
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.