blob: 53e65978727525f44ba3f7b77f6cd332ea4cc80a [file] [log] [blame]
// 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
}