软件成分分析系统——项目经历

软件成分分析系统

**项目简介:**开发基于包管理器构建过程的软件成分分析系统,采用BuildScan动态检测技术,支持Java/JS/Python/Go多语言依赖 关系解析,实现漏洞风险识别、许可证合规分析、软件资产统一管理,并自动化生成符合行业标准的SBOM(软件物料清单)及 可视化安全报告。

**技术栈:**SpringBoot,PostgreSQL,Redis,RocketMQ,maven/gradle/npm/pip/go modules包管理器

主要功能:

  1. 提取项目中的配置文件,基于包管理器构建完整依赖树,并即时返回初步结果。

  2. 采用 RocketMQ 将组件信息采集、漏洞匹配和许可证识别任务异步拆分,并结合多线程并发处理,加速整体数据处理流程。

  3. 依据CPE规范构建组件标识,通过PostgreSQL的pg_trgm插件+GIN索引实现相似度高效匹配漏洞信息。

  4. 聚合多源许可证数据,分析权利/义务条款并构建冲突检测规则引擎。

  5. 生成符合CycloneDX/SPDX规范的软件物料清单,支持XML/JSON多格式输出

异步处理

在 SCA(软件成分分析)系统的异步设计中,我们利用 RocketMQ 进行解耦,将 组件查询、缺失组件爬取、漏洞匹配 这三个 I/O 密集型任务异步化,提高系统的吞吐量和并发能力。

具体来说,我们采用 消息队列 设计,将 解析依赖树后 的组件查询任务推送到 component-query-queue,如果组件缺失,则发送爬取任务到 component-crawl-queue,每一个组件查询完成后,再异步执行 vulnerability-match-queue 进行漏洞匹配。

在实现上,我们使用 RocketMQ 生产者推送任务,消费者监听队列 进行异步处理,并结合 线程池并发查询 以及 Redis 幂等性校验,确保任务高效执行,同时防止重复消费或数据不一致的问题。

这样,整个 SCA 任务执行过程能够更快响应,避免同步阻塞,提高系统扩展性和稳定性,同时也为后续增加更多语言或优化流程提供了良好的架构支持。

多语言框架

SCA 系统需要分析 Java、Python、JavaScript、Go 四种语言的项目,每种语言的处理方式不同,但总体流程相同,具体包括以下步骤:

  1. 上传项目
  2. 生成依赖树(执行包管理器命令)
  3. 解析依赖树,(然后将组件查询任务推送到 component-query-queue,最后返回前端)
  4. 查询组件信息,(将缺失组件爬取任务推送到 component-crawl-queue,然后结束)
  5. 爬取缺失组件信息,(同时匹配许可证),线程池处理,并将漏洞匹配任务推送到vulnerability-match-queue ,然后结束
  6. 匹配组件的漏洞信息,结束

具体的,首先定义一个统一的sca处理器,和一个统一的消费者。然后定义不同语言的策略,和一个工厂用来根据语言生成策略。上传项目后,sca处理器根据语言,从工厂获得对应语言的策略,然后调用策略的方法生成依赖树、解析依赖树,然后将组件查询任务推送到 component-query-queue,并返回前端。

消费者接受消息后,首先根据topic选择,

漏洞匹配

CPE

CPE(Common Platform Enumeration)是一个用于标识软件和硬件平台的标准化命名系统。CPE 使用特定格式来表示一个软件、硬件或操作系统的标识符。通常,CPE 标识符遵循类似于下面的格式:

1
cpe:2.3:a:apache:http_server:2.4.49:*:*:*:*:*:*:*
  • cpe:2.3:表示使用的是 CPE 版本 2.3。
  • a:表示组件类型(软件/硬件/OS)
  • apache:厂商名。
  • http_server:产品名。
  • 2.4.49:版本号。
  • *:*:*:*:*:*:*:更新版本、发行版、语言、软件类型、硬件架构、其他等

pg_trgm 插件

pg_trgm 插件是 PostgreSQL 中用于加速模糊匹配和相似度计算的工具,它使用 trigram(三元组)来将字符串分解为连续的三字符片段,并使用这些片段来计算两个字符串之间的相似度。通过这种方式,pg_trgm 可以帮助我们计算字符串间的相似度,即使它们存在拼写错误、字符交换或省略。

