Why Cull?

Cull is built to answer a specific question: which known vulnerabilities apply to this product and version? We tested that with 50 exact CPE queries placed on either side of known patch boundaries, then checked the returned CVEs against a reviewed reference set.

Cull had the strongest version-aware result in this benchmark: the most confirmed findings, the fewest known misses and the lowest response latency.

What we tested

The benchmark contains 50 exact CPE queries covering 25 vendor-documented fixes. For each fix we queried one affected release and the first release on the safe side of the boundary. Findings are checked against a frozen reference set; vendor and CNA records take precedence over imported NVD data.

Cull, Search Vulns, cve-search and NVD completed all 50 queries. cve-search ran locally and ignores the submitted version, so it appears here as a version-blind candidate-retrieval baseline. Vulners returned 31 answers before its quota failed.

Extended 2026-08-29 · 25 affected/fixed pairs · exact product CPEs

Quality
Each case/CVE pair is marked as a true positive, a false positive or a known miss.
Reference
The expected results come from reviewed vendor, CNA and first-party records frozen for this run.
Speed
Latency includes the full response, body download and JSON decoding. Quota waiting is excluded.
Scope
LOCALHOST identifies the local cve-search run. 31/50 identifies Vulners' partial run.
Can an ordinary user search by product and version?
ToolDirect queryPractical input workflow
CullYesFree text such as Apache 2.4.49, aliases, CPEs, PURLs and package coordinates are accepted directly.
Search VulnsYesAccepts a product query directly; this quality run still used the same exact CPE as the other version-aware tools.
cve-searchPartialRequires vendor and product fields and does not evaluate the submitted version.
VulnersNoThe measured software-audit endpoint requires an exact software coordinate such as a CPE.
NVDTwo stepsSearch the separate CPE dictionary, choose the correct CPE, then submit it to the CVE API. The benchmark supplied that CPE.
The answer-quality charts start after product identification. This table makes the otherwise hidden CPE-discovery burden explicit; it is deliberately not disguised inside a combined score.

Answer quality

50 exact product/version CPEs · Vulners returned 31

Best version-aware result

True positives higher is better

250012500 2383 2377 2177 1408 2146 CullSearchcve-s.VulnersNVD NO VERSION31/50

False positives lower is better · axis break

700065003001500 24 250 6977 237 17 CullSearchcve-s.VulnersNVD NO VERSION31/50

Known misses lower is better

2801400 39 45 245 52 276 CullSearchcve-s.VulnersNVD NO VERSION31/50
Cull returned 2,383 confirmed case/CVE matches, 24 false positives and 39 known misses. NVD returned 2,146, 17 and 276 respectively. Uncertain Cull review candidates are not promoted to affected findings. The cve-search figures show what a product-only lookup looks like when treated as a version answer; its false-positive chart skips the unused range between 300 and 6,500. Vulners covers only 31 queries.

Recall

Share of known relevant findings returned

100%50%0% 98.39%Cull 98.14%Search 89.88%cve-s. 96.44%Vulners 88.60%NVD NO VERSION31/50
Recall shows how much of the reviewed affected reference set each provider returned. Higher is better.

Complete-response latency

Milliseconds · quota pacing excluded

p50p95
10505000 ms 18.1 / 29.1Cull 33.0 / 120.4Search 136.8 / 1010.7cve-s. 56.8 / 148.9Vulners 560.9 / 835.7NVD NO VERSION · LOCALHOST31/50
Cull's p95 was 29.1 ms; the slowest measured p95 was local cve-search at 1,010.7 ms.
Limits of the benchmark. A “known miss” is a CVE in this frozen reference set that a tool did not return. The counts are case/CVE pairs, not unique CVEs across the whole dataset. These 50 queries are useful regression cases, but they are not a market-weighted sample of all software. cve-search is included to show the output of a version-blind product lookup. Vulners produced 31 valid answers and is not extrapolated to 50.

All 50 benchmark queries

