openlibing-security-compilation-options-action

安全编译选项扫描插件 - 检测 ELF 文件安全编译选项开启率。

功能

检测构建产物(ELF 二进制文件)的安全编译选项(默认 14 项;其中 stackClash/fstackCheck 依赖编译器记录段 .GCC.command.line/.llvmcmd 识别,构建未开启 -frecord-gcc-switches/-frecord-command-line 或节被 strip 时返回 N/A):

选项 key 说明
bindNow BIND_NOW 立即绑定(全 RELRO)
nx 堆栈不可执行
pic 地址无关代码
pie 位置无关可执行文件
relro GOT 表保护
rpath 动态库搜索路径(YES 表示存在不安全搜索路径)
sp 栈保护 / Stack Protector
strip 符号表剥离
fortify _FORTIFY_SOURCE 宏(编译器生成的 _chk 函数)
fvisibility -fvisibility=hidden 符号可见性隐藏
ftrapv -ftrapv 有符号整数运算溢出陷阱
stackClash -fstack-clash-protection 栈冲突保护(依赖编译器记录段,未开启记录或无记录段返回 N/A)
fstackCheck -fstack-check 栈空间越界检查(依赖编译器记录段,未开启记录或无记录段返回 N/A)
aslr ASLR 支持(依赖 PIE)

为什么 stackClash/fstackCheck 可能同时为 N/A:这两个是编译期代码生成选项,产物没有符号级标记,需读取编译器写入的记录段(GCC -frecord-gcc-switches 生成 .GCC.command.line、Clang -frecord-command-line 生成 .llvmcmd)才能确认。若构建未开启这两个记录开关,或产物经 strip --strip-all/--strip-debug 删除了该非分配节,扫描器无从判断,这两项返回 N/A(不计入开启率分母)。若两者同时为 N/A,请检查构建是否添加 -frecord-gcc-switches/-frecord-command-line。

参考:Ascend 安全编译指南

按文件内容魔数自动识别并解压包格式(识别与文件名后缀无关):zip 类(.jar/.war/.ear/.whl/.zip)、ar 类(.a 静态库、.deb)、tar 类(.tar/.tar.gz/.tar.bz2/.tar.xz);.run 自解压包按后缀识别后探测内部压缩载荷。

排除范围:.dbg 后缀调试符号文件、编译器探测桩文件(CMakeCCompilerId/CMakeCXXCompilerId/feature_tests,按去后缀基名匹配)、版本控制与依赖缓存目录(.git/.svn/node_modules/__pycache__)。构建中间目录(CMakeFiles 等)不再跳过,其内 ELF 产物按内容识别纳入扫描。

使用方式

场景一:PR 流水线(构建产物后直接扫描,obs-upload 上传)

在 PR 触发的流水线中,构建产物后立即扫描,配合 obs-upload 上传产物到 OBS 用于溯源:

name: build

on:
  workflow_dispatch:
  pull_request_target: ## 使用 pull_request_target 确保 PR 流水线始终读取默认分支(master)的最新配置文件
    branches:
      - "*"
  pull_request_comment: ## PR 评论事件:当用户在 PR 中发表评论时触发
    types: [created]
    comments: ["/build-and-scan"]
    branches:
      - "*"
  schedule:
    - cron: "0 0 0 * * ?"

permissions:
  repository: read
  id-token: write

jobs:
  build:
    name: build
    runs-on: ["codearts-hosted", "ubuntu-latest", "x64", "large"]
    steps:
      - name: Checkout
        uses: checkout
        with:
          ref: ${{ atomgit.event.pull_request.merge_commit_sha || atomgit.event.pull_request.head.sha }}

      # 请替换为实际的构建脚本
      - name: build
        id: build
        run: |
          echo "===== CMake 构建打包 ====="
          mkdir build && cd build
          cmake .. -DCMAKE_BUILD_TYPE=Release
          make -j$(nproc)
          # 查找主构建产物
          ARTIFACT=$(find . -maxdepth 2 -type f \( -name "*.tar.gz" -o -name "*.zip" -o -perm -111 \) | head -1)
          if [ -z "$ARTIFACT" ]; then
            echo "ERROR: No build artifact found"
            exit 1
          fi
          echo "artifact-path=${ARTIFACT}" >> "$ATOMGIT_OUTPUT"
          echo "Found artifact: ${ARTIFACT}"

      - name: upload-artifact-to-obs
        id: upload-obs
        uses: obs-upload
        with:
          # 请使用实际的端点和 OBS 桶
          endpoint: "obs.cn-southwest-2.myhuaweicloud.com"
          bucket: "my-package"
          access-key: "${{ secrets.OBS_AK }}"
          secret-key: "${{ secrets.OBS_SK }}"
          artifact-path: "${{ steps.build.outputs.artifact-path }}"
          object-prefix: "${{ atomgit.repository }}/${{ atomgit.ref_name }}/${{ atomgit.run_id }}/"

      - name: security-compilation-options-action
        # 当前插件已上传至开源仓,可直接调用
        uses: openlibing/security-compilation-options-action@v1.0.2
        with:
          artifact-path: "${{ steps.build.outputs.artifact-path }}"
          artifact-download-url: "${{ steps.upload-obs.outputs.primary-url }}"
          # 可选:只扫描部分项,不传 scan-options 默认扫描 14 项(stackClash/fstackCheck 未开记录时返回 N/A)
          # scan-options: "bindNow,nx,pic,pie,relro,rpath,sp,strip"