在这个过程中,pg_trgm 插件可以对 CPE 标识符进行拆解,以便通过类似的字符串或不完全匹配找到相似的漏洞信息。CPE 标识符可能会有一些拼写错误或不同版本的表达方式,因此使用 pg_trgm 插件能够有效捕捉到相似的组件信息。

具体的原理:例如对于hello,可以分为{“ h”, “ he”, “hel”, “ell”, “llo”, “lo “},另一个字符串也同样划分后,通过计算 **两个三元组集合的交集 / 并集 **作为相似度。

GIN索引(倒排索引)

GIN(Generalized Inverted Index)是 PostgreSQL 的一种索引类型,用于加速处理包含多值数据的字段,如数组、文本搜索、JSONB 等。GIN 索引为每个值构建倒排索引,这使得查询速度显著提高,特别是在需要进行模糊匹配、全文搜索时。

倒排索引会记录每个值在哪些文档(或行)中出现。比如,对于一个文本字段,你的倒排索引可能看起来像这样:

  • word1 -> {1, 2, 5}
  • word2 -> {1, 3}
  • word3 -> {2, 4}

这个倒排结构允许快速定位包含某个单词的行。

整体流程

  1. 根据组件名、厂商、版本等信息构建自定义cpe,其中有含版本号和不含版本号两个cpe

  2. 根据自定义cpe,到cve_cpe表中进行相似度查询,找到所有对应的cve_cpe。

    2.1. trgm将cpe拆解为一个个三元组

    2.2. 每一个三元组都建立了倒排索引,可以快速定位到匹配的行

    2.3. 再计算这些行的cpe与自定义cpe的相似度,筛选出相似度高的

  3. 对这些找到的cve_cpe进行召回,限定cpe中的单词至少需精准包含组件名称单词的半数以上以防止误判,以及检查组件版本是否处于规定的漏洞影响起始和终止版本之间

  4. 根据cve_cpe中的cve,到cve表中查到对应的漏洞

RocketMQ

顺序性

Rocketmq有两种顺序消费模式。

全局顺序(Global Order)

  • 所有消息 只能进入 同一个队列(Queue),并且由 一个消费者 进行顺序消费。
  • 优点:保证全局严格顺序。
  • 缺点:吞吐量受限,单个消费者成为性能瓶颈。

分区顺序(Partition Order)

  • 不同业务标识(如订单 ID) 绑定到特定 消息队列(Queue),同一个 Queue 内保证顺序,但不同 Queue 可并行消费。
  • 优点:既保证局部顺序,又能提高吞吐量。
  • 缺点:如果一个 Queue 处理变慢,可能会影响该 Queue 内的所有消息。

分区消费实现

生产者在sendMessage时,自定义一个MessageQueueSelector消息队列选择器,在里面手动选择queue,例如可以:

1
2
3
4
5
SendResult sendResult = producer.send(msg, (mqs, msg1, arg) -> {
int orderId = (int) arg; // 从 arg 获取 orderId
int queueIndex = orderId % mqs.size(); // 计算要进入的队列索引
return mqs.get(queueIndex);
}, orderId); // 这里传入 orderId 作为 arg

消息发送的可靠性

RocketMQ 提供了几种不同的 发送消息的模式,可以保证消息是否成功发送到 Broker。

同步发送(Sync)

  • 同步发送是最常见的方式,生产者发送消息后,会 阻塞等待 Broker 的返回结果,直到收到确认信息。同步发送能确保消息确实到达 Broker,并提供准确的发送结果。
  • 同步发送在发送失败时,通常会依赖于 重试机制。RocketMQ 提供了自动重试的功能,在发生发送失败时,它会 自动重试 多次,直到达到最大重试次数,或者成功发送消息。

异步发送(Async)

  • 异步发送是另一种方式,生产者发送消息后不会等待返回结果,而是 通过回调函数 处理发送结果。虽然不阻塞,但可能会因为消息发送失败而需要在回调中处理。
  • 异步发送的失败需要在回调函数中显式处理,RocketMQ 不会自动重试。

一次消息发送(Oneway)

  • 在这种模式下,生产者 不等待任何确认,直接将消息发送到 Broker。适用于 不关心消息是否成功发送 的场景。

消息保存的可靠性

  • 异步/同步刷盘
  • 异步/同步主从复制

消息接收的可靠性

取消自动,消费完成后手动ack

延迟重试(每次延迟增大)

多次重试失败后进入死信队列,手动处理或定时任务处理

幂等

redis id / 数据库id