The exact CPE strings used for the run.

  1. OpenSSL 1.0.1fcpe:2.3:a:openssl:openssl:1.0.1f:*:*:*:*:*:*:*
  2. OpenSSL 1.0.1gcpe:2.3:a:openssl:openssl:1.0.1g:*:*:*:*:*:*:*
  3. nginx 1.20.0cpe:2.3:a:f5:nginx:1.20.0:*:*:*:*:*:*:*
  4. nginx 1.20.1cpe:2.3:a:f5:nginx:1.20.1:*:*:*:*:*:*:*
  5. Apache Log4j 2.14.1cpe:2.3:a:apache:log4j:2.14.1:*:*:*:*:*:*:*
  6. Apache Log4j 2.17.1cpe:2.3:a:apache:log4j:2.17.1:*:*:*:*:*:*:*
  7. Apache HTTP Server 2.4.49cpe:2.3:a:apache:http_server:2.4.49:*:*:*:*:*:*:*
  8. Apache HTTP Server 2.4.51cpe:2.3:a:apache:http_server:2.4.51:*:*:*:*:*:*:*
  9. curl 8.3.0cpe:2.3:a:haxx:libcurl:8.3.0:*:*:*:*:*:*:*
  10. curl 8.4.0cpe:2.3:a:haxx:libcurl:8.4.0:*:*:*:*:*:*:*
  11. Spring Framework 5.3.17cpe:2.3:a:vmware:spring_framework:5.3.17:*:*:*:*:*:*:*
  12. Spring Framework 5.3.18cpe:2.3:a:vmware:spring_framework:5.3.18:*:*:*:*:*:*:*
  13. OpenSSH 9.7p1cpe:2.3:a:openbsd:openssh:9.7:p1:*:*:*:*:*:*
  14. OpenSSH 9.8p1cpe:2.3:a:openbsd:openssh:9.8:p1:*:*:*:*:*:*
  15. sudo 1.9.5p1cpe:2.3:a:sudo_project:sudo:1.9.5:patch1:*:*:*:*:*:*
  16. sudo 1.9.5p2cpe:2.3:a:sudo_project:sudo:1.9.5:patch2:*:*:*:*:*:*
  17. XZ Utils 5.6.1cpe:2.3:a:tukaani:xz:5.6.1:*:*:*:*:*:*:*
  18. XZ Utils 5.6.2cpe:2.3:a:tukaani:xz:5.6.2:*:*:*:*:*:*:*
  19. runc 1.1.11cpe:2.3:a:linuxfoundation:runc:1.1.11:*:*:*:*:*:*:*
  20. runc 1.1.12cpe:2.3:a:linuxfoundation:runc:1.1.12:*:*:*:*:*:*:*
  21. libwebp 1.3.1cpe:2.3:a:webmproject:libwebp:1.3.1:-:*:*:*:*:*:*
  22. libwebp 1.3.2cpe:2.3:a:webmproject:libwebp:1.3.2:*:*:*:*:*:*:*
  23. Atlassian Confluence Server 8.5.1cpe:2.3:a:atlassian:confluence_server:8.5.1:*:*:*:*:*:*:*
  24. Atlassian Confluence Server 8.5.2cpe:2.3:a:atlassian:confluence_server:8.5.2:*:*:*:*:*:*:*
  25. GitLab Enterprise Edition 16.7.1cpe:2.3:a:gitlab:gitlab:16.7.1:*:*:*:enterprise:*:*:*
  26. GitLab Enterprise Edition 16.7.2cpe:2.3:a:gitlab:gitlab:16.7.2:*:*:*:enterprise:*:*:*
  27. Jenkins 2.441cpe:2.3:a:jenkins:jenkins:2.441:*:*:*:-:*:*:*
  28. Jenkins 2.442cpe:2.3:a:jenkins:jenkins:2.442:*:*:*:-:*:*:*
  29. JetBrains TeamCity 2023.11.3cpe:2.3:a:jetbrains:teamcity:2023.11.3:*:*:*:*:*:*:*
  30. JetBrains TeamCity 2023.11.4cpe:2.3:a:jetbrains:teamcity:2023.11.4:*:*:*:*:*:*:*
  31. Fortinet FortiOS 7.2.4cpe:2.3:o:fortinet:fortios:7.2.4:*:*:*:*:*:*:*
  32. Fortinet FortiOS 7.2.5cpe:2.3:o:fortinet:fortios:7.2.5:*:*:*:*:*:*:*
  33. Palo Alto Networks PAN-OS 10.2.9cpe:2.3:o:paloaltonetworks:pan-os:10.2.9:-:*:*:*:*:*:*
  34. Palo Alto Networks PAN-OS 10.2.9-h1cpe:2.3:o:paloaltonetworks:pan-os:10.2.9:h1:*:*:*:*:*:*
  35. ConnectWise ScreenConnect 23.8.5cpe:2.3:a:connectwise:screenconnect:23.8.5:*:*:*:*:*:*:*
  36. ConnectWise ScreenConnect 23.9.8cpe:2.3:a:connectwise:screenconnect:23.9.8:*:*:*:*:*:*:*
  37. Fortra GoAnywhere MFT 7.4.0cpe:2.3:a:fortra:goanywhere_managed_file_transfer:7.4.0:*:*:*:*:*:*:*
  38. Fortra GoAnywhere MFT 7.4.1cpe:2.3:a:fortra:goanywhere_managed_file_transfer:7.4.1:*:*:*:*:*:*:*
  39. Progress WS_FTP Server 8.8.1cpe:2.3:a:progress:ws_ftp_server:8.8.1:*:*:*:*:*:*:*
  40. Progress WS_FTP Server 8.8.2cpe:2.3:a:progress:ws_ftp_server:8.8.2:*:*:*:*:*:*:*
  41. Joomla 4.2.7cpe:2.3:a:joomla:joomla\!:4.2.7:*:*:*:*:*:*:*
  42. Joomla 4.2.8cpe:2.3:a:joomla:joomla\!:4.2.8:*:*:*:*:*:*:*
  43. Grafana 8.3.0cpe:2.3:a:grafana:grafana:8.3.0:*:*:*:*:*:*:*
  44. Grafana 8.3.1cpe:2.3:a:grafana:grafana:8.3.1:*:*:*:*:*:*:*
  45. Apache Tomcat 9.0.30cpe:2.3:a:apache:tomcat:9.0.30:*:*:*:*:*:*:*
  46. Apache Tomcat 9.0.31cpe:2.3:a:apache:tomcat:9.0.31:*:*:*:*:*:*:*
  47. Git 2.45.0cpe:2.3:a:git:git:2.45.0:*:*:*:*:*:*:*
  48. Git 2.45.1cpe:2.3:a:git:git:2.45.1:*:*:*:*:*:*:*
  49. PostgreSQL 16.4cpe:2.3:a:postgresql:postgresql:16.4:*:*:*:*:*:*:*
  50. PostgreSQL 16.5cpe:2.3:a:postgresql:postgresql:16.5:*:*:*:*:*:*:*
