Мне одному кажется, что атак на цепочку поставок ПО становится всё больше? То и дело злоумышленники публикуют вредоносную версию популярного пакета, пользователи устанавливают её на свои компьютеры — и в результате оказываются скомпрометированы. Только в марте такие атаки затронули npm‑пакет Axios, сканер уязвимостей Trivy и Python‑пакет LiteLLM.
Пока меня это не коснулось: во всех этих случаях атаки затрагивали библиотеки и пакеты, которыми я не пользуюсь. Но было бы наивно думать, что так будет всегда. У меня много локальных Python‑проектов, и я задумался, что буду делать, если скомпрометируют пакет, который использую я.
Первый шаг — обнаружение. Допустим, я уже знаю, что конкретная версия пакета вредоносна. Как понять, устанавливал ли я её? Из‑за того, что я использую виртуальные окружения, ответить на этот вопрос не так‑то просто.
Что такое виртуальные окружения?
Виртуальные окружения (virtual environments, или virtualenvs) позволяют создавать изолированные окружения Python, в каждом из которых установлен собственный набор пакетов. Благодаря им разные проекты могут использовать разные зависимости. Например, если двум проектам нужны разные версии одного пакета, для каждого можно создать отдельное виртуальное окружение с подходящей версией.
Виртуальное окружение хранится в каталоге, где находятся символические ссылки на глобальный интерпретатор Python и установленные в это окружение пакеты. После «активации» окружения команды вроде pip install устанавливают пакеты в его каталог, а не в глобальную установку Python.
Например:
# `python3` указывает на мой глобальный интерпретатор which python3 /Library/Frameworks/Python.framework/Versions/3.13/bin/python3 # Создаём виртуальное окружение python3 -m venv .venv # Активируем виртуальное окружение. Теперь команды `python3` и `pip` # будут выполняться внутри него source .venv/bin/activate # Теперь `python3` указывает на символическую ссылку в виртуальном окружении which python3 /private/tmp/example/.venv/bin/python3 # Pillow будет установлен в каталог `.venv` pip install Pillow
Для каждого Python‑проекта я создаю отдельное виртуальное окружение, поэтому на моём личном Mac их накопилось довольно много.
Чтобы проверить, устанавливал ли я версию X пакета Y, пришлось бы просмотреть каждое виртуальное окружение. Сам Python не ведёт список созданных окружений, поэтому такой список приходится поддерживать самостоятельно.
Как получить список всех виртуальных окружений
Я очень последователен в именовании виртуальных окружений: их каталог всегда называется.venv. На самом деле у меня даже есть shell‑функция для создания виртуальных окружений, которая жёстко задаёт это соглашение.
Благодаря этому я могу найти все виртуальные окружения в домашнем каталоге одной командой:
find ~ -type d -name .venv /Users/alexwlchan/repos/snippets/.venv /Users/alexwlchan/repos/alexwlchan.net/.venv /Users/alexwlchan/repos/colour-scheme/.venv …
Точно так же можно искать виртуальные окружения на внешних дисках и томах:
find /Volumes/Media/ -type d -name .venv /Volumes/Media/Screenshots/.venv /Volumes/Media/Social Media/.venv /Volumes/Media/Bookmarks/.venv …
Эти команды выполняются около 30 секунд — как раз достаточно долго, чтобы начать раздражать, — поэтому я сохранил результаты в текстовый файл:
find ~ -type d -name .venv >> ~/.venv_registry find /Volumes/Media/ -type d -name .venv >> ~/.venv_registry
Кроме того, я изменил shell‑функцию, которая создаёт виртуальные окружения: теперь при каждом создании нового окружения она обновляет этот файл. В итоге у меня всегда есть актуальный список всех виртуальных окружений, по которому можно искать уязвимые зависимости.
А что с Python‑пакетами, установленными вне виртуальных окружений?
Если выполнить pip install, не активировав виртуальное окружение, пакеты установятся в глобальную установку Python и не попадут в этот список. Обычно так делать не стоит, потому что вы снова столкнётесь с несовместимыми зависимостями в разных проектах.
Можно настроить pip так, чтобы он работал только внутри виртуальных окружений — через переменную окружения или конфигурационный файл. После этого pip будет отказываться устанавливать пакеты вне виртуального окружения.
Если же вместо pip вы используете uv, установить пакеты вне виртуального окружения не получится, пока вы явно не передадите флаг ‑system, разрешающий изменить системный Python.
В конфигурационном файле командной оболочки у меня задано PIP_REQUIRE_VIRTUALENV=true, а для установки пакетов я использую uv, поэтому вне виртуальных окружений Python‑пакетов у меня нет.
Поиск версий пакетов во всех виртуальных окружениях
Теперь, когда у меня есть текстовый файл со списком всех виртуальных окружений, я могу писать скрипты, которые запускают команды в каждом из них.
Например, вот bash‑скрипт, который выполняет uv pip freeze во всех виртуальных окружениях и выводит список установленных зависимостей:
#!/usr/bin/env bash set -o errexit set -o nounset while read -r venv_dir; do if ! test -d "$venv_dir"; then echo "does not exist: $venv_dir" >&2 continue fi echo "== $venv_dir ==" uv pip freeze --python "$venv_dir/bin/python" echo "" done < ~/.venv_registry
Меньше чем за полсекунды я получаю полный список всех Python‑пакетов, установленных во всех виртуальных окружениях на моём Mac. Я сохраняю вывод в текстовый файл, а затем могу искать скомпрометированные версии пакетов или убедиться, что конкретный пакет у меня вообще нигде не установлен, даже как транзитивная зависимость.
Отсутствующие виртуальные окружения я пропускаю. Скорее всего, это временные окружения, которые я ещё не успел удалить из реестра, либо окружения на внешних дисках, которые сейчас не смонтированы.
Мне нравится, что этот скрипт не запускает сам интерпретатор Python, а значит, не усугубит ситуацию, если вредоносный пакет уже установлен. uv написан на Rust и не выполняет Python‑код: он просто умеет разбирать структуру установок Python.
Например, во время недавней атаки на LiteLLM злоумышленники устанавливали файл.pth, который выполнялся при любом запуске Python, даже если сам LiteLLM не импортировался. Достаточно было выполнить обычную команду python ‑version или pip freeze, чтобы скомпрометировать компьютер. Этот скрипт несложно изменить так, чтобы он искал вредоносный файл.pth во всех моих Python‑окружениях, ни разу не запуская Python.
Другие способы использовать реестр виртуальных окружений
Изначально я написал этот инструмент для поиска скомпрометированных пакетов, но затем нашёл ему и другие применения:
Я могу находить устаревшие версии пакетов и следить за тем, чтобы все виртуальные окружения оставались актуальными.
Если я хочу отказаться от какого‑то пакета, то могу найти все проекты, где он ещё используется, и удалить его. Например, сейчас я постепенно заменяю некоторые сторонние HTTP‑библиотеки средствами стандартной библиотеки, а эти скрипты помогают понять, где сторонние зависимости всё ещё остались.
Я могу искать по всему своему Python‑коду места, где используются определённые функции или возможности, не прогоняя grep по всему диску. Например, у меня есть несколько собственных вспомогательных библиотек, и так я могу увидеть, какие функции ещё используются, а какие уже можно удалить. Для этого я ищу в родительском каталоге каждого пути.venv, то есть в корне соответствующего проекта.
Надеюсь, ни одна из библиотек, которыми я пользуюсь, никогда не окажется скомпрометирована. Но если это всё же случится, я буду готов. А пока этот инструмент и без того приносит немало пользы.

Если хотите глубже разобраться в устройстве Python‑приложений и увереннее работать с их основными компонентами, приходите на бесплатные уроки. На них можно разобрать практические вопросы с преподавателями и посмотреть, как эти подходы применяются в реальных проектах.
— 3 августа, 20:00. «Оживляем код: первые шаги в ООП на Python». Записаться
— 11 августа, 20:00. «Работа с SQLAlchemy и Alembic в FastAPI». Записаться
— 18 августа, 20:00. «Python asyncio: gather, wait, TaskGroup на практике». Записаться
rSedoy
Есть же uv audit, ну и для общего развития - pysentry-rs, pip-audit