diff --git a/detector/detector.go b/detector/detector.go index 60dc03f317..2f3e9d0ad1 100644 --- a/detector/detector.go +++ b/detector/detector.go @@ -36,66 +36,82 @@ func Detect(rs []models.ScanResult, dir string) ([]models.ScanResult, error) { return nil, xerrors.Errorf("Failed to fill with Library dependency: %w", err) } - if err := DetectPkgCves(&r, config.Conf.Vuls2, config.Conf.NoProgress); err != nil { - return nil, xerrors.Errorf("Failed to detect Pkg CVE: %w", err) - } + // One vuls2 db session for this server's package/CPE detection and the + // enrichment that follows: detection warms the read cache and + // enrichment, querying the same CVEs, reuses it instead of opening and + // rebuilding a fresh cache. The session opens lazily on the first path + // that queries the db (as the unshared code did), and is closed at the + // end of this server's turn so each server holds the db only while it + // needs it. + if err := func() error { + sesh := vuls2.NewSession(config.Conf.Vuls2, config.Conf.NoProgress) + defer sesh.Close() + + if err := DetectPkgCves(&r, sesh); err != nil { + return xerrors.Errorf("Failed to detect Pkg CVE: %w", err) + } - // Collect the user-supplied CPE URIs to check. Sources, in order: - // 1. r.Config.Scan.Servers[...].CpeNames — the per-server CPE list - // that was captured at scan time and shipped in the result JSON. - // Using the scan-time snapshot keeps detection coupled to the - // server that was actually scanned, and lets detection run - // without re-loading config.toml. - // 2. OWASP DC XML, if configured. - // - // Synthesised Apple CPEs for macOS scans are detected separately in - // DetectPkgCves (macOS has no package security database). - // Prefer the scan-time snapshot; results produced by an older Vuls - // (or an external producer) may not embed config.scan.servers, so - // fall back to the report-time config.Conf.Servers in that case to - // keep CPE detection working for such inputs. - serverInfo, serverFound := r.Config.Scan.Servers[r.ServerName] - if !serverFound { - serverInfo, serverFound = config.Conf.Servers[r.ServerName] - } - cpeURIs, owaspDCXMLPath := []string{}, "" - cpes := []vuls2.CPE{} - if serverFound { - if len(r.Container.ContainerID) == 0 { - cpeURIs = serverInfo.CpeNames - owaspDCXMLPath = serverInfo.OwaspDCXMLPath - } else { - if con, ok := serverInfo.Containers[r.Container.Name]; ok { - cpeURIs = con.Cpes - owaspDCXMLPath = con.OwaspDCXMLPath + // Collect the user-supplied CPE URIs to check. Sources, in order: + // 1. r.Config.Scan.Servers[...].CpeNames — the per-server CPE list + // that was captured at scan time and shipped in the result JSON. + // Using the scan-time snapshot keeps detection coupled to the + // server that was actually scanned, and lets detection run + // without re-loading config.toml. + // 2. OWASP DC XML, if configured. + // + // Synthesised Apple CPEs for macOS scans are detected separately in + // DetectPkgCves (macOS has no package security database). + // Prefer the scan-time snapshot; results produced by an older Vuls + // (or an external producer) may not embed config.scan.servers, so + // fall back to the report-time config.Conf.Servers in that case to + // keep CPE detection working for such inputs. + serverInfo, serverFound := r.Config.Scan.Servers[r.ServerName] + if !serverFound { + serverInfo, serverFound = config.Conf.Servers[r.ServerName] + } + cpeURIs, owaspDCXMLPath := []string{}, "" + cpes := []vuls2.CPE{} + if serverFound { + if len(r.Container.ContainerID) == 0 { + cpeURIs = serverInfo.CpeNames + owaspDCXMLPath = serverInfo.OwaspDCXMLPath + } else { + if con, ok := serverInfo.Containers[r.Container.Name]; ok { + cpeURIs = con.Cpes + owaspDCXMLPath = con.OwaspDCXMLPath + } } } - } - if owaspDCXMLPath != "" { - owaspCPEs, err := parser.Parse(owaspDCXMLPath) - if err != nil { - return nil, xerrors.Errorf("Failed to read OWASP Dependency Check XML on %s, `%s`, err: %w", - r.ServerInfo(), owaspDCXMLPath, err) + if owaspDCXMLPath != "" { + owaspCPEs, err := parser.Parse(owaspDCXMLPath) + if err != nil { + return xerrors.Errorf("Failed to read OWASP Dependency Check XML on %s, `%s`, err: %w", + r.ServerInfo(), owaspDCXMLPath, err) + } + cpeURIs = append(cpeURIs, owaspCPEs...) + } + for _, uri := range cpeURIs { + cpes = append(cpes, vuls2.CPE{ + URI: uri, + UseJVN: true, + }) } - cpeURIs = append(cpeURIs, owaspCPEs...) - } - for _, uri := range cpeURIs { - cpes = append(cpes, vuls2.CPE{ - URI: uri, - UseJVN: true, - }) - } - if err := DetectCpeURIsCves(&r, cpes, config.Conf.Vuls2, config.Conf.NoProgress); err != nil { - return nil, xerrors.Errorf("Failed to detect CVE of `%s`: %w", cpeURIs, err) - } + if err := DetectCpeURIsCves(&r, cpes, sesh); err != nil { + return xerrors.Errorf("Failed to detect CVE of `%s`: %w", cpeURIs, err) + } - if err := DetectWordPressCves(&r, config.Conf.WpScan); err != nil { - return nil, xerrors.Errorf("Failed to detect WordPress Cves: %w", err) - } + if err := DetectWordPressCves(&r, config.Conf.WpScan); err != nil { + return xerrors.Errorf("Failed to detect WordPress Cves: %w", err) + } - if err := vuls2.EnrichVulnInfos(&r, config.Conf.Vuls2, config.Conf.NoProgress); err != nil { - return nil, xerrors.Errorf("Failed to enrich vulnerability data with vuls2: %w", err) + if err := vuls2.EnrichVulnInfos(&r, sesh); err != nil { + return xerrors.Errorf("Failed to enrich vulnerability data with vuls2: %w", err) + } + + return nil + }(); err != nil { + return nil, xerrors.Errorf("Failed to detect CVEs of %s. err: %w", r.FormatServerName(), err) } r.ReportedBy, _ = os.Hostname() @@ -188,11 +204,16 @@ func Detect(rs []models.ScanResult, dir string) ([]models.ScanResult, error) { // library (family-gated), and applies the FixState / ListenPortStats // post-processing. macOS has no package security database, so its installed // applications and OS are translated to Apple CPEs (vuls2.MacOSCPEs) and -// detected through the CPE path here. It keeps the master-era name and -// calling convention so library consumers that drive detection as -// DetectPkgCves -> DetectCpeURIsCves keep working; user-supplied CPE-URI -// detection lives in DetectCpeURIsCves. -func DetectPkgCves(r *models.ScanResult, vuls2Conf config.Vuls2Conf, noProgress bool) error { +// detected through the CPE path here. Its name and its place in the call +// order are kept from the master era: it runs first and DetectCpeURIsCves +// second, so OS-package / KB detection happens here while user-supplied +// CPE-URI detection is DetectCpeURIsCves' job. +// +// sesh is the vuls2 db session to query (see vuls2.Session), created with +// vuls2.NewSession and owned (Closed) by the caller; it is shared with this +// server's CPE detection and enrichment so all three reuse one warm db +// connection. +func DetectPkgCves(r *models.ScanResult, sesh *vuls2.Session) error { switch r.Family { case constant.MacOSX, constant.MacOSXServer, constant.MacOS, constant.MacOSServer: // macOS has no package security database; the OS itself (when its release @@ -205,14 +226,14 @@ func DetectPkgCves(r *models.ScanResult, vuls2Conf config.Vuls2Conf, noProgress case 0: r.Errors = append(r.Errors, xerrors.Errorf("Failed to detect CVE for %s: no OS release and no detectable applications", r.Family).Error()) default: - if err := vuls2.DetectCPEs(r, cpes, vuls2Conf, noProgress); err != nil { + if err := vuls2.DetectCPEs(r, cpes, sesh); err != nil { return xerrors.Errorf("Failed to detect CVE with Vuls2: %w", err) } } case constant.FreeBSD, constant.ServerTypePseudo: logging.Log.Infof("%s type. Skip vuls2 detection", r.Family) case constant.Windows: - if err := vuls2.DetectPkgs(r, vuls2Conf, noProgress); err != nil { + if err := vuls2.DetectPkgs(r, sesh); err != nil { return xerrors.Errorf("Failed to detect CVE with Vuls2: %w", err) } case constant.RedHat, constant.CentOS, constant.Fedora, constant.Alma, constant.Rocky, constant.Oracle, constant.Amazon, @@ -231,7 +252,7 @@ func DetectPkgCves(r *models.ScanResult, vuls2Conf config.Vuls2Conf, noProgress case len(r.Packages)+len(r.SrcPackages) == 0: r.Errors = append(r.Errors, xerrors.Errorf("Failed to detect CVE for %s: no binary or source packages", r.Family).Error()) default: - if err := vuls2.DetectPkgs(r, vuls2Conf, noProgress); err != nil { + if err := vuls2.DetectPkgs(r, sesh); err != nil { return xerrors.Errorf("Failed to detect CVE with Vuls2: %w", err) } } @@ -295,13 +316,16 @@ func DetectWordPressCves(r *models.ScanResult, wpCnf config.WpScanConf) error { } // DetectCpeURIsCves detects CVEs of given CPE-URIs — the complete CPE -// detection pipeline, keeping the master-era name and calling convention so -// library consumers that drive detection as DetectPkgCves -> -// DetectCpeURIsCves keep working. +// detection pipeline. Its name and its place in the call order are kept from +// the master era: it runs after DetectPkgCves (which handles OS-package / KB +// detection). // // All CPE detection sources (NVD with cpematch-expanded criteria, VulnCheck // NVD++, JVN, Fortinet, Cisco and PaloAlto) are detected by vuls2. -func DetectCpeURIsCves(r *models.ScanResult, cpes []vuls2.CPE, vuls2Conf config.Vuls2Conf, noProgress bool) error { +// +// sesh is the vuls2 db session to query (see vuls2.Session), created with +// vuls2.NewSession and owned (Closed) by the caller. +func DetectCpeURIsCves(r *models.ScanResult, cpes []vuls2.CPE, sesh *vuls2.Session) error { // A caller-provided result may carry a nil ScannedCves map (e.g. a // zero-value ScanResult from a library consumer); initialize before the // detection paths write into it. @@ -309,7 +333,7 @@ func DetectCpeURIsCves(r *models.ScanResult, cpes []vuls2.CPE, vuls2Conf config. r.ScannedCves = models.VulnInfos{} } - if err := vuls2.DetectCPEs(r, cpes, vuls2Conf, noProgress); err != nil { + if err := vuls2.DetectCPEs(r, cpes, sesh); err != nil { return xerrors.Errorf("Failed to detect CVEs with vuls2. err: %w", err) } diff --git a/detector/vuls2/vuls2.go b/detector/vuls2/vuls2.go index 447ab58045..52e6b9604b 100644 --- a/detector/vuls2/vuls2.go +++ b/detector/vuls2/vuls2.go @@ -60,8 +60,11 @@ const defaultRegistory = "ghcr.io/vulsio/vuls-nightly-db" // in DetectCPEs so the two paths (detector.DetectPkgCves for packages, // detector.DetectCpeURIsCves for CPEs) can run separately without // double-detecting packages. -func DetectPkgs(r *models.ScanResult, vuls2Conf config.Vuls2Conf, noProgress bool) error { - return detectWith(r, preConvertPkgs(r), nil, nil, vuls2Conf, noProgress) +// +// sesh is the db session to query, created with NewSession and owned (Closed) +// by the caller. +func DetectPkgs(r *models.ScanResult, sesh *Session) error { + return detectWith(r, preConvertPkgs(r), nil, nil, sesh) } // CPE is one scanned CPE to detect (a CPE 2.2 URI or 2.3 FS string), paired @@ -79,7 +82,10 @@ type CPE struct { // the vuls2 database. OS-package / Microsoft-KB detection is suppressed here: // it already ran via DetectPkgs (detector.DetectPkgCves), and running it // twice would duplicate AffectedPackages on merge. -func DetectCPEs(r *models.ScanResult, cpes []CPE, vuls2Conf config.Vuls2Conf, noProgress bool) error { +// +// sesh is the db session to query, created with NewSession and owned (Closed) +// by the caller. +func DetectCPEs(r *models.ScanResult, cpes []CPE, sesh *Session) error { if len(cpes) == 0 { return nil } @@ -87,14 +93,79 @@ func DetectCPEs(r *models.ScanResult, cpes []CPE, vuls2Conf config.Vuls2Conf, no if err != nil { return xerrors.Errorf("Failed to convert CPEs. err: %w", err) } - return detectWith(r, vuls2Scanned, fsToOriginalCPE, noJVNCPEs, vuls2Conf, noProgress) + return detectWith(r, vuls2Scanned, fsToOriginalCPE, noJVNCPEs, sesh) +} + +// Session is a lazily-opened, shared vuls2 db handle scoped to one server's +// detection and enrichment. The db is opened at most once — on the first path +// that actually queries it — and reused by every later path, so the read cache +// that OS-package / CPE detection warms is still warm for the enrichment that +// immediately follows instead of being discarded and rebuilt. It is opened +// only when some path genuinely needs it, exactly as the unshared code did: +// a family that skips vuls2 detection (FreeBSD, pseudo, trivy-scanned, ...) +// does not open it during detection, but enrichment still opens it when the +// result carries CVEs (e.g. FreeBSD's pkg-audit findings); a server with +// nothing to detect and no CVEs to enrich never opens it at all. Create one +// with NewSession, thread it through DetectPkgs / DetectCPEs / +// EnrichVulnInfos, and Close it when the server is done. It is not safe for +// concurrent use; one server's detection runs sequentially, which is the +// intended scope. +type Session struct { + vuls2Conf config.Vuls2Conf + noProgress bool + + sesh *session.Session +} + +// NewSession returns a not-yet-opened shared Session; the db is opened lazily +// on the first path that queries it (see Session). +func NewSession(vuls2Conf config.Vuls2Conf, noProgress bool) *Session { + return &Session{vuls2Conf: vuls2Conf, noProgress: noProgress} +} + +// open opens the db on the first call and reuses the connection on every later +// one, so all paths sharing this Session query the same db. A nil Session is +// rejected rather than dereferenced, so a caller that forgot to create one gets +// an error instead of a panic. A failed open is not cached: the first caller +// propagates the error and the detection run aborts before any other path would +// reach open again. +func (s *Session) open() (*session.Session, error) { + if s == nil { + return nil, xerrors.Errorf("db session is nil; create one with NewSession") + } + if s.sesh == nil { + sesh, err := openSession(s.vuls2Conf, s.noProgress) + if err != nil { + return nil, xerrors.Errorf("Failed to open db session. err: %w", err) + } + s.sesh = sesh + } + return s.sesh, nil } -func detectWith(r *models.ScanResult, vuls2Scanned scanTypes.ScanResult, fsToOriginalCPE map[string][]string, noJVNCPEs map[string]struct{}, vuls2Conf config.Vuls2Conf, noProgress bool) error { +// Close releases the db storage and cache if this Session was opened. It is a +// no-op on a nil or never-opened Session, and clears the handle afterwards so a +// second Close (or a stray open) neither double-closes nor reuses a closed +// session — always safe to defer. +func (s *Session) Close() { + if s == nil || s.sesh == nil { + return + } + _ = s.sesh.Storage().Close() + s.sesh.Cache().Close() + s.sesh = nil +} + +// openSession opens a cache-backed vuls2 db session: it fills in the default +// repository/path, downloads or refreshes the db if due and validates the +// schema version (both inside newDBConfig), opens storage, and records the db +// digest on the global config. The caller owns the returned session and must +// close its Storage() and Cache(). +func openSession(vuls2Conf config.Vuls2Conf, noProgress bool) (*session.Session, error) { if vuls2Conf.Repository == "" { sv, err := session.SchemaVersion("boltdb") if err != nil { - return xerrors.Errorf("Failed to get schema version. err: %w", err) + return nil, xerrors.Errorf("Failed to get schema version. err: %w", err) } vuls2Conf.Repository = fmt.Sprintf("%s:%d", defaultRegistory, sv) @@ -105,27 +176,41 @@ func detectWith(r *models.ScanResult, vuls2Scanned scanTypes.ScanResult, fsToOri dbConfig, err := newDBConfig(vuls2Conf, noProgress) if err != nil { - return xerrors.Errorf("Failed to get new db connection. err: %w", err) + return nil, xerrors.Errorf("Failed to get new db connection. err: %w", err) } sesh, err := dbConfig.New() if err != nil { - return xerrors.Errorf("Failed to new db session. err: %w", err) + return nil, xerrors.Errorf("Failed to new db session. err: %w", err) } - defer sesh.Cache().Close() - if err := sesh.Storage().Open(); err != nil { - return xerrors.Errorf("Failed to open db. err: %w", err) + sesh.Cache().Close() + return nil, xerrors.Errorf("Failed to open db. err: %w", err) } - defer sesh.Storage().Close() metadata, err := sesh.Storage().GetMetadata() if err != nil { - return xerrors.Errorf("Failed to get metadata. err: %w", err) + _ = sesh.Storage().Close() + sesh.Cache().Close() + return nil, xerrors.Errorf("Failed to get metadata. err: %w", err) + } + if metadata == nil { + _ = sesh.Storage().Close() + sesh.Cache().Close() + return nil, xerrors.Errorf("unexpected vuls2 db metadata. metadata: nil, path: %s", vuls2Conf.Path) } config.Conf.Vuls2.Digest = metadata.Digest + return sesh, nil +} + +func detectWith(r *models.ScanResult, vuls2Scanned scanTypes.ScanResult, fsToOriginalCPE map[string][]string, noJVNCPEs map[string]struct{}, shared *Session) error { + sesh, err := shared.open() + if err != nil { + return xerrors.Errorf("Failed to open db session. err: %w", err) + } + vuls2Detected, err := detect(sesh, vuls2Scanned) if err != nil { return xerrors.Errorf("Failed to detect. err: %w", err) @@ -256,39 +341,18 @@ func mergeIntoScannedCves(r *models.ScanResult, vulnInfos models.VulnInfos) { // EnrichVulnInfos enriches all ScannedCves in the ScanResult with additional vulnerability data // (e.g., Red Hat API) from the vuls2 database. // This should be called after all detection paths have completed. -func EnrichVulnInfos(r *models.ScanResult, vuls2Conf config.Vuls2Conf, noProgress bool) error { +// +// shared is the db session to query, created with NewSession and owned (Closed) +// by the caller. +func EnrichVulnInfos(r *models.ScanResult, shared *Session) error { if len(r.ScannedCves) == 0 { return nil } - if vuls2Conf.Repository == "" { - sv, err := session.SchemaVersion("boltdb") - if err != nil { - return xerrors.Errorf("Failed to get schema version. err: %w", err) - } - - vuls2Conf.Repository = fmt.Sprintf("%s:%d", defaultRegistory, sv) - } - if vuls2Conf.Path == "" { - vuls2Conf.Path = DefaultPath - } - - dbConfig, err := newDBConfig(vuls2Conf, noProgress) - if err != nil { - return xerrors.Errorf("Failed to get new db connection. err: %w", err) - } - - sesh, err := dbConfig.New() + sesh, err := shared.open() if err != nil { - return xerrors.Errorf("Failed to new db session. err: %w", err) - } - - defer sesh.Cache().Close() - - if err := sesh.Storage().Open(); err != nil { - return xerrors.Errorf("Failed to open db. err: %w", err) + return xerrors.Errorf("Failed to open db session. err: %w", err) } - defer sesh.Storage().Close() if err := enrich(sesh, r.ScannedCves); err != nil { return xerrors.Errorf("Failed to enrich vulnerability data. err: %w", err) diff --git a/detector/vuls2/vuls2_test.go b/detector/vuls2/vuls2_test.go index 0d154b97e2..e092e33fc1 100644 --- a/detector/vuls2/vuls2_test.go +++ b/detector/vuls2/vuls2_test.go @@ -48,6 +48,7 @@ import ( detectTypes "github.com/MaineK00n/vuls2/pkg/detect/types" scanTypes "github.com/MaineK00n/vuls2/pkg/scan/types" + "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/detector/vuls2" testutil "github.com/future-architect/vuls/detector/vuls2/internal/test" "github.com/future-architect/vuls/models" @@ -14136,3 +14137,114 @@ func Test_collectVerifiedProducts(t *testing.T) { }) } } + +func TestSession_Close(t *testing.T) { + // Close must be safe to defer even when the db was never opened: a nil + // *Session (defensive) and a Session whose server never queried the db + // (opened lazily, so still unopened) must both no-op instead of panicking. + tests := []struct { + name string + sesh *vuls2.Session + }{ + { + name: "nil session", + sesh: nil, + }, + { + name: "never-opened session", + sesh: vuls2.NewSession(config.Vuls2Conf{}, true), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(_ *testing.T) { + tt.sesh.Close() // must not panic + }) + } +} + +func TestDetectPkgs(t *testing.T) { + tests := []struct { + name string + r *models.ScanResult + sesh *vuls2.Session + wantErr bool + }{ + { + // A required session that is nil is rejected with an error rather + // than dereferenced (which would panic). + name: "nil session", + r: &models.ScanResult{}, + sesh: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := vuls2.DetectPkgs(tt.r, tt.sesh); (err != nil) != tt.wantErr { + t.Errorf("DetectPkgs() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestEnrichVulnInfos(t *testing.T) { + tests := []struct { + name string + r *models.ScanResult + sesh *vuls2.Session + wantErr bool + }{ + { + // EnrichVulnInfos reaches the db only when there are CVEs to enrich; + // give it one so it does not return early, then confirm a nil session + // errors instead of being dereferenced. + name: "nil session with CVEs to enrich", + r: &models.ScanResult{ScannedCves: models.VulnInfos{"CVE-0000-0000": {}}}, + sesh: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := vuls2.EnrichVulnInfos(tt.r, tt.sesh); (err != nil) != tt.wantErr { + t.Errorf("EnrichVulnInfos() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestDetectCPEs(t *testing.T) { + tests := []struct { + name string + r *models.ScanResult + cpes []vuls2.CPE + sesh *vuls2.Session + wantErr bool + }{ + { + // With no CPEs there is nothing to detect, so DetectCPEs returns + // before it would touch the session — a nil session is fine here. + name: "no CPEs returns before using the session", + r: &models.ScanResult{}, + cpes: nil, + sesh: nil, + wantErr: false, + }, + { + // With CPEs to detect, a required session that is nil is rejected + // with an error rather than dereferenced (which would panic). + name: "nil session with CPEs to detect", + r: &models.ScanResult{}, + cpes: []vuls2.CPE{{URI: "cpe:/a:vendor:product:1.0", UseJVN: true}}, + sesh: nil, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := vuls2.DetectCPEs(tt.r, tt.cpes, tt.sesh); (err != nil) != tt.wantErr { + t.Errorf("DetectCPEs() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/server/server.go b/server/server.go index a1742ec12a..5213770a58 100644 --- a/server/server.go +++ b/server/server.go @@ -62,13 +62,19 @@ func (h VulsHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - if err := detector.DetectPkgCves(&r, config.Conf.Vuls2, config.Conf.NoProgress); err != nil { + // Share one lazily-opened vuls2 db session across this request's package + // detection and the enrichment that follows, so the read cache detection + // warms is reused by enrichment rather than opened and rebuilt twice. + sesh := vuls2.NewSession(config.Conf.Vuls2, config.Conf.NoProgress) + defer sesh.Close() + + if err := detector.DetectPkgCves(&r, sesh); err != nil { logging.Log.Errorf("Failed to detect Pkg CVE: %+v", err) http.Error(w, err.Error(), http.StatusServiceUnavailable) return } - if err := vuls2.EnrichVulnInfos(&r, config.Conf.Vuls2, config.Conf.NoProgress); err != nil { + if err := vuls2.EnrichVulnInfos(&r, sesh); err != nil { logging.Log.Errorf("Failed to enrich vulnerability data with vuls2: %+v", err) http.Error(w, err.Error(), http.StatusServiceUnavailable) return