all: add support for excluded reports

Add support for recording the reason no report exists for a CVE or GHSA.
Excluded reports are placed in the excluded/ directory, and follow the
same format as normal reports except:

  - Excluded reports have a "excluded" field indicating why the
    report has been excluded.
  - Excluded reports must have at least one associated CVE or GHSA.
  - Excluded reports need have no other fields set.

Change-Id: I4b346567bd2b0ac08c78a9bc5ae26f721a8c3147
Reviewed-on: https://go-review.googlesource.com/c/vulndb/+/422638
Reviewed-by: Julie Qiu <julieqiu@google.com>
diff --git a/all_test.go b/all_test.go
index eefe2ef..d121abe 100644
--- a/all_test.go
+++ b/all_test.go
@@ -8,11 +8,13 @@
 package main
 
 import (
+	"errors"
 	"io/ioutil"
 	"os"
 	"os/exec"
 	"path/filepath"
 	"runtime"
+	"sort"
 	"strings"
 	"testing"
 
@@ -33,7 +35,10 @@
 	}
 }
 
-const reportsDir = "reports"
+const (
+	reportsDir  = "reports"
+	excludedDir = "excluded"
+)
 
 func TestLintReports(t *testing.T) {
 	if runtime.GOOS == "js" {
@@ -42,20 +47,43 @@
 	if runtime.GOOS == "android" {
 		t.Skipf("android builder does not have access to reports/")
 	}
-	reports, err := ioutil.ReadDir(reportsDir)
-	if err != nil {
-		t.Fatalf("unable to read reports/: %s", err)
-	}
-	for _, rf := range reports {
-		if rf.IsDir() {
-			continue
+	allFiles := make(map[string]string)
+	var reports []string
+	for _, dir := range []string{reportsDir, excludedDir} {
+		files, err := ioutil.ReadDir(dir)
+		if err != nil && !errors.Is(err, os.ErrNotExist) {
+			t.Fatalf("unable to read %v/: %s", dir, err)
 		}
-		t.Run(rf.Name(), func(t *testing.T) {
-			fn := filepath.Join(reportsDir, rf.Name())
-			lints, err := report.LintFile(fn)
+		for _, fi := range files {
+			if fi.IsDir() {
+				continue
+			}
+			fn := filepath.Join(dir, fi.Name())
+			if allFiles[fi.Name()] != "" {
+				t.Errorf("report appears in multiple locations: %v, %v", allFiles[fi.Name()], fn)
+			}
+			allFiles[fi.Name()] = fn
+			reports = append(reports, fn)
+		}
+	}
+	sort.Strings(reports)
+	for _, fn := range reports {
+		t.Run(fn, func(t *testing.T) {
+			r, err := report.Read(fn)
 			if err != nil {
 				t.Fatal(err)
 			}
+			switch filepath.Base(filepath.Dir(fn)) {
+			case reportsDir:
+				if r.Excluded != "" {
+					t.Errorf("report in %q must not have excluded set", reportsDir)
+				}
+			case excludedDir:
+				if r.Excluded == "" {
+					t.Errorf("report in %q must have excluded set", excludedDir)
+				}
+			}
+			lints := r.Lint(fn)
 			if len(lints) > 0 {
 				t.Errorf(strings.Join(lints, "\n"))
 			}