Vulnerability Management

DevGuard Vulnerability Database and the EUVD

When it comes to Vulnerability Management Tools, having a good vulnerability database is crucial. While it seems simple to just synchronize CVEs from a source like the NVD and call it a day, but building a good vulnerability database is far more nuanced than that. Here's what sources DevGuard synchronizes and why, how the VulnDB pipeline actually operates, and how we approached synchronizing the EUVD's sources.

Patrick Rißmann
Patrick RißmannSoftwaredeveloper - Backend
Tim Bastin
Tim BastinCTO & Software Security Specialist
Published 17 min read

Introduction

When it comes to Vulnerability Management Tools, having a good vulnerability database is crucial. While it seems simple to just synchronize CVEs from a source like the NVD and call it a day, it is actually more nuanced then one might think and there are a lot of decisions to be made. So in this blog we are gonna tackle the following topics:

  • What sources does DevGuard synchronize and why?
  • How does DevGuard’s Vulnerability Database operate?
  • How we synchronize the EUVD’s sources?

So if you are interested in how DevGuard works under the hood, what challenges we faced with the vulnerability database and what are some of our key learnings from this process, this is the right place for you to look.

What sources does DevGuard synchronize?

To detect vulnerabilities, you need to answer two questions: which components are present in your software, and do those components have any known vulnerabilities? Components can be anything from a linkable library to a binary or a container image. Both questions turn out to be surprisingly hard to answer.

To answer the first question, you need a tool often called a “scanner.” Few tools truly deserve that name. Most so-called scanners, like trivy, npm audit, and cdxgen, mostly just read package manager files, and fail to detect anything beyond a simple entry in a package.json, a pom.xml, or Debian’s apt list. Different languages and binaries add their own quirks: the Go compiler embeds dependency information directly into a Go binary, while gcc does not. Even then, that embedded information can’t be reconstructed into a full dependency tree – all information about how dependencies relate to one another is already lost.

Once you’ve identified a software’s components, you need to write them down, typically in a JSON file, which means naming them in a standardized way. Naming software components turns out not to be as simple as it sounds: there are countless ecosystems, compiler configurations, and processor architectures to account for, so several competing naming schemes exist, such as CPEs, pURLs, and plain package names. Each comes with its own quirks and pitfalls. CPEs, for instance, are vendor-classified strings, which makes them awkward to use for open source software. pURLs, on the other hand, are standardized and open-source-friendly, but they’re not compatible with CPEs, and you can attach essentially any qualifier you like to one. If you need both formats, you have to convert one into the other – and since there’s no formalized conversion algorithm, that process is inherently error-prone.

Example CPE: cpe:2.3:a:apache:log4j:2.17.1:*:*:*:*:*:*:*
Example PURL: pkg:maven/org.apache.logging.log4j/log4j-core@2.17.1?upstream=central

So at this point, we’ve already lost a lot of information. We lose components outright when scanners miss them, for example, tools installed without a package manager (curl isn’t one). Scanning OCI images used as build environments is especially tricky: once the software is compiled, the build tools that produced it are invisible to the scanner. Scanning a Redis image, for instance, surfaces every installed (through a package manager) package except Redis itself. And even for components we do find, we lose information simply by writing them down as text and deciding on a format to store them in.

To answer the second question, you need data on vulnerabilities. But let’s take a step back and look at the whole process of creating that data in the first place, since it has a big effect on how you synchronize it and what you need to look for.

Imagine you’re developing software – you probably are, since you’re reading this blog post. You’re writing everything from scratch: no external libraries, nothing. You’re building a beautiful web admin dashboard, and at some point you need to implement a login form. Since you don’t want a database involved, you decide to just store user credentials in a file on your server: the filename is the username, the content is the password. You’re a genius – no authentication server, no library, just Linux. Beautiful. But you’re also a human being, and humans make mistakes. You forget to check the username for filename-escaping characters, and you forget to check the password for special characters. So a malicious user sends a request with a username like ../../../../.bashrc and a password like "; rm -rf / --no-preserve-root; echo ", and your server happily writes that command straight into your bash config – deleting every file on your server the next time you open a shell. Congratulations, you just created a vulnerability.

If you decide to notify your users, you have to write a security advisory. That’s surprisingly hard, and no one actually forces you to do it (at least not yet– looking at you, Cyber Resilience Act). But let’s say you’re a decent human being and you do it anyway. What should it contain? You want to help your users, so you start with the impact: “deletes all files on your server.” Then you remember this login form only shipped in the new version, so older releases aren’t affected – worth noting. Maybe you can add a workaround, too: Linux has file permissions, so at least you can tell users to run the application as a non-root user and limit the blast radius. You settle on “Severity,” “Workaround,” and “Who is affected,” and publish it on your website.

