Skip to content

🔒 security_advisories — 安全公告查询

本示例演示如何获取 Composer 包的安全漏洞公告,涵盖「按时间增量拉取」与「按包名精准查询」两种方式,并打印 CVE、受影响版本等关键字段。

🎯 示例定位

security_advisoriesPackagist 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 的区别:CLI audit 审计的是本地 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)
		}
	}
}

🧩 代码讲解

  • 🏗️ 初始化 Repositoryrepo := &repository.Repository{} 以零值构造底层 HTTP 客户端。options 字段未导出,方法内部固定请求 https://packagist.org 官方端点,因此本例中 options 仅作配置示意(_ = options)。
  • 🕐 按时间增量拉取oneYearAgo := time.Now().AddDate(-1, 0, 0) 构造一年前的时间点,传给 repo.ListSecurityAdvisories(ctx, oneYearAgo),SDK 取其毫秒级 UnixMilli() 时间戳作为 updatedSince 查询参数,返回该时间点之后有过更新的全部公告。
  • 🗺️ 理解返回结构advisoriesResp.Advisoriesmap[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 >= 3i >= 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 方法

方法名所属包端点 / 查询参数文档链接
ListSecurityAdvisoriespkg/repositoryGET /api/security-advisories/?updatedSince={ms}/sdk/packagist/repository#listsecurityadvisories
ListAdvisoriespkg/repositoryGET /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,有命中即阻断构建,把安全左移到提交阶段。

基于 MIT 许可证发布