Skip to content

Commit 34f4918

Browse files
committed
feat: add go symbol matching qualifier for better go vuln matching
govulndb's per-symbol reachability information used as a go-imports package qualifier called at match time to reduce false positives for Go binaries. When a Go binary is cataloged with symbol capture enabled, the qualifier only matches a vulnerability if at least one of the vulnerable symbols is actually present in the binary. Packages without symbol evidence continue to match at module granularity, preserving existing behavior. Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
1 parent c41e34d commit 34f4918

11 files changed

Lines changed: 291 additions & 8 deletions

File tree

go.mod

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ require (
1818
github.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4
1919
github.com/anchore/packageurl-go v0.2.0
2020
github.com/anchore/stereoscope v0.2.2
21-
github.com/anchore/syft v1.46.0
21+
github.com/anchore/syft v1.46.1-0.20260701033749-a1831ce3b499
2222
github.com/aquasecurity/go-pep440-version v0.0.1
2323
github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
2424
github.com/bitnami/go-version v0.0.0-20250505154626-452e8c5ee607
@@ -339,6 +339,7 @@ require (
339339
google.golang.org/grpc v1.80.0 // indirect
340340
google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect
341341
gopkg.in/warnings.v0 v0.1.2 // indirect
342+
howett.net/plist v1.0.1 // indirect
342343
modernc.org/libc v1.72.3 // indirect
343344
modernc.org/mathutil v1.7.1 // indirect
344345
modernc.org/memory v1.11.0 // indirect

go.sum

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,8 @@ github.com/anchore/packageurl-go v0.2.0 h1:CkrM4RMUwrEGAiE1OVlxaZNzWj0TuHRey7o4T
154154
github.com/anchore/packageurl-go v0.2.0/go.mod h1:2JCgOQMIsqZ7TmliXG4PnUthPJAKE3mWQbsW2XHjAOE=
155155
github.com/anchore/stereoscope v0.2.2 h1:SGTLGoF6GHmKEn9Bb6LYpzzgz9nhvMZgN/c4fTSQjYY=
156156
github.com/anchore/stereoscope v0.2.2/go.mod h1:ylJXJKebLctP7u9ewguB1d0zUETJxvAA7r2DiuljDTM=
157-
github.com/anchore/syft v1.46.0 h1:qIXMOJMlznWsWgzOT6xzK8aDhpOG7wnkbA0BS2UKF0c=
158-
github.com/anchore/syft v1.46.0/go.mod h1:LwBimjMV20nC1zAsN5jzpGTg/6f0X4Acvvx1WymA53g=
157+
github.com/anchore/syft v1.46.1-0.20260701033749-a1831ce3b499 h1:gxKUd0qvSHyHe9/nW8oaG78tFwF7kHStmcQwYf99Kkc=
158+
github.com/anchore/syft v1.46.1-0.20260701033749-a1831ce3b499/go.mod h1:D2EesMQiokuYW5w5y7tVM4pNYGsaYxqNIImffSSgGnY=
159159
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
160160
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
161161
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
@@ -651,6 +651,7 @@ github.com/invopop/jsonschema v0.14.0 h1:MHQqLhvpNUZfw+hM3AZDYK7jxO8FZoQeQM77g8i
651651
github.com/invopop/jsonschema v0.14.0/go.mod h1:ygm6C2EaVNMBDPpaPlnOA2pFAxBnxGjFlMZABxm9n2I=
652652
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
653653
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
654+
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
654655
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
655656
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
656657
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
@@ -1558,6 +1559,7 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
15581559
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
15591560
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
15601561
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
1562+
gopkg.in/yaml.v1 v1.0.0-20140924161607-9f9df34309c0/go.mod h1:WDnlLJ4WF5VGsH/HVa3CI79GS0ol3YnhVnKP89i0kNg=
15611563
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
15621564
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
15631565
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -1580,6 +1582,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
15801582
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
15811583
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
15821584
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
1585+
howett.net/plist v1.0.1 h1:37GdZ8tP09Q35o9ych3ehygcsL+HqKSwzctveSlarvM=
1586+
howett.net/plist v1.0.1/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g=
15831587
modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
15841588
modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
15851589
modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ=

grype/db/v6/blobs.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,21 @@ type PackageQualifiers struct {
159159
// RootIO indicates that the vulnerability applies only to Root IO packages (packages with Root IO fixes).
160160
// When true, standard packages will not match this vulnerability (NAK pattern).
161161
RootIO *bool `json:"rootio,omitempty"`
162+
163+
// GoImports lists the packages and symbols within an affected Go module that contain the vulnerability
164+
// (from govulndb's ecosystem_specific.imports). When set, packages carrying binary symbol evidence only
165+
// match if at least one of the listed symbols is present in the binary.
166+
GoImports []GoImport `json:"go_imports,omitempty"`
167+
}
168+
169+
// GoImport describes a single package within an affected Go module and the vulnerable symbols it contains.
170+
type GoImport struct {
171+
// Path is the import path of the package within the affected module (e.g. "golang.org/x/net/html").
172+
Path string `json:"path"`
173+
174+
// Symbols lists the vulnerable function/method names within the package (e.g. "Parse" or "Decoder.Decode").
175+
// An empty list means the entire package is considered vulnerable.
176+
Symbols []string `json:"symbols,omitempty"`
162177
}
163178

164179
// Range defines a specific range of package versions pertaining to a vulnerability.

grype/db/v6/build/transformers/osv/transform_go_vuln_db.go

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package osv
22

33
import (
4+
"encoding/json"
45
"fmt"
56
"sort"
67
"strings"
@@ -20,16 +21,16 @@ import (
2021
"github.com/anchore/syft/syft/pkg"
2122
)
2223

23-
// govulndbStrategy handles GO-* records from the Go vulnerability database
2424
// (vuln.go.dev): affected version ranges for Go modules, plus the "stdlib" and
2525
// "toolchain" pseudo-modules. Govulndb specifics:
2626
// - aliases hold CVEs/GHSAs directly; the schema has no `related` field.
2727
// - ecosystem is always "Go" → syft go-module type, found by the matcher's
2828
// ecosystem-name search.
2929
// - range type is always SEMVER → "go" format, which parses pseudo-versions
3030
// (v0.0.0-<timestamp>-<commit>).
31-
// - ecosystem_specific.imports (per-symbol reachability) is dropped; grype
32-
// matches at module granularity.
31+
// - ecosystem_specific.imports (per-symbol reachability) is carried into the
32+
// package blob as a go-imports qualifier, so packages cataloged with binary
33+
// symbol evidence only match when at least one vulnerable symbol is present.
3334
// - references pass through, OSV type as tag, refID empty (canonical advisory
3435
// page is in database_specific.url, not refs).
3536
type govulndbStrategy struct{}
@@ -124,11 +125,16 @@ func govulndbAffectedPackages(vuln unmarshal.OSVVulnerability) []db.AffectedPack
124125
if !govulndbEmits(affected.Package.Name) {
125126
continue
126127
}
128+
var qualifiers *db.PackageQualifiers
129+
if imports := govulndbImports(affected); len(imports) > 0 {
130+
qualifiers = &db.PackageQualifiers{GoImports: imports}
131+
}
127132
aphs = append(aphs, db.AffectedPackageHandle{
128133
Package: govulndbPackage(affected.Package),
129134
BlobValue: &db.PackageBlob{
130-
CVEs: vuln.Aliases,
131-
Ranges: govulndbRanges(affected.Ranges, govulndbCustomRanges(affected, vuln.ID)),
135+
CVEs: vuln.Aliases,
136+
Qualifiers: qualifiers,
137+
Ranges: govulndbRanges(affected.Ranges, govulndbCustomRanges(affected, vuln.ID)),
132138
},
133139
})
134140
}
@@ -312,6 +318,27 @@ func govulndbCustomRanges(affected osvmodel.Affected, id string) []osvmodel.Rang
312318
return ranges
313319
}
314320

321+
// govulndbImports extracts the affected package import paths and vulnerable symbols from the
322+
// OSV `ecosystem_specific.imports` field (see https://go.dev/security/vuln/database#schema).
323+
func govulndbImports(affected osvmodel.Affected) []db.GoImport {
324+
raw, ok := affected.EcosystemSpecific["imports"]
325+
if !ok {
326+
return nil
327+
}
328+
329+
// the ecosystem_specific field is unmarshalled as a generic map, so round-trip through JSON
330+
// to get the typed shape
331+
encoded, err := json.Marshal(raw)
332+
if err != nil {
333+
return nil
334+
}
335+
var imports []db.GoImport
336+
if err := json.Unmarshal(encoded, &imports); err != nil {
337+
return nil
338+
}
339+
return imports
340+
}
341+
315342
func govulndbPackage(p osvmodel.Package) *db.Package {
316343
return &db.Package{
317344
Ecosystem: string(pkg.GoModulePkg),

grype/db/v6/build/transformers/osv/transform_go_vuln_db_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@ func TestGoVulnDBTransform(t *testing.T) {
5959
},
6060
BlobValue: &db.PackageBlob{
6161
CVEs: []string{"CVE-2022-27664", "GHSA-69cg-p879-7622"},
62+
Qualifiers: &db.PackageQualifiers{
63+
GoImports: []db.GoImport{{
64+
Path: "golang.org/x/net/http2",
65+
Symbols: []string{"Server.ServeConn", "serverConn.goAway"},
66+
}},
67+
},
6268
Ranges: []db.Range{{
6369
Version: db.Version{
6470
Type: "go",
@@ -78,6 +84,16 @@ func TestGoVulnDBTransform(t *testing.T) {
7884
},
7985
BlobValue: &db.PackageBlob{
8086
CVEs: []string{"CVE-2022-27664", "GHSA-69cg-p879-7622"},
87+
Qualifiers: &db.PackageQualifiers{
88+
GoImports: []db.GoImport{{
89+
Path: "net/http",
90+
Symbols: []string{
91+
"ListenAndServe", "ListenAndServeTLS", "Serve", "ServeTLS",
92+
"Server.ListenAndServe", "Server.ListenAndServeTLS", "Server.Serve", "Server.ServeTLS",
93+
"http2Server.ServeConn", "http2serverConn.goAway",
94+
},
95+
}},
96+
},
8197
Ranges: []db.Range{{
8298
Version: db.Version{
8399
Type: "go",

grype/db/v6/vulnerability.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99

1010
"github.com/anchore/grype/grype/pkg/qualifier"
1111
"github.com/anchore/grype/grype/pkg/qualifier/architecture"
12+
"github.com/anchore/grype/grype/pkg/qualifier/gosymbols"
1213
"github.com/anchore/grype/grype/pkg/qualifier/platformcpe"
1314
"github.com/anchore/grype/grype/pkg/qualifier/rootio"
1415
"github.com/anchore/grype/grype/pkg/qualifier/rpmmodularity"
@@ -317,6 +318,13 @@ func toPackageQualifiers(qualifiers *PackageQualifiers) []qualifier.Qualifier {
317318
if qualifiers.RootIO != nil && *qualifiers.RootIO {
318319
out = append(out, rootio.New())
319320
}
321+
if len(qualifiers.GoImports) > 0 {
322+
var imports []gosymbols.Import
323+
for _, i := range qualifiers.GoImports {
324+
imports = append(imports, gosymbols.Import{Path: i.Path, Symbols: i.Symbols})
325+
}
326+
out = append(out, gosymbols.New(imports))
327+
}
320328
return out
321329
}
322330

grype/pkg/golang_metadata.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ type GolangBinMetadata struct {
99
H1Digest string `json:"h1Digest,omitempty" cyclonedx:"h1Digest"`
1010
MainModule string `json:"mainModule,omitempty" cyclonedx:"mainModule"`
1111
GoCryptoSettings []string `json:"goCryptoSettings,omitempty" cyclonedx:"goCryptoSettings"`
12+
Symbols []string `json:"symbols,omitempty"`
1213
}
1314

1415
type GolangModMetadata struct {

grype/pkg/package.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,7 @@ func golangMetadataFromPkg(p syftPkg.Package) any {
408408
metadata.Architecture = value.Architecture
409409
metadata.H1Digest = value.H1Digest
410410
metadata.MainModule = value.MainModule
411+
metadata.Symbols = value.Symbols
411412
return metadata
412413
case syftPkg.GolangModuleEntry:
413414
metadata := GolangModMetadata{}

grype/pkg/package_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -977,6 +977,16 @@ func TestNew(t *testing.T) {
977977
},
978978
},
979979
},
980+
{
981+
name: "apple-app-bundle",
982+
syftPkg: syftPkg.Package{
983+
Metadata: syftPkg.AppleAppBundleEntry{
984+
BundleIdentifier: "com.apple.Safari",
985+
Name: "Safari",
986+
ShortVersion: "17.0",
987+
},
988+
},
989+
},
980990
}
981991

982992
// capture each observed metadata type, we should see all of them relate to what syft provides by the end of testing
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package gosymbols
2+
3+
import (
4+
"regexp"
5+
"strings"
6+
7+
"github.com/anchore/grype/grype/pkg"
8+
"github.com/anchore/grype/grype/pkg/qualifier"
9+
)
10+
11+
// Import describes a single package within an affected Go module along with the vulnerable
12+
// symbols it contains (mirroring govulndb's `ecosystem_specific.imports` entries).
13+
type Import struct {
14+
// Path is the import path of the package within the affected module (e.g. "golang.org/x/net/html").
15+
Path string
16+
17+
// Symbols lists the vulnerable function/method names within the package (e.g. "Parse" or
18+
// "Decoder.Decode"). An empty list means the entire package is considered vulnerable.
19+
Symbols []string
20+
}
21+
22+
type gosymbolsQualifier struct {
23+
imports []Import
24+
}
25+
26+
func New(imports []Import) qualifier.Qualifier {
27+
return &gosymbolsQualifier{imports: imports}
28+
}
29+
30+
// Satisfied reports whether the package plausibly uses any of the vulnerable symbols. The check
31+
// only applies when the scanned package carries binary symbol evidence (go binaries cataloged with
32+
// symbol capture enabled); packages without symbol evidence always satisfy the qualifier so that
33+
// module-granularity matching behavior is preserved.
34+
func (q gosymbolsQualifier) Satisfied(p pkg.Package) (bool, error) {
35+
if len(q.imports) == 0 {
36+
return true, nil
37+
}
38+
39+
m, ok := p.Metadata.(pkg.GolangBinMetadata)
40+
if !ok || len(m.Symbols) == 0 {
41+
return true, nil
42+
}
43+
44+
present := make(map[string]struct{}, len(m.Symbols))
45+
for _, sym := range m.Symbols {
46+
present[normalizeSymbol(sym)] = struct{}{}
47+
}
48+
49+
for _, imp := range q.imports {
50+
if len(imp.Symbols) == 0 {
51+
// the entire package is vulnerable: any symbol from the package counts
52+
prefix := imp.Path + "."
53+
for sym := range present {
54+
if strings.HasPrefix(sym, prefix) {
55+
return true, nil
56+
}
57+
}
58+
continue
59+
}
60+
for _, vulnSym := range imp.Symbols {
61+
if _, found := present[imp.Path+"."+vulnSym]; found {
62+
return true, nil
63+
}
64+
}
65+
}
66+
67+
return false, nil
68+
}
69+
70+
var typeParamPattern = regexp.MustCompile(`\[[^]]*]`)
71+
72+
// normalizeSymbol converts a symbol name as found in a binary symbol table into govulndb's symbol
73+
// naming convention so the two can be compared:
74+
// - pointer-receiver decoration is removed: "pkg.(*T).M" -> "pkg.T.M"
75+
// - generic instantiations lose their type parameters: "pkg.(*T[go.shape.int]).M" -> "pkg.T.M"
76+
// - the compiler's "-fm" method-value-wrapper suffix is removed: "pkg.(*T).M-fm" -> "pkg.T.M".
77+
// A method-value wrapper is emitted when a method is referenced as a value (e.g. passed as a
78+
// callback), so its presence means the underlying method is used.
79+
func normalizeSymbol(symbol string) string {
80+
symbol = strings.ReplaceAll(symbol, "(*", "")
81+
symbol = strings.ReplaceAll(symbol, ")", "")
82+
symbol = strings.TrimSuffix(symbol, "-fm")
83+
return typeParamPattern.ReplaceAllString(symbol, "")
84+
}

0 commit comments

Comments
 (0)