ASOC from scrap. Как реализовать автоматизацию DevSecOps контролей за несколько вечеров, используя OpenSource и LLM

от автора

Рубрика: «Очумелые ручки»

Привет, Хабр! Сегодня я хочу поделиться своим опытом разработки ASOC платформы.

В статье я расскажу о преимуществах такого решения, затем опишу особенности реализации всех ключевых компонентов. Моя цель показать, что разработка собственных ИБ инструментов сегодня не является сложной задачей и, возможно, вдохновить кого‑то на самостоятельное улучшение практик AppSec и DevSecOps в своей компании.

Дисклеймер

Прошу внимания. Спасибо за внимание!

Идея проекта возникла не на пустом месте. У меня имеется опыт разработки и сопровождения подобной платформы в продуктовой компании на объеме в ~6000 активных репозиториев и большим штатом разработчиков. Я долгое время хотел создать что‑то свое с учетом всех набитых шишек, и вот я здесь)

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

На текущий момент я не могу назвать разработанный инструмент production‑ready проектом. Для получения желаемого результата его нужно будет определенным образом «докручивать» в части автоматизации.

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

Почему нам не всегда достаточно pipeline сканирования

Рисунок 1 - Pipeline security

Рисунок 1 — Pipeline security

Одним из важнейших шагов к зрелым AppSec процессам является покрытие OWASP TOP 10 и других видов уязвимостей. К примеру, оставленных в коде секретов, уязвимых версией внешних зависимостей и так далее.

Зачастую внедрение практик сканирования реализуется через интеграцию DevSecOps сканеров в пайплайн сборки проекта (например, в GitLab). В некоторых случаях, ответственные специалисты дополнительно кастомизируют выдаваемые отчеты для удобного взаимодействия с рядовыми разработчиками, но на этом, обычно, все заканчивается.

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

  • Во‑первых мы сразу же теряем из виду Богом забытые (архивные) репозитории. В случае харденинга секретов для доступа к инфраструктуре или личных данных, нам важно, чтобы они было подчищены ВЕЗДЕ.

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

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

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

Кто такой этот ваш ASOC

Рисунок 2 - Пингвины

Рисунок 2 — Пингвины

К сожалению, речь далее пойдет не о пингвинах…

ASOC (Application Security Orchestration and Correlation) — это класс платформ для автоматизации и централизованного управления безопасностью программного обеспечения. Системы ASOC координируют работу различных инструментов сканирования (SAST, DAST, SCA), собирают данные об уязвимостях и передают их разработчикам, помогая выстроить процессы безопасной разработки (DevSecOps)
… вещает нам LLM.

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

  • Вывод статистики по найденным уязвимостям в виде таблиц, графиков и диаграм.

  • Удобная реализация механизма FalsePostives. К примеру, добавление путей до папок с тестами для конкретных репозиториев в исключения сканеров.

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

  • Интеграция с таск‑трекерами. К примеру, автоматическое заведение тасок на critical уязвимости.

  • Расчет всех возможных метрик и динамики по исправлению уязвимостей для каждой продуктовой команды на основе собираемых исторических данных и так далее. Пишите свои идеи в комментариях)

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

Именно поэтому необходимо использовать оба упомянутых метода сканирования (ASOC и pipeline) в связке. Если используем только ASOC — рискуем узнать о наличии уязвимостей в коде уже после выкатки его в прод. В ином случае не видим общей динамики и не можем контролировать процесс по устранению уязвимостей в своих продуктовых командах.

Мы слепили это из того, что было

Рисунок 3 - OpenSource

Рисунок 3 — OpenSource

Рецепт ASOC платформы довольно прост. В процессе приготовления вам потребуется:

  1. База данных

  2. Модуль инвентаризации активов.

  3. «Зоопарк» сканеров безопаности с необходимыми обвязками вокруг каждого них.

  4. Веб‑сервер.

  5. Сервис оркестрации.

По желанию, можно добавить следующие ингредиенты:

  • Сервис визуализации. Те самые дашборды из коробки для ленивых)

  • Сервис логирования.

  • Сервис хранения секретов. Для тех, кто не уважает.env файлы.

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

Основной язык программирования

Python

Веб‑фреймворк

FastAPI

База данных

PostgreSQL + SQLAlchemy + alembic

Визуализация

