Skip to content

Commit b4ac0e6

Browse files
MaineK00nclaude
andcommitted
perf(detector): share one vuls2 db session across a server's detect and enrich
detect (DetectPkgs/DetectCPEs) and enrich (EnrichVulnInfos) each opened, used, and discarded their own vuls2 boltDB session — so within one report run a single server opened the db multiple times, each redoing the metadata read / schema-version check and, more importantly, throwing away the WithCache read cache on close. detect and enrich query largely the same CVEs, so enrich was rebuilding a cache detect had just warmed. Introduce a lazily-opened, shared vuls2.Session: the db is opened at most once (on the first path that queries it) and reused by every later path within one server's turn, then Closed by the owner. Detection warms the cache and the enrichment that immediately follows reuses it. The session opens exactly when and only when the unshared code opened one — no earlier, no more often: 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), and a server with nothing to detect and no CVEs to enrich never opens it at all. So no unnecessary db open/download is forced. DetectPkgs / DetectCPEs / EnrichVulnInfos / DetectPkgCves / DetectCpeURIsCves now take the db handle as a required *vuls2.Session, created with NewSession (which carries the Vuls2Conf/noProgress the db needs) and Closed by the caller. The Session is the single handle threaded through one server's detection and enrichment; detector.Detect (per-server loop) and the server-mode handler create one per server / request and share it across all three paths. A nil Session is rejected with an error rather than panicking. Scope is per-server: the session — and its unbounded read cache — is freed after each server, keeping peak memory at ~one server's working set rather than growing across the whole run, and leaving detection naturally safe to parallelize over servers later. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent b03f24e commit b4ac0e6

4 files changed

Lines changed: 305 additions & 106 deletions

File tree

detector/detector.go

Lines changed: 86 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -36,66 +36,82 @@ func Detect(rs []models.ScanResult, dir string) ([]models.ScanResult, error) {
3636
return nil, xerrors.Errorf("Failed to fill with Library dependency: %w", err)
3737
}
3838

