Skip to content

🛡️ security_monitor — 安全监控

本示例演示如何基于 Packagist 安全公告端点构建一个持续运行的安全监控器:拉取全量公告、按需筛选关注包、把结果落盘成带时间戳的 JSON 报告,并给出接入 cron 定时任务的方案。

🎯 示例定位

security_monitorPackagist 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>.jsontracked_advisories_<ts>.json。每次运行各产一份快照,历史可追溯、不会互相覆盖。
  • 💾 美化输出 JSONjson.MarshalIndent(advisories, "", " ") 用两空格缩进序列化,落盘文件可直接用 jq 或文本编辑器阅读,便于人工巡检与 diff。
  • 🔍 按关注包筛选:遍历 trackedPackages,对每个包名查 advisories.Advisories 映射——命中即收集到 trackedAdvisories,并打印命中数量。这比全量遍历更高效,因为关注列表通常远小于全量公告。
  • 🧾 两份报告分离:全量报告留作审计底稿,跟踪报告只含关注包,体积小、信号强,适合直接喂给下游告警逻辑。
  • 🛡️ 错误包裹与中断:每步用 fmt.Errorf("...: %w", err) 包裹上下文,最终在 mainlog.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 方法

方法名所属包端点文档链接
GetSecurityAdvisoriespkg/client (ComposerClient)GET /api/security-advisories/?updatedSince=0/sdk/packagist/methods/get-security-advisories
NewComposerClientpkg/client构造函数(注入 HTTP 超时)/sdk/packagist/methods/get-security-advisories

📝 说明:本例核心只调用一个 SDK 方法 GetSecurityAdvisories。它返回的 domain.AdvisoriesResponse.Advisoriesmap[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(状态 + 时间戳),便于排障。

基于 MIT 许可证发布