August 12, 2026
We built a scanner. Then we found out the hard part was the data.
Part 2 of why we built Codeward and why our scanner now reads its vulnerability data from our own intelligence database

By Codeward
8 min read
Part 2 of why we built Codeward and why our scanner now reads its vulnerability data from our own intelligence database
Our first post here was about why we built Codeward: enterprise governance tools cost more than our runway, the open-source pieces didn't fit together, and we got tired of stitching. That post ended with "we built the tool we wished existed."
There was a part we skipped over.
The scanner shipped with a bundled trivy-db. It worked. It was also somebody else's database, baked into our binary, and we couldn't answer basic questions about it.
A developer would ask why a package got flagged. We'd say "it's in the database." They'd ask which database, and where that entry came from, and whether the severity was NVD's opinion or GitHub's, because those two often disagree. We didn't know. The bundle had already picked a winner and thrown the argument away.
That bothered us more the longer it went on. We'd built a governance tool whose entire premise is that you should be able to see why something is being blocked. And the most important input to that decision was a black box we shipped inside our own binary.
Three things that kept coming up
Somebody has to break the tie, and it's usually invisible. Here's a real one out of our own data: CVE-2026–33036, an entity-expansion bug in fast-xml-parser. NVD, GHSA and OSV all rate it HIGH. Red Hat rates it MEDIUM. Four sources, one dissent. Most tools hand you a single severity with no sign there was ever an argument.
"Is there a CVE" is the easy question. The one people actually ask is "does this matter to us, this week, in this service." Here's the shape of it: of the roughly 894,000 vulnerability records we track, 4,230 have a known public exploit and 1,661 are on CISA's exploited list. Half a percent, and a fifth of a percent. A CVE count tells you almost nothing. The thin slice that matters is spread across feeds nobody joins for you.
CI is where free tiers die. Query a public vulnerability API from every runner on every build across every repo and you find the rate limit fast. The usual fix is to pay someone. The other fix is to download a bulk dump and re-plumb it yourself, which is a second product you didn't want to build.
So we built the join
Codeward Intel currently runs 25 sources and agents. The vulnerability side is NVD, OSV, GitHub Advisories and Red Hat's CVE feed. Severity and exploitability come from EPSS, CISA KEV, and proof-of-concept data aggregated from Exploit-DB, Nuclei templates and GitHub. Licences come from the SPDX list, ScanCode LicenseDB, choosealicense.com and the OSADL compatibility matrix. Package facts come from the registries, deps.dev, GitHub repo metadata, endoflife.date and OpenSSF's malicious-package database. Weaknesses come from MITRE CWE.
Right now that's about 879,000 vulnerability records, 370,000 packages, 1.5 million versions, 2,435 licences and 969 CWEs across ten ecosystems.
Show the working
Every field keeps its sources attached. That same advisory, asked for its provenance, gives you the working rather than just the answer:
{"field": "severity",
"value": "HIGH",
"winner_source": "nvd",
"strategy": "priority",
"candidates": [
{"source":"nvd", "priority":90, "value":"HIGH", "winner":true},
{"source":"ghsa", "priority":80, "value":"HIGH"},
{"source":"osv", "priority":80, "value":"HIGH"},
{"source":"redhat", "priority":80, "value":"MEDIUM"}]}{"field": "severity",
"value": "HIGH",
"winner_source": "nvd",
"strategy": "priority",
"candidates": [
{"source":"nvd", "priority":90, "value":"HIGH", "winner":true},
{"source":"ghsa", "priority":80, "value":"HIGH"},
{"source":"osv", "priority":80, "value":"HIGH"},
{"source":"redhat", "priority":80, "value":"MEDIUM"}]}NVD wins because it outranks the others, 90 against 80, and strategy names the rule that decided it here priority, elsewhere longest value or union across sources. Red Hat's dissenting MEDIUM isn't thrown away. It stays in the record where anyone can see it and disagree with us.
The more interesting field on that same CVE is affected_packages. NVD wins severity and returns null there — it has nothing to say about which versions are affected. OSV and GHSA both have the range. Red Hat has neither the range nor the same severity, but it does have the RHSA advisory and the fix state per RPM.
No single one of those four is the answer. That is the entire argument for merging rather than picking a favourite, and it's why we stopped trying to find the one good source.
There's an endpoint that lists the fields where sources currently disagree, which is how we found this example. You can check it yourself — public URL, no key.
Deciding what to do, not just how bad it is
Severity tells you how bad a bug is. It doesn't tell you what to do on Tuesday. So every vulnerability also carries an SSVC decision. CISA's Stakeholder-Specific Vulnerability Categorization which comes out as one of four actions: Track, Track*, Attend, Act.
The fast-xml-parser CVE resolves to Attend: "remediate sooner than your next regular cycle; involve whoever owns this dependency." The decision points that got it there are shown rather than implied — exploitation: none, automatable: yes, technical impact: total. Its exploit maturity is tracked separately, because "someone published a proof of concept" and "someone weaponised it" are not the same Tuesday.
Underneath that sits the line we're proudest of:
"Derived from attacker-side signals only (KEV listing, exploit maturity, EPSS, CVSS vector). It does not know your deployment, so mission and exposure are still yours to weigh."
We can compute the half of SSVC that comes from the attacker's side. We cannot compute the half that depends on what your service does and who can reach it, and a tool that pretended otherwise would be quietly lying. So it says so, on every record.
That action also lands in the list view, which is the part that actually changed how we work. You can sort a live vulnerability feed by what to do about it instead of by CVSS.
Aggregating isn't the interesting part
Once the join existed, we had a pile of facts about a package and still no opinion. That's where the actual work turned out to be — and it's the difference between a database and something you'd let block a build.
Take langchain. The raw fact is "24 advisories, 15 of them critical." That number on its own tells you to panic, and it would be the wrong reaction.
What we compute on top of it: a track record grade, with the sentence behind it — "24 advisories over 3.8 years, 24 of them with a published fix, weighted for recency and normalised by how widely the package is used." The grade is poor. But the median time from disclosure to fix is one day. That's not a neglected package. That's a fast-moving one with a lot of surface, maintained by people who turn fixes around in 24 hours, and no single number expresses that.
Then it resolves the advisories against the version you'd actually install, rather than counting them. lodash has ten advisories on record and the page says so plainly: none of them affect the current release. The scary number and the actionable number are different numbers, and most tools only show you the scary one.
It also walks the tree. langchain has 22 known vulnerabilities directly and 64 in its transitive dependencies, across 5 distinct licences — one of which is the SPDX expression MPL-2.0 AND (Apache-2.0 OR MIT), because real dependency trees are not tidy.
The score itself is four weighted dimensions — vulnerability out of 60, maintenance out of 25, licence out of 20, popularity out of 15 — each with its own subtotal, under a versioned rule set, so the same inputs give the same number and you can tell when we changed our minds.
And the field we're most attached to is unmeasured. If a factor has no data behind it, it doesn't quietly score zero and drag the total down — it's named as unmeasured and the confidence drops. langchain sits at medium confidence for a specific, stated reason: "One advisory's version range could not be parsed for this ecosystem, so the exposure factor is partly an estimate."
A zero we measured and a zero we invented look different, because they are different. If you only take one idea from this post, take that one.
Some of what fell out was uncomfortable. lodash is one maintainer and 167 million weekly downloads. We track release-cadence anomalies — a package dormant for a year that suddenly ships — because that shape turns up before a lot of account takeovers. And when a package is on a malware feed it says so at the top with the report ID and every affected version listed, rather than leaving it to a risk number.
The part we didn't expect
We thought the API was the product. It isn't.
Once Intel existed, the obvious move was to have the scanner call it. We did that, and it worked, and then it nagged at us. We'd replaced a database we didn't control with an API we did control. Better for us. Not obviously better for you. Your build pipeline now depended on our uptime, and you needed a key, and we were one product decision away from putting a login in front of it.
So we did the other thing.
The scanner now reads its data from files instead of calling anything. Signed SQLite snapshots, published as release assets at github.com/codeward-io/intel-db, rebuilt on a schedule.
The corpus is about 2 GB across 22 segments, and it's split so you only take your own ecosystem. A Go project pulls four segments — core, CWEs, Go packages, Go advisories — which is about 8 MB compressed. A Python project is about 12 MB, Maven about 7 MB. npm is the honest exception at roughly 88 MB, because npm genuinely is most of the corpus.
Every segment is checksummed into a manifest, and the manifest is Ed25519-signed. Verification is on by default. The attribution for the CC-BY sources is carried inside that signed manifest too, so the credit travels with the data instead of living on a page someone forgets to copy.
And for CI you don't re-download any of it. Once you have a baseline there's a delta endpoint that returns only what changed since a timestamp:
curl "https://intel.codeward.io/api/v1/db/delta?segment=vulnerabilities&since=2026-04-01T00:00:00Z"curl "https://intel.codeward.io/api/v1/db/delta?segment=vulnerabilities&since=2026-04-01T00:00:00Z"No key. No rate limit. No account. It works in air-gapped environments, because after the first download there's nothing left to reach.
We kept the API too, and it's public and needs no signup:
curl https://intel.codeward.io/api/v1/packages/npm/lodash/assessmentcurl https://intel.codeward.io/api/v1/packages/npm/lodash/assessmentWhat this is
A vulnerability, package and licence database you can use without asking us for anything. Query it over HTTP, or download the SQLite files and never talk to us again. It feeds our scanner. It'll feed yours — the API doesn't know whose client is calling, and the snapshots are just SQLite.
There's also just a website, and every filter lives in the URL. /packages?malicious=true is a live list of the 10,000-plus packages OpenSSF has confirmed malicious, sorted by weekly downloads, and it is a bracing thing to scroll. Search takes npm/lodash, CVE-2021-23337, MIT or CWE-79 and jumps straight to the record. And there's an end-of-life view covering 29 runtimes and frameworks, which also shows up per package — lodash will tell you which Node versions still have support.
What this isn't
It isn't a new vulnerability source. Nearly everything in Intel comes from public feeds other people maintain, and we say which ones on every record. The work is in the joining, the tie-breaking and the delivery, not in discovering anything. There's an attribution page that names who did the hard part.
It isn't complete, and the gap is bigger than we'd like. About 403,000 of those 879,000 vulnerability records carry no severity from any source — that's 46% sitting at unknown, and no amount of joining fixes it, because nobody upstream has rated them. Coverage is also thinner outside npm, PyPI, Go and Maven, and plenty of packages have no health data at all.
Some of it is model-generated and we label it as such. Plain-language summaries, remediation notes and reachability analysis come from LLM agents, and they carry a confidence field. We keep them strictly separate from the sourced facts, because a summary is not evidence, and we'd rather you knew which one you were reading.
It isn't a replacement for a security programme. Same as last time.
Mostly we want to know if it's useful
We built this because our own scanner needed it. We've been running on it for months and it solved our problem. That's the entire basis on which we're telling you about it — not market research, not a roadmap, just "this worked for us, maybe it works for you."
If you try it and something obvious is missing, tell us. If you point your own scanner at it, we'd genuinely like to hear what broke.
intel.codeward.io · docs · scanner · intel-db