🛡️ security_monitor — 安全监控
本示例演示如何基于 Packagist 安全公告端点构建一个持续运行的安全监控器:拉取全量公告、按需筛选关注包、把结果落盘成带时间戳的 JSON 报告,并给出接入 cron 定时任务的方案。
🎯 示例定位
security_monitor 是 Packagist API 远程操作 系列的第 7 个、也是最综合的一个示例。它把前面学到的客户端初始化、安全公告查询串联成一个可落地的小工具。
- 📚 你将学到:如何把 SDK 的
GetSecurityAdvisories封装进一个自定义结构体,结合os/encoding/json/path/filepath实现「拉取 → 筛选 → 落盘 → 定时复跑」的完整流水线。 - 🔗 对应 SDK 方法:
client.ComposerClient.GetSecurityAdvisories。 - 💡 与
security_advisories的区别:security_advisories只演示「一次性调用 + 打印」;本例把它工程化——加入数据目录、时间戳文件名、关注包过滤与 cron 提示,是迈向生产可用监控的第一步。 - 🧱 设计要点:
SecurityMonitor结构体持有客户端、数据目录、关注包列表三态,FetchAdvisories方法对外暴露「执行一轮扫描」的语义,便于被调度器反复调用。
💻 完整代码
go
package main
import (
"encoding/json"
"fmt"
"log"
"os"
"path/filepath"
"time"
"github.com/scagogogo/composer-skills/pkg/client"
"github.com/scagogogo/composer-skills/pkg/domain"
)
// SecurityMonitor 表示安全监控器
type SecurityMonitor struct {
client *client.ComposerClient
dataDir string
trackedPackages []string
}
// NewSecurityMonitor 创建一个新的安全监控器
func NewSecurityMonitor(dataDir string, packages []string) *SecurityMonitor {
return &SecurityMonitor{
client: client.NewComposerClient(30 * time.Second),
dataDir: dataDir,
trackedPackages: packages,
}
}
// FetchAdvisories 获取并保存安全公告
func (m *SecurityMonitor) FetchAdvisories() error {
// 获取所有安全公告
fmt.Println("获取所有安全公告...")
advisories, err := m.client.GetSecurityAdvisories()
if err != nil {
return fmt.Errorf("获取安全公告失败: %w", err)
}
// 确保数据目录存在
if err := os.MkdirAll(m.dataDir, 0755); err != nil {
return fmt.Errorf("创建数据目录失败: %w", err)
}
// 保存完整的公告数据
timestamp := time.Now().Format("20060102-150405")
fullDataPath := filepath.Join(m.dataDir, fmt.Sprintf("all_advisories_%s.json", timestamp))
data, err := json.MarshalIndent(advisories, "", " ")
if err != nil {
return fmt.Errorf("序列化公告数据失败: %w", err)
}
if err := os.WriteFile(fullDataPath, data, 0644); err != nil {
return fmt.Errorf("保存公告数据失败: %w", err)
}
fmt.Printf("已保存所有公告数据到 %s\n", fullDataPath)
// 筛选跟踪的包的公告
trackedAdvisories := make(map[string][]*domain.Advisory)
for _, pkgName := range m.trackedPackages {
if advisories, ok := advisories.Advisories[pkgName]; ok {
trackedAdvisories[pkgName] = advisories
fmt.Printf("发现 %s 有 %d 个安全公告\n", pkgName, len(advisories))
}
}
// 如果有跟踪的包存在公告,保存单独的报告
if len(trackedAdvisories) > 0 {
trackedDataPath := filepath.Join(m.dataDir, fmt.Sprintf("tracked_advisories_%s.json", timestamp))
data, err := json.MarshalIndent(trackedAdvisories, "", " ")
if err != nil {
return fmt.Errorf("序列化跟踪包的公告数据失败: %w", err)
}
if err := os.WriteFile(trackedDataPath, data, 0644); err != nil {
return fmt.Errorf("保存跟踪包的公告数据失败: %w", err)
}
fmt.Printf("已保存跟踪包的公告数据到 %s\n", trackedDataPath)
} else {
fmt.Println("跟踪的包中没有发现安全公告")
}
return nil
}
func main() {
// 要跟踪的包列表
trackedPackages := []string{
"symfony/symfony",
"laravel/framework",
"guzzlehttp/guzzle",
"monolog/monolog",
"phpunit/phpunit",
}
// 创建安全监控器
monitor := NewSecurityMonitor("security_data", trackedPackages)
// 获取并保存安全公告
if err := monitor.FetchAdvisories(); err != nil {
log.Fatalf("监控安全公告失败: %v", err)
}
fmt.Println("\n安全监控完成。可以设置此脚本通过 cron 作业定期运行,以持续监控安全公告。")
fmt.Println("示例 cron 表达式(每天运行一次): 0 0 * * * /path/to/security_monitor")
}🧩 代码讲解
- 🏗️ 封装监控器结构体:
SecurityMonitor把*client.ComposerClient、数据目录dataDir、关注包列表trackedPackages三者收拢为一个对象。这样「一轮扫描」所需的状态都被封装好,外部调用方只需monitor.FetchAdvisories(),便于被 cron 反复调度。 - ⏱️ 带超时的客户端:
client.NewComposerClient(30 * time.Second)在构造时注入 HTTP 超时,避免安全公告端点返回慢时进程长时间挂起。生产环境可按数据量调大该值。 - 🌐 一次性拉全量:
m.client.GetSecurityAdvisories()命中/api/security-advisories/?updatedSince=0,返回*domain.AdvisoriesResponse,其Advisories字段是map[包名][]*Advisory,便于按包名做 O(1) 查找。 - 📁 幂等建目录:
os.MkdirAll(m.dataDir, 0755)在目录已存在时不报错,保证首次运行和后续复跑行为一致,无需提前手动mkdir。 - 🕒 时间戳文件名:
time.Now().Format("20060102-150405")生成YYYYMMDD-HHMMSS串,拼进all_advisories_<ts>.json与tracked_advisories_<ts>.json。每次运行各产一份快照,历史可追溯、不会互相覆盖。 - 💾 美化输出 JSON:
json.MarshalIndent(advisories, "", " ")用两空格缩进序列化,落盘文件可直接用jq或文本编辑器阅读,便于人工巡检与 diff。 - 🔍 按关注包筛选:遍历
trackedPackages,对每个包名查advisories.Advisories映射——命中即收集到trackedAdvisories,并打印命中数量。这比全量遍历更高效,因为关注列表通常远小于全量公告。 - 🧾 两份报告分离:全量报告留作审计底稿,跟踪报告只含关注包,体积小、信号强,适合直接喂给下游告警逻辑。
- 🛡️ 错误包裹与中断:每步用
fmt.Errorf("...: %w", err)包裹上下文,最终在main里log.Fatalf中止;任何一步失败都不会带着脏数据继续往下写。 - ⏰ cron 化提示:末尾打印
0 0 * * * /path/to/security_monitor,把「跑一次」升级为「每天跑一次」的持续监控,只需把编译产物塞进 crontab。
▶️ 运行方式
在示例目录下直接运行:
bash
cd /home/cc11001100/github/scagogogo/composer-skills/examples/security_monitor
go run main.go也可编译成二进制后交给 cron 调度:
bash
go build -o security_monitor
./security_monitor
# 加入 crontab,每天午夜跑一次
# 0 0 * * * /path/to/security_monitor⚠️ 本示例会发起真实的 Packagist API 调用,且全量公告数据较大(MB 量级)。请避免高频运行;接入 cron 时建议每天一次,并确保运行目录有写
security_data/的权限。
📚 涉及的 SDK 方法
| 方法名 | 所属包 | 端点 | 文档链接 |
|---|---|---|---|
GetSecurityAdvisories | pkg/client (ComposerClient) | GET /api/security-advisories/?updatedSince=0 | /sdk/packagist/methods/get-security-advisories |
NewComposerClient | pkg/client | 构造函数(注入 HTTP 超时) | /sdk/packagist/methods/get-security-advisories |
📝 说明:本例核心只调用一个 SDK 方法
GetSecurityAdvisories。它返回的domain.AdvisoriesResponse.Advisories是map[string][]*domain.Advisory,因此后续筛选、计数、序列化都基于标准库与domain类型完成,无需再调别的 SDK 方法。若只需查少数几个包,可改用更省流量的GetSecurityAdvisoriesForPackages。
🚀 进阶
- ⏱️ 增量拉取:把
GetSecurityAdvisories换成GetSecurityAdvisoriesSince(lastRunTime),只拉上次运行后更新的公告,大幅减少流量和落盘体积;用本地文件记录lastRunTime即可。 - 🎯 精准查询:若关注包数量不多,直接用
GetSecurityAdvisoriesForPackages(trackedPackages)一步到位,服务端按packages[]过滤,省去本地全量筛选。 - 📢 接告警通道:在
trackedAdvisories非空时,把命中的*domain.Advisory(含 CVE 编号、受影响版本、修复版本)格式化后推送到 Slack 飞书 Webhook 或邮件,实现「发现即告警」。 - 🗃️ 历史对比与去重:扫描时读取上一份
tracked_advisories_*.json,对新出现的 advisory ID 打「NEW」标记,避免重复告警;保留近 N 天快照后自动轮转清理。 - 🧾 结构化入库:把
*domain.Advisory落到 SQLite/PostgreSQL,按包名、严重等级、CVE 建索引,前端再做一张安全态势看板。 - 🔁 更健壮的调度:用 systemd timer 或 Go 内置
time.Ticker取代 cron,加上失败重试与指数退避;并在每次运行结尾写一份last_run.json(状态 + 时间戳),便于排障。