How it works

How a search becomes a verdict

Cull starts with public vulnerability and package data. It keeps the original source statements, connects the different names used for the same product, applies the right version rules and shows why a result was included or ruled out. The browser and API use the same matching result.

2,004,975advisories in memory
605,726resolved products
25independent sources
4explicit API verdict states

In short: collect the records, preserve what each source said, resolve the product, compare the version and return the evidence with the answer.

CollectDownload advisory, package and exploit data
NormalizeParse each format without dropping its scope
ConnectLink names, CPEs, package URLs and advisory ids
DecideCompare the requested version with the evidence
UseReview the result in the browser or call the API
01 · Sources

Where the data comes from

No single feed has the complete picture. NVD may describe an upstream range, a Linux vendor may document a backported fix, and an exploit project may have working code. Cull currently combines 25 sources and keeps track of what each one is qualified to say.

Core records

Published vulnerabilities

NVD, the CVE List and EUVD supply identifiers, descriptions, CPE configurations, severity and CNA product statements.

Fixes

Vendor and distribution advisories

curl, Red Hat, Debian, Ubuntu, Alpine, Wolfi, SUSE, Alma and Oracle provide package fixes and backport status.

Packages

Ecosystem advisories

OSV and GitHub connect vulnerabilities to package URLs and native version ranges for npm, PyPI, Go, crates, Maven, RubyGems, NuGet and other registries.

Exploitation