39-
if err := DetectPkgCves(&r, config.Conf.Vuls2, config.Conf.NoProgress); err != nil {
40-
return nil, xerrors.Errorf("Failed to detect Pkg CVE: %w", err)
41-
}
39+
// One vuls2 db session for this server's package/CPE detection and the
40+
// enrichment that follows: detection warms the read cache and
41+
// enrichment, querying the same CVEs, reuses it instead of opening and
42+
// rebuilding a fresh cache. The session opens lazily on the first path
43+
// that queries the db (as the unshared code did), and is closed at the
44+
// end of this server's turn so each server holds the db only while it
45+
// needs it.
46+
if err := func() error {
47+
sesh := vuls2.NewSession(config.Conf.Vuls2, config.Conf.NoProgress)
48+
defer sesh.Close()
49+
50+
if err := DetectPkgCves(&r, sesh); err != nil {
51+
return xerrors.Errorf("Failed to detect Pkg CVE: %w", err)
52+
}
4253

43-
// Collect the user-supplied CPE URIs to check. Sources, in order:
44-
// 1. r.Config.Scan.Servers[...].CpeNames — the per-server CPE list
45-
// that was captured at scan time and shipped in the result JSON.
46-
// Using the scan-time snapshot keeps detection coupled to the
47-
// server that was actually scanned, and lets detection run
48-
// without re-loading config.toml.
49-
// 2. OWASP DC XML, if configured.
50-
//
51-
// Synthesised Apple CPEs for macOS scans are detected separately in
52-
// DetectPkgCves (macOS has no package security database).
53-
// Prefer the scan-time snapshot; results produced by an older Vuls
54-
// (or an external producer) may not embed config.scan.servers, so
55-
// fall back to the report-time config.Conf.Servers in that case to
56-
// keep CPE detection working for such inputs.
57-
serverInfo, serverFound := r.Config.Scan.Servers[r.ServerName]
58-
if !serverFound {
59-
serverInfo, serverFound = config.Conf.Servers[r.ServerName]
60-
}
61-
cpeURIs, owaspDCXMLPath := []string{}, ""
62-
cpes := []vuls2.CPE{}
63-
if serverFound {
64-
if len(r.Container.ContainerID) == 0 {
65-
cpeURIs = serverInfo.CpeNames
66-
owaspDCXMLPath = serverInfo.OwaspDCXMLPath
67-
} else {
68-
if con, ok := serverInfo.Containers[r.Container.Name]; ok {
69-
cpeURIs = con.Cpes
70-
owaspDCXMLPath = con.OwaspDCXMLPath
54+
// Collect the user-supplied CPE URIs to check. Sources, in order:
55+
// 1. r.Config.Scan.Servers[...].CpeNames — the per-server CPE list
56+
// that was captured at scan time and shipped in the result JSON.
57+
// Using the scan-time snapshot keeps detection coupled to the
58+
// server that was actually scanned, and lets detection run
59+
// without re-loading config.toml.
60+
// 2. OWASP DC XML, if configured.
61+
//
62+
// Synthesised Apple CPEs for macOS scans are detected separately in
63+
// DetectPkgCves (macOS has no package security database).
64+
// Prefer the scan-time snapshot; results produced by an older Vuls
65+
// (or an external producer) may not embed config.scan.servers, so
66+
// fall back to the report-time config.Conf.Servers in that case to
67+
// keep CPE detection working for such inputs.
68+
serverInfo, serverFound := r.Config.Scan.Servers[r.ServerName]
69+
if !serverFound {
70+
serverInfo, serverFound = config.Conf.Servers[r.ServerName]
71+
}
72+
cpeURIs, owaspDCXMLPath := []string{}, ""
73+
cpes := []vuls2.CPE{}
74+
if serverFound {
75+
if len(r.Container.ContainerID) == 0 {
76+
cpeURIs = serverInfo.CpeNames
77+
owaspDCXMLPath = serverInfo.OwaspDCXMLPath
78+
} else {
79+
if con, ok := serverInfo.Containers[r.Container.Name]; ok {
80+
cpeURIs = con.Cpes
81+
owaspDCXMLPath = con.OwaspDCXMLPath
82+
}
7183
}
7284
}
73-
}
74-
if owaspDCXMLPath != "" {
75-
owaspCPEs, err := parser.Parse(owaspDCXMLPath)
76-
if err != nil {
77-
return nil, xerrors.Errorf("Failed to read OWASP Dependency Check XML on %s, `%s`, err: %w",
78-
r.ServerInfo(), owaspDCXMLPath, err)
85+
if owaspDCXMLPath != "" {
86+
owaspCPEs, err := parser.Parse(owaspDCXMLPath)
87+
if err != nil {
88+
return xerrors.Errorf("Failed to read OWASP Dependency Check XML on %s, `%s`, err: %w",
89+
r.ServerInfo(), owaspDCXMLPath, err)
90+
}
91+
cpeURIs = append(cpeURIs, owaspCPEs...)
92+
}
93+
for _, uri := range cpeURIs {
94+
cpes = append(cpes, vuls2.CPE{
95+
URI: uri,
96+
UseJVN: true,
97+
})
7998
}
80-
cpeURIs = append(cpeURIs, owaspCPEs...)
81-
}
82-
for _, uri := range cpeURIs {
83-
cpes = append(cpes, vuls2.CPE{
84-
URI: uri,
85-
UseJVN: true,
86-
})
87-
}
8899

89-
if err := DetectCpeURIsCves(&r, cpes, config.Conf.Vuls2, config.Conf.NoProgress); err != nil {
90-
return nil, xerrors.Errorf("Failed to detect CVE of `%s`: %w", cpeURIs, err)
91-
}
100+
if err := DetectCpeURIsCves(&r, cpes, sesh); err != nil {
101+
return xerrors.Errorf("Failed to detect CVE of `%s`: %w", cpeURIs, err)
102+
}
92103

93-
if err := DetectWordPressCves(&r, config.Conf.WpScan); err != nil {
94-
return nil, xerrors.Errorf("Failed to detect WordPress Cves: %w", err)
95-
}
104+
if err := DetectWordPressCves(&r, config.Conf.WpScan); err != nil {
105+
return xerrors.Errorf("Failed to detect WordPress Cves: %w", err)
106+
}
96107

97-
if err := vuls2.EnrichVulnInfos(&r, config.Conf.Vuls2, config.Conf.NoProgress); err != nil {
98-
return nil, xerrors.Errorf("Failed to enrich vulnerability data with vuls2: %w", err)
108+
if err := vuls2.EnrichVulnInfos(&r, sesh); err != nil {
109+
return xerrors.Errorf("Failed to enrich vulnerability data with vuls2: %w", err)
110+
}
111+
112+
return nil
113+
}(); err != nil {
114+
return nil, xerrors.Errorf("Failed to detect CVEs of %s. err: %w", r.FormatServerName(), err)
99115
}
100116

101117
r.ReportedBy, _ = os.Hostname()
@@ -189,10 +205,14 @@ func Detect(rs []models.ScanResult, dir string) ([]models.ScanResult, error) {
189205
// post-processing. macOS has no package security database, so its installed
190206
// applications and OS are translated to Apple CPEs (vuls2.MacOSCPEs) and
191207
// detected through the CPE path here. It keeps the master-era name and
192-
// calling convention so library consumers that drive detection as
193-
// DetectPkgCves -> DetectCpeURIsCves keep working; user-supplied CPE-URI
208+
// DetectPkgCves -> DetectCpeURIsCves driving order; user-supplied CPE-URI
194209
// detection lives in DetectCpeURIsCves.
195-
func DetectPkgCves(r *models.ScanResult, vuls2Conf config.Vuls2Conf, noProgress bool) error {
210+
//
211+
// sesh is the vuls2 db session to query (see vuls2.Session), created with
212+
// vuls2.NewSession and owned (Closed) by the caller; it is shared with this
213+
// server's CPE detection and enrichment so all three reuse one warm db
214+
// connection.
215+
func DetectPkgCves(r *models.ScanResult, sesh *vuls2.Session) error {
196216
switch r.Family {
197217
case constant.MacOSX, constant.MacOSXServer, constant.MacOS, constant.MacOSServer:
198218
// macOS has no package security database; the OS itself (when its release
@@ -205,14 +225,14 @@ func DetectPkgCves(r *models.ScanResult, vuls2Conf config.Vuls2Conf, noProgress
205225
case 0:
206226
r.Errors = append(r.Errors, xerrors.Errorf("Failed to detect CVE for %s: no OS release and no detectable applications", r.Family).Error())
207227
default:
208-
if err := vuls2.DetectCPEs(r, cpes, vuls2Conf, noProgress); err != nil {
228+
if err := vuls2.DetectCPEs(r, cpes, sesh); err != nil {
209229
return xerrors.Errorf("Failed to detect CVE with Vuls2: %w", err)
210230
}
211231
}
212232
case constant.FreeBSD, constant.ServerTypePseudo:
213233
logging.Log.Infof("%s type. Skip vuls2 detection", r.Family)
214234
case constant.Windows:
215-
if err := vuls2.DetectPkgs(r, vuls2Conf, noProgress); err != nil {
235+
if err := vuls2.DetectPkgs(r, sesh); err != nil {
216236
return xerrors.Errorf("Failed to detect CVE with Vuls2: %w", err)
217237
}
218238
case constant.RedHat, constant.CentOS, constant.Fedora, constant.Alma, constant.Rocky, constant.Oracle, constant.Amazon,
@@ -231,7 +251,7 @@ func DetectPkgCves(r *models.ScanResult, vuls2Conf config.Vuls2Conf, noProgress
231251
case len(r.Packages)+len(r.SrcPackages) == 0:
232252
r.Errors = append(r.Errors, xerrors.Errorf("Failed to detect CVE for %s: no binary or source packages", r.Family).Error())
233253
default:
234-
if err := vuls2.DetectPkgs(r, vuls2Conf, noProgress); err != nil {
254+
if err := vuls2.DetectPkgs(r, sesh); err != nil {
235255
return xerrors.Errorf("Failed to detect CVE with Vuls2: %w", err)
236256
}
237257
}
@@ -295,21 +315,23 @@ func DetectWordPressCves(r *models.ScanResult, wpCnf config.WpScanConf) error {
295315
}
296316

297317
// DetectCpeURIsCves detects CVEs of given CPE-URIs — the complete CPE
298-
// detection pipeline, keeping the master-era name and calling convention so
299-
// library consumers that drive detection as DetectPkgCves ->
300-
// DetectCpeURIsCves keep working.
318+
// detection pipeline, keeping the master-era name and the DetectPkgCves ->
319+
// DetectCpeURIsCves driving order.
301320
//
302321
// All CPE detection sources (NVD with cpematch-expanded criteria, VulnCheck
303322
// NVD++, JVN, Fortinet, Cisco and PaloAlto) are detected by vuls2.
304-
func DetectCpeURIsCves(r *models.ScanResult, cpes []vuls2.CPE, vuls2Conf config.Vuls2Conf, noProgress bool) error {
323+
//
324+
// sesh is the vuls2 db session to query (see vuls2.Session), created with
325+
// vuls2.NewSession and owned (Closed) by the caller.
326+
func DetectCpeURIsCves(r *models.ScanResult, cpes []vuls2.CPE, sesh *vuls2.Session) error {
305327
// A caller-provided result may carry a nil ScannedCves map (e.g. a
306328
// zero-value ScanResult from a library consumer); initialize before the
307329
// detection paths write into it.
308330
if r.ScannedCves == nil {
309331
r.ScannedCves = models.VulnInfos{}
310332
}
311333

312-
if err := vuls2.DetectCPEs(r, cpes, vuls2Conf, noProgress); err != nil {
334+
if err := vuls2.DetectCPEs(r, cpes, sesh); err != nil {
313335
return xerrors.Errorf("Failed to detect CVEs with vuls2. err: %w", err)
314336
}
315337

0 commit comments

Comments
 (0)