场景二:Nightly 流水线(构建 + upload-artifact 暂存 + 扫描)

在 Nightly 定时流水线中,构建产物后使用 GitCode 官方 upload-artifact 插件暂存,无需自管 OBS。retention-days 控制产物保留天数,到期自动清理:

name: build-and-scan-nightly

on:
  workflow_dispatch:
  schedule:
    - cron: "0 0 0 * * ?" # 每天北京时间 00:00 执行

permissions:
  repository: read
  id-token: write

jobs:
  build:
    name: nightly-build-and-scan
    runs-on: ["codearts-hosted", "ubuntu-latest", "x64", "large"]
    steps:
      - name: Checkout
        uses: checkout

      # 请替换为实际的构建脚本
      - name: build
        id: build
        run: |
          echo "===== CMake 构建打包 ====="
          mkdir build && cd build
          cmake .. -DCMAKE_BUILD_TYPE=Release
          make -j$(nproc)
          # 查找主构建产物
          ARTIFACT=$(find . -maxdepth 2 -type f \( -name "*.tar.gz" -o -name "*.zip" -o -perm -111 \) | head -1)
          if [ -z "$ARTIFACT" ]; then
            echo "ERROR: No build artifact found"
            exit 1
          fi
          echo "artifact-path=${ARTIFACT}" >> "$ATOMGIT_OUTPUT"
          echo "Found artifact: ${ARTIFACT}"

      # 使用官方 upload-artifact 暂存产物,无需自管 OBS 桶
      # retention-days 控制产物保留天数,到期自动清理
      - name: upload-artifact
        id: upload-artifact
        uses: upload-artifact
        with:
          name: nightly-build-artifact
          path: "${{ steps.build.outputs.artifact-path }}"
          retention-days: 3
          overwrite: true
          if-no-files-found: error

      # 扫描本地产物文件,artifact-download-url 使用 upload-artifact 输出的制品页面 URL
      - name: security-compilation-options-scan
        uses: openlibing/security-compilation-options-action@v1.0.2
        with:
          artifact-path: "${{ steps.build.outputs.artifact-path }}"
          artifact-download-url: "${{ steps.upload-artifact.outputs.artifact-url }}"

场景三:Nightly 流水线(扫描指定 OBS 产物)

在 Nightly 定时流水线中,扫描预先存储在 OBS 上的产物包。artifact-path 支持多行指定多个文件,插件逐个扫描并独立上报:

name: security-compilation-options-scan-nightly

on:
  workflow_dispatch:
  schedule:
    - cron: "0 0 0 * * ?"

permissions:
  repository: read
  id-token: write

jobs:
  build:
    name: scan-artifacts
    runs-on: ["codearts-hosted", "ubuntu-latest", "x64", "large"]
    steps:
      - name: Checkout
        uses: checkout

      # ===== 批量下载两个架构的构建产物 =====
      - name: download-artifacts
        id: download-artifacts
        uses: obs-download
        with:
          endpoint: "obs.cn-north-4.myhuaweicloud.com"
          bucket: "my-package"
          key: |
            cache/master/package_aarch64.tar.gz
            cache/master/package_x86_64.tar.gz
          path: "./artifacts/"
          file-concurrency: "2"

      # ===== 扫描指定产物并上传结果 =====
      # artifact-path 精确指定只扫描这两个包,避免误扫目录中其他产物
      # artifact-download-url 以 / 结尾视为URL前缀,插件自动拼接产物文件名
      - name: security-compilation-options-scan
        uses: openlibing/security-compilation-options-action@v1.0.2
        with:
          artifact-path: |
            ./artifacts/package_aarch64.tar.gz
            ./artifacts/package_x86_64.tar.gz

触发方式

触发方式 配置关键字 适用场景
手动触发 workflow_dispatch 按需验证、首次接入验证
代码推送触发 pull_request_target / pull_request_comment 提PR/PR评论时自动检测(场景一)
定时任务触发 schedule.cron Nightly 流水线,每日定期检测跟踪趋势(场景二/三)

输入参数

