Читать 7 мин

Подмена запросов на подтверждение в macOS при проверке AI-агентов

Подмена запросов на подтверждение в macOS может обмануть занятого проверяющего. Узнайте, как проверять личность приложения, отклонять поддельные окна и безопасно проверять действия AI-агентов.

Подмена запросов на подтверждение в macOS при проверке AI-агентов

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

Проблема становится острее, когда автономные агенты для программирования могут вызывать API или выполнять команды SSH. Проверяющий может одновременно смотреть на pull request, терминал, окно чата и целую очередь уведомлений. Если по интерфейсу подтверждения нельзя сразу понять его источник и область действия, проверяющий занимается безопасностью через миниатюру.

Решение не в том, чтобы учить людей запоминать каждое диалоговое окно macOS. Зрительная память проигрывает поддельному окну, созданному человеком, который изучил те же скриншоты. Проверяющим нужен повторяемый способ связать подтверждение с тремя фактами: процессом, который его запрашивает, предстоящим действием и доверенным местом, где собирается подтверждение.

Знакомое окно не доказывает, кому оно принадлежит

Окно, похожее на системный запрос, остаётся всего лишь набором пикселей, пока вы не свяжете его с работающим процессом и конкретным запросом. Скруглённые углы, знакомый порядок кнопок, значок в строке меню, упоминание Touch ID и фраза в стиле Apple не подтверждают источник. Они лишь показывают, что кто-то знает, как выглядит macOS.

Когда это сформулировано прямо, различие кажется очевидным, но привычки проверяющих часто его стирают. Люди спрашивают: «Похоже ли это на запрос, который я видел на прошлой неделе?» Лучше спросить: «Какой процесс вызвал этот запрос и где я могу проверить этот процесс, не полагаясь на это окно?»

macOS предоставляет полезные средства проверки происхождения программ. Apple объясняет в документации macOS о процессе подписывания приложений, что подпись Developer ID позволяет убедиться: после подписи разработчиком программа не была изменена. Apple отдельно рассматривает нотариальную проверку, которая проверяет переданное программное обеспечение на известное вредоносное содержимое. Эти средства важны, но ни одно из них не превращает каждую фразу подписанного приложения в надёжный запрос на авторизацию.

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

Считайте любое отображение запроса на подтверждение одним из четырёх вариантов, пока не установите обратное:

  • системная поверхность согласия или аутентификации, принадлежащая macOS;
  • окно приложения, которым вы собирались пользоваться;
  • уведомление о том, что приложение требует внимания;
  • веб-страница или накладка, стилизованная под один из первых трёх вариантов.

Первая категория может использовать системные элементы управления, но не принимайте решение только по внешнему виду. Остальные категории содержат данные приложения. Они могут быть полезны, но требуют более тщательной проверки, чем «я узнаю форму этой кнопки».

Каждое подтверждение должно связывать исполнителя, действие и канал

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

Исполнитель, это локальный процесс, который делает запрос. В рабочем процессе агента это не просто «помощник для программирования». Это конкретный процесс, запущенный для текущего сеанса, с определяемым подписантом там, где рабочий процесс может его предоставить. Если написано лишь «AI-помощнику нужно подтверждение», от проверяющего скрыт самый полезный факт.

Действие, это конкретная операция. «Разрешить доступ к сети» не описывает операцию, если на самом деле запрашивается создание пользователя production через API с известной конечной точкой. «Подтвердить SSH» тоже недостаточно, если команда может быть безопасной проверкой статуса или разрушительным удалённым развёртыванием. Проверяющему нужны глагол, назначение и последствия, которые инструмент может описать честно.

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

Вот как это выглядит на практике.

Слабый запрос:

Помощнику развёртывания нужно разрешение. Разрешить?

Запрос, который можно проверить:

Процесс: подписанный локальный запуск агента из Terminal

Действие: POST /v1/releases на api.example.internal

Полномочия: учётные данные для развёртывания в production

Область действия: только этот запрос

Канал решения: запущенное приложение подтверждения

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

Это также показывает ошибку, которую команды совершают с «доверенными агентами». Доверие к исполнителю не означает разрешение на любое действие. Процесс может быть знакомым, а его назначение ошибочным. Назначение может быть ожидаемым, а область действия учётных данных чрезмерной. Безопасный процесс проверки показывает эти факты рядом, а не превращает подтверждение в общее одобрение.

Уведомления должны направлять к проверке, а не собирать согласие

Уведомление служит способом прервать работу, но не надёжной поверхностью авторизации. Место в нём ограничено, оно конкурирует со всеми остальными предупреждениями на рабочем столе и может содержать текст, рассчитанный на быстрый щелчок. Если уведомление говорит «Срочно: подтвердите до истечения срока развёртывания», оно уже оказывает давление, хотя вы ещё не знаете, кто создал сообщение и существует ли вообще такое развёртывание.