Priority and tooling

EPSS, CISA KEV, SSVC, Exploit-DB, Metasploit, Nuclei, PoC-in-GitHub and Wordfence add likelihood, observed exploitation and available tooling.

Lifecycle

Supported release lines

endoflife.date adds upstream active-support and end-of-life dates for exact product versions. Cull keeps this operational context separate from CVE applicability.

The records are allowed to disagree. If an upstream range includes a version but Debian says its package already contains the fix, both statements remain visible. An exploit module proves that tooling exists; it does not get to decide whether a particular package is affected. Likewise, an upstream end-of-life date is never substituted for a Linux distribution's own package support policy.

What is kept

Native identifiers, aliases, range types, fix statements, withdrawn status, scores, references and exploit provenance stay attached to their source. They are still available when Cull produces a verdict and when someone reviews it.

02 · Import

Keeping the meaning of each source

Every feed uses a different schema. The importers translate those schemas into Cull's internal model while retaining the source, range boundaries and the difference between affected, fixed, not affected, withdrawn and unknown.

  • 1Use a parser for each source. CPE configurations, OSV events, distro ranges, aliases and exploit references do not pass through a generic text importer.
  • 2Connect aliases without erasing them. A CVE and a source-native advisory id can refer to the same issue while remaining traceable to their original records.
  • 3Keep range boundaries exact. Inclusive, exclusive, exact, introduced, last-affected, fixed and open-ended ranges remain different cases.
  • 4Stop a bad import. Source volume bounds, parser skip ratios, corpus shrinkage checks and identity fan-out limits block incomplete or implausible datasets.

A successful parse only means the file was readable. Cull also checks freshness, record counts and the assembled corpus. A feed that suddenly empties, a format change that skips half the records, or an identity rule that joins too many products will fail the build.

source Aaffected before 2.4.50
source Bfixed in package 2.4.49-3+deb11u1
resulttwo scoped statements, not one overwritten field
03 · Identity

Matching different names for the same product

The same software can appear as an NVD CPE, an SBOM package URL, a distribution package or a former product name. Apache HTTP Server and Red Hat's httpd package need to meet in one search. Chrome and the Skia library it contains must remain separate even when they share CVEs.

Cull groups identifiers into a product entity while building the dataset. The links come from seven places:

  • 1Separator variants. Names such as sd_675_firmware and sd675_firmware are linked. A changed letter or meaningful symbol is not ignored: 865 and 865+ can be different chips.
  • 2NVD renames. Cull uses NVD's deprecatedBy field when it describes a clean one-to-one rename. A key that splits into several successors is left alone.
  • 3Registry links. If a CPE points to npmjs.com/package/lodash, that is direct evidence that it names the npm package.
  • 4Overlap between schemes. An opaque EUVD product key is linked to a CPE only when an exact name and shared CVEs support the match. Generic names such as “Reader” are not enough.
  • 5Reviewed pairs. Roughly 179 acquisition and rebrand links cover cases rules cannot infer safely, including Sun to Oracle and Macromedia to Adobe. Each entry must include a reason.
  • 6Reviewed names. This covers familiar spellings that registries do not provide, such as iOS for NVD's iphone_os.
  • 7Component relations. A suite can point to one of its parts without becoming the same product. Versions cross that relation only where the comparison is safe.

A union-find pass merges the accepted links so searches do not have to walk the graph. No product may collect more than 64 identifiers. If a bad rule crosses that limit, the build fails instead of publishing an oversized product group.

Several identifiers merging into one product entity cpe .../http_server cpe .../http-server cpe redhat/httpd purl rpm/httpd euvd:9f2c... linked by spelling & renames registry refs & CVE overlap one product Apache HTTP Server union-find, capped at 64 names
Spelling rules, renames, registry links and reviewed mappings can all point to the same product. A group larger than 64 identifiers causes the build to fail.
A shared CVE is not enough

Bundles and dependencies often appear in the same advisory. That does not make them one product. Reviewed exclusions also keep known mixed EUVD keys from being joined automatically.

05 · Version comparison

Comparing versions

Finding the product is only the first step. Cull must then place the submitted version inside or outside every relevant advisory range. It parses the version into segments and chooses comparison rules from the package ecosystem or distribution.