参数 必填 默认值 说明
artifact-path 是 - 构建产物文件路径,支持多行指定多个文件(每行一个路径)。推荐配合构建步骤的 step output 使用
output 否 sec-option-result.json 结果输出文件路径
package-name 否 '' 构建产物压缩包文件名。不传则自动从 artifact-path 推断
artifact-download-url 否 '' 构建产物下载链接。以 / 结尾视为URL前缀,插件自动拼接前缀+产物文件名作为每个包的下载链接(多产物包只需写一次前缀);不以 / 结尾视为完整URL。如未提供下载链接或下载链接已失效/需要认证信息,openlibing 平台将不支持产物下载
scan-options 否 '' 实际扫描项列表(逗号分隔),不传或为空表示扫描默认 14 项(stackClash/fstackCheck 构建未开启 -frecord-gcc-switches/-frecord-command-line 或无记录段时返回 N/A)。仅扫描的项会出现在 overviewData/fileDetails 的 options 字段中,未扫描项 key 缺失
apig-app-key 否 '' 已废弃:APIG 网关 AppKey(AK)。请使用新的 OIDC 认证方式(见下方「上传认证」),无需任何凭证
apig-app-secret 否 '' 已废弃:APIG 网关 AppSecret(SK),与 apig-app-key 配套。请使用新的 OIDC 认证方式,无需任何凭证

scan-options 可选 key:bindNow,nx,pic,pie,relro,rpath,sp,strip,fortify,fvisibility,ftrapv,stackClash,fstackCheck,aslr

上传认证

上传功能默认开启,插件使用OIDC 联邦认证,无需任何凭证。已适配 OIDC 的 workflow 示例:

permissions:
  id-token: write

输出

每个安全编译选项开启率选项输出 3 个值(仅 scan-options 指定的项或默认 14 项):

输出 说明
{option}-total-files 参与计算的文件数(排除 N/A)
{option}-yes-count 满足数
{option}-rate 开启率(%)

默认输出的安全编译选项包括:bindNow, nx, pic, pie, relro, rpath, sp, strip, fortify, fvisibility, ftrapv, stackClash, fstackCheck, aslr

概览输出(ATOMGIT_OUTPUT)

扫描完成后,插件会将各产物包在不同扫描项上的开启率通过 ATOMGIT_OUTPUT 输出,便于工作流页面直接查看概览(不含明细)。每个实际扫描项对应一个输出标识(中文名(英文名)),值为 包名 -> 开启率 的映射,开启率为带 % 的百分数或 N/A。输出标识固定为 中文名(英文名),引用时以此为准;内部键(scan-options 输入/排序用)与输出标识的对应关系见下表,仅输出实际扫描到的项。使用方式:${{ steps.<scan-id>.outputs['立即绑定(BIND_NOW)'] }}。

输出标识 值
立即绑定(BIND_NOW) {"app.jar":"100%"}
堆栈不可执行(NX) {"app.jar":"95.83%"}
栈冲突保护(Fstackclash) {"app.jar":"N/A"}

各扫描项的内部键(scan-options 输入/排序用)与输出标识(引用用)对应关系:

内部键 输出标识(引用用)
bindNow 立即绑定(BIND_NOW)
nx 堆栈不可执行(NX)
pic 地址无关(PIC)
pie 随机化(PIE)
relro GOT表保护(RELRO)
sp 栈保护(SP)
rpath 禁选动态库搜索路径(NO Rpath)
fortify 缓冲区溢出保护(FS)
ftrapv 整数溢出检查(Ftrapv)
strip 删除符号表(Strip)
aslr 地址空间布局随机化(ASLR)
fvisibility 符号全局不可见(Fvisibility)
fstackCheck 栈检查(Fstackcheck)
stackClash 栈冲突保护(Fstackclash)

多个产物包时,同一扫描项标识的值为各包开启率的映射,如 {"app.jar":"100%","lib.so":"80%"}。

平台详情链接输出

上报成功后,插件为每个产物包输出 1 条该产物包在 openLiBing 平台的详情页跳转链接(每包固定 1 行,多产物包按包分别输出);运行日志(core.info)同时打印实际链接便于查看。

输出标识为扫描结果(app.jar)url,示例:

扫描结果(app.jar)url=https://www.openlibing.com/apps/securityOptions/package-detail?recordId=2103041785973575680&projectId=3

未录入 openLiBing 平台的代码仓(repo_info 无匹配)时不输出链接,输出提示行(文案由服务端返回,插件不硬编码):

扫描结果(app.jar)=当前仓未录入openLiBing平台,无法生成跳转链接,请先联系SRE完成代码仓录入。

说明

  • 概览输出通过 ATOMGIT_OUTPUT 写入。本插件为 Node 自定义 action,写入 ATOMGIT_OUTPUT 的输出无需在 action.yml 中声明 outputs: 即可被 ${{ steps.<scan-id>.outputs.<...> }} 引用(中文键使用中括号字符串,如 ${{ steps.scan.outputs['立即绑定(BIND_NOW)'] }});平台详情链接输出的键同样无需声明即可引用。
  • 概览值为供工作流页面展示的可读字符串,开启率带 % 或为 N/A;而「输出」节中 core.setOutput 输出为纯数值(如 {option}-rate=95.83),同名项单位同为 %。两者不混用:编程取用建议用 core.setOutput 的纯数值键,概览值仅作展示。
  • 实际链接由服务端拼接,同一代码仓关联多个项目时,固定输出查询第一个项目对应的 1 条链接(不输出多链接)。
  • 上报失败或链接构建异常时不输出链接与提示;写入失败时跳过输出,不影响上报结果。

依赖

  • pyelftools - ELF 文件解析(Python 包,插件自动安装,无需手动配置)