package parser import ( "context" "encoding/json" "fmt" "os" "sort" "strings" "time" "loupe/common" "loupe/internal/fetcher" "loupe/internal/logger" "loupe/internal/vulndb" "gorm.io/gorm" ) // Debian JSON 结构定义,对应 security-tracker.debian.org 的 data/json。 // 参考示例见 data/debian/debian-security-tracker.json。 // // 顶层:{"pkg-name": {"CVE-xxxx": { ... }}} // 其中每个 package 下是若干 CVE 条目。 // DebianTrackerJSON 顶层结构:包名 -> (CVE ID -> 条目) type DebianTrackerJSON map[string]map[string]DebianCVEEntry // DebianCVEEntry 单条 Debian CVE 条目 // 这里只建模当前解析流程需要的字段。 type DebianCVEEntry struct { Description string `json:"description"` Scope string `json:"scope"` Releases map[string]DebianReleaseInfo `json:"releases"` } // DebianReleaseInfo 描述某个 Debian 发行版在该 CVE 下的信息。 // 示例: // "bookworm": { // "status": "resolved", // "repositories": {"bookworm": "2.3.1+dfsg1-1+deb12u1"}, // "fixed_version": "0", // "urgency": "unimportant" // } type DebianReleaseInfo struct { Status string `json:"status"` Repositories map[string]string `json:"repositories"` FixedVersion string `json:"fixed_version"` Urgency string `json:"urgency"` } // DebianParser 负责解析 Debian security-tracker JSON,并写入统一的 VulnDB 结构。 // 注意:根据设计,Debian 仅负责补充 AffectedPackage,不向 Vulnerability 表写入记录。 type DebianParser struct { f fetcher.Fetcher db *gorm.DB } func NewDebianParser(f fetcher.Fetcher, db *vulndb.DB) *DebianParser { return &DebianParser{f: f, db: db.GetOrm()} } // debianStreamItem 表示从 Debian JSON 解析出的流式结果,这里只携带受影响包列表。 type debianStreamItem struct { VulnID string APs []vulndb.AffectedPackage } // Parse 将本地 Debian JSON 解析为一个流,通过 outCh 持续输出解析结果。 // // 行为: // - 由 DebianFetcher.Fetch 提供本地 debian-security-tracker.json 文件; // - 仅使用 Metadata["file_path"] 进行文件解析; // - 遍历所有 package 与 CVE,按 CVE 维度输出受影响包列表; // - 上层 UpsertDB 仅对 affected_packages 做增删,不改动 vulnerabilities 表。 func (p *DebianParser) Parse(ctx context.Context, outCh chan<- debianStreamItem) error { raws, err := p.f.Fetch(ctx) if err != nil { return err } if len(raws) == 0 { fmt.Printf("[Debian] no local Debian security-tracker JSON found, nothing to parse\n") logger.Info("[Debian] no local Debian security-tracker JSON found, nothing to parse") return nil } for _, raw := range raws { select { case <-ctx.Done(): return ctx.Err() default: } if err := p.parseOneFile(ctx, raw, outCh); err != nil { return err } } return nil } // UpsertDB 实现 Parser 接口:解析 Debian JSON,并仅对 affected_packages 做更新。 // Vulnerability 表由 NVD 等上游源统一维护,Debian 不直接写入漏洞元数据。 func (p *DebianParser) UpsertDB(ctx context.Context) error { items := make(chan debianStreamItem, 1024) parseErrCh := make(chan error, 1) // 先删除所有 ecosystem = "debian" 的 AffectedPackage,避免旧数据残留 if err := p.db.WithContext(ctx). Where("ecosystem = ?", common.VulnSourceDebian). Unscoped(). Delete(&vulndb.AffectedPackage{}). Error; err != nil { return fmt.Errorf("[Debian] delete old affected packages failed: %w", err) } else { fmt.Println("[Debian] hard delete old affected packages") logger.Info("[Debian] hard delete old affected packages") } // 异步启动解析协程,通过 items channel 向下游推送数据 go func() { defer close(items) if err := p.Parse(ctx, items); err != nil { parseErrCh <- err return } parseErrCh <- nil }() var countAP int var lastLog time.Time for it := range items { if len(it.APs) == 0 { continue } // 周期性打印数据库写入进度,避免长时间无输出 if time.Since(lastLog) > 10*time.Second { logger.Debug("[Debian] UpsertDB progress: %d affected packages written so far\n", countAP) lastLog = time.Now() } // logger.Debug("[Debian] db receive debian data vulnID=%s, aps=%d\n", it.VulnID, len(it.APs)) if err := p.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error { // 仅写入 AffectedPackage,不触碰 Vulnerability 表 const batchSize = 200 for start := 0; start < len(it.APs); start += batchSize { end := start + batchSize if end > len(it.APs) { end = len(it.APs) } batch := it.APs[start:end] if err := tx.Create(&batch).Error; err != nil { return fmt.Errorf("[Debian] insert affected packages failed: %w", err) } countAP += len(batch) } return nil }); err != nil { return err } } if err := <-parseErrCh; err != nil { return fmt.Errorf("[Debian] parse failed: %w", err) } msg := fmt.Sprintf("[Debian] UpsertDB: streamed %d affected packages (vulnerabilities untouched)\n", countAP) fmt.Print(msg) logger.Info(msg) return nil } // parseOneFile 解析单个 Debian JSON 文件。 func (p *DebianParser) parseOneFile(ctx context.Context, raw fetcher.RawAdvisory, outCh chan<- debianStreamItem) error { path, ok := raw.Metadata["file_path"] if !ok || path == "" { return fmt.Errorf("debian parser: missing file_path metadata") } fmt.Printf("[Debian] start parse debian security-tracker JSON: %s\n", path) logger.Info("[Debian] start parse debian security-tracker JSON: %s", path) file, err := os.Open(path) if err != nil { return fmt.Errorf("debian parser: open file %s: %w", path, err) } defer file.Close() dec := json.NewDecoder(file) var full DebianTrackerJSON if err := dec.Decode(&full); err != nil { return fmt.Errorf("debian parser: decode json %s: %w", path, err) } // 为了有稳定的遍历顺序,先收集包名+排序(调试更友好)。 pkgNames := make([]string, 0, len(full)) for pkg := range full { pkgNames = append(pkgNames, pkg) } sort.Strings(pkgNames) var totalVulns int lastLog := time.Now() for _, pkg := range pkgNames { cveMap := full[pkg] // Debian JSON 中的键一般形如 "CVE-2012-0833",也可能有 "TEMP-xxxxxxxx" 等。 cveIDs := make([]string, 0, len(cveMap)) for cveID := range cveMap { cveIDs = append(cveIDs, cveID) } sort.Strings(cveIDs) for _, cveID := range cveIDs { select { case <-ctx.Done(): return ctx.Err() default: } entry := cveMap[cveID] aps := p.buildAPs(pkg, cveID, &entry) // Debian 源可能存在非 CVE 格式的 ID(如 DSA-xxx),这里仅保留以 "CVE-" 开头的记录, // 其它记录可后续根据需要再扩展。 if !strings.HasPrefix(strings.ToUpper(cveID), "CVE-") { continue } if len(aps) == 0 { continue } // 计数与进度日志 totalVulns++ if totalVulns%1000 == 0 || time.Since(lastLog) > 10*time.Second { fmt.Printf("[Debian] parsing debian JSON %s: processed %d vulnerabilities (with affected packages)\n", path, totalVulns) logger.Debug("[Debian] parsing debian JSON %s: processed %d vulnerabilities (with affected packages)", path, totalVulns) lastLog = time.Now() } outCh <- debianStreamItem{VulnID: cveID, APs: aps} } } fmt.Printf("[Debian] finished parsing debian security-tracker JSON %s: total %d vulnerabilities with affected packages\n", path, totalVulns) logger.Info("[Debian] finished parsing debian security-tracker JSON %s: total %d vulnerabilities with affected packages", path, totalVulns) return nil } // buildAPs 将单个 DebianCVEEntry 转换为统一的 AffectedPackage 列表。 // 注意:这里不构造 Vulnerability,只根据 CVE ID 和 Debian 维度的信息生成受影响包数据。 func (p *DebianParser) buildAPs(pkgName, cveID string, entry *DebianCVEEntry) []vulndb.AffectedPackage { // 解析 releases -> AffectedPackage。 aps := make([]vulndb.AffectedPackage, 0, len(entry.Releases)) for rel, info := range entry.Releases { status := strings.ToLower(strings.TrimSpace(info.Status)) // 粗略将 status 映射到一个“是否已修复”语义(当前仅用来决定 FixedVersion 是否有意义): // - vulnerable / unresolved / open -> 视为未修复 // - resolved / fixed -> 视为已修复 // - ignored / not-affected 等 -> 可以标记为未受影响 var unaffected bool if strings.Contains(status, "not-affected") || status == "ignored" { unaffected = true } // Debian JSON 中,repositories 一般包含该 release 对应的仓库名和版本信息: // { // "repositories": { // "bookworm": { // "version": "1.2.4-1" // } // } // } // 这里的 version 是“仓库中当前有的版本”,不要与 fixed_version 混淆。 var firstRepo string if len(info.Repositories) > 0 { // 为保证稳定性,对 key 排序后取第一个仓库作为代表。 repoNames := make([]string, 0, len(info.Repositories)) for name := range info.Repositories { repoNames = append(repoNames, name) } sort.Strings(repoNames) firstRepoName := repoNames[0] firstRepo = strings.TrimSpace(firstRepoName) } osVersion := DebianCodenameToVersion(rel) if osVersion == "" { // 如果未能识别该代号,则原样保留 osVersion = rel } ap := vulndb.AffectedPackage{ VulnID: cveID, PackageName: pkgName, Ecosystem: string(common.VulnSourceDebian), OSFamily: common.OSDebian, OSVersion: osVersion, Repository: firstRepo, } // fixed_version 是“修复该漏洞的版本”,与 repositories 中的 version 含义不同。 fixedVer := strings.TrimSpace(info.FixedVersion) affectedAll := false switch fixedVer { case "", "0": // 不提供修复版本 => 所有版本受影响 affectedAll = true default: if !unaffected { ap.FixedVersion = fixedVer } } // VersionConstraint 暂时使用一个简单的 "< fixedVer" 或 "*" 表示,后续可以根据 // Debian 的版本规则扩展为更精确的约束表达式。 if !unaffected { if affectedAll { ap.VersionConstraint = "*" } else if ap.FixedVersion != "" { ap.VersionConstraint = "<" + ap.FixedVersion } } aps = append(aps, ap) } return aps } func DebianCodenameToVersion(code string) string { code = strings.ToLower(strings.TrimSpace(code)) if v, ok := common.DebianReleaseMap[code]; ok { return v } return "" }