golang.org/x/tools/gopls@v0.15.3/internal/vulncheck/vulntest/db.go (about) 1 // Copyright 2022 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 //go:build go1.18 6 // +build go1.18 7 8 // Package vulntest provides helpers for vulncheck functionality testing. 9 package vulntest 10 11 import ( 12 "bytes" 13 "context" 14 "encoding/json" 15 "fmt" 16 "os" 17 "path/filepath" 18 "sort" 19 "strings" 20 "time" 21 22 "golang.org/x/tools/gopls/internal/protocol" 23 "golang.org/x/tools/gopls/internal/vulncheck/osv" 24 "golang.org/x/tools/txtar" 25 ) 26 27 // NewDatabase returns a read-only DB containing the provided 28 // txtar-format collection of vulnerability reports. 29 // Each vulnerability report is a YAML file whose format 30 // is defined in golang.org/x/vulndb/doc/format.md. 31 // A report file name must have the id as its base name, 32 // and have .yaml as its extension. 33 // 34 // db, err := NewDatabase(ctx, reports) 35 // ... 36 // defer db.Clean() 37 // client, err := NewClient(db) 38 // ... 39 // 40 // The returned DB's Clean method must be called to clean up the 41 // generated database. 42 func NewDatabase(ctx context.Context, txtarReports []byte) (*DB, error) { 43 disk, err := os.MkdirTemp("", "vulndb-test") 44 if err != nil { 45 return nil, err 46 } 47 if err := generateDB(ctx, txtarReports, disk, false); err != nil { 48 os.RemoveAll(disk) 49 return nil, err 50 } 51 52 return &DB{disk: disk}, nil 53 } 54 55 // DB is a read-only vulnerability database on disk. 56 // Users can use this database with golang.org/x/vuln APIs 57 // by setting the `VULNDB“ environment variable. 58 type DB struct { 59 disk string 60 } 61 62 // URI returns the file URI that can be used for VULNDB environment 63 // variable. 64 func (db *DB) URI() string { 65 u := protocol.URIFromPath(filepath.Join(db.disk, "ID")) 66 return string(u) 67 } 68 69 // Clean deletes the database. 70 func (db *DB) Clean() error { 71 return os.RemoveAll(db.disk) 72 } 73 74 // 75 // The following was selectively copied from golang.org/x/vulndb/internal/database 76 // 77 78 const ( 79 dbURL = "https://pkg.go.dev/vuln/" 80 81 // idDirectory is the name of the directory that contains entries 82 // listed by their IDs. 83 idDirectory = "ID" 84 85 // cmdModule is the name of the module containing Go toolchain 86 // binaries. 87 cmdModule = "cmd" 88 89 // stdModule is the name of the module containing Go std packages. 90 stdModule = "std" 91 ) 92 93 // generateDB generates the file-based vuln DB in the directory jsonDir. 94 func generateDB(ctx context.Context, txtarData []byte, jsonDir string, indent bool) error { 95 archive := txtar.Parse(txtarData) 96 97 entries, err := generateEntries(ctx, archive) 98 if err != nil { 99 return err 100 } 101 return writeEntriesByID(filepath.Join(jsonDir, idDirectory), entries, indent) 102 } 103 104 func generateEntries(_ context.Context, archive *txtar.Archive) ([]osv.Entry, error) { 105 now := time.Now() 106 var entries []osv.Entry 107 for _, f := range archive.Files { 108 if !strings.HasSuffix(f.Name, ".yaml") { 109 continue 110 } 111 r, err := readReport(bytes.NewReader(f.Data)) 112 if err != nil { 113 return nil, err 114 } 115 name := strings.TrimSuffix(filepath.Base(f.Name), filepath.Ext(f.Name)) 116 linkName := fmt.Sprintf("%s%s", dbURL, name) 117 entry := generateOSVEntry(name, linkName, now, *r) 118 entries = append(entries, entry) 119 } 120 return entries, nil 121 } 122 123 func writeEntriesByID(idDir string, entries []osv.Entry, indent bool) error { 124 // Write a directory containing entries by ID. 125 if err := os.MkdirAll(idDir, 0755); err != nil { 126 return fmt.Errorf("failed to create directory %q: %v", idDir, err) 127 } 128 for _, e := range entries { 129 outPath := filepath.Join(idDir, e.ID+".json") 130 if err := writeJSON(outPath, e, indent); err != nil { 131 return err 132 } 133 } 134 return nil 135 } 136 137 func writeJSON(filename string, value any, indent bool) (err error) { 138 j, err := jsonMarshal(value, indent) 139 if err != nil { 140 return err 141 } 142 return os.WriteFile(filename, j, 0644) 143 } 144 145 func jsonMarshal(v any, indent bool) ([]byte, error) { 146 if indent { 147 return json.MarshalIndent(v, "", " ") 148 } 149 return json.Marshal(v) 150 } 151 152 // generateOSVEntry create an osv.Entry for a report. In addition to the report, it 153 // takes the ID for the vuln and a URL that will point to the entry in the vuln DB. 154 // It returns the osv.Entry and a list of module paths that the vuln affects. 155 func generateOSVEntry(id, url string, lastModified time.Time, r Report) osv.Entry { 156 entry := osv.Entry{ 157 ID: id, 158 Published: r.Published, 159 Modified: lastModified, 160 Withdrawn: r.Withdrawn, 161 Summary: r.Summary, 162 Details: r.Description, 163 DatabaseSpecific: &osv.DatabaseSpecific{URL: url}, 164 } 165 166 moduleMap := make(map[string]bool) 167 for _, m := range r.Modules { 168 switch m.Module { 169 case stdModule: 170 moduleMap[osv.GoStdModulePath] = true 171 case cmdModule: 172 moduleMap[osv.GoCmdModulePath] = true 173 default: 174 moduleMap[m.Module] = true 175 } 176 entry.Affected = append(entry.Affected, toAffected(m)) 177 } 178 for _, ref := range r.References { 179 entry.References = append(entry.References, osv.Reference{ 180 Type: ref.Type, 181 URL: ref.URL, 182 }) 183 } 184 return entry 185 } 186 187 func AffectedRanges(versions []VersionRange) []osv.Range { 188 a := osv.Range{Type: osv.RangeTypeSemver} 189 if len(versions) == 0 || versions[0].Introduced == "" { 190 a.Events = append(a.Events, osv.RangeEvent{Introduced: "0"}) 191 } 192 for _, v := range versions { 193 if v.Introduced != "" { 194 a.Events = append(a.Events, osv.RangeEvent{Introduced: v.Introduced.Canonical()}) 195 } 196 if v.Fixed != "" { 197 a.Events = append(a.Events, osv.RangeEvent{Fixed: v.Fixed.Canonical()}) 198 } 199 } 200 return []osv.Range{a} 201 } 202 203 func toOSVPackages(pkgs []*Package) (imps []osv.Package) { 204 for _, p := range pkgs { 205 syms := append([]string{}, p.Symbols...) 206 syms = append(syms, p.DerivedSymbols...) 207 sort.Strings(syms) 208 imps = append(imps, osv.Package{ 209 Path: p.Package, 210 GOOS: p.GOOS, 211 GOARCH: p.GOARCH, 212 Symbols: syms, 213 }) 214 } 215 return imps 216 } 217 218 func toAffected(m *Module) osv.Affected { 219 name := m.Module 220 switch name { 221 case stdModule: 222 name = osv.GoStdModulePath 223 case cmdModule: 224 name = osv.GoCmdModulePath 225 } 226 return osv.Affected{ 227 Module: osv.Module{ 228 Path: name, 229 Ecosystem: osv.GoEcosystem, 230 }, 231 Ranges: AffectedRanges(m.Versions), 232 EcosystemSpecific: osv.EcosystemSpecific{ 233 Packages: toOSVPackages(m.Packages), 234 }, 235 } 236 }