Пиков / КАПО
Как читать авторский курс и нормативные материалы

Это учебные материалы, а не заключение о соответствии конкретного продукта. Организационные модели курса отделяйте от терминов стандарта: ППК в опубликованном проекте — машиночитаемый перечень, а живой реестр его версий является практикой сопровождения. Термин OSA в разных материалах имеет разный охват. Статус и обязательность нормативного документа проверяются отдельно; проект КАПО опубликован как проект первой редакции.

LIEF как программный анализ ELF

Почему это важно для SCA/OSA

Ручные readelf/objdump хороши для одного бинаря, но конвейер SCA разбирает десятки и сотни артефактов: все исполняемые файлы образа, каждую .so в /usr/lib, бинарники из нескольких сборок. Парсить их через grep по текстовому выводу утилит — хрупко: формат вывода меняется между версиями binutils, поля приходится вытаскивать регулярками, а нестандартные или слегка повреждённые ELF ломают разбор. LIEF — библиотека (Python/C++), которая читает ELF (а также PE/Mach-O) как объект: секции, DT_NEEDED, символы, RPATH становятся полями, а не строками для парсинга.

Для SCA это даёт повторяемость и масштаб: один скрипт извлекает динамические зависимости из всего дерева бинарей и сразу складывает их в структуру для маппинга на пакеты. Это превращает разовый ручной анализ в воспроизводимый шаг пайплайна.

Как это проявляется

Та же информация, что даёт readelf -d, но как программные поля — и сразу по множеству файлов:

import lief

binary = lief.parse("./app")

# DT_NEEDED как список строк, без парсинга текста
needed = [e.name for e in binary.dynamic_entries
          if e.tag == lief.ELF.DynamicEntry.TAG.NEEDED]
# RPATH/RUNPATH — отдельными тегами
runpath = [e.runpath for e in binary.dynamic_entries
           if e.tag == lief.ELF.DynamicEntry.TAG.RUNPATH]

print(needed)    # ['libssl.so.3', 'libcrypto.so.3', 'libc.so.6']
print(runpath)   # ['$ORIGIN/../lib']

# признак stripped: пустая таблица символов .symtab при наличии .dynsym
has_symtab = any(s.name == ".symtab" for s in binary.sections)

Дальше этот список напрямую идёт в маппинг .so → пакет без промежуточного текста. Прогон по дереву (os.walk + lief.parse) даёт инвентаризацию всех бинарей образа за один запуск.

Когда LIEF, а когда readelf Один-два бинаря, интерактивно — readelf/objdump быстрее (см. ldd readelf objdump). Пачка артефактов, повторяемый шаг CI, нестандартные/частично битые ELF, нужно складывать результат в JSON/SBOM — LIEF: он не зависит от формата вывода утилит и устойчивее к краевым случаям.

Слепая зона или приём

Важная граница: LIEF — это парсинг, а не резолвинг. Он прочитает DT_NEEDED = libssl.so.3 из файла, но не скажет, какой именно файл и версия подставятся в рантайме на конкретном хосте — это задача загрузчика (ldd) и пакетного менеджера. Поэтому LIEF закрывает извлечение фактов из бинаря, но не отменяет шаг «soname → файл → пакет → версия» (см. динамическая линковка).

Приём — использовать LIEF как сборщик evidence в масштабе: пройти все ELF, вытащить NEEDED/RPATH/RUNPATH/признак stripped, нормализовать в таблицу и только потом маппить на пакеты и версии. И помнить, что статически вшитый код LIEF тоже не видит как зависимость — для него остаётся анализ символов/строк и build-scan (статическая линковка).

Что делать на проекте

  1. Для массового разбора (образ, дерево /usr/lib, набор сборок) использовать программный парсер, а не grep по readelf.
  2. Извлекать единым проходом: DT_NEEDED, DT_SONAME, RPATH/RUNPATH, наличие .symtab (признак stripped) — и складывать в структуру.
  3. Результат парсинга не считать финальным составом: добавлять отдельный шаг резолвинга .so → пакет → версия.
  4. Обрабатывать ошибки парсинга как сигнал, а не как сбой: нераспознанный ELF — кандидат в реестр неопознанных, а не «пропустить молча».
  5. Встраивать скрипт в CI как воспроизводимый шаг и сохранять его вывод как аудиторский артефакт (см. build logs как аудиторский артефакт).
  6. Не пытаться через LIEF восстановить статику — для неё уходить в build-scan и анализ символов.

Связь