| // Copyright 2022 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Package legacydb provides functionality for generating, reading, writing, |
| // and validating vulnerability databases according to the legacy schema. |
| package legacydb |
| |
| import ( |
| "context" |
| "encoding/json" |
| "fmt" |
| "path" |
| "time" |
| |
| "github.com/go-git/go-git/v5" |
| "github.com/go-git/go-git/v5/plumbing/object" |
| "golang.org/x/vulndb/internal/derrors" |
| "golang.org/x/vulndb/internal/gitrepo" |
| "golang.org/x/vulndb/internal/osv" |
| "golang.org/x/vulndb/internal/report" |
| ) |
| |
| // Database is an in-memory representation of a Go vulnerability database, |
| // following the legacy specification at |
| // https://go.dev/security/vuln/database#api. |
| type Database struct { |
| // A map from module names to the last modified time. |
| // Represents $dbPath/index.json |
| Index DBIndex |
| // Map from each Go ID to its OSV entry. |
| // Represents $dbPath/ID/index.json and the contents of $dbPath/ID/ |
| EntriesByID EntriesByID |
| // Map from each module path to a list of corresponding OSV entries. |
| // Each map entry represents the contents of a $dbPath/$modulePath.json |
| // file. |
| EntriesByModule EntriesByModule |
| // Map from each alias (CVE and GHSA) ID to a list of Go IDs for that |
| // alias. |
| // Represents $dbPath/aliases.json |
| IDsByAlias IDsByAlias |
| } |
| |
| type ( |
| DBIndex map[string]time.Time |
| EntriesByID map[string]*osv.Entry |
| EntriesByModule map[string][]*osv.Entry |
| IDsByAlias map[string][]string |
| ) |
| |
| const ( |
| // indexFile is the name of the file that contains the database |
| // index. |
| indexFile = "index.json" |
| |
| // aliasesFile is the name of the file that contains the database |
| // aliases index. |
| aliasesFile = "aliases.json" |
| |
| // idDirectory is the name of the directory that contains entries |
| // listed by their IDs. |
| idDirectory = "ID" |
| ) |
| |
| // New creates a new Database based on the contents of the "data/osv" |
| // folder in the given repo. |
| // |
| // It reads each OSV file, marshals it into a struct, updates the |
| // modified and published times based on the time of latest and first |
| // CL to modify the file, and stores the struct in the Database (and updates |
| // associated index maps). The result is an in-memory vulnerability database |
| // that can be written to files via Database.Write. |
| // |
| // The repo must contain a "data/osv" folder with files in |
| // OSV JSON format with filenames of the form GO-YYYY-XXXX.json. |
| // |
| // New does not modify the repo. |
| func New(ctx context.Context, repo *git.Repository) (_ *Database, err error) { |
| defer derrors.Wrap(&err, "New()") |
| |
| d := newEmpty() |
| |
| root, err := gitrepo.Root(repo) |
| if err != nil { |
| return nil, err |
| } |
| |
| commitDates, err := gitrepo.AllCommitDates(repo, gitrepo.HeadReference, report.OSVDir) |
| if err != nil { |
| return nil, err |
| } |
| |
| if err = root.Files().ForEach(func(f *object.File) error { |
| if path.Dir(f.Name) != report.OSVDir || |
| path.Ext(f.Name) != ".json" { |
| return nil |
| } |
| |
| // Read the entry. |
| contents, err := f.Contents() |
| if err != nil { |
| return fmt.Errorf("could not read contents of file %s: %v", f.Name, err) |
| } |
| var entry osv.Entry |
| err = json.Unmarshal([]byte(contents), &entry) |
| if err != nil { |
| return err |
| } |
| |
| // Set the modified and published times. |
| dates, ok := commitDates[f.Name] |
| if !ok { |
| return fmt.Errorf("can't find git repo commit dates for %q", f.Name) |
| } |
| addTimestamps(&entry, dates) |
| |
| d.addEntry(&entry) |
| |
| return nil |
| }); err != nil { |
| return nil, err |
| } |
| |
| return d, nil |
| } |
| |
| func newEmpty() *Database { |
| return &Database{ |
| Index: make(DBIndex), |
| EntriesByID: make(EntriesByID), |
| EntriesByModule: make(EntriesByModule), |
| IDsByAlias: make(IDsByAlias), |
| } |
| } |
| |
| func (d *Database) addEntry(entry *osv.Entry) { |
| for _, module := range report.ModulesForEntry(*entry) { |
| d.EntriesByModule[module] = append(d.EntriesByModule[module], entry) |
| if entry.Modified.After(d.Index[module]) { |
| d.Index[module] = entry.Modified.Time |
| } |
| } |
| d.EntriesByID[entry.ID] = entry |
| for _, alias := range entry.Aliases { |
| d.IDsByAlias[alias] = append(d.IDsByAlias[alias], entry.ID) |
| } |
| } |
| |
| func addTimestamps(entry *osv.Entry, dates gitrepo.Dates) { |
| // If a report contains a published field, consider it |
| // the authoritative source of truth. |
| // Otherwise, use the time of the earliest commit in the git history. |
| if entry.Published.IsZero() { |
| entry.Published = osv.Time{Time: dates.Oldest} |
| } |
| |
| // The modified time is the time of the latest commit for the file. |
| entry.Modified = osv.Time{Time: dates.Newest} |
| } |