🗄️ 把 CVSS 向量存入数据库并按分排序
问题
你想把 CVSS 向量和 CVE 编号一起存进 SQL 数据库,读回时拿到强类型的 Cvss3x 对象,并按严重性返回——但又不想存一个要去同步维护的 score 冗余列。
方案
流程如下:
1. 表结构
把向量串存在 VARCHAR/TEXT 列。一个由生成列/触发器维护的 score 列是可选的——讨论里会说为什么通常不需要。
sql
CREATE TABLE vulns (
id VARCHAR(32) PRIMARY KEY,
cvss TEXT NOT NULL
);2. 写入:Value(driver.Valuer)
Cvss3x.Value() 返回规范向量串,所以你可以直接把 *Cvss3x 传给 db.Exec。(*Cvss3x)(nil).Value() 写入 SQL NULL。
go
import (
"database/sql"
"github.com/scagogogo/cvss-skills/pkg/cvss"
)
func insert(db *sql.DB, id string, v *cvss.Cvss3x) error {
_, err := db.Exec("INSERT INTO vulns (id, cvss) VALUES (?, ?)", id, v)
return err
}
// 用法:
// insert(db, "CVE-2024-0001", cvss.CriticalV31())
// insert(db, "CVE-2024-0002", cvss.LowV31())3. 读取:Scan(sql.Scanner)
Cvss3x.Scan 接受 string、[]byte 或 nil(NULL → 空的 Cvss3x)。Scan 到 cvss.Cvss3x(不要 Scan 到指针),保证行值总是被实例化:
go
func listAll(db *sql.DB) ([]*cvss.Cvss3x, error) {
rows, err := db.Query("SELECT cvss FROM vulns")
if err != nil {
return nil, err
}
defer rows.Close()
var out []*cvss.Cvss3x
for rows.Next() {
var cv cvss.Cvss3x
if err := rows.Scan(&cv); err != nil {
return nil, err
}
out = append(out, &cv)
}
return out, rows.Err()
}4. 用 Cvss3xSlice 按分排序
拿到切片后在内存里排序——NewCvss3xSlice 给每个向量算一次分并缓存:
go
import (
"fmt"
"github.com/scagogogo/cvss-skills/pkg/cvss"
)
func printBySeverity(db *sql.DB) error {
vectors, err := listAll(db)
if err != nil {
return err
}
// 降序——Critical 在前(默认)。
slice := cvss.NewCvss3xSlice(vectors...).Sort()
for i, cv := range slice.Items() {
fmt.Printf("#%d %.1f %s\n", i+1, slice.ScoreAt(i), cv.String())
}
return nil
}对一组给定输入,输出形如:
text
#1 10.0 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
#2 9.8 CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
#3 3.7 CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N讨论
不要存 score 列
score 列会让向量去规范化——每次改指标都要重算分,过期的分是静默 bug。把向量串当唯一真相存起来,读的时候用 NewCvss3xSlice 算分。表很大时,分页算、每页算。
- NULL ↔ 空的
Cvss3x。Scan(nil)产生Cvss3x{Cvss3xBase: &Cvss3xBase{}}(不是 nil 指针),nil 的*Cvss3x写入 SQLNULL。Scan 目标用*Cvss3x(不要**Cvss3x)。 - 非法串会让 scan 失败。
Scan把解析错误包成"failed to scan Cvss3x: %w",所以坏行会在rows.Scan时暴露。如果列里可能有历史脏数据,先 Scan 到sql.NullString,再防御性解析。 - SQL 里的
ORDER BY没法按分排序。 分数不是一列,而且标准 SQL 算不了 CVSS。要么在 Go 里用Cvss3xSlice排(推荐),要么用应用代码维护一个score REAL列(仅当需要服务端按分分页时)。 - 无效向量被排到 −1。
NewCvss3xSlice对Calculate失败的项记-1;降序时它们排到最前,用if slice.ScoreAt(i) < 0 { continue }跳过。 - PostgreSQL 的
?占位符。 示例用的是?(MySQL/SQLite)。PostgreSQL 用$1、$2;SQL Server 用@p1。Scan/Value接口与驱动无关。