🔒 security_advisories — 安全公告查询
本示例演示如何获取 Composer 包的安全漏洞公告,涵盖「按时间增量拉取」与「按包名精准查询」两种方式,并打印 CVE、受影响版本等关键字段。
🎯 示例定位
security_advisories 是 Packagist API 远程操作 系列的第 5 个示例,属于「进阶」难度。在掌握了基本的客户端初始化、包列表与统计查询之后,本例把视角转向安全维度——这是任何严肃 PHP 项目都绕不开的一环。
- 📚 你将学到:如何用
pkg/repository包的底层方法,分别按「更新时间」和「包名」两个维度查询 Packagist 已披露的安全公告(CVE / GHSA),并解析Advisory结构的核心字段。 - 🔗 对应 SDK 方法:
repository.Repository.ListSecurityAdvisories(按时间增量)与repository.Repository.ListAdvisories(按包名查询)。 - 🧭 与
security_monitor的区别:security_monitor是综合实战,把公告查询、版本约束比对、告警通知串成一条监控流水线;而本例只聚焦于如何把公告数据拉回来并读懂它,是security_monitor的前置基础课。 - ⚖️ 与 Composer CLI
audit的区别:CLIaudit审计的是本地composer.lock中实际锁定版本,需要 PHP;本例查的是 Packagist 全库公告,纯 HTTP,无需 PHP,适合做 SCA 全景扫描与漏洞库同步。
💻 完整代码
go
package main
import (
"context"
"fmt"
"time"
"github.com/scagogogo/composer-skills/pkg/repository"
)
func main() {
// 示例 5: 获取 Composer 包的安全公告
// 包括按时间获取和按包名获取两种方式,可用于安全审计。
// 步骤 1: 初始化仓库客户端
options := &repository.Options{
ServerUrl: "https://packagist.org", // 使用官方仓库
}
repo := &repository.Repository{}
_ = options // 本例中仅作配置示意,方法内部固定使用官方端点
ctx := context.Background()
// 步骤 2: 按时间获取安全公告
fmt.Println("=== 按时间获取安全公告 ===")
oneYearAgo := time.Now().AddDate(-1, 0, 0)
fmt.Printf("获取 %s 之后的安全公告...\n", oneYearAgo.Format("2006-01-02"))
advisoriesResp, err := repo.ListSecurityAdvisories(ctx, oneYearAgo)
if err != nil {
fmt.Printf("获取安全公告失败: %v\n", err)
return
}
// 统计公告数量
totalAdvisories := 0
for _, advisories := range advisoriesResp.Advisories {
totalAdvisories += len(advisories)
}
fmt.Printf("找到 %d 个包含安全公告的包,共 %d 个公告\n",
len(advisoriesResp.Advisories), totalAdvisories)
// 打印部分公告详情(前 3 个包,每包前 2 条)
count := 0
fmt.Println("\n部分安全公告详情:")
for pkgName, advisories := range advisoriesResp.Advisories {
if count >= 3 {
break
}
fmt.Printf("\n包: %s\n", pkgName)
for i, advisory := range advisories {
if i >= 2 {
fmt.Printf(" ...还有 %d 个公告未显示\n", len(advisories)-i)
break
}
fmt.Printf(" - 标题: %s\n", advisory.Title)
fmt.Printf(" CVE: %s\n", advisory.Cve)
fmt.Printf(" 报告时间: %s\n", advisory.ReportedAt)
fmt.Printf(" 影响版本: %s\n", advisory.AffectedVersions)
}
count++
}
// 步骤 3: 获取特定包的安全公告
fmt.Println("\n\n=== 获取特定包的安全公告 ===")
packageName := "symfony/http-kernel"
fmt.Printf("获取 %s 包的安全公告...\n", packageName)
packageAdvisories, err := repo.ListAdvisories(ctx, packageName)
if err != nil {
fmt.Printf("获取 %s 包的安全公告失败: %v\n", packageName, err)
} else {
fmt.Printf("找到 %d 个安全公告\n", len(packageAdvisories))
fmt.Println("\n安全公告详情:")
for i, advisory := range packageAdvisories {
if i >= 5 {
fmt.Printf("...还有 %d 个公告未显示\n", len(packageAdvisories)-i)
break
}
fmt.Printf("\n%d. %s\n", i+1, advisory.Title)
fmt.Printf(" - 公告 ID: %s\n", advisory.AdvisoryID)
fmt.Printf(" - CVE: %s\n", advisory.Cve)
fmt.Printf(" - 报告时间: %s\n", advisory.ReportedAt)
fmt.Printf(" - 影响版本: %s\n", advisory.AffectedVersions)
fmt.Printf(" - 链接: %s\n", advisory.Link)
}
}
}🧩 代码讲解
- 🏗️ 初始化 Repository:
repo := &repository.Repository{}以零值构造底层 HTTP 客户端。options字段未导出,方法内部固定请求https://packagist.org官方端点,因此本例中options仅作配置示意(_ = options)。 - 🕐 按时间增量拉取:
oneYearAgo := time.Now().AddDate(-1, 0, 0)构造一年前的时间点,传给repo.ListSecurityAdvisories(ctx, oneYearAgo),SDK 取其毫秒级UnixMilli()时间戳作为updatedSince查询参数,返回该时间点之后有过更新的全部公告。 - 🗺️ 理解返回结构:
advisoriesResp.Advisories是map[string][]*Advisory——键为包名(如symfony/http-kernel),值为该包的公告列表。外层for遍历包、内层for遍历该包的公告,统计总数时对每个包的切片长度求和。 - 📝 关键字段解读:每条
Advisory重点打印Title(标题)、Cve(CVE 编号)、ReportedAt(报告时间)、AffectedVersions(受影响版本范围,Composer 约束语法如>=4.4.0,<4.4.44||>=5.0.0,<5.4.15)。这些字段是后续判断「是否受影响」的核心依据。 - 🎯 按包名精准查询:
repo.ListAdvisories(ctx, "symfony/http-kernel")直接传包名,返回[]*Advisory扁平切片(不再是 map)。它命中同一端点,但用packages[]=symfony/http-kernel作为查询参数,适合 CI 门禁里对单个依赖做定向检查。 - ✂️ 截断输出:示例用
count >= 3和i >= 2/i >= 5做了输出截断,避免公告多时刷屏——生产代码中应替换为持久化入库或结构化日志。 - 🛡️ 错误处理:按时间拉取失败时直接
return(后续步骤无意义);按包名查询失败时仅打印告警并继续,演示了「非致命错误」的容错策略,实际中可按业务重要性调整。
▶️ 运行方式
在示例目录下直接运行:
bash
cd /home/cc11001100/github/scagogogo/composer-skills/examples/security_advisories
go run main.go或从仓库根目录运行:
bash
go run examples/security_advisories/main.go纯 Go,无需 PHP
本例只调用 Packagist HTTP API,不需要本地 PHP 或 Composer,clone 下来直接 go run 即可。
⚠️ 本示例会发起真实的 Packagist API 调用,请避免高频运行。按时间拉取的响应可能较大(数十 MB 量级),请确保网络与内存充足;如需长期监控,请改用
GetSecurityAdvisoriesSince配合持久化的「上次同步时间」做增量。
📚 涉及的 SDK 方法
| 方法名 | 所属包 | 端点 / 查询参数 | 文档链接 |
|---|---|---|---|
ListSecurityAdvisories | pkg/repository | GET /api/security-advisories/?updatedSince={ms} | /sdk/packagist/repository#listsecurityadvisories |
ListAdvisories | pkg/repository | GET /api/security-advisories/?packages[]={name} | /sdk/packagist/repository#listadvisories |
📝 说明:这两个方法属于底层 Repository 层,接收
context.Context便于传透超时与取消。高层门面pkg/client.ComposerClient提供了等价的三件套——GetSecurityAdvisories(全量)、GetSecurityAdvisoriesSince(按时间,秒级时间戳)、GetSecurityAdvisoriesForPackages(按包名列表),详见 /sdk/packagist/advisories。两者命中的是同一端点,仅时间戳精度与参数封装不同,按你的语义需求选择。
🚀 进阶
- ⏱️ 加超时控制:把
context.Background()换成ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)并defer cancel(),防止全量拉取时网络卡死。 - 🗄️ 持久化入库:把
AdvisoriesResponse.Advisories序列化写入数据库(按AdvisoryID去重 upsert),配合ReportedAt做时间索引,构建自己的漏洞知识库。 - 🔁 增量同步:每次查询后把
time.Now()存为「检查点」,下次以它为updatedSince参数,只拉新增/更新的公告,避免重复全量请求。 - 🎯 版本约束比对:拿到
AffectedVersions(如>=5.4.0,<5.4.19||>=6.0.0,<6.0.4)后,结合项目composer.lock中实际安装的版本做约束匹配,判断是否真正受影响——这正是 security_monitor 示例要解决的核心问题。 - 🔔 接通知渠道:发现新公告时推送到 Slack / 飞书 / 邮件,或写入待办系统,形成「发现 → 通知 → 修复」的闭环。
- 🧪 CI 门禁:在 CI 流水线里对
composer.lock的依赖清单调ListAdvisories,有命中即阻断构建,把安全左移到提交阶段。