资讯动态

深入 syft 的 ELF 二进制包目录器:从 elf-testdata 测试夹具看 .note.package 解析与 SBOM 包级依赖关系构建

发布时间:2026/9/16 21:36:47 来源:尧图企业网站定制
深入 syft 的 ELF 二进制包目录器从 elf-testdata 测试夹具看 .note.package 解析与 SBOM 包级依赖关系构建【免费下载链接】syftCLI tool and library for generating a Software Bill of Materials from container images and filesystems项目地址: https://gitcode.com/GitHub_Trending/sy/syft导读本文以 syft 仓库中的 elf-testdata 测试夹具文档 为主线讲解 syft 如何从 ELF 可执行文件与共享库的.note.package段中提取包元数据名称、版本、许可证、PURL、CPE 等并据此在 SBOM 中建立包到包package-to-package而非包到文件package-to-file的依赖关系。读完本文你将掌握 syft ELF 二进制包目录器的底层原理、该测试夹具的构建与验证方法以及一个真实仓库中逻辑包聚合 依赖关系表达的完整设计思路。一、背景syft 的二进制包目录器与.note.package段syft 是一个从容器镜像与文件系统生成软件物料清单SBOM的 CLI 工具与库。对于 ELF 格式的可执行文件与共享库syft 提供了一套专门的目录器cataloger其核心实现位于 elf_package_cataloger.go注册名为elf-binary-package-cataloger见 Name() 方法。该目录器的工作思路是读取 ELF 文件中的.note.package段。该段通常由打包工具如 RPM、dpkg、systemd 生态下的构建工具链注入携带一段 JSON 格式的包元数据其规范背景可追溯到 systemd 的 ELF_PACKAGE_METADATA 设计。源码注释中明确指出见 elf_package_cataloger.go当前实现只针对.note.package段中单一的 JSON 数据形态未来需要泛化以支持非 JSON 数据例如 Fedora 会在 JSON 载荷前附加一个 ELF section header 前缀——这也正是源码中getELFNotes对段头前缀 JSON双格式兼容处理的由来见 elf_package_cataloger.go。为了持续验证这套解析与关系建模逻辑仓库在 testdata/elf-testdata 目录下维护了一组专门构造的 ELF 测试夹具配套的 README 文档正是本文的主体素材。二、elf-testdata 测试夹具的目录结构与构建方式2.1 三个测试场景elf-testdata目录下包含三个独立的构造场景syft/pkg/cataloger/binary/testdata/elf-testdata/ ├── elfbinwithnestedlib/ # 嵌套共享库场景库位于 bin/lib 子目录 ├── elfbinwithsisterlib/ # 姊妹库场景elfsrc1 与 elfsrc2 两个源码目录 ├── elfbinwithcorrupt/ # 损坏/异常场景 ├── Dockerfile # 一键构建镜像的 Dockerfile └── README.mdREADME 明确说明该夹具的用途This image illustrates a few examples of how ELF executables can be assembled and illustrated in an SBOM.即演示 ELF 可执行文件如何在 SBOM 中被装配与表达。2.2 用 objcopy 注入包元数据夹具的核心构造技巧每个场景的源码目录中都包含makefile、*.cpp与*.h文件。以 elfbinwithnestedlib/elfsrc/makefile 为例其核心构造逻辑分两步编译共享库使用g -stdc17 -Wall -Wextra -pedantic编译hello_world.cpp为位置无关目标文件再链接为libhello_world.so注入元数据通过objcopy将一段 JSON 字符串写入.note.package段echo {type: testfixture,license:MIT,commit:5534c38d0ffef9a3f83154f0b7a7fb6ab0ab6dbb,sourceRepo:https://github.com/someone/somewhere.git,vendor: syft,system: syftsys,name: libhello_world.so,version: 0.01,purl: pkg:generic/syftsys/syfttestfixture0.01,cpe: cpe:/o:syft:syftsys_testfixture_syfttestfixture:0.01} | objcopy --add-section .note.package/dev/stdin --set-section-flags .note.packagenoload,readonly $关键点--add-section .note.package/dev/stdin从标准输入读取 JSON 并作为新段写入--set-section-flags .note.packagenoload,readonly将该段标记为不加载noload、只读readonly这保证注入的元数据段不会在程序加载时占用内存与真实发行版的做法一致。可执行文件本身也会注入一段内容几乎相同、但name为syfttestfixture的 JSON见同一 makefile 中$(BIN_DIR)/$(EXECUTABLE)目标。这正对应了目录器的数据模型elfBinaryPackageNotes见 elf_package_cataloger.go中的字段与上述 JSON 一一对应JSON 字段Go 结构体字段说明nameName包名versionVersion版本号licenseLicense许可证如MITpurlPURL包 URL 标识cpeCPECPE 标识type/vendor/system/sourceRepo/commitpkg.ELFBinaryPackageNoteJSONPayload内嵌类型、厂商、系统、源码仓库、提交哈希osCpeCorrectOSCPE与 systemd ELF 包元数据规范对齐的 OS CPE大小写修正版2.3 多阶段 Docker 构建模拟真实 RPM 系统环境Dockerfile 采用两阶段构建第一阶段base基于rockylinux:8安装make automake gcc gcc-c kernel-devel等编译工具链随后将三个场景源码 COPY 到/usr/local/bin/elftests/下并逐个make并通过ENV LD_LIBRARY_PATH/usr/local/bin/elftests/elfbinwithnestedlib/bin/lib保证运行时能找到嵌套库第二阶段基于busybox:1.36.1-musl精简镜像仅保留编译产物与运行依赖——/usr/local/bin/elftests、RPM 数据库/var/lib/rpm、libstdc.so.6与libc.so.6并执行ln -s /usr/lib64 /lib64创建符号链接专门用于验证 syft 能正确处理符号链接路径注释写明 prove we can operate over symlinks。RPM 数据库与 glibc/libstdc 的保留并非偶然README 的期望状态部分明确提出除二进制与共享库之间的依赖外还希望表达二进制与由共享库带来的 RPM 包传递依赖如glibc、libstdc之间的关系这一点在下方第 4 节的关系图中会再次体现。三、案例详解elfbinwithsisterlib 与同名库的聚合3.1 场景构成README 的 Example 1elf-testdata/elfbinwithsisterlib构建了两个二进制elfsrc1/编译出elfwithparallellibbin1与共享库libhello_world.soelfsrc2/编译出elfwithparallellibbin2与共享库libhello_world2.so。三个共享库中两个同名libhello_world.so、一个不同名libhello_world2.so且位于不同目录nested/bin/lib/与sister/lib/。README 特别指出尽管位置不同它们的.note.package段内容输出一致因此它们应当被聚合成同一个逻辑包。3.2 用 objdump 验证注入结果README 提供了用objdump验证元数据段内容的标准手段objdump -s -j .note.package /usr/local/bin/elftests/elfbinwithnestedlib/bin/lib/libhello_world.so输出显示该段为elf64-littleaarch64格式此处以 aarch64 文件为例十六进制内容可还原为 JSON 明文{type: testfixture,license:MIT,commit:5534c38d0ffef9a3f83154f0b7a7fb6ab0ab6dbb,sourceRepo:https://github.com/someone/somewhere.git,vendor: syft,system: syftsys,name: libhello_world.so,version: 0.01,purl: pkg:generic/syftsys/syfttestfixture0.01,cpe: cpe:/o:syft:syftsys_testfixture_syfttestfixture:0.01}。这条验证命令是测试夹具设计的关键一环它确保段内容可被外部工具读取、且字段与目录器解析逻辑对齐使夹具不仅能驱动 syft 测试也能被人工审计。3.3 产物清单与期望关系README 给出的最终产物清单如下Binaries可执行文件/usr/local/bin/elftests/elfbinwithnestedlib/bin/elfbinwithnestedlib /usr/local/bin/elftests/elfbinwithsisterlib/bin/elfwithparallellibbin2 /usr/local/bin/elftests/elfbinwithsisterlib/bin/elfwithparallellibbin1Libraries共享库/usr/local/bin/elftests/elfbinwithnestedlib/bin/lib/libhello_world.so /usr/local/bin/elftests/elfbinwithsisterlib/lib/libhello_world.so /usr/local/bin/elftests/elfbinwithsisterlib/lib/libhello_world2.soBinaries related to Libraries期望的二进制→库关系elfbinwithnestedlib - libhello_world.so elfwithparallellibbin2 - libhello_world.so elfwithparallellibbin1 - libhello_world2.so注意最后一行是 README 原文的期望关系elfwithparallellibbin1 - libhello_world2.so结合后文源码测试可以发现实际目录器是按动态库导入 同名聚合来统一建模的具体见第 5 节。四、实际状态与期望状态两张关系图的背后含义README 的核心诉求在Desired State一节中表达得非常明确We want to drop the package to file relationships and instead do package to package.即放弃包到文件关系改为包到包关系。期望的单条关系是ElfPackage libhello_world.so - ElfPackage syfttestfixture共享库同时还要表达二进制与 RPM 包传递依赖之间的关系。4.1 实际状态Actual StateREADME 用 mermaid 图描述了未经聚合时的实际状态——每个二进制/库节点指向其直接导入的共享库最终收敛到系统库libc.so.6与libstdc.so.6这张图展示了实际扫描时发现的完整导入网络3 个二进制、3 个共享库其中libhello_world.so出现于两个不同位置、2 个系统库共 11 个节点与 12 条 import 边。4.2 期望状态Desired Relationships期望的关系图将分散的文件聚合为两个逻辑包再表达包间依赖该图传达三条核心设计意图文件→逻辑包3 个可执行文件聚合为syfttestfixture包因为它们的.note.package中name均为syfttestfixture3 个共享库聚合为libhello_world.so包包间依赖libhello_world.so包是syfttestfixture应用包的依赖dependency-ofRPM 传递依赖glibc同时是应用包与库包的依赖libstdc是应用包的依赖——这正是 Dockerfile 中保留/var/lib/rpm与系统库的原因用于让测试夹具能同时驱动 RPM 目录器产生这些传递依赖节点。五、源码印证目录器如何实现同名聚合与逻辑包5.1 两遍扫描与按键聚合elf_package_cataloger.go 的Catalog方法采用两遍扫描策略第一遍通过resolver.FilesByMIMEType按 MIME 类型mimetype.ExecutableMIMETypeSet筛选出所有可执行文件逐个解析.note.package段解析成功的文件按elfPackageKeyName/Version/PURL/CPE 四元组见 elf_package_cataloger.go聚合到notesByLocation映射中第二遍对每个 key 下的所有 note将它们的 Location 合并进同一个file.NewLocationSet()并为每个 note 标注pkg.PrimaryEvidenceAnnotation证据注解最后基于第一个 note 生成一个逻辑包。源码注释elf_package_cataloger.go明确指出这样设计的原因可能存在多个 ELF 二进制具有相同的 name 与 version它们集合起来才共同代表一个逻辑包——这与 README 中三个位置的同名库应聚合成一个包的诉求完全对应。5.2 包对象的组装newELFPackage 与 PURL 生成elf_package.go 中的newELFPackage将解析出的元数据组装为pkg.PackageName、Version取自 noteLicenses由metadata.License构建测试中可看到MIT被解析为 SPDX 表达式MIT、类型为declaredPURL由elfPackageURL动态生成Type由元数据中的type字段映射rpm→pkg.RpmPkg、deb→pkg.DebPkg、apk→pkg.ApkPkg、alpm→pkg.AlpmPkg其余回落为pkg.BinaryPkg见 elf_package.go元数据主体存入Metadatapkg.ELFBinaryPackageNoteJSONPayload。elfPackageURL见 elf_package.go还会根据type与 OS 信息构造带distro限定符的 PURLOS 信息优先取元数据中的os/osVersion字段缺失时回退解析osCpe见osNameAndVersionFromMetadataelf_package.go。以本夹具为例purl字段被直接注入为pkg:generic/syftsys/syfttestfixture0.01。5.3 测试用例中的期望输出elf_package_cataloger_test.go 的Test_ELFPackageCataloger中名为 go case 的用例直接使用elf-testdata夹具其期望输出精确验证了聚合逻辑包 1libhello_world.so0.01PURL 为pkg:generic/syftsys/libhello_world.so0.01LocationSet 包含 3 个路径nested 的bin/lib/libhello_world.so、sister 的lib/libhello_world.so、sister 的lib/libhello_world2.so——即三个不同路径的同名/同元数据库被聚合为一个包包 2syfttestfixture0.01LocationSet 包含 3 个可执行文件路径且每个 Location 都带PrimaryEvidenceAnnotation——三个二进制聚合为一个应用逻辑包。同一测试文件中的 fedora / debian / wolfi 用例则展示了生产环境的真实形态coreutils的 note 元数据type: rpm、Architecture、OSCPE被映射为pkg:RpmPkg与带?distro限定符的 PURL。5.4 依赖关系的落点交给最终关系任务值得注意的设计细节elfPackageCataloger.Catalog的返回值中relationships 恒为 nil。源码注释elf_package_cataloger.go解释了原因二进制目录器会记录每个二进制导入的动态库但包到包、包到文件的关系不在目录器内直接生成而是由扫描管线末端的专门任务见 syft/internal/task/relationship_tasks.go以及 elf_package_cataloger.go 的注释基于每个二进制导入的动态库集合统一创建。这保证了关系建模的全局一致性——README 中Actual State导入网络图所呈现的多对多关系正是这一最终任务依据ldd式动态库导入信息含libc.so.6、libstdc.so.6这类系统库绘制出来的。5.5 健壮性保障elfutil 的节大小上限目录器解析 ELF 时并非直接使用 Go 标准库debug/elf而是通过 syft/internal/elfutil/elfutil.go 提供的受控 ELF 打开器。该包专门防御压缩节声明了不合理的解压后大小导致的 Go 内存爆炸问题maxDeclaredSectionSize为 128MB/节见 elfutil.go对.note.package这类 syft 实际会读取的节做解压大小上限约束超限时拒绝展开并返回ErrDeclaredSizeExceeded宁可跳过该文件也不让整个扫描 OOM。这为.note.package解析链路提供了关键的安全边界。六、如何复现与验证6.1 构建测试镜像在 elf-testdata 目录 下执行docker build -t syft/elf-testdata .构建完成后镜像内/usr/local/bin/elftests/即包含三个场景的完整产物可执行文件、共享库、RPM 数据库与系统库。6.2 用 syft 扫描并核对 SBOM# 扫描镜像 syft scan syft/elf-testdata -o syft-json # 扫描本地目录等价 syft scan /usr/local/bin/elftests -o syft-json对照测试期望elf_package_cataloger_test.go检查输出中的libhello_world.so0.01与syfttestfixture0.01两个包以及它们与glibc、libstdc之间的依赖关系。6.3 用 objdump 独立校验注入段objdump -s -j .note.package /usr/local/bin/elftests/elfbinwithnestedlib/bin/lib/libhello_world.so比对十六进制输出与 makefile 中注入的 JSON即可确认夹具元数据与目录器解析假设的一致性。6.4 运行目录器测试# 仅运行 elf-testdata 相关用例默认使用已入库的 fixture go test -v ./syft/pkg/cataloger/binary -run Test_ELFPackageCataloger关于该目录器测试夹具的通用管理方式make list/make download/make add-snippet以及 snippets 与完整二进制的取舍可进一步参考 binary 目录器 README。七、总结elf-testdata测试夹具是理解 syft ELF 二进制包建模的最佳入口。它以极小的构造成本g objcopy 注入 JSON复现了真实发行版中.note.package段的形态并围绕三个精心设计的场景——同名共享库、多位置聚合、嵌套库、符号链接、RPM 传递依赖——把 syft 目录器最复杂的行为边界同名逻辑包聚合、包到包关系替换包到文件关系具象化。配合 elf_package_cataloger.go 的两遍扫描聚合逻辑、elf_package.go 的 PURL/类型映射以及 elf_package_cataloger_test.go 的期望输出读者可以完整还原 syft 从ELF 文件的元数据段到SBOM 中逻辑包与依赖关系的整条数据链路。【免费下载链接】syftCLI tool and library for generating a Software Bill of Materials from container images and filesystems项目地址: https://gitcode.com/GitHub_Trending/sy/syft创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价