Now you need to notify your users. Hopefully they all signed up for a mailing list. But fewer and fewer people do that these days, and it would clearly help to have some shared standard for reporting vulnerabilities – regulators seem to expect one. That’s where the backbone of the whole CVE-Program comes in: The MITRE Corporation, a central US-based vulnerability advisory store. MITRE publishes vulnerability advisories to the world through its own website and various APIs, each tagged with a CVE, a unique identifier for a vulnerability. That solves the notification problem by centralizing the database. But not everyone can submit a vulnerability to it; you need to be a so-called CVE Numbering Authority (CNA). There are many CNAs: Red Hat, Debian, Gentoo, and so on, and to get a CVE assigned, you have to ask one of them. Maybe they will write back to you and ask for more information (maybe it takes some time), or maybe they will just assign a CVE to your vulnerability.

MITRE is the backbone of the CVE Program, but it just lists bare CVE records, an ID, a description. You already thought about some more information you want to provide your users like “Who is affected” and the “Severity” of the vulnerability. This process is called “CVE-Enrichment”. Scoring a vulnerability is often done using the CVSS (Common vulnerability scoring system). You can do it yourself, and if a CNA supports that, Mitre will be happy to list it. About the package identifier you really have to think hard. If you get it wrong, users of your software won’t be able to identify the vulnerability in their software stack. What is the name of your software? What will scanner call it? Do you know all scanners available? Hopefully, or your users will be left vulnerable. As you see, the process of enrichment of a vulnerability is work, and most maintainers of software products – which are already so nice to even create a security advisory – don’t have the time to do it well themselves. So that enrichment tends to fall to someone else after the fact. One of the most prominent examples is the National Vulnerability Database (NVD), which takes CVEs assigned by MITRE and adds its own CVSS vector, weakness category and CPE data on top. To this day we are curious how they managed to do that for every CVE in the world. But in April 2026 they announced they will stop enriching CVEs with a low priority just because of the sheer amount of work and a 263% increase in CVE submissions.

Enough storytelling: when we first started building DevGuard, we relied on exactly that: NVD’s enriched CVE data. It was pretty hard to get the synchronization right. The NVD uses a pretty strict rate limiting. If you have an API-Key available you are allowed todo 50 Requests per 30 second window with an optimized response size of 2000 entries. This means a single sync takes around 2 minutes to fetch – if, and that pretty much never happened, you are not running into 502 errors. So we had to implement a retry logic with exponential backoff and a lot of error handling. And even if you have the data back, the NVD does not publish any pURLs but CPEs. This forced us to always convert one into the other, which was prone to a lot of errors, leading to crucial bugs invulnerability detection.

So we needed a database that provided us with affected components as pURLs to be able to properly match them to our SBOM’s component pURLs. And we needed to decide on a single scanner which will produce the same purls so we could optimize our matching process on. We decided to use trivy for that and at the same time we stumbled upon the Open Source Vulnerabilities (OSV) Database. The OSV was actually a project which only was invented because of the problems during matching CPEs to open source software.

The OSV also happens to solve a completely different problem we hadn’t run into yet: getting a CVE assigned through MITRE and a CNA can take days or weeks, since a human has to review every request. That’s much too slow for a fast-moving ecosystem – if you maintain a popular npm package and a critical vulnerability is actively being exploited, your users can’t afford to wait weeks for a CNA to get back to you. So several ecosystems stopped waiting on MITRE altogether and built their own advisory databases: GitHub Security Advisories (GHSA) for anything hosted on GitHub, PyPA’s advisory database (PYSEC) for Python, and more. These let maintainers publish an advisory in minutes rather than weeks – at the cost of yet another format and yet another database to synchronize. OSV was built specifically to exchange vulnerability information across exactly these open-source ecosystems. It aggregates a lot of programming language ecosystem databases and solves the different format problem by inventing a new format called Open Source Vulnerability format. This lets us deliberately choose which ecosystems we import. Besides that it was built for mass data imports. The OSV provides the data in zip archives, hugely simplifying bulk imports. In General we completly switched to the OSV and are not synchronizing CVEs anymore. This is one of the reason why you won’t see to many CVEs in DevGuard, but rather a lot of GHSA-, PYSEC-, GOSEC-Advisories etc. Some CVEs are still imported through the alias of the OSV, but we are not relying on them anymore. The OSV is the single source of truth for DevGuard.