Grafana

Логирование

Graylog

«Оркестрация»

Docker + docker‑compose

Сканеры безопасности

KICS, Trivy, Nosey Parker

В процессе проектирования мною были рассмотрены разные варианты сохранения отчетов сканеров безопасности в базу данных. В итоге было решено производить запись через REST API «прослойку» для большего контроля за процессом.

Рисунок 4 - Архитектура

Рисунок 4 — Архитектура

Компонент № 1 — База данных

Рисунок 5 - Модель данных

Рисунок 5 — Модель данных

При проектировании платформы особенное внимание уделялось предметной области. Ниже представлено краткое описание всех необходимых сущностей:

Видим две таблицы: Пользователь и Коннектор. Обе сущности взаимодействуют с API веб‑сервера через стандартную JWT авторизацию. О том, что такое коннектор расскажу чуть ниже.

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

Отдельно хочу остановится на таблице с уязвимостями.

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

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

@field_serializer("hash")def generate_hash(self, _, _info) -> str:    return hashlib.md5(        f"{self.project_id},"        f"{self.connector_type},"        f"{self.severity},"        f"{self.filepath},"        f"{self.line},"        f"{json.dumps(self.custom_fields)}".encode(),    ).hexdigest()

Компонент № 2 — Модуль инвентаризации активов

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

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

Общая логика работы здесь довольна проста и сводится к получению актуальных метаданных о репозитории и сохранию их в базу данных. В моем случае это реализовано при помощи библиотеки git‑python. Здесь мы собираем всю необходимую информацию, пакуем ее в pydantic‑модель и отправляем на запись в базу.