Настройки Notifications в Apple позволяют управлять предварительным просмотром, отображением уведомлений на экране блокировки, а также их показом при совместном использовании или зеркалировании экрана. Apple также позволяет настраивать разрешения и способ отображения для каждого приложения. Эти настройки уменьшают нежелательное раскрытие данных, но не превращают текст уведомления в надёжную запись транзакции.

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

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

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

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

Накладки используют внимание, а не уязвимость macOS

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

Распространённое объяснение слишком упрощено: «Поддельный запрос должен быть вредоносной программой с особыми правами доступа к экрану». Иногда вредоносная программа действительно добивается широких разрешений, но убедительной накладке они нужны не всегда. Любое приложение может показывать собственное содержимое. Вкладка браузера может открыть страницу, похожую на экран согласия. Сеанс удалённой поддержки может попросить пользователя подтвердить действие, пока оператор управляет всей историей происходящего.

Не путайте возможность видеть экран с возможностью создавать окно. Apple рассматривает Screen & System Audio Recording как разрешение Privacy & Security, которое пользователь может разрешить или запретить отдельным приложениям и веб-сайтам. Оно управляет захватом содержимого экрана и звука. Оно не сертифицирует запрос, а выданное разрешение не показывает, кому принадлежит окно перед вами.

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

Полезная проверка накладки начинается с поведения, а не с оформления:

  • Появился ли запрос сразу после действия, которое вы инициировали?
  • Показывает ли ожидаемое приложение тот же запрос, если переключиться на него и открыть заново?
  • Называет ли запрос назначение и область действия в терминах, соответствующих вашей работе?
  • Требует ли он пароль, фразу восстановления или секрет, которые этому процессу никогда не должны быть нужны?
  • Пытается ли он помешать вам проверить запрос в другом месте?

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

Убедительная подмена развивается по узнаваемому сценарию

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

Представьте, что инженер просит агента обновить staging-среду. В выводе терминала агент сообщает, что ему нужно подтверждение для вызова API. Почти одновременно появляется уведомление: «Требуется авторизация развёртывания. Откройте Security Center».

Инженер нажимает на него. Открывается окно с заголовком, похожим на знакомую панель безопасности macOS. В нём есть краткое описание, назначение, начинающееся с ожидаемого домена компании, и кнопки «Отмена» и «Разрешить». Оно даже упоминает Touch ID, хотя окно не запускает настоящую биометрическую проверку.

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

  1. Уведомление могло прийти не от приложения подтверждения.
  2. Нажатие могло открыть страницу браузера или вспомогательный инструмент, а не настоящий элемент управления подтверждением.
  3. Показанное назначение может быть похожим доменом, перенаправлением или широким разрешением аккаунта, а не конкретным запросом развёртывания.
  4. Процесс, запустивший агента, может отличаться от процесса, запрашивающего подтверждение.
  5. После нажатия может не остаться постоянной записи, поэтому проверяющий не сможет восстановить, что именно было разрешено.

Поэтому протокол проверки должен выдерживать визуально идеальную имитацию. Инженеру следует оставить окно открытым, не нажимая ни одну кнопку, вывести ожидаемое приложение подтверждения через строку меню или папку Applications и найти запрос, созданный текущим запуском агента. Если совпадения нет, всплывающее окно не становится надёжнее из-за аккуратного оформления.

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

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

Проверьте установленное приложение до появления запроса

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

Для приложения, установленного по известному пути, эта команда выводит сведения о подписи, которые видит macOS:

codesign -dvvv \"/Applications/Expected Approval App.app\" 2>&1 | grep -E 'Identifier=|TeamIdentifier=|Authority='

Вывод обычно выглядит так:

Identifier=com.example.approvals
TeamIdentifier=ABCDE12345
Authority=Developer ID Application: Example Software, Inc. (ABCDE12345)
Authority=Developer ID Certification Authority
Authority=Apple Root CA

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

Проверьте также статус оценки установленного пакета:

spctl -a -vv \"/Applications/Expected Approval App.app\"

Обычно вывод содержит результат вроде accepted, а также источник и происхождение. Формулировки могут отличаться в разных версиях macOS и при разных способах распространения, поэтому не создавайте скрипт, который ломается из-за ожидания одной точной фразы. Используйте эту команду как средство расследования: неожиданное отклонение, источник, путь или личность подписанта требуют остановки и проверки.

В документации Apple о Gatekeeper объясняется, что macOS в стандартных настройках проверяет загруженные программы на наличие известного разработчика, нотариальной проверки и изменений. Это защищает процесс запуска, что необходимо. Но такая проверка не заменяет вопрос: запрашивает ли приложение правильное действие сейчас.

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

Не выбирайте ленивое объяснение: «Оно находится в Applications, значит, это нужное приложение». Applications, это место, а не подтверждение личности. Там тоже может лежать скопированный пакет или пакет с похожим названием. Путь даёт стабильный объект для проверки. Подпись сообщает заявленную личность. А ожидаемость этой личности определяется процессом команды.

Проверяйте запрос независимым способом

