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

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

компиляция и линковка

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

Зависимость C/C++ входит в продукт не в момент #include, а в момент линковки: компилятор превращает каждую единицу трансляции (.cpp → .o), а линкер сшивает объектные файлы и подставляет внешние библиотеки по флагам -l/-L. Именно на стыке «компиляция → линковка» состав ПО окончательно фиксируется — и именно здесь его проще всего честно зафиксировать. Поздний скан готового бинаря восстанавливает только часть этой картины (динамическую секцию), потому что к финальному артефакту уже не приложена команда сборки.

Для SCA это значит, что полноту перечня компонентов даёт не сканер, а перехват реальных вызовов компилятора и линкера. Если эту точку упустить, неполный SBOM выдаётся за полный, а false negative при матчинге на уязвимости появляется не из-за плохого сканера, а из-за того, что зависимость никогда не была записана.

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

Конвейер виден в verbose-логе сборки. У компиляции и линковки — разные строки, и разный набор флагов, по которым читается состав:

# компиляция: единица трансляции -> объектный файл; интересны -I и -D, но НЕ состав линковки
g++ -c -O2 -I/opt/openssl/include -DUSE_TLS net.cpp -o net.o

# линковка: здесь появляются реальные компоненты (-l, -L, -l:lib*.a)
g++ -o app main.o net.o -L/usr/lib/x86_64-linux-gnu -lssl -lcrypto -lz

Команда -c (только компиляция) даёт -I — путь к заголовкам, но это ещё не зависимость (см. include не равен зависимости). Состав появляется лишь в финальной строке линковки с -o app. Если перехватывать только компиляцию, видны #include, но не библиотеки; если ловить только линковку — наоборот.

Инкрементальная сборка прячет линковку При повторном make без изменений линкер не вызывается — цель уже актуальна (Nothing to be done for 'all'), и строка с -l в лог не попадает. Перехват состава имеет смысл только на чистой пересборке (make clean или сборка в пустом дереве). Это самая частая причина, по которой build-scan «ничего не нашёл».

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

Приём — детерминированно собрать в чистом дереве с перехватом команд компилятора, а не полагаться на флаги конкретной системы сборки (которые везде разные: make V=1, make VERBOSE=1, ninja -v). Кросс-системный способ — обёртка, перехватывающая exec компилятора:

# compile_commands.json: что именно компилировалось и с какими -I/-D
bear -- make            # или: compiledb make

# перехват линковки: реальные строки с -l / -L / -static
make clean && make V=1 2>&1 | tee build.log

compile_commands.json фиксирует стадию компиляции (заголовки, дефайны), build.log — стадию линковки (библиотеки). Вместе они закрывают обе половины конвейера. Слепая зона остаётся одна: код, попадающий в бинарь через уже собранные объектные файлы или статические архивы без отдельного -l, — его ловит только разбор линковки и последующий бинарный анализ.

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

  1. Всегда снимать состав на чистой пересборке — make clean перед make V=1/VERBOSE=1; инкрементальный лог не годится.
  2. Разделять два артефакта: compile_commands.json (через bear/compiledb) для -I/-D и build.log для строк линковки.
  3. Из лога вытаскивать именно строки линкера (grep -E 'gcc|g\+\+|c\+\+|ld') и нормализовать список -l — это и есть кандидаты в компоненты.
  4. Сверять число библиотек по -l с числом сторонних #include: заметное расхождение — сигнал о header-only или статически вшитом коде.
  5. Различать стадии в отчёте: «видели в компиляции» (заголовок) и «видели в линковке» (компонент) — это разный confidence.
  6. Складывать build.log в аудиторские артефакты сборки — он доказывает происхождение состава.

Связь