Skip to content

Commit b621b65

Browse files
committed
fix: cleanup
Signed-off-by: Christopher Phillips <32073428+spiffcs@users.noreply.github.com>
1 parent 34f4918 commit b621b65

8 files changed

Lines changed: 849 additions & 14 deletions

File tree

cmd/grype/cli/commands/internal/dbsearch/versions.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ package dbsearch
22

33
const (
44
// MatchesSchemaVersion is the schema version for the `db search` command
5-
MatchesSchemaVersion = "1.1.6"
5+
MatchesSchemaVersion = "1.1.7"
66

77
// MatchesSchemaVersion Changelog:
88
// 1.0.0 - Initial schema 🎉
@@ -16,6 +16,7 @@ const (
1616
// 1.1.4 - Add rpm_arch field to PackageQualifiers (source/binary tagging for the CSAF VEX transformer)
1717
// 1.1.5 - Add rootio field to PackageQualifiers (for Root IO NAK-pattern matching via the OSV rootio strategy)
1818
// 1.1.6 - Rename rpm_arch field on PackageQualifiers to architecture (semantics unchanged; rpm-specific prefix dropped)
19+
// 1.1.7 - Add go_imports field to PackageQualifiers (per-symbol reachability from govulndb ecosystem_specific.imports, used for Go binary symbol matching via the gosymbols qualifier)
1920

2021
// VulnerabilitiesSchemaVersion is the schema version for the `db search vuln` command
2122
VulnerabilitiesSchemaVersion = "1.0.5"

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

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

33
import (
4-
"encoding/json"
54
"fmt"
65
"sort"
76
"strings"
@@ -126,7 +125,7 @@ func govulndbAffectedPackages(vuln unmarshal.OSVVulnerability) []db.AffectedPack
126125
continue
127126
}
128127
var qualifiers *db.PackageQualifiers
129-
if imports := govulndbImports(affected); len(imports) > 0 {
128+
if imports := govulndbImports(affected, vuln.ID); len(imports) > 0 {
130129
qualifiers = &db.PackageQualifiers{GoImports: imports}
131130
}
132131
aphs = append(aphs, db.AffectedPackageHandle{
@@ -320,20 +319,25 @@ func govulndbCustomRanges(affected osvmodel.Affected, id string) []osvmodel.Rang
320319

321320
// govulndbImports extracts the affected package import paths and vulnerable symbols from the
322321
// OSV `ecosystem_specific.imports` field (see https://go.dev/security/vuln/database#schema).
323-
func govulndbImports(affected osvmodel.Affected) []db.GoImport {
322+
// Returns nil if absent or undecodable (logged), so malformed imports degrade to module-
323+
// granularity matching instead of erroring.
324+
func govulndbImports(affected osvmodel.Affected, id string) []db.GoImport {
324325
raw, ok := affected.EcosystemSpecific["imports"]
325326
if !ok {
326327
return nil
327328
}
328329

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)
330+
var imports []db.GoImport
331+
decoder, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
332+
Result: &imports,
333+
TagName: "json",
334+
})
332335
if err != nil {
333336
return nil
334337
}
335-
var imports []db.GoImport
336-
if err := json.Unmarshal(encoded, &imports); err != nil {
338+
if err := decoder.Decode(raw); err != nil {
339+
log.WithFields("id", id, "package", affected.Package.Name, "error", err).
340+
Warn("unable to decode govulndb imports; matching at module granularity")
337341
return nil
338342
}
339343
return imports

grype/db/v6/db.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const (
2424
Revision = 1
2525

2626
// Addition indicates how many changes have been introduced that are compatible with all historical data
27-
Addition = 7
27+
Addition = 8
2828

2929
// v6 model changelog:
3030
// 6.0.0: Initial version 🎉
@@ -48,6 +48,11 @@ const (
4848
// architecture). The field's semantics are unchanged; the rename drops the rpm-
4949
// specific prefix because the value already lives in PackageQualifiers and can
5050
// carry any architecture string for future arch-scoped advisories.
51+
// 6.1.8: Add GoImports field to PackageQualifiers (used by the govulndb OSV strategy to
52+
// carry per-symbol reachability from ecosystem_specific.imports; the gosymbols
53+
// runtime qualifier in pkg/qualifier/gosymbols matches captured Go binary symbols
54+
// so stdlib and golang.org/x/* advisories don't FP-match binaries that don't use
55+
// the vulnerable symbols)
5156
)
5257

5358
const (

grype/db/v6/vulnerability_test.go

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,53 @@ import (
99
"github.com/stretchr/testify/assert"
1010
"github.com/stretchr/testify/require"
1111

12+
"github.com/anchore/grype/grype/pkg"
1213
"github.com/anchore/grype/grype/version"
1314
"github.com/anchore/grype/grype/vulnerability"
1415
)
1516

17+
// Test_toPackageQualifiers_goImports verifies that a GoImports package qualifier, as it is stored in
18+
// the vulnerability DB (see the govulndb transformer), is converted into a working gosymbols qualifier
19+
// at load time. It uses the "stdlib" imports of GO-2022-0969 (net/http HTTP/2 server DoS) against
20+
// realistic captured Go binary symbols to confirm the DB data actually drives symbol-level matching.
21+
func Test_toPackageQualifiers_goImports(t *testing.T) {
22+
// as emitted by the transformer from ecosystem_specific.imports
23+
blob := &PackageQualifiers{
24+
GoImports: []GoImport{{
25+
Path: "net/http",
26+
Symbols: []string{
27+
"ListenAndServe", "ListenAndServeTLS", "Serve", "ServeTLS",
28+
"Server.ListenAndServe", "Server.ListenAndServeTLS", "Server.Serve", "Server.ServeTLS",
29+
"http2Server.ServeConn", "http2serverConn.goAway",
30+
},
31+
}},
32+
}
33+
34+
qualifiers := toPackageQualifiers(blob)
35+
require.Len(t, qualifiers, 1, "expected GoImports to produce a single gosymbols qualifier")
36+
37+
serverBinary := pkg.Package{
38+
Name: "stdlib",
39+
Metadata: pkg.GolangBinMetadata{
40+
Symbols: []string{"net/http.ListenAndServe", "net/http.(*http2Server).ServeConn"},
41+
},
42+
}
43+
clientBinary := pkg.Package{
44+
Name: "stdlib",
45+
Metadata: pkg.GolangBinMetadata{
46+
Symbols: []string{"net/http.Get", "net/http.(*Client).Do"},
47+
},
48+
}
49+
50+
satisfied, err := qualifiers[0].Satisfied(serverBinary)
51+
require.NoError(t, err)
52+
assert.True(t, satisfied, "an http server binary should match the net/http server vuln")
53+
54+
satisfied, err = qualifiers[0].Satisfied(clientBinary)
55+
require.NoError(t, err)
56+
assert.False(t, satisfied, "an http client-only binary should not match the net/http server vuln")
57+
}
58+
1659
func TestV5Namespace(t *testing.T) {
1760
// provider input should be derived from the Providers table:
1861
// +------------+---------+---------------+----------------------------------+------------------------+

grype/pkg/qualifier/gosymbols/qualifier.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,13 @@ var typeParamPattern = regexp.MustCompile(`\[[^]]*]`)
7676
// - the compiler's "-fm" method-value-wrapper suffix is removed: "pkg.(*T).M-fm" -> "pkg.T.M".
7777
// A method-value wrapper is emitted when a method is referenced as a value (e.g. passed as a
7878
// callback), so its presence means the underlying method is used.
79+
//
80+
// This is a best-effort textual heuristic, not a full demangler. The type-parameter stripping
81+
// assumes a single, non-nested bracket group; a nested instantiation like
82+
// "pkg.(*T[go.shape.[]int]).M" is not normalized cleanly. Such a symbol simply fails to match its
83+
// govulndb counterpart, so the failure mode is a missed match (false negative) for that one symbol
84+
// rather than a false positive — acceptable because generic vulnerable symbols with slice/nested
85+
// type arguments are vanishingly rare in govulndb advisories.
7986
func normalizeSymbol(symbol string) string {
8087
symbol = strings.ReplaceAll(symbol, "(*", "")
8188
symbol = strings.ReplaceAll(symbol, ")", "")

grype/pkg/qualifier/gosymbols/qualifier_test.go

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,48 @@ func TestGoSymbolsQualifier_Satisfied(t *testing.T) {
2828
Metadata: pkg.GolangBinMetadata{},
2929
}
3030

31+
// stdlibHTTPServerVuln mirrors the ecosystem_specific.imports for the "stdlib" module in
32+
// GO-2022-0969 (net/http HTTP/2 server DoS) as carried into the DB by the govulndb transformer.
33+
// The listed symbols are all server-side entrypoints.
34+
stdlibHTTPServerVuln := []Import{{
35+
Path: "net/http",
36+
Symbols: []string{
37+
"ListenAndServe", "ListenAndServeTLS", "Serve", "ServeTLS",
38+
"Server.ListenAndServe", "Server.ListenAndServeTLS", "Server.Serve", "Server.ServeTLS",
39+
"http2Server.ServeConn", "http2serverConn.goAway",
40+
},
41+
}}
42+
43+
// stdlibServerPkg is a go binary that runs an HTTP server: its captured stdlib symbols include the
44+
// vulnerable net/http server entrypoints. Symbol strings use syft's binary naming (import-path
45+
// qualified, pointer-receiver decorated); the bundled HTTP/2 code appears under the http2* prefix.
46+
stdlibServerPkg := pkg.Package{
47+
Name: "stdlib",
48+
Metadata: pkg.GolangBinMetadata{
49+
Symbols: []string{
50+
"net/http.ListenAndServe",
51+
"net/http.(*Server).Serve",
52+
"net/http.(*http2Server).ServeConn",
53+
"net/http.(*Client).Do",
54+
},
55+
},
56+
}
57+
58+
// stdlibClientPkg is a go binary that only makes HTTP *client* calls: it links net/http but none of
59+
// the vulnerable server symbols, so it must not match the server-side DoS (the false-positive grype
60+
// surfaced before symbol matching, when every net/http-linking binary matched the stdlib advisory).
61+
stdlibClientPkg := pkg.Package{
62+
Name: "stdlib",
63+
Metadata: pkg.GolangBinMetadata{
64+
Symbols: []string{
65+
"net/http.Get",
66+
"net/http.NewRequest",
67+
"net/http.(*Client).Do",
68+
"net/http.(*Transport).RoundTrip",
69+
},
70+
},
71+
}
72+
3173
tests := []struct {
3274
name string
3375
imports []Import
@@ -103,6 +145,24 @@ func TestGoSymbolsQualifier_Satisfied(t *testing.T) {
103145
pkg: binaryPkg,
104146
satisfied: true,
105147
},
148+
{
149+
name: "stdlib http server binary matches the net/http server vuln",
150+
imports: stdlibHTTPServerVuln,
151+
pkg: stdlibServerPkg,
152+
satisfied: true,
153+
},
154+
{
155+
name: "stdlib bundled http2 server method matches (http2* prefix, pointer-receiver normalization)",
156+
imports: []Import{{Path: "net/http", Symbols: []string{"http2Server.ServeConn"}}},
157+
pkg: stdlibServerPkg,
158+
satisfied: true,
159+
},
160+
{
161+
name: "stdlib http client-only binary does not match the net/http server vuln (no false positive)",
162+
imports: stdlibHTTPServerVuln,
163+
pkg: stdlibClientPkg,
164+
satisfied: false,
165+
},
106166
}
107167

108168
for _, tt := range tests {
@@ -114,3 +174,52 @@ func TestGoSymbolsQualifier_Satisfied(t *testing.T) {
114174
})
115175
}
116176
}
177+
178+
func Test_normalizeSymbol(t *testing.T) {
179+
tests := []struct {
180+
name string
181+
symbol string
182+
want string
183+
}{
184+
{
185+
name: "plain function is unchanged",
186+
symbol: "golang.org/x/net/html.Parse",
187+
want: "golang.org/x/net/html.Parse",
188+
},
189+
{
190+
name: "value-receiver method is unchanged",
191+
symbol: "net/http.Header.Get",
192+
want: "net/http.Header.Get",
193+
},
194+
{
195+
name: "pointer-receiver decoration is removed",
196+
symbol: "net/http.(*Server).Serve",
197+
want: "net/http.Server.Serve",
198+
},
199+
{
200+
name: "generic instantiation loses its type parameters",
201+
symbol: "golang.org/x/net/http2.(*Framer[go.shape.int]).ReadFrame",
202+
want: "golang.org/x/net/http2.Framer.ReadFrame",
203+
},
204+
{
205+
name: "method-value wrapper suffix is removed",
206+
symbol: "golang.org/x/net/html.(*Tokenizer).readComment-fm",
207+
want: "golang.org/x/net/html.Tokenizer.readComment",
208+
},
209+
{
210+
// known limitation: the type-parameter regex assumes a single, non-nested bracket
211+
// group, so a nested instantiation is not normalized to "pkg.T.M". This pins the
212+
// current (imperfect) behavior; the consequence is a missed match for that symbol,
213+
// not a false positive. See normalizeSymbol's doc comment.
214+
name: "nested type parameter is not normalized cleanly (known limitation)",
215+
symbol: "example.com/pkg.(*Cache[go.shape.[]int]).Get",
216+
want: "example.com/pkg.Cacheint].Get",
217+
},
218+
}
219+
220+
for _, tt := range tests {
221+
t.Run(tt.name, func(t *testing.T) {
222+
assert.Equal(t, tt.want, normalizeSymbol(tt.symbol))
223+
})
224+
}
225+
}

0 commit comments

Comments
 (0)