1.2rc1 sorts before 1.2, while 1.2p1 sorts after it. The same string can change meaning between ecosystems: SemVer treats 1.0.0-1 as a pre-release, while Debian treats the suffix as a package revision. Maven, RubyGems, NuGet and Alpine have their own ordering rules as well.

The version ordering ladder and the epoch trap pre-release release patch and update newer alpha beta rc 1.2 1.2p1 1.2u3 the epoch trap rpm -q prints 2.4.37-43.el8_5, no epoch When input omits the epoch, cull compares the visible version and release instead.
Pre-releases sort below the release and patches above it. Package epochs need special handling because command output often leaves them out.
The epoch trap

Red Hat ships Apache with epoch 1, written 1:2.4.37, but rpm -q may print only 2.4.37-43.el8_5. Treating that as epoch 0 can make a fixed package look vulnerable. If only one side includes an epoch, Cull compares the visible version and release. If both include it, normal RPM ordering applies.

Distribution rules are used only when the query names a distribution or contains a recognizable package version such as 7.88.1-10+deb12u5. A plain apache 2.4.49 search does not borrow Debian's backport status. If the available evidence belongs to an unspecified distribution, Cull leaves the result undecided.

06 · Verdicts

Affected, ruled out or still unknown

Each relevant advisory receives a result and a reason. Unknown stays separate from ruled out so a lack of usable range data cannot look like a clean result.

Verdicts sorted into affected, ruled out, and could not check advisory vs your version Affected in range, act on it Ruled out out of range, or fixed Could not check exists, but nobody said distro says fixed clears it, keeps the split only fixed, not affected or out of range can clear
A distribution backport can clear an upstream match. Both source statements remain attached to the result, and missing evidence stays in its own bucket.
  • AAffected. The version is inside an affected range or an advisory names it directly.
  • BRuled out. The version is outside the affected ranges, fixed, or explicitly marked not affected.
  • CCould not check. The advisory is relevant, but its version or distribution scope cannot answer this query.

An unknown advisory id is reported as unchecked instead of being dropped. This matters in automation, where an empty array can otherwise look like success. A distribution may clear an upstream match only with positive evidence: fixed, not affected or out of range. An inconclusive or withdrawn record cannot clear it.

The API exposes the same distinction as AFFECTED, NOT_AFFECTED, UNKNOWN_OR_UNCHECKED and UNRESOLVED.

07 · Exploitation

Exploit evidence and urgency

An affected result does not say whether exploitation is likely or whether usable tooling exists. Cull keeps exploit evidence separate from CVSS and shows where that evidence came from.

EPSS estimates likelihood. CISA KEV records observed exploitation. SSVC provides decision context. Exploit-DB, Metasploit, Nuclei and PoC-in-GitHub indicate what public tooling exists. Wordfence adds coverage for WordPress plugins that often have no CVE.

The strongest evidence determines the finding's exploit tier:

The five-rung exploit ladder unreviewed proof of concept verifiable weaponized observed PoC in GitHub Exploit-DB Nuclei template Metasploit module CISA KEV, in the wild
The scale runs from unreviewed code to observed exploitation. Verified tool identifiers can also be used to build a command.

Results are ordered by this evidence before severity. Findings are ranked by whether they are known-exploited, then whether any exploit exists, then EPSS probability, and only then CVSS. A 9.8 that nobody can reach sits below a 7.5 with a maintained Metasploit module. Severity, EPSS, publication date and identifier remain available as explicit sort keys, and the tier itself is a property of the finding that does not change with the order. If Cull has a validated Nuclei template id, Metasploit module path or Exploit-DB id, it can show the corresponding command. A reference URL is never inserted into a shell command.

Scores are not interchangeable

CVSS describes technical severity, EPSS estimates likelihood, KEV records observed exploitation and SSVC adds decision context. If one of those values is missing, Cull leaves it missing rather than filling the gap with another score.

08 · Research workflow

Working with the results

Cull's research workbench keeps the finding list next to the selected CVE. Applicability, source statements, aliases, affected ranges, fixes, exploit tooling and the event timeline stay in one place.