In addition to the raw severity of vulnerabilities there are more interesting information. DevGuard doesn’t want to rely on CVSS data alone to determine the risk of a vulnerability but rather enrich it as much as possible with complementary information. An example of that is the temporal/threat score of a vulnerability, which gets determined based on the availability and maturity of related exploits. For that reason, we also import exploit data from various sources: PoC-in-Github and ExploitDB for PoC exploits, CISA’s KEV catalog for information about active exploitation, and EPSS scores to determine the likelihood of exploitation. We then use all of these different metrics to determine a more informed risk score for each vulnerability in DevGuard.

Matching Vulnerabilities to Components

Now we have vulnerabilities in DevGuard, we have CVSS-Scores (mostly done by Maintainers themselves) and we have some affected components. Let’s take a look at the next step: matching vulnerabilities to components. We agreed on PURLs already but how does a vulnerability database, especially across different ecosystems, encode something like: “Everything in the 2.x branch; Maybe everything < 3; or build before 2022-01-01”. As you already guessed, we are trying to use software versions. Software versions are a mess. There are a lot of different versioning schemes out there. The most common used in modern ecosystems like npm, and by far the best one, is semver: Encoding your software version into Major.Minor.Patch. You increment the major version after you do a breaking change, to notify your users they now really have to finally visit your changelog. You increase the minor version for backwards compatible feature releases and finally the patch version gets increased when fixing a bug. But how would you now encode a debian patched backport for an openssl version? Or how would you encode the processor architecture like arm64? Thats the reason, you won’t find any semver parsable package version in the debian repository and inside debian security advisories. Golang uses semver but decided to enforce a “v”-prefix to your version number. Which is, by semvers own definition, not a semver version number.

The OSV decided to solve this problem by giving you a version range – and a type. One of the types is semver. Those types can be stored and sorted by PostgreSQL itself, another type is ecosystem. Ecosystem basically means: We have no idea, figure it out yourself. Those ranges are really important when storing this data. If you don’t have a version range you need to enumerate all versions in between. Currently our relational database table affected_componentsis the second largest table of devguard and holds around 2.2 million rows. Around 1.5 million are not storing a version range at all but enumerating all version in between. Around 650k are using the ecosystem specific type, where we build Golang Implementation for – but we need to fetch all affected components for a software package and decide using golang, if the version does even match. Only 41k are using the semver version range, fast to query and fast to sort.

How does DevGuard’s Vulnerability Database operate?

Now that we know what sources DevGuard needs to synchronize, the next question is how. DevGuard splits this process into two parts. First, the export step: an hourly GitHub Action Workflow pulls all the data from the sources described above, computes the vulnerability data from it, and publishes it to the repo’s package registry (VulnDB). Second, the import step: a DevGuard instance pulls the data from that package registry and imports it into its PostgreSQL database. You might expect each DevGuard instance to just pull the data on its own and be done with it, but there are actually several compelling reasons for separating these two steps.

First, a centralized data source eliminates syncing issues between different DevGuard instances. It doesn’t matter when an instance imports the latest VulnDB – it will always get the same data as another instance importing at the same time, since the package registry acts as a single source of truth for all imports. That also means that if there’s a bug in the VulnDB, we can fix it in one place and every instance receives the corrected data swiftly. Closely related is our use of checksums and signatures to guarantee the integrity and authenticity of the pulled files: with one aggregated source to pull from, we can make sure that nothing pulled during the import step has been tampered with.

Another benefit of running the export step in a GitHub Action is the outsourcing of computational work. The workflow does not just pull the data but also converts it into ready-to-import database rows. This includes applying EPSS and KEV data to vulnerabilities, calculating relationships and affected components, filtering out obsolete data, and optimizing data types, for instance, storing IDs derived from content hashes as bigints instead of strings, which shrank the affected tables and pivot tables by orders of magnitude. One pivot table alone holds around 11.5 million rows and, thanks to this, is stored as nothing but bigint-to-bigint pairs: we simply hash the cve_idinto a stable value and store that hash instead of the strin (thanks @Patrick for this idea!). This way, all the heavy-duty work is done once, and all DevGuard instances can just use the computed result without doing the work themselves, greatly increasing the import speed and reducing the network traffic. The workflow also publishes the data in a compressed format, only containing the information DevGuard actually needs to import, which further reduces the needed network bandwidth. We additionally switched from CSV to Go’s binary gob encoding for the published data, which cut the payload size down even further.

