🗄️ SQL & Sorting
Persist CVSS vectors in a SQL column with Scan/Value, sort a collection by score with Cvss3xSlice, and normalize vector strings to spec order with Canonicalize.
Synopsis
// database/sql round-trip
var cv cvss.Cvss3x
rows.Scan(&cv) // sql.Scanner
db.Exec("INSERT ... VALUES (?)", cvss.CriticalV31()) // driver.Valuer
// sort by score, worst first
slice := cvss.NewCvss3xSlice(v1, v2, v3).Sort()
for _, cv := range slice.Items() { ... }
// canonicalize a messy string
norm, _ := cvss.Canonicalize("CVSS:3.1/S:U/C:H/I:H/A:H/AV:N/AC:L/PR:N/UI:N")How It Works
Value serializes to the vector string for storage; Scan accepts string/[]byte (or nil) and re-parses via fromVectorString. Cvss3xSlice pre-computes each vector's score (invalid vectors score -1 so they sort last) and implements sort.Interface; Canonicalize parses then re-emits String(), which already orders metrics Base→Temporal→Environmental.
API Reference
SQL interfaces
func (x *Cvss3x) Scan(src interface{}) error
func (x *Cvss3x) Value() (driver.Value, error)Scanimplementssql.Scanner. It acceptsstring,[]byte, ornil(NULL → emptyCvss3x). Any other type returns an error, and a malformed vector string is wrapped as"failed to scan Cvss3x: %w".Valueimplementsdriver.Valuer. Anilreceiver returnsnil, nil(SQL NULL); otherwise it returnsx.String()— the canonical vector string. Store it in aVARCHAR/TEXTcolumn.
NULL ↔ nil
Scan(nil) produces an empty Cvss3x{Cvss3xBase: &Cvss3xBase{}} (not a nil pointer), and (*Cvss3x)(nil).Value() writes SQL NULL. Use *Cvss3x (not **Cvss3x) as the scan target so the row value is always materialized.
Score-sorted slice
type Cvss3xSlice struct { /* unexported: items, scores, desc */ }
func NewCvss3xSlice(items ...*Cvss3x) *Cvss3xSlice
func (s *Cvss3xSlice) Len() int
func (s *Cvss3xSlice) Less(i, j int) bool
func (s *Cvss3xSlice) Swap(i, j int)
func (s *Cvss3xSlice) Items() []*Cvss3x
func (s *Cvss3xSlice) Asc() *Cvss3xSlice
func (s *Cvss3xSlice) Desc() *Cvss3xSlice
func (s *Cvss3xSlice) Sort() *Cvss3xSlice
func (s *Cvss3xSlice) ScoreAt(i int) float64NewCvss3xSliceprecomputes each item's score viaNewCalculator(cv).Calculate(). Items that fail to score (incomplete/invalid) get-1, so they sort last in ascending order and first in descending — guard them withScoreAt(i) < 0if you need to skip invalids.Cvss3xSliceimplementssort.Interface, so it works withsort.Sort/sort.Stabledirectly. The convenience methodsAsc()/Desc()/Sort()are chainable and return the slice itself.- Default order is descending (Critical first). Call
.Asc()before.Sort()for ascending. ScoreAtreturns the cached score (0 for out-of-range index), avoiding a re-calculation after sorting.
Scores are cached at construction
NewCvss3xSlice computes scores once. If you mutate an item's metrics after creating the slice, the cached score is stale — rebuild the slice. Swap keeps items and scores in lockstep, so sorting stays consistent.
Canonicalization
func Canonicalize(vectorString string) (string, error)
func (x *Cvss3x) CanonicalizeString() string
func IsCanonical(vectorString string) boolCanonicalizeparses any vector string and re-emits it in spec order: Base (AV, AC, PR, UI, S, C, I, A) → Temporal (E, RL, RC) → Environmental (CR, IR, AR, MAV, MAC, MPR, MUI, MS, MC, MI, MA), with set metrics only. A parse error is wrapped as"cannot canonicalize: %w".CanonicalizeStringon a*Cvss3xis sugar forx.String()(already canonical). A nil receiver returns"".IsCanonicalreturnstrueiffCanonicalize(s) == s(byte-equal). On a parse error it returnsfalse.
Example
package main
import (
"database/sql"
"fmt"
"sort"
"github.com/scagogogo/cvss-skills/pkg/cvss"
"github.com/scagogogo/cvss-skills/pkg/mock"
)
func main() {
// Build a collection to sort.
v1 := cvss.CriticalV31()
v2 := cvss.LowV31()
v3 := cvss.HighV31()
// Descending (default) — Critical first.
desc := cvss.NewCvss3xSlice(v1, v2, v3).Sort()
for i, cv := range desc.Items() {
fmt.Printf("#%d %.1f %s\n", i+1, desc.ScoreAt(i), cv.String())
}
// Ascending — None/Low first.
asc := cvss.NewCvss3xSlice(v1, v2, v3).Asc().Sort()
_ = asc
// Also works with sort.Sort directly.
s := cvss.NewCvss3xSlice(v1, v2, v3).Asc()
sort.Stable(s)
// Canonicalize a reordered vector string.
messy := "CVSS:3.1/S:U/C:H/I:H/A:H/AV:N/AC:L/PR:N/UI:N"
norm, _ := cvss.Canonicalize(messy)
fmt.Println(norm) // CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
fmt.Println(cvss.IsCanonical(norm)) // true
fmt.Println(cvss.IsCanonical(messy)) // false
// Value: write to a DB.
_ = func(db *sql.DB) error {
_, err := db.Exec("INSERT INTO vulns (cvss) VALUES (?)", cvss.CriticalV31())
return err
}
// Scan: read back. (Illustrative — assumes a rows iterator.)
_ = func(rows *sql.Rows) {
var cv cvss.Cvss3x
for rows.Next() {
_ = rows.Scan(&cv)
fmt.Println(cv.String())
}
}
// A random vector joins the ranking; its score is computed lazily.
rand := mock.RandomCvss3x(1)
slice := cvss.NewCvss3xSlice(rand, v1)
fmt.Println("score[0]:", slice.ScoreAt(0))
}Related
- pkg/cvss —
String()is the canonical formScan/Valuerely on - pkg/parser —
Canonicalizeshares the internal parser - Scoring (calculator) —
Cvss3xSlicecaches scores viaCalculate