def _get_project_info(self, clone_url: str, temp_dir: str) -> ProjectSchema | None:    try:        repo = Repo.clone_from(clone_url, temp_dir)    except git.exc.GitError:        return    if repo.bare:        log.logger.info(f'Repository {clone_url} is empty!')        return    project_clone_url = next(repo.remote().urls)    project_name = project_clone_url.split('/')[-1].split('.')[0]    try:        project_default_branch = repo.remote().refs['HEAD'].ref.name.split('/')[-1]    except Exception:        project_default_branch = repo.active_branch.name    last_commit = repo.head.commit    last_commit_author = last_commit.author.name    last_commit_email = last_commit.author.email    last_commit_created_at = datetime.fromtimestamp(last_commit.committed_date)    last_commit_hash = last_commit.hexsha    return ProjectSchema(            project=ProjectMetadataSchema(                # Здесь пока использую заглушку. Функционал еще в разработке                team=random.choice([UserTeam.ti, UserTeam.sandbox]),                name=project_name,                status=ProjectStatus.active,                default_branch=project_default_branch,                clone_url=project_clone_url            ),            last_commit=CommitMetadataSchema(                author=last_commit_author,                email=last_commit_email,                created_at=last_commit_created_at,                hash=last_commit_hash            )        )

Компонент № 3 — Сканеры безопасности

Процесс сканирования, обычно, состоит из следующих этапов:

  1. Поднять контейнер.

  2. Скачать актуальную версию исходников нужного сканера.

  3. Клонировать репозиторий.

  4. «Натравить» бинарник сканера на директорию репозитория.

  5. Обработать результаты отчета сканера.

  6. Отправить результаты на запись в базу данных.

  7. Удалить отчет и директорию с репозиторием.

  8. Повторить пункты 3–7 до тех пор, пока все репозитории не будут просканированы.

Далее пару слов о наиболее важных этапах.

Этап 2

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

Этап 4

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

@staticmethoddef _execute_scanner(    clone_path: str,    datastore_path: str,    report_path: str,) -> None:    subprocess.run(        [            Config.SCANER_PATH,            'scan',            '--quiet',            clone_path,            '--datastore',            datastore_path,        ],        timeout=60 * 10,    )    subprocess.run(        [            Config.SCANER_PATH,            'report',            '--quiet',            '--datastore',            datastore_path,            '--format',            'json',            '-o',            report_path,        ],        timeout=60 * 30,    )

Этап 5

Продолжаем мучать Nosey Parker!

Для любителей типизированного Python есть небольшой лайфхак. LLM прекрасно конверитруют JSON репорты в pydanic‑модели. Благодаря этому, мы имеем возможность прямо «на лету» преобразовать отчет сканера в объекты Python и прилично упростить процесс парсинга.

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

async def _parse_report(    self,    report: list[dict],    clone_path: str,    project: ProjectResponseSchema,) -> dict[str, VulnSchema] | None:    vulns = dict()    for finding in Vulns(findings=report).findings:        for match in finding.matches:            first_commit = match.provenance[0].first_commit            filepath = first_commit.blob_path if first_commit else 'no info'            commit_id = first_commit.commit_metadata.commit_id if first_commit else 'no info'            branch = (                await self._get_branch(first_commit.commit_metadata.commit_id, clone_path)                 if first_commit                else 'no info'            )            line = str(match.location.source_span.start.line)            rule_name = match.rule_name            rule_text_id = match.rule_text_id            code_snippet = await sanitize(match.snippet.matching)            custom_fields = {                'commit_id': commit_id,                'rule_name': rule_name,                'rule_text_id': rule_text_id,                'branch': await sanitize(branch),            }            vuln_hash, vuln = await build_vuln(                project=project,                connector_type=ConnectorType.gss,                severity=VulnSeverity.critical,                filepath=filepath,                line=line,                custom_fields=custom_fields,                code_snippet=code_snippet,            )                        vulns[vuln_hash] = vuln    return vulns

Для всех бинарных сканов процесс будет практически идентичным. Исключения могут возникнуть при использовании сканеров по API. К примеру, Checkmarx SAST или JFrog Artifactory.


Вспомогательные инструменты

Коннекторы

Пришло время рассказать, что же такое коннектор. Коннектор — это отдельно взятый Docker‑контейнер, состоящий из двух основных элементов:

  1. Инструмент, реализующий основную логику (к примеру, модуль инвентаризации активов, либо сканер безопасности).

  2. SDK (Software Development Kit) — набор библиотек, позволяющих автоматизировать взаимодействие с необходимым инструментом и отправить результат на запись.

SDK

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

В моем случае, SDK реализует следующий функционал:

  1. Асинхронная объектно‑ориентированная обертка над HTTP запросами к API веб‑сервера.

  2. Модуль для постановки коннекторов на расписание через Cron Jobs.

  3. Модуль, содержащий набор инструментов для автоматизации сканеров безопасности.

  4. Модуль логирования с оберткой над GrayLog

Grafana

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

Из некоторых таблиц можно перейти в веб‑интерфейс для просмотра подробной информации по выбранному репозиторию.

Рисунок 6 - Дашборды

Рисунок 6 — Дашборды

Компонент № 4 — Веб‑сервер

В целом, на этом этапе можно было бы просто подключить Grafana к базе данных и успокоиться, но останавливаться еще слишком рано!

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

  1. Коннекторы, с авторизацией по JWT

  2. Пользователи, с авторизацией через куки уже поверх JWT

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

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

Веб‑интерфейс

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

Рисунок 7 - Карточка уязвимости

Рисунок 7 — Карточка уязвимости

В описании проекта можно посмотреть на дополнительные экраны и дашборды.


Компонент № 5 — Оркестрация

На этапе POC (Proof of Concept) достаточно было создать связи между всеми компонентами и развернуть проект. С этой задачей прекрасно справились Docker + docker‑compose.

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

  • Скрипт для применения стартовой миграции через alembic + SQLAlchemy.

  • Скрипт для создания SQL‑вьюшек.

  • Скрипт для регистрации администратора и коннекторов в базе данных.

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

  • Так же была автоматизирована синхронизация Grafana с базой данных, при помощи механизма provisioning.

Как было отмечено, текущая реализация не допустима для production‑ready инструмента. На мой взгляд есть два варианта улучшения: перевести проект на Apache Airflow, либо на kubernetes, что было бы наиболее предпочтительным решением из‑за возможности управления компонентами платформы прямо из веб‑приложения.


Заключение

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

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

В этом случае у специалистов ИБ есть только два пути: использовать полностью готовые и бесплатные продукты, либо разрабатывать внутренние инструменты (третий вариант — забить на все не рассматриваем). В обоих случаях главным подспорьем в решении насущных проблем являются OpenSource инструменты и LLM, объединение которых может дать неожиданно быстрый и полезный результат!

ссылка на оригинал статьи https://habr.com/ru/articles/1053176/