Going further: integrity and speed

We also want to take some time to highlight the integrity checks of the VulnDB. In the past we often ran into the problem that some DevGuard instances silently had inconsistent and plainly incorrect vulnerability data. There were multiple reasons for this: a bad diff import, a batch failing halfway through, a leftover table we forgot to drop afterward. Most of these came down to us simply not being database experts yet – we became one along the way. Missing transaction handling and not knowing about ON COMMIT DROPcaused a lot of these problems. To eliminate this problem once and for all, we thought about a way to detect invalid states. The solution was simple and highly effective. When the export step is done preparing the data, it also loads the rows into its database and then calculates a checksum over all imported tables and publishes it along the vulnerability information. This way, if a DevGuard instance has finished importing the data it runs the exact same checksum calculation over those tables, and if the two don’t match, the import fails instantly. It took us some time to get a deterministic import and export but now we are sure, that all DevGuard instances across the globe always run with the byte-for-byte identical vulnerability data, no more inconsistencies, no more incorrect data.

Talking about import speed; this next improvement is actually a follow-up to an older idea: in earlier implementations, each update consisted of just the differential changes from the previous one. Since the workflow runs hourly and most DevGuard instances import hourly too, the changes per import are minimal on average – so re-importing the whole dataset every time is quite inefficient. Nevertheless there would be some instances which need the whole set of data.

Earlier we tried to publish a full snapshot once and then we were publishing diffs against that same snapshot on every export. A fresh instance would pull the latest snapshot and then replay every diff published since – in order, one by one. This turned out to be a bad idea: instead of one bulk import, an instance catching up after some downtime ended up applying dozens of tiny, sequential transactions, each with its own overhead, and the whole thing was time-dependent – skip one diff or apply them out of order, and the database ends up in an inconsistent state. So we simplified it this iteration.

The export step already loads all the data into the tables to calculate the checksums – so why not also load the previous VulnDB, compute the diff between the two, and publish that diff alongside the full import? On the other side, the import step checks whether its database is currently on the previous VulnDB state. If it is, it tries to apply the diff; if the diff import fails for any reason, it falls back to the full import. This way we cut the import time for the vast majority of imports down to just a few seconds – another example of computing something once and profiting. The full VulnDB export shrank from around 0.5GB to just 60MB, diff included. It also means we no longer need to keep multiple snapshots around: our OCI registry only ever holds a single version of the VulnDB, always the latest one. That’s a big improvement over our previous implementation, which stored every version ever published and made cleaning up old ones impossible.

EUVD

When We talked about the sources DevGuard’s VulnDB synchronizes, We purposely left out the EUVD since it’s a very recent change (and kinda the reason for this blog post). It therefore deserves its own section. Commissioned by the ZenDiS, the EUVD was integrated into DevGuard just last week (start of August 2026).

The motivation behind that decision is, quite obviously, digital sovereignty. It’s no secret that most of the cybersecurity infrastructure and vulnerability sources is based in the United States (e.g. CISA, NVD, OSV). If, for whatever geopolitical reason, CISA decided tomorrow to cut off all traffic outside the US, DevGuard could no longer operate properly. It would be missing its Known-Exploited-Vulnerabilities catalog, which we use during Risk calculation. Therefore they have a lot of power over the availability and effectiveness of DevGuard; obviously we want to avoid that as much as possible.

That’s why the EUVD was brought to life to act as a counterpart to American staples. The EUVD itself is part of the CSIRT Network and acts as an aggregator of vulnerability information inside said network. It exposes a web interface to browse and read on vulnerabilities, but for our use case, the API is more interesting.

The API currently exposes three types of data: general vulnerability information filterable using query parameters, KEV data, and EUVD aliases. Whereas the EUVD aliases are just a large CSV file that maps existing CVE-IDs to the EUVD’s internal EUVD-IDs.

The EUVD KEV, like the CISA KEV, is a catalog containing CVEs that were observed to be actively exploited by attackers. This is very valuable information when it comes to determining the severity of a vulnerability. Similarly to the CISA KEV, the EUVD KEV contains information about the CVE-ID and the date when the entry was added; but it also contains information unique to the EUVD, namely the EUVD-alias and the sources of the entry’s information. The EUVD alias is basically just the equivalent EUVD-ID for the CVE-ID. The sources contain a list of sources where the information originates from. What’s interesting is that out of the 1658 currently listed entries, only 5 do not list CISA as a source and only 35 list EUVD as a source. So in its current state, this acts more like a mirror of the CISA KEV catalog than an independent source.