Filter

Reduce the result set

Search ids, descriptions, CWE, products, sources and tooling. Structured filters cover CVSS, EPSS, KEV, exploit type, certainty, fixes, dates, vector fields and attack primitives.

Inspect

Read the evidence

The inspector shows source verdicts, disagreements, remediation, affected branches, commands and the timeline for the selected finding.

Queue

Keep a working list

Save findings locally in the browser, add comments, reorder them and filter the queue by KEV, tooling or severity.

Export

Hand the work over

Copy selected findings as Markdown, CSV or JSON. Queue exports include notes, and verified Nuclei templates can be combined into one command.

The query language and the controls use the same fields. openssl 1.0.1 is:kev cvss:>=9 vector:AV:N can be bookmarked and rerun. Filters are applied before totals, sorting and pagination, so the displayed count matches the filtered result.

No combined priority score

CVSS, EPSS, known exploitation, exploit maturity, fix availability and publication date remain separate fields. You can choose which one matters for the current job.

09 · API and automation

Using the API

The HTTP API calls the same matcher as the web interface. It accepts the same targets and filters, returns the same verdict states and adds pagination and stable response schemas for integrations.

GET /api/v1/search

Search one product, package coordinate or advisory id. Results can be filtered and sorted, or reduced to ids with view=ids.

POST /api/v1/bulk

Check up to 100 targets in JSON or newline-delimited text. Compact output is available for inventories and boundary checks.

GET /api/v1/openapi.json

Download the OpenAPI 3.1 description of parameters, result states, findings, pagination and errors.

The full response includes aliases, source verdicts, affected and ruled-out evidence, unchecked records, CVSS, EPSS, KEV, SSVC, CWE, attack primitives, exploit references, commands, timelines, fixes and source disagreements. CI jobs that only need identifiers can request view=ids.

The status field removes ambiguity around empty results. AFFECTED has applicable evidence, NOT_AFFECTED resolved the target without one, UNKNOWN_OR_UNCHECKED has relevant but inconclusive evidence, and UNRESOLVED could not identify the product.

One matcher

The workbench, single-search endpoint and bulk endpoint render the same underlying result. Browser and API behavior therefore change together.

10 · Serving engine

How the dataset fits in memory

Cull serves about 2,004,975 advisories from immutable in-memory structures on one machine. In the measured 2026-08-24 corpus, the SQLite database was 2.4 GB and the separate prose file 558 MB. The loaded server used about 1.6 GiB of live Go heap and 2.5 GiB steady RSS.

Structured matching data lives in the Go heap. Titles, descriptions and reference URLs live in a separate memory-mapped file because the matcher does not need them while deciding applicability. The file contains no Go pointers, so the garbage collector does not scan it. The operating system can also discard and reload those pages under memory pressure.

Repeated values are interned. In that corpus, 12.8 million range-list occurrences reduced to 311 thousand distinct values, and 3.1 million parsed-version occurrences reduced to 250 thousand. Parsed versions use one allocation each. Temporary build maps are discarded before the server reports ready.

GOMEMLIMIT is not compression

GOMEMLIMIT cannot shrink live data. A value below the working set only makes garbage collection more frequent. The actual savings come from compact structures, interning and the pointer-free mapped file.

11 · Evaluation and improvement

How mistakes become tests

Most matching bugs happen at boundaries: a rename, an exclusive upper range, a package epoch, a backport, an alias collision or a disagreement between upstream and a distribution. Once one of these cases is fixed, it stays in the regression suite.

Unit tests

Keep the awkward examples

Hundreds of focused automated tests cover version rules, query parsing, product links, source precedence, verdicts, filters, pagination and API schemas.

Integration

Check the whole answer

Corpus fixtures run through import, matching and response rendering. Tests check the bucket, reason, evidence, aliases, fixes and status around an id.

Data checks

Reject suspicious refreshes

Freshness, source volume, parse coverage, corpus shrinkage and product fan-out are checked before a dataset can serve searches.

Benchmark

Rerun the reviewed cases

The 50-query benchmark uses fixed inputs and a reviewed reference set. It records false positives, known misses and latency separately.