Skip to content

🔒 构建安全审计流水线

15 分钟,组合三层安全数据源——本地 AuditWithJSON、远程 GetSecurityAdvisoriesValidateStructured——产出一份可落库的 JSON 报告。需要本机有 PHP + Composer。

为什么需要流水线

单一来源不够用:本地审计只看已安装版本,远程公告能看到全生态最新漏洞,validate 则能挡住"composer.json 本身就写错了"。三层叠加 + 一份统一报告,才算可上线的安全流水线。

第 1 步:本地审计

comp.AuditWithJSON() 执行 composer audit --format=json,返回 *composer.AuditResult。其中 Found 是漏洞总数,Advisories 是漏洞列表,每条含 PackageTitleSeverityLinkCVE 等字段。

第 2 步:拉远程公告

client.GetSecurityAdvisories() 返回 *domain.AdvisoriesResponseAdvisoriesmap[包名][]*Advisory。只关心几个关键包时用 GetSecurityAdvisoriesForPackages(names)

第 3 步:校验 composer.json

comp.ValidateStructured() 返回 *composer.ValidateResult,含 Valid 布尔、ErrorsWarnings 切片。

完整可运行示例

go
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"os"
	"time"

	"github.com/scagogogo/composer-skills/pkg/client"
	"github.com/scagogogo/composer-skills/pkg/composer"
)

type Report struct {
	GeneratedAt      string                 `json:"generated_at"`
	LocalVulns       int                    `json:"local_vulns"`
	LocalAdvisories  []composer.Vulnerability `json:"local_advisories,omitempty"`
	RemotePackages   int                    `json:"remote_packages_with_advisories"`
	ComposerJSON     *composer.ValidateResult `json:"composer_json,omitempty"`
}

func main() {
	comp, err := composer.New(composer.DefaultOptions())
	if err != nil {
		log.Fatal(err)
	}
	comp.SetWorkingDir("/tmp/my-php-project")

	report := Report{GeneratedAt: time.Now().Format(time.RFC3339)}

	// 1️⃣ 本地审计
	if audit, err := comp.AuditWithJSON(); err == nil {
		report.LocalVulns = audit.Found
		report.LocalAdvisories = audit.Advisories
		for _, v := range audit.Advisories {
			fmt.Printf("⚠️  %s: %s [%s] %s\n", v.Package, v.Title, v.Severity, v.Link)
		}
	}

	// 2️⃣ 远程公告(仅看本项目的直接依赖)
	deps := comp.GetDirectDependencyNames()
	c := client.NewComposerClient(30 * time.Second)
	if adv, err := c.GetSecurityAdvisoriesForPackages(deps); err == nil {
		report.RemotePackages = len(adv.Advisories)
		for pkg, list := range adv.Advisories {
			fmt.Printf("🌐 %s: %d 条远程公告\n", pkg, len(list))
		}
	}

	// 3️⃣ 校验 composer.json
	if vr, err := comp.ValidateStructured(); err == nil {
		report.ComposerJSON = vr
		if !vr.Valid {
			fmt.Printf("❌ composer.json 无效:%v\n", vr.Errors)
		}
	}

	// 4️⃣ 落盘报告
	b, _ := json.MarshalIndent(report, "", "  ")
	if err := os.WriteFile("security-report.json", b, 0644); err != nil {
		log.Fatal(err)
	}
	fmt.Printf("\n✅ 报告已写入 security-report.json(本地漏洞 %d,远程受影响包 %d\n",
		report.LocalVulns, report.RemotePackages)
}

预期输出

⚠️  guzzlehttp/psr7: Objective PHPSR7 vulnerability [high] https://...
🌐 monolog/monolog: 1 条远程公告
❌ composer.json 无效:[The key "require.monolog/monolog" is invalid]

✅ 报告已写入 security-report.json(本地漏洞 1,远程受影响包 1)

进阶:增量公告与高严重性筛选

  • GetSecurityAdvisoriesSince(t) 只返回 t 之后更新的公告,适合定时轮询。
  • comp.GetHighSeverityVulnerabilities() 在本地审计后筛出 high/critical 的漏洞,用于阻断流水线。
go
// 流水线阻断:发现高危漏洞就退出非零
high, _ := comp.GetHighSeverityVulnerabilities()
if len(high) > 0 {
    log.Fatalf("发现 %d 个高危漏洞,流水线失败", len(high))
}

结构化返回值的意义

AuditResultValidateResult 都是纯 Go 结构体,可直接 json.Marshal 入库或喂给告警系统,无需正则解析 CLI 文本。

audit 的退出码

composer audit 发现漏洞时会以非零退出码结束,但 SDK 已把这种情况转换成 *AuditResultFound > 0),不会当作 error 抛出。判断是否有漏洞请用 result.Found,而不是 err != nil

下一步

基于 MIT 许可证发布