Отзывайте подозрительные сеансы
Журнал Sessions позволяет мгновенно отозвать разрешение запущенного сеанса агента.

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

  1. Остановитесь при появлении первого запроса. Не нажимайте «Разрешить», «Отклонить» и встроенные ссылки, пока не поймёте, что его открыло.
  2. Опишите ожидаемое действие простыми словами. Например: «Я попросил staging-агента прочитать статус развёртывания». Если запрос описывает запись в production, сеанс SSH или широкий доступ к аккаунту, расхождение уже обнаружено.
  3. Независимо откройте приложение подтверждения через ожидаемый элемент строки меню, место в Applications или известный запускатель. Не используйте подозрительное окно как средство навигации.
  4. Найдите ожидающий запрос и сравните инициирующий процесс, действие, назначение, полномочия и область действия. Одного совпадения имени процесса недостаточно.
  5. Подтверждайте только минимальный запрос, необходимый для текущей задачи. Отклоняйте неясный, чрезмерный или не соответствующий инициированной работе запрос.

Эта процедура отвергает популярную, но плохую рекомендацию: «Один раз подтвердите знакомого агента, чтобы он перестал вас отвлекать». Она популярна, потому что повторяющиеся запросы раздражают, а агентам часто нужны несколько связанных вызовов. Но проблема в количестве запросов, а не в необходимости убрать границу проверки.

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

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

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

Формулировка запроса должна показывать область действия раньше срочности

Не передавайте секреты в промптах
Его зашифрованное хранилище передаёт HTTP- и SSH-учётные данные, не раскрывая их агенту.

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

Сначала укажите действие. «Создать релиз в production» сообщает, что произойдёт. «Авторизовать процесс развёртывания» скрывает операцию за внутренним названием.

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

Опишите область действия понятными словами. Разрешение на один запрос GET к известной конечной точке существенно отличается от доступа к учётным данным для всех вызовов сеанса. Подтверждение одной команды SSH отличается от неограниченной оболочки. Запрос должен описывать настоящую границу, а не предполагаемую.

Честно сообщайте, чего проверяющий не видит. Если инструмент знает лишь, что агент инициирует соединение SSH, пусть так и скажет, а не создаёт видимость, будто проверил каждую удалённую команду. Ложная точность приучает доверять меткам, которые не соответствуют фактическим ограничениям.

Избегайте расплывчатых названий действий, особенно таких:

  • «Продолжить»
  • «Предоставить доступ»
  • «Завершить настройку»
  • «Подтвердить личность»
  • «Разрешить работу агента»

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

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

Журналы превращают подозрение в расследование

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

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

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

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

Sallyport записывает сведения о сеансах и отдельных действиях в зашифрованный журнал аудита с цепочкой хешей, а sp audit verify может проверять эту цепочку офлайн, не расшифровывая данные. Это не делает ошибочное подтверждение безвредным, но даёт проверяющему и следователю надёжный ответ на главный вопрос после исчезновения окна: какое действие этот процесс действительно выполнил?

Цель не в том, чтобы сделать каждого проверяющего специалистом по безопасности macOS. Нужно сделать безопасное действие привычным: уйти с подозрительной поверхности, заново открыть ожидаемую панель управления, сравнить факты и подтвердить только запрос, соответствующий текущей работе. Запрос, который не выдерживает такой проверки, не заслуживает щелчка.

Вопросы и ответы

Что делать, если я получил неожиданное уведомление о подтверждении на Mac?

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

Может ли настоящее уведомление macOS быть частью атаки с подменой?

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

Доказывает ли подпись кода безопасность запроса на подтверждение?

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

Как проверить, какое приложение на Mac запрашивает подтверждение?

Проверьте именно установленное приложение, которым вы собирались пользоваться, а не приложение с похожим названием в Downloads или скопированный пакет на рабочем столе. Выполните codesign -dvvv для его пути, заранее запишите Identifier и TeamIdentifier и разберитесь с любым изменением до подтверждения действий.

Может ли обычное приложение создать поддельное системное окно macOS?

Обычное приложение может разместить обычное окно поверх других окон и имитировать знакомый текст. Доступ Screen Recording позволяет приложению записывать содержимое экрана, а доступ Accessibility может дать ему возможность управлять частями интерфейса. Ни одно из этих разрешений не должно становиться быстрым основанием доверять окну.

Стоит ли подтверждать важные действия прямо из уведомлений?

Уведомления полезны, чтобы сообщить о необходимости внимания. Они плохо подходят для подтверждения перевода денег, изменений в production, использования секретов или удалённых команд: видимая область мала, а контекст может быть неполным.

Какие сведения должен показывать безопасный запрос AI-агента?

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

Что делать, если я, кажется, подтвердил поддельный запрос?

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

Почему недостаточно определить процесс, который запрашивает подтверждение?

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

Как Sallyport уменьшает путаницу с запросами на подтверждение?

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

Sallyport

Sallyport выполняет API-вызовы и SSH-команды за вашего ИИ-агента. Ключи остаются в локальном хранилище на вашем Mac; вы подтверждаете каждый запуск, и каждое действие попадает в запечатанный журнал.

© 2026 Sallyport · Открытый код по лицензии Apache-2.0 · Oleg Sotnikov