-
Notifications
You must be signed in to change notification settings - Fork 62
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
internal/database: add Validate function
Adds a function, Validate, which checks a Go vulnerability for internal consistency. Also adds a command line tool, "checkdb" which can be used to validate databases. This tool will be used in the deploy script for vulndb. For golang/go#56417 Change-Id: I427eab6b5385d3c858d4a371d90e6e5f54f10812 Reviewed-on: https://go-review.googlesource.com/c/vulndb/+/448842 Run-TryBot: Tatiana Bradley <[email protected]> Reviewed-by: Tatiana Bradley <[email protected]> Reviewed-by: Damien Neil <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
- Loading branch information
Showing
5 changed files
with
239 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
// 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. | ||
|
||
// Command checkdb validates Go vulnerability databases. | ||
package main | ||
|
||
import ( | ||
"flag" | ||
"log" | ||
|
||
"golang.org/x/vulndb/internal/database" | ||
) | ||
|
||
var ( | ||
path = flag.String("path", "", "path to database") | ||
) | ||
|
||
func main() { | ||
flag.Parse() | ||
if *path == "" { | ||
log.Fatalf("flag -path must be set") | ||
} | ||
if err := database.Validate(*path); err != nil { | ||
log.Fatal(err) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
// 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 database | ||
|
||
import ( | ||
"fmt" | ||
"io/fs" | ||
"path/filepath" | ||
"reflect" | ||
"strings" | ||
"time" | ||
|
||
"golang.org/x/exp/slices" | ||
"golang.org/x/vuln/client" | ||
"golang.org/x/vulndb/internal/derrors" | ||
"golang.org/x/vulndb/internal/report" | ||
) | ||
|
||
func Validate(dbPath string) (err error) { | ||
derrors.Wrap(&err, "Validate(%s)", dbPath) | ||
|
||
// Load will fail if any files are missing. | ||
d, err := Load(dbPath) | ||
if err != nil { | ||
return err | ||
} | ||
if err = d.validate(dbPath); err != nil { | ||
return err | ||
} | ||
return nil | ||
} | ||
|
||
func (d *Database) validate(dbPath string) error { | ||
if err := d.checkNoUnexpectedFiles(dbPath); err != nil { | ||
return err | ||
} | ||
if err := d.checkInternalConsistency(); err != nil { | ||
return err | ||
} | ||
return nil | ||
} | ||
|
||
func (d *Database) checkNoUnexpectedFiles(dbPath string) error { | ||
return filepath.WalkDir(dbPath, func(path string, f fs.DirEntry, err error) error { | ||
if err != nil { | ||
return err | ||
} | ||
|
||
fname := f.Name() | ||
ext := filepath.Ext(fname) | ||
dir := filepath.Dir(path) | ||
|
||
switch { | ||
// Skip directories. | ||
case f.IsDir(): | ||
return nil | ||
// In the top-level directory, web files and index files are OK. | ||
case dir == dbPath && isIndexOrWebFile(fname, ext): | ||
return nil | ||
// All non-directory and non-web files should end in ".json". | ||
case ext != ".json": | ||
return fmt.Errorf("found unexpected non-JSON file %s", path) | ||
// All files in the ID directory (except the index) should have | ||
// corresponding entries in EntriesByID. | ||
case dir == filepath.Join(dbPath, idDirectory): | ||
if fname == indexFile { | ||
return nil | ||
} | ||
id := report.GetGoIDFromFilename(fname) | ||
if _, ok := d.EntriesByID[id]; !ok { | ||
return fmt.Errorf("found unexpected ID %q which is not present in %s", id, filepath.Join(idDirectory, indexFile)) | ||
} | ||
// All other files should have corresponding entries in | ||
// EntriesByModule. | ||
default: | ||
module := strings.TrimSuffix(strings.TrimPrefix(strings.TrimPrefix(path, dbPath), "/"), ".json") | ||
unescaped, err := client.UnescapeModulePath(module) | ||
if err != nil { | ||
return fmt.Errorf("could not unescape module file %s: %v", path, err) | ||
} | ||
if _, ok := d.EntriesByModule[unescaped]; !ok { | ||
return fmt.Errorf("found unexpected module %q which is not present in %s", unescaped, indexFile) | ||
} | ||
} | ||
return nil | ||
}) | ||
} | ||
|
||
func isIndexOrWebFile(filename, ext string) bool { | ||
return ext == ".ico" || | ||
ext == ".html" || | ||
// HTML files may have no extension. | ||
ext == "" || | ||
filename == indexFile || | ||
filename == aliasesFile | ||
} | ||
|
||
func (d *Database) checkInternalConsistency() error { | ||
if il, ml := len(d.Index), len(d.EntriesByModule); il != ml { | ||
return fmt.Errorf("length mismatch: there are %d module entries in the index, and %d module directory entries", il, ml) | ||
} | ||
|
||
for module, modified := range d.Index { | ||
entries, ok := d.EntriesByModule[module] | ||
if !ok || len(entries) == 0 { | ||
return fmt.Errorf("no module directory found for indexed module %s", module) | ||
} | ||
|
||
var wantModified time.Time | ||
for _, entry := range entries { | ||
if mod := entry.Modified; mod.After(wantModified) { | ||
wantModified = mod | ||
} | ||
|
||
entryByID, ok := d.EntriesByID[entry.ID] | ||
if !ok { | ||
return fmt.Errorf("no advisory found for ID %s listed in %s", entry.ID, module) | ||
} | ||
if !reflect.DeepEqual(entry, entryByID) { | ||
return fmt.Errorf("inconsistent OSV contents in module and ID advisory for %s", entry.ID) | ||
} | ||
} | ||
if modified != wantModified { | ||
return fmt.Errorf("incorrect modified timestamp for module %s: want %s, got %s", module, wantModified, modified) | ||
} | ||
} | ||
|
||
for id, entry := range d.EntriesByID { | ||
for _, affected := range entry.Affected { | ||
module := affected.Package.Name | ||
entries, ok := d.EntriesByModule[module] | ||
if !ok || len(entries) == 0 { | ||
return fmt.Errorf("module %s not found (referenced by %s)", module, id) | ||
} | ||
found := false | ||
for _, gotEntry := range entries { | ||
if gotEntry.ID == id { | ||
found = true | ||
break | ||
} | ||
} | ||
if !found { | ||
return fmt.Errorf("%s does not have an entry in %s", id, module) | ||
} | ||
} | ||
for _, alias := range entry.Aliases { | ||
gotEntries, ok := d.IDsByAlias[alias] | ||
if !ok || len(gotEntries) == 0 { | ||
return fmt.Errorf("alias %s not found in aliases.json (alias of %s)", alias, id) | ||
} | ||
found := false | ||
for _, gotID := range gotEntries { | ||
if gotID == id { | ||
found = true | ||
break | ||
} | ||
} | ||
if !found { | ||
return fmt.Errorf("%s is not listed as an alias of %s", entry.ID, alias) | ||
} | ||
} | ||
} | ||
|
||
for alias, goIDs := range d.IDsByAlias { | ||
for _, goID := range goIDs { | ||
entry, ok := d.EntriesByID[goID] | ||
if !ok { | ||
return fmt.Errorf("no advisory found for ID %s listed under %s", goID, alias) | ||
} | ||
|
||
if !slices.Contains(entry.Aliases, alias) { | ||
return fmt.Errorf("advisory %s does not reference alias %s", goID, alias) | ||
} | ||
} | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,16 @@ | ||
// 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 database | ||
|
||
import "testing" | ||
|
||
// TODO(https://github.com/golang/go#56417): Write unit tests for various | ||
// invalid databases. | ||
|
||
func TestValidate(t *testing.T) { | ||
if err := Validate(validDir); err != nil { | ||
t.Error(err) | ||
} | ||
} |