Skip to content

Commit cc26493

Browse files
authored
feat(nvd): Add functions to help monitor performance - ConductAnalysis and CpuProfile (#4817)
Added CPU profiling to nvd conversion so we can easily check what's taking the most time, and ConductAnalysis, which runs through all of the outputted metrics files after conversion runs and produces a CSV file on the outcomes.
1 parent 65be82e commit cc26493

2 files changed

Lines changed: 89 additions & 0 deletions

File tree

vulnfeeds/cmd/converters/cve/nvd-cve-osv/main.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,13 @@ import (
99
"log/slog"
1010
"net/http"
1111
"os"
12+
"path/filepath"
13+
"regexp"
14+
"runtime/pprof"
1215
"slices"
1316
"sync"
1417

18+
"github.com/google/osv/vulnfeeds/conversion"
1519
"github.com/google/osv/vulnfeeds/conversion/nvd"
1620
"github.com/google/osv/vulnfeeds/cves"
1721
"github.com/google/osv/vulnfeeds/git"
@@ -25,6 +29,8 @@ var (
2529
outDir = flag.String("out-dir", "", "Path to output results.")
2630
outFormat = flag.String("out-format", "OSV", "Format to output {OSV,PackageInfo}")
2731
workers = flag.Int("workers", 30, "The number of concurrent workers to use for processing CVEs.")
32+
outputMetrics = flag.Bool("output-metrics", true, "If true, output the metrics information about the conversion")
33+
cpuProfile = flag.String("cpuprofile", "", "Path to write cpu profile to file (default = no output)")
2834
)
2935

3036
func loadCPEDictionary(productToRepo *cves.VPRepoCache, f string) error {
@@ -49,6 +55,18 @@ func main() {
4955
os.Exit(1)
5056
}
5157

58+
if *cpuProfile != "" {
59+
f, err := os.Create(*cpuProfile)
60+
if err != nil {
61+
logger.Fatal("could not create CPU profile: ", slog.Any("err", err))
62+
}
63+
defer f.Close()
64+
if err := pprof.StartCPUProfile(f); err != nil {
65+
logger.Fatal("could not start CPU profile: ", slog.Any("err", err))
66+
}
67+
defer pprof.StopCPUProfile()
68+
}
69+
5270
logger.InitGlobalLogger()
5371

5472
data, err := os.ReadFile(*jsonPath)
@@ -88,6 +106,20 @@ func main() {
88106
close(jobs)
89107
wg.Wait()
90108
logger.Info("NVD Conversion run complete")
109+
110+
// Conduct analysis on the outcome of the converted files and output to a csv
111+
if *outputMetrics {
112+
// Try to extract year from path, otherwise use "xxxx" filler
113+
filename := filepath.Base(*jsonPath)
114+
re := regexp.MustCompile(`nvdcve-2\.0-([0-9]{4})\.json`)
115+
matches := re.FindStringSubmatch(filename)
116+
if len(matches) >= 2 {
117+
year := matches[1]
118+
conversion.ConductAnalysis(year, *outDir)
119+
} else {
120+
conversion.ConductAnalysis("xxxx", *outDir)
121+
}
122+
}
91123
}
92124

93125
func processCVE(cve models.NVDCVE, vpRepoCache *cves.VPRepoCache, repoTagsCache *git.RepoTagsCache) models.ConversionOutcome {

vulnfeeds/conversion/common.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,16 @@
33
package conversion
44

55
import (
6+
"encoding/csv"
67
"encoding/json"
78
"fmt"
9+
"io/fs"
810
"log/slog"
911
"os"
1012
"path/filepath"
1113
"slices"
1214
"strings"
15+
"time"
1316

1417
"github.com/google/osv/vulnfeeds/models"
1518
"github.com/google/osv/vulnfeeds/utility/logger"
@@ -69,6 +72,60 @@ func DeduplicateRefs(refs []models.Reference) []models.Reference {
6972
return refs
7073
}
7174

75+
// ConductAnalysis conducts an analysis of the conversion results after completion by reading
76+
// all of the .metrics.json files and extracting conversion outcomes.
77+
func ConductAnalysis(year string, dir string) {
78+
// get the current time in minutes
79+
currentTime := time.Now().Format("2006-01-02T15:04")
80+
outcomesCSV := "nvd-conversion-outcomes-" + year + "-" + currentTime + ".csv"
81+
csvFile, err := os.Create(filepath.Join(dir, outcomesCSV))
82+
if err != nil {
83+
logger.Fatal("Failed to create analysis CSV file", slog.Any("err", err))
84+
}
85+
defer csvFile.Close()
86+
87+
csvWriter := csv.NewWriter(csvFile)
88+
defer csvWriter.Flush()
89+
90+
header := []string{"CVEID", "Outcome"}
91+
if err := csvWriter.Write(header); err != nil {
92+
logger.Fatal("Failed to write header to CSV", slog.Any("err", err))
93+
}
94+
95+
err = filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
96+
if err != nil {
97+
return err
98+
}
99+
if !d.IsDir() && strings.HasSuffix(d.Name(), ".metrics.json") {
100+
data, err := os.ReadFile(path)
101+
if err != nil {
102+
logger.Warn("Failed to read metrics file", slog.String("path", path), slog.Any("err", err))
103+
return nil // Continue
104+
}
105+
106+
var metrics models.ConversionMetrics
107+
if err := json.Unmarshal(data, &metrics); err != nil {
108+
logger.Warn("Failed to unmarshal metrics JSON", slog.String("path", path), slog.Any("err", err))
109+
return nil // Continue
110+
}
111+
112+
record := []string{
113+
string(metrics.CVEID),
114+
metrics.Outcome.String(),
115+
}
116+
if err := csvWriter.Write(record); err != nil {
117+
logger.Warn("Failed to write record to CSV", slog.String("cve", string(metrics.CVEID)), slog.Any("err", err))
118+
}
119+
}
120+
121+
return nil
122+
})
123+
124+
if err != nil {
125+
logger.Error("Failed to walk directory for analysis", slog.Any("err", err))
126+
}
127+
}
128+
72129
// CreateMetricsFile creates the initial file for the metrics record.
73130
func CreateMetricsFile(id models.CVEID, vulnDir string) (*os.File, error) {
74131
metricsFile := filepath.Join(vulnDir, string(id)+".metrics"+models.Extension)

0 commit comments

Comments
 (0)