Зависимости наших зависимостей или несколько слов об уязвимости наших проектов

    Зависимости наших зависимостей



    Эта история началась 30 ноября, утром. Когда вполне обычный билд на Test environment внезапно упал. Наверное, какой-то линтер отвалился, не проснувшись подумал я и был не прав.


    Кому интересно чем закончилась эта история и на какие мысли навела – прошу под кат.


    Открыв билд-лог, я увидел, что упал npm install. Странно подумал, я. Еще вчера вечером все работало отлично. После некоторого изучения логов, была найдена подозрительная строка:


    это подозрительно...
    node --eval 'if (require("./package.json").name === "coffee-script") { var red, yellow, cyan, reset; red = yellow = cyan = reset = ""; if (!process.env.NODE_DISABLE_COLORS) { red = "\x1b[31m"; yellow = "\x1b[33m"; cyan = "\x1b[36m"; reset = "\x1b[0m"; } console.warn(red + "CoffeeScript has moved!" + reset + " Please update references to " + yellow + "\"coffee-script\"" + reset + " to use " + yellow + "\"coffeescript\"" + reset + " (no hyphen) instead."); console.warn("Also, a new major version has been released under the " + yellow + "coffeescript" + reset + " name on NPM. This new release targets modern JavaScript, with minimal breaking changes. Learn more at " + cyan + "http://coffeescript.org" + reset + "."); console.warn(""); }

    Здесь я опять удивился. Опять же, еще вчера мы coffee-script на проекте не использовали и за ночь вряд ли что-то сильно изменилось. Быстрый просмотр package.json подтвердил, что никакой супостат ничего нового туда не добавлял. Значит, наверное, у нас обновилась какая-то зависимость, которая использует coffee-script. Но против этой идеи говорило то, что уже довольно давно я выставил строгие версии для всех зависимостей проекта и, как мне казалось, такого случиться не могло. Бесплодно поискав похожую проблему в интернете, я опять вернулся к мысли об обновившейся зависимости. Поэтому на коленке был набросан скрипт который обошел все package.json-ы в node_modules в поисках coffee-script. Таких зависимостей оказалось около 5-6 штук. Это еще больше укрепило мои подозрения, и не сильно долго размышляя я снес весь node_modules, а заодно и все dependencies кроме одной в локальном репозитории и запустил npm install снова. Процесс прошел успешно. Дальше, шаг за шагом была найдена та зависимость, которая и валила install.


    Это оказалась karma-typescript у которой в транзитивной зависимости оказался "pad", который в свою очередь зависел от coffee-script. И тут я снова приуныл. Вариантов было немного. Или временно отключать тесты, или ждать фикса, или делать форк и чинить самому (причем не очень понятно, что же конкретно нужно чинить). Без особой надежды я отправился на Github создавать issue. Каково же было мое удивление, когда мне ответили буквально через 20 минут. Оказалось, что некоторый товарищ, решил обновить coffee-script пакет в npm и вместо того, чтобы объявить старый пакет устаревшим он просто сделал ему unpublish.


    один из комментариев от сообщества:

    На мое счастье, пользователь @ondrejbase уже предложил костыль временное решение которое мне помогло. И вся эта история закончилась относительно дешево. Но могло быть совсем не так.


    Это была присказка. А теперь я предлагаю поговорить о зависимостях наших проектов и проблемах, которые они нам приносят.


    Давайте начнём с простого.


    Глобальные зависимости


    Недавно я в очередной раз наткнулся на статью, для новичков которая начиналась со строки


    npm install -g typescript


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


    • Конфликт локальных репозиториев. Собирая весь код с помощью глобальных зависимостей (tsc, например) или валидируя его глобальным линтерами мы уже не можем так легко обновлять эти зависимости. Ведь обновив что-то глобальное, мы ставим под угрозу все репозитории в которых это используется. В итоге, придется или тратить время на то что бы обновить эти зависимости во всех репозиториях, или не обновлять их совсем.
    • Конфликт с членами команды. Использование глобальных зависимостей в командной работе приводит к их рассинхронизации. И, как следствие, к тому, что мой код не соберется или не пройдет проверку у моего коллеги.
    • Конфликт с билд-машиной. То, что соберется у меня, не гарантировано соберется на билд машине. Или, что гораздо хуже и коварнее — соберется не так, как у меня. А это гарантированный ад дебага на ближайший час или больше.

    И все это происходит потому, большинство пособий начинается с npm install abc -g. Хотя можно поставить все локально и подключить в package.json как ./node_modules/.bin/tsc


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


    N.B. Глобально устанавливать генераторы кода (create-react-app, create-angular-app) это нормально. Они один раз отработают и все. Кроме того, вам не понадобится ставить их заново, когда вы решите создать следующий репозиторий.


    Нестрогие зависимости


    Давайте идти дальше. Установим create-react-app, и создадим базовое приложение. Заходим в package.json и что мы там видим?


    "react": "^16.2.0"


    Все (или почти все) знают, что значит символ ^. Он значит, что npm может установить любую версию старше или равной указанной, в пределах мажорного релиза. И что в этом плохого? Не хочется быть категоричным, но, как по мне этот подход тоже "не очень", и вот почему:


    • Потеря контроля. Как только вы оставили ^ или ~ в своем package.json вы потеряли контроль над своим кодом. Конечно я утрирую, но подумайте. Вы, грузите в проект версию сторонней библиотеки о которой не знаете даже ее версии. Только то, что она лежит в репозитории от какого-то издателя. И вдруг что случится, остается только надеяться, что у этого пакета достаточно организованное сообщество что бы это заметить.
    • Изменения ломающие обратную совместимость. Конечно, публикуя мажорную версию, разработчики обещают, что в ее рамках breaking changes не будет. Но, господа, давайте быть реалистами. Это интернет, и это open source: вам никто ничего не должен. То, что работало час назад, может отвалиться с комментарием типа: “This new release targets modern JavaScript, with minimal breaking changes”.
    • Неочевидность обновлений. Допустим есть у меня библиотека вида abc: ^2.1.1. И, получается, я не знаю какая на самом деле версия библиотеки у меня стоит. Может быть 2.1.1, а может быть 2.9.9. И вроде бы это не проблема, наоборот. Мне не нужно искать документацию для определенной версии, достаточно всегда смотреть самую свежую. И вот я ее смотрю, и вижу новую фишку, и она не работает. А не работает она потому что я просто забыл обновить библиотеку. Я ее обновляю, комичу код, и через 30 минут ко мне прибегает мой коллега потому что у него приложение не работает! А за ним приходят еще трое и делают мне грустно. Кому это нужно?
    • И наконец: зависимости зависимостей. Это вишенка на торте и это то, о чем я бы хотел, чтобы вы задумались.

    Зависимости наших зависимостей или ЗНЗ


    Давайте начнем с того, почему у нас сломался билд. Все зависимости были заданы жестко и тем не менее мы свалились. Несмотря на то, что версии наших основных зависимостей оставались прежними (напомню, никаких ^, ~ в package.json) их зависимости были не такими строгими. Мы не контролировали зависимости наших зависимостей, хотя и пытались. И, что самое неприятное такое поведение поощряется по умолчанию. Я не знаю кто и зачем это сделал, но он подложил большую свинью всем нам, а особенно тем, кто практикует continuous-integration.


    Конечно, конкретно эту проблему исправить легко. Достаточно создать lock-file (например, с помощью команды npm shrinkwrap) или использовать yarn — пакетный менеджер который по умолчанию фиксирует все зависимости вашего проекта.


    Однако это решает только часть проблемы. Остается еще одна, гораздо более опасная ее часть и имя ей unpublish. Если вы не сталкивались с этой проблемой до этого, то вот тут прекрасная статья которая показывает всю уязвимость современной веб разработки. В любой момент, умышленно или по неосторожности, ваш проект может перестать собираться только потому, что кто-то удалил свой пакет из npm. И сделать это отнюдь не сложно. Достаточно просто ввести команду unpublish. С этой бедой можно бороться. Но давайте будем честны перед собой? У кого из нас стоит свой локальный npm? А кто хотя бы задумывался об этом? Боюсь, что не так много. И я лишь надеюсь, что теперь вы предупреждены.
    Кстати, если вы зайдете в мой репозиторий (не делайте этого, прошу), то увидите, что почти все мои проекты нарушают все, что было сказано выше. И это еще одно доказательство того, насколько проблема распространена.


    Выводы


    • Используйте флаг глобальной установки с умом. Не стоит использовать его для тех зависимостей от которых будет постоянно зависеть ваш проект.
    • Если вы работаете не один или на нескольких физических машинах, подумайте о том, чтобы задать зависимости строго. Это позволит вам иметь одинаковые основные зависимости проекта во всех репозиториях и у коллег. Кроме того, вы увеличите контроль над собственным кодом и процессом его обновления.
    • Используйте lock-файл. Особенно если вы собираетесь использовать continues-integration.
    • Задумайтесь о частном регистре пакетов. Это не только убережет вас от внезапного удаления пакетов, но и ускорит установку зависимостей на билд машине. А это сэкономит ваше время и количество кофе, которое выпивается пока проходит билд во время пулл реквеста.

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


    И приношу свои извинения за английские слова, но в русском эквиваленте они сильно режут глаз.

    Поделиться публикацией
    Похожие публикации
    Ой, у вас баннер убежал!

    Ну, и что?
    Реклама
    Комментарии 34
    • 0

      При реализации третьего правила, второе особо не нужно.

      • 0
        Вообще не нужно, но это решение немного проще.
        • +1
          Указание основных зависимостей строго это ведь полумера, лок файл решает задачу целиком.
          • 0

            lock файл в npm — это чисто информационная штука. Он не гарантирует то, что будут установлены именно те зависимости, что в нем указаны.


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


            Хотите рабочий lock файл — используйте yarn

            • 0
              А об npm выше никто и не писал.
      • +2

        Странно мне казалось что после скандала с left-pad такое не должно больше происходить. Да и в документации указано что:


        With the default registry (registry.npmjs.org), unpublish is only allowed with versions published in the last 24 hours. If you are trying to unpublish a version published longer ago than that, contact support@npmjs.com.
        • +1
          Кейс свежий, от 30.11.2017 года. Так что все не так просто как оказалось.
        • +3
          Первое правило package.json: чем меньше зависимостей, тем лучше.
          Второе правило package.json: смотри первое правило.
          • –1
            То есть предлагаете писать свои велосипеды для всех нужд вместо использования модулей? А ведь их нужно будет тестировать самому, поддерживать и тд.
            • 0

              Их и так приходится тестировать, поскольку никакой гарантии, что это кто-то сделал в апстриме.

              • 0
                Тестировать реализованный функицонал в своем приложении, и тестировать дополнительно функционал зависимостей это очень разные вещи. Если существует достаточно прилично сделанный модуль, почему не использовать его? Приличность модуля определить не сложно просмотрев код, тесты и issues, это делается один раз и занимает ну пол часа времени.
          • +1
            Строгое указания без ^, ~ особо не поможет, ведь нет гарантии что у зависимостей будет также, поэтому лок файлы. Указывать ^, ~ для библиотек не такая и плохая идея, а для проектов предпочитаю указывать точные версии и допустим раз в неделю или раз в месяц обновлять используя npm-check-updates.
            • +1
              Если все зависимости намертво прибиты гвоздями, то зачем вообще нужен пакетный менеджер? Какую роль в этом случае он выполняет в проекте?
              • 0
                А какую роль решают пакетные менеджеры допустим в линуксах (pacman например)?
                • 0
                  Собирают зависимости на клиенте, чтобы ему не таскать лишнего. В случае веб приложения смысла собирать зависимости на рабочем сервере я не вижу — ведь можно просто выложить рабочий и протестированный комплект кода вместе с зависимостями. Тем более что зависимости все равно предлагается обновлять вручную.
                  • 0
                    Существуют разные типы софта, например библиотеки и приложения (проекты которые куда-либо деплояться). Если ты релизишь веб библиотеку, то релизить ее с намертво прибитыми гвоздями зависимостями идея так себе, тк при включении этой бибиотеки в проект ее зависимость может например конфликтовать с такой же зависимостью но другой версии которая уже использовалась в проекте для других целей, уже не пишу об обновлениях безопасности. В вебе в принципе можно изолировать зависимости библиотеки и предотвратить тем самым конфликты, но это тоже плохая идея, так отдавать в браузере разные версии одной и тойже библиотеки идея так себе (трафик).
                    • +1
                      Даже зафиксированную зависимость можно обновить, перейти на более новую версию, используя тот же менеджер пакетов, это не совсем вручную.
                      Плюс менеджер пакетов предоставляет возможность видеть списком зависимости и их верссии и поддерживать этот список в системе контроля версий, что само по себе уже немало.

                      Если же вы положите зависимости к себе явно, то вы будете лишены этих возможностей. Да еще потеряете уверенность, что никто не вносит туда изменения, что лишит вас возможности переходить на новые версии. Класть зависимости в свой код — очень плохая практика. Или вы поддерживаете код или нет.

                      Альтернативой является например зависимые гит репозитории. Где можно и обновляться и при этом отслеживать локальные изменения. Но это тоже по сути разновидность пакетного менеджера.
                      • 0

                        Гит субмодули — это такие лок файлы с криптографической гарантией целостности :-)

                  • +1

                    организационную. ну и удобство при обновлениях

                    • +1
                      • не хранить их в репе
                      • легко проверить доступность обновлений
                      • легко обновить
                      • унифицированный способ управления зависимостями
                      • 0
                        Можно сделать форки на все зависимости и не переживать, что кто-то что-то поменяет.
                      • +1

                        Никогда такого не было, и вот опять.


                        1. У вас нарушена воспроизводимость билдов, один и тот же билд одного и того же коммита произведет разное в зависимости от неконтролируемого вами окружения.
                        2. Я не знаю как принято у js-хипстеров, в мире java всегда было хорошим тоном поднять внутри компании локальный кэширующий maven-proxy. Чтобы не попадать в ситуацию "все пропало" при отсутствии интернета, да и просто чтобы все было быстрее
                        3. В npm что, нельзя взять и распечатать дерево зависимостей?
                        • +1
                          В npm что, нельзя взять и распечатать дерево зависимостей?

                          Можно. Но простейшая комбинация babel-cli + babel-preset-latest + webpack уже дает 636 узлов в дереве и 21 килобайт текстового представления. :-)

                          • 0
                            Если можно, зачем тогда вот это вот всё?

                            Поэтому на коленке был набросан скрипт который обошел все package.json-ы в node_modules в поисках coffee-script. Таких зависимостей оказалось около 5-6 штук. Это еще больше укрепило мои подозрения, и не сильно долго размышляя я снес весь node_modules, а заодно и все dependencies кроме одной в локальном репозитории и запустил npm install снова. Процесс прошел успешно. Дальше, шаг за шагом была найдена та зависимость, которая и валила install.
                            • 0
                              Здесь все просто, по запарке просто не пришло в голову :).
                          • +1
                            1. Об этом и речь. И это не только у меня, это подход по-умолчанию.
                            2. У кого-то принято у кого-то нет.
                            3. Можно см. ниже.

                            И давайте без странных подначек про хипстеров, это не к месту.
                            • 0
                              И давайте без странных подначек


                              А вы не провоцируйте. Второй раз наступание на те же грабли что были с left-pad. Потом будет что-нибудь типа «у хостинга npm-репозитория отгнило электричество, поэтому репозиторий был несколько часов недоступен и половина интернета сломалась».
                              • +1
                                А в мире Java все наступают на грабли только один раз? Или в мире .Net? Не надо применять стереотипы, если не хотите что бы их применяли к вам.
                            • 0
                              1. Это дополнительные сложности. В крупной компании или просто при наличии квалифицированных админов/девопсов (причём особо не нагруженными задачами к разработке мало относящихся) может разработка поставить задачу под поднятию, настройке и поддержке такого прокси. Если же это условие не выполняется, грубо, вы единственный в компании кто вообще понимает о чём речь, то ресурсы вам на это могут и не выделить.
                              • –1
                                Если вы не можете выполнить инструкцию на www.npmjs.com/package/npm-proxy-cache то так и скажите, зачем приплетать какие-то дополнительные сложности с «особо не нагруженными админами/девопсами, ещё и не входящими в команду разработки».
                              • +1
                                У вас нарушена воспроизводимость билдов, один и тот же билд одного и того же коммита произведет разное в зависимости от неконтролируемого вами окружения.

                                Интересно как вы гарантируете воспрозводимость, работая с Java. Можно подробнее?

                                • +2
                                  В java тоже бывает library hell, иногда он проявляется только в рантайме, там еще и класслоадеры есть разные, не стоит как-то возвышать эти жавы.
                                  • 0

                                    Я и не возвышаю, просто автор выше говорит, что у них нет проблем. Я такого не видел. Вот и хочу узнать как.

                              • 0

                                Если речь идёт о фронт-энд, то там особых последствий не будет. Ну придется потратить времени и немного покопаться. Критичного ничего нет.


                                А вот когда мы уже говорим о бек-энд. На сколько люди реально проверяют библиотеки, которые они используют?
                                В принципе, в большинстве фреймворков никто не мешает получить объект запроса, проверить ip, и если он нужный отправить в ответ всю базу данных. И это можно сделать не сразу, а с каким-то обновлением. Все ли отслеживают каждую строчку? Конечно за основными библиотеками следят много людей и большая вероятность, что что-то найдут, но если речь идёт о какой-то второстепенной вещи без большого сообщества.
                                Я понимаю, что пример про базу данных сильно натянутый, но сама идея, что код может вообще все делать на сервере.


                                А если совместить с темой поднятой автором во второй части статьи, что и у зависимостей есть свои зависимости...


                                Жизнь — боль!

                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                Самое читаемое