Published vulnerabilities
NVD, the CVE List and EUVD supply identifiers, descriptions, CPE configurations, severity and CNA product statements.
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.
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
LOCALHOST identifies the local cve-search run. 31/50 identifies Vulners' partial run.| Tool | Direct query | Practical input workflow |
|---|---|---|
| Cull | Yes | Free text such as Apache 2.4.49, aliases, CPEs, PURLs and package coordinates are accepted directly. |
| Search Vulns | Yes | Accepts a product query directly; this quality run still used the same exact CPE as the other version-aware tools. |
| cve-search | Partial | Requires vendor and product fields and does not evaluate the submitted version. |
| Vulners | No | The measured software-audit endpoint requires an exact software coordinate such as a CPE. |
| NVD | Two steps | Search the separate CPE dictionary, choose the correct CPE, then submit it to the CVE API. The benchmark supplied that CPE. |
50 exact product/version CPEs · Vulners returned 31
Share of known relevant findings returned
Milliseconds · quota pacing excluded
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.
In short: collect the records, preserve what each source said, resolve the product, compare the version and return the evidence with the answer.
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.
NVD, the CVE List and EUVD supply identifiers, descriptions, CPE configurations, severity and CNA product statements.
curl, Red Hat, Debian, Ubuntu, Alpine, Wolfi, SUSE, Alma and Oracle provide package fixes and backport status.
OSV and GitHub connect vulnerabilities to package URLs and native version ranges for npm, PyPI, Go, crates, Maven, RubyGems, NuGet and other registries.
EPSS, CISA KEV, SSVC, Exploit-DB, Metasploit, Nuclei, PoC-in-GitHub and Wordfence add likelihood, observed exploitation and available tooling.
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.
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.
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.
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.
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:
sd_675_firmware and sd675_firmware are linked. A changed letter or meaningful symbol is not ignored: 865 and 865+ can be different chips.deprecatedBy field when it describes a clean one-to-one rename. A key that splits into several successors is left alone.npmjs.com/package/lodash, that is direct evidence that it names the npm package.iOS for NVD's iphone_os.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.
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.
Free-text searches are ranked with BM25. Rare terms and short matching names score well; repeating the same word adds little. Exact identifiers skip that ranking step.
The query can also be an exact CPE, a package URL, a distribution package version or an advisory id such as CVE, GHSA, EUVD, DSA or USN. Bulk search accepts up to 100 targets.
Two details prevent common but convincing search errors.
Putting every alias into one BM25 document penalizes products with many names. That once caused apache to fall out of the top results after httpd was linked to it. Cull now stores the best score for each individual name when it builds the index. Adding an alias cannot push the product down.
A search for postgressql returns no findings and offers PostgreSQL as a correction. It does not silently run the corrected query. Short inputs receive no correction because one changed character can name another real product; curl and cull are a good example.
Query parsing also has to distinguish names from versions. Windows 10 1809 means Windows 10 at build 1809, not Windows at version 10. Distribution names are removed before version detection, so the final 12 in Squid 5.7-2 Debian 12 belongs to Debian.
| Constant | Value | Why |
|---|---|---|
| k1 | 0.9 | Below the usual 1.2. Product names are short, so a repeated word says little. |
| b | 0.6 | Length normalization, kept mild so an entity with many aliases is not penalized. |
| exact-name | 2.5x | A whole name equal to the query outranks a mere term match. |
| prior | up to 1.6x | Advisory volume breaks ambiguous textual ties without overriding a strong name match. |
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.
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.
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.
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.
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:
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.
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.
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.
Search ids, descriptions, CWE, products, sources and tooling. Structured filters cover CVSS, EPSS, KEV, exploit type, certainty, fixes, dates, vector fields and attack primitives.
The inspector shows source verdicts, disagreements, remediation, affected branches, commands and the timeline for the selected finding.
Save findings locally in the browser, add comments, reorder them and filter the queue by KEV, tooling or severity.
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.
CVSS, EPSS, known exploitation, exploit maturity, fix availability and publication date remain separate fields. You can choose which one matters for the current job.
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/searchSearch one product, package coordinate or advisory id. Results can be filtered and sorted, or reduced to ids with view=ids.
POST /api/v1/bulkCheck up to 100 targets in JSON or newline-delimited text. Compact output is available for inventories and boundary checks.
GET /api/v1/openapi.jsonDownload 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.
The workbench, single-search endpoint and bulk endpoint render the same underlying result. Browser and API behavior therefore change together.
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 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.
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.
Hundreds of focused automated tests cover version rules, query parsing, product links, source precedence, verdicts, filters, pagination and API schemas.
Corpus fixtures run through import, matching and response rendering. Tests check the bucket, reason, evidence, aliases, fixes and status around an id.
Freshness, source volume, parse coverage, corpus shrinkage and product fan-out are checked before a dataset can serve searches.
The 50-query benchmark uses fixed inputs and a reviewed reference set. It records false positives, known misses and latency separately.