The last of the three data endpoints is the common vulnerability information. Now you would expect to hear that we also synchronize that as an additional data source but the truth is we do not. And sadly the reason is twofold: the EUVD’s poor compatibility with open source scanners, and the way you have to retrieve this information in the first place – the API itself. As mentioned in Chapter one, we decided to switch from the NVD to the OSV mainly because of the hard-to-use CPEs used as product identifiers in the NVD. Well as it turns out the EUVD is even worse: instead of CPEs, it identifies the affected product using a field called enisaIdVendor– which is essentially free text rather than anything a scanner could ever be expected to produce (please let us know if we got anything wrong here. We really couldn’t find a way to match those). So there is no point in importing this data if we can’t match it with our SBOMs.

Now you might say: well, then just use the rest of the information – CVSS, relationships, maybe even advisories – and import that into DevGuard. But when you search for a bulk download option all you will find is an API which lets you query the vulnerabilities in pagination form, with a limit per page of – I do not kid you – 100. So in order to sync all 369,638 entries you would have to send approximately 3600 individual http requests. In combination with the rate limits, this takes forever and its not feasible in comparison to the OSV sync. In defense of the EUVD it does provide a fromUpdate, toUpdate logic which suggests a differential update strategy but our current export/import architecture does not support this method and we do not want to go back.

But we did not want to give up yet and had the idea to just aggregate from the EUVD’s sources ourselves. Or to be more precise, we would aggregate advisory information from the different CSIRT members, process it and add it to our VulnDB. The idea is that each member country of the CSIRT network provides a centralized endpoint for advisories using the CSAF format, which could then easily be fetched individually since they all provide the information in a uniform way. Since we already have a lot of experience in downloading and generating CSAF we thought this would be a feasable approach. Well as it turns out again, of all the over 25 CSIRT members exactly two publish their vulnerability information in the CSAF format. Only two. That being Germany and the Netherlands. That’s it.

But at least we have 2 sources, so for our recent VulnDB update we added this advisory information to DevGuard. So when you inspect a dependency vulnerability in DevGuard, if there are security advisories available for that vulnerability they will be displayed. They will provide additional information and also include a link to the respective advisory page of its authority. In addition to that feature, we now also explicitly display if a dependency vulnerability was found in one of our KEV sources. This aims to further help DevGuard users prioritize urgent vulnerabilities they should address immediately. Last but not least you can find the EUVD aliases for your dependency vulnerability in the relationships section.

To summarize our experience with EUVD: the direction is definitely the right one. We need to be more independent of individual organizations and build our own cybersecurity infrastructure in Europe. But the current state of the EUVD is far from mature enough to be used as a serious alternative to the industry staples like the OSV. So we are gonna hold back on fully relying on the EUVD for now but will definitely keep an eye on the evolution of this project. In the future we expect more CSIRT members to publish their advisory information via CSAF endpoints, which we can then integrate into DevGuard’s Vulnerability Database.

FAQ

Why does DevGuard not find Vulnerability XY but another scanner does?

The most common reason for that is the fact that each scanner has their own vulnerability database to match against. As discussed in Chapter 1 DevGuard only synchronizes open source vulnerabilities through the OSV so if another scanner uses other data sources it can happen that the results of both scanners are not identical, even if the underlying SBOM is the same. DevGuard will see the PURL of the component but finds no matching affected component so it does not mark it as vulnerable and vice versa

Patrick Rißmann
Patrick RißmannSoftwaredeveloper - Backend

Patrick Rißmann is a backend software developer at L3montree who co-built DevGuard's vulnerability database, its CSAF integration, and the connection to the European Vulnerability Database (EUVD). He has spoken at the BSI's international CSAF Community Days and is completing a degree in cybersecurity.

View full profile
Tim Bastin
Tim BastinCTO & Software Security Specialist

Tim Bastin is the CTO of L3montree Cybersecurity, an experienced software architect and software security specialist with a clear mission: high-quality, secure code preferably 100% open source. As an active open-source maintainer and a member of the Technical Advisory Board for the Container.gov.de initiative, he advocates for secure, digitally sovereign software infrastructures and regularly speaks at conferences such as FrOSCon, the BSI’s CSAF Community Days, and OWASP events.

View full profile

Questions or feedback? Get in touch directly!

Send us an email