The daily sweep found new repositories within 2 to 9 hours where their
name carried the CVE, and up to 30 hours where only the README did. The
hourly run now searches repositories pushed in the last day for this
year's and last year's CVEs, reads their READMEs and applies the same
classifier, so a PoC published this morning is on the site within the
hour. The daily run keeps covering every year and the CVE record
references; the hourly run skips those, and skipping leaves the delta
checkpoint alone.
A sample of two hundred indexed repositories held six with nothing in
them: an empty README or none, a placeholder, a link to a paywall. A
repository with no directory, no code, no file named for the CVE or as a
PoC, and a README with no code block, command, request or real write-up
is bare, and neither discovery nor the daily audit keeps it. The audit
now reads each repository's README and root to judge that, and sweeps
4,000 repositories a day instead of 1,750, since one link in eight
pointed at a deleted repository under the fortnightly cycle.
The upstream PoC-in-GitHub collection is judged daily rather than
weekly: fourteen of the last thirty-nine additions came from it, which
means it finds repositories our own search does not.
Security slang and NVD prose rarely share a word. "sqli" found 843 CVEs
where "sql injection" found 12,719, "lpe" 80 against 1,311 for "privilege
escalation". A term now expands to its phrases and an entry matches on any
of them, with the best-scoring form counted.
A vulnerability searched by the name it is known by leads with the record
it belongs to, whatever the prose and the repository names say: ghostcat
put a different record first, moveit a newer one. Forty names are mapped.
A term with no word in the index at all is nearly always a typo. The
closest word one edit away stands in, weighed by how many records use it,
and the status line says which: citrx finds citrix, fortinte fortinet,
l0g4j log4j.
CVE pages carry BreadcrumbList markup for the home, year and block trail,
and the site publishes an OpenSearch descriptor so a browser can add it
as a search engine from the address bar.
A CVE List V5 record names what it is about: a title such as "PAN-OS:
Arbitrary File Creation Leads to OS Command Injection Vulnerability in
GlobalProtect", a vendor and its products. None of it was kept, so a query
for pan-os or prisma access only found records whose prose happened to say
so. Every record now opens with the title in bold and a Vendor badge per
affected vendor, refreshed from a local CVE List checkout in one pass and
kept current by the daily sync for the records it fetches. Only the lines
above the description move; descriptions and PoC sections are untouched.
The pass also settled the heading link on cve.org and trimmed the stray
space some version badges carried.
The site index carries title, vendor and product for the 40 percent of
indexed CVEs whose records have them. Search scores a hit in those above
any position in the description, results show the title as a headline,
and each page puts it under the CVE id, in the browser title, the meta
description and an Affected row, with the block hubs listing it in place
of the description's opening.
The 35 MB metadata file the homepage needed before anything could be
filtered is split in two for the page: the CVSS rows load with the rest,
the advisories follow the index instead of competing with it for the
connection. cve_metadata.json stays whole for anyone reading the API.
A sitemap gets a page discovered; a link from a page already indexed gets
it crawled and ranked. 62,000 of the CVE pages had no internal link at
all, only a sitemap entry, since the Related block only exists where two
CVEs share a PoC author. The tree is now home, year, block, CVE: one page
per year listing its thousand-id blocks, one page per block listing every
CVE in it with its PoC count and known-exploited mark, crumbs above every
CVE title pointing back up, and a year row on the homepage. No page
carries more than a thousand links. The hubs get their own sitemap shard,
dated from the newest page they contain.
The two filter menus hid their state behind ALL and took two clicks per
value. Each severity and source is now its own chip in the row, always
visible: the first click narrows to that value, further clicks widen the
set, and switching the last lit chip off clears. Active chips use the
colours the result rows already use for the same words, and the
selection rides in the URL as sev= and src=, so a narrowed view can be
linked like a query.
Standalone CVE pages carried a smaller search box beside the wordmark
that read as another site's. They now open with the homepage bar itself,
same height, type and placeholder, under the wordmark with the index
count on the right. Enter still submits to /?q= because the pages are
static, and the form has a real submit button for screen readers.
Every writer now has a timeout, so a hung download cannot hold the
repository-updates lock for six hours and cancel the runs queued behind
it. The link audit moves from one 12,000-and-12,000 batch a week, which
measured an hour under that lock, to a seventh of it every day.
The social card PNG is drawn at deploy time and no longer committed: it
changed about four times a day at 68 KB a copy. nuclei.json stops
mirroring EPSS scores; every indexed CVE already has one in epss.json,
the site never fell back to the copy, and the daily recomputation
rewrote 3,400 lines of it each night.
The metadata sync retries GitHub advisory calls on 403, 429 and 5xx
instead of failing on the first one, and refuses to publish a year
shard that an NVD feed would shrink by a tenth or delete outright.
The per-CVE pages read as a different site and disagreed with search about
their own contents. Both traced to the page generator having grown its own
copy of rules the browser already had.
Security:
- JSON-LD was serialised with json.dumps alone, which leaves < > and & raw. A
description containing "</script>" ended the element and turned the rest into
live markup. 78 descriptions in the corpus already carry that sequence; only
the 155-character meta trim kept them out of the payload. Escape to \u00XX.
Correctness:
- The PoC count summed only the repository column, so 19,940 pages announced
"0 public PoC exploits" directly above the ExploitDB and Metasploit entries
they were listing, in the title and meta description too. Count every source
the page renders, matching POC_FIELDS in logic.js.
- The severity chip took the highest score while search took the first, so
18,434 CVEs showed one severity in the results and another on the page. Both
now take the primary assessment, with the same Nuclei fallback search uses.
- Identical CVSS rows printed two and three times on 13,340 pages. Dedupe on
(version, score, vector) and label each survivor with its assessor from
row[4]; row[5] is the assessment type and left 129 pages showing two rows
both marked "Secondary".
- repo_from_url accepted any host containing "github" and kept .git suffixes,
so a clone URL and its plain form counted as two sources on the page and one
in search. 273 CVEs disagreed. Match repoFromUrl in logic.js exactly.
- 41 CVEs nobody has scored were unreachable by search, including by exact ID:
the severity filter tested membership of a list naming only the four scored
bands, so ALL excluded them. ALL now means all.
- EPSS percentiles printed a hard-coded "th": 51th, 62th, 93th on 21,100 pages.
- Search reported "1 PoCs", and the empty-results branch returned before
updating the live region, leaving it reading "loading index..." forever.
Deploy safety:
- A failed EPSS fetch returned {} and published it, silently erasing every
score from 82,000 pages. It now raises, as does a feed covering under 90%
of the index.
Presentation:
- CVE pages never loaded the webfonts the homepage loads, so all 82,468 fell
back to system fonts. This is why they looked unlike the rest of the site.
- 404.html referenced four CSS classes that no longer existed and rendered
unstyled, and its meta refresh threw a reader out of a search after two
seconds. It is now generated from the same constants and header as every
other page, and offers the search box instead.
- The homepage and the generated footer credited different sources. Both now
come from brand.SOURCES_LINE, and the build refuses to publish a homepage
that has drifted from it.
- Three aria-labels sat on div elements with no role, where they are ignored.
SEO:
- 61,936 pages filled "Related" with numerically adjacent CVEs, which are not
related to anything. Keep only shared-author relations; 20,532 pages have one.
- Sitemap lastmod described the CVE record, so a CVE that gained forty PoCs
still advertised a date from 2021. It now reflects the page, and each shard
carries its newest page's date rather than today's.
- TechArticle is not a type Google draws a rich result from, and its dates
claimed the CVE record's publication as the page's. Use WebPage with the
vulnerability as mainEntity.
- Curated sources and the CVE and NVD records no longer carry nofollow; the
crowd-sourced repository list still does.
The About box, the meta description, the og and twitter tags and the JSON-LD all
ended in a tacked-on list of features: "Updated hourly", "Free JSON API, no
signup", "Plain JSON, no key". None of it describes what the thing is, and the
meta description ran past where a result gets truncated anyway.
Four strings collapse to two so they cannot drift apart again. SUBTITLE is the
line under the wordmark and the social description; DESCRIPTION is the meta
description, the JSON-LD description and the GitHub About box. Both say what the
site is and stop.
82,000 CVEs sat behind one URL. Each of them is a query somebody types, and the
pages answering those queries belonged to other people: searching
"CVE-2026-72898 poc" returns SentinelOne, Horizon3, Wiz and BishopFox, all of
them serving one page per CVE.
Pages are flat files at /CVE-2026-72898. No /cve/ prefix, no extension, no
trailing slash. GitHub Pages resolves an extensionless request to the .html
beside it, checked against the live site rather than assumed.
Each page carries what the index already knows and nothing invented: every CVSS
assessment with its vector, EPSS with percentile, KEV date and ransomware flag,
Nuclei severity and CWE, and the PoC links grouped by source with stars and last
push. 94% of them carry three or more of those signals.
Discovery works two ways. sitemap.xml is now an index over one shard per year,
which fits the 50,000 URL cap on the split the data already has and lets a
crawler refetch only the year that moved. Search results link to the CVE page
instead of straight out to NVD, which moves to a chip beside CVE.ORG, and each
page links six related CVEs picked by shared PoC author.
Rendering all of them takes 20s and adds nothing to the repository: the payloads
they read are already gitignored and built in CI, so the pages go into the Pages
artifact and git never sees them.
The slug moved to 0xMarcio/pocindex, so the raw.githubusercontent URLs in the
README banner were riding a redirect. SLUG now lives in brand.py with the rest
of the identity and getTrending imports it.
The About description lost "with CVSS, KEV and EPSS. Free JSON API, no signup."
GitHub truncates listings near 150 characters, so that tail rarely rendered and
read as a feature list rather than a description. 142 characters now.
The MITRE disclaimer comes out of the footer. The brand carries no MITRE mark,
which was the reason for picking it, and referring to CVE identifiers by name is
ordinary descriptive use. NVD, Tenable and Rapid7 all do it without a notice.
_urltest.html is temporary and comes out in the next commit.
The site called itself three different things: CVE PoC Hub in the title, CVE
Radar in the h1 and the social metadata, and cve on GitHub. Google had no
consistent site name to settle on, so none of the mentions compounded. CVE
Radar was taken anyway, by SOCRadar's free tool and by cveradar.com.
Every user-facing name and URL now comes from scripts/brand.py, so the next
move is one edit rather than a sweep through the tree.
Also repairs the parts of the site a crawler could not use:
- robots.txt and sitemap.xml were both 404. scripts/build_seo.py writes them,
wired into the Pages job. trending_poc.json, kev.json and stats.json stay
crawlable because the first paint needs them; the five bulk payloads are
disallowed, which is 13.2 MB per crawl spent on data no crawler queries.
- Search state lives in ?q= now. Results were unlinkable, so nobody could cite
one and there was no route for a SearchAction to declare. replaceState
rather than pushState, so typing does not bury the back button.
- A WebSite JSON-LD block carries the name Google prints as the site name.
- The footer carries the MITRE trademark notice.
Discovery asks GitHub for repositories pushed in the last three days, so a
proof of concept written for a 2019 CVE in 2021 is unreachable however long
the job runs. nomi-sec/PoC-in-GitHub has collected them daily since 1999.
It collects without filtering, which is how this index inherited its junk
the first time, so nothing arrives trusted: every candidate goes through
qualifying_repo_cves, the same gate a repository found by search must pass.
Of 6,945 candidates the gate rejected 4,110 and accepted 2,835, adding
links to 1,528 entries that had none or had fewer.
Vulhub joins the curated archives beside ExploitDB and Metasploit. It is
241 entries, and each one is a docker-compose file and reproduction steps
rather than a description of a flaw.
Candidates are judged again every week rather than remembered as rejected:
an empty shell in March is often a working exploit by June.
Twenty-eight year folders sat above the README, so anyone opening the
repository scrolled past 1999 through 2026 before reaching a word about what
it is. The entries move to cves/ and the files the pipeline reads and writes
move to index/, which leaves the root at README, LICENSE and five
directories.
Nothing about the data changes. Every script takes its two roots from the
same place, and all five syncs report zero writes against the moved tree,
which is what a correct repoint looks like.
An Oracle Critical Patch Update page, an Ubuntu Security Notice and a CERT
vulnerability note all say the same thing: a fix exists. None of them shows
anyone how the flaw works, so none belongs in a list of proofs of concept.
26,070 of them were in there, inherited from a workflow that kept every
reference a CVE record carried.
advisory_hosts.txt names the destinations, matched as host plus leading
path, and update_cves consults it so nothing of the shape returns.
securityfocus.com/bid joins them for a different reason: the service is
retired and every deep link now lands on a search box.
The list is deliberately short. Packet Storm, Full Disclosure, openwall,
bugzilla, hackerone and the exploit archives all carry working code often
enough that a host-wide rule would delete real proofs of concept, so they
stay and are judged link by link.
Wordfence templates do not exploit anything. They fetch a plugin's
readme.txt, read the version string and report that a vulnerable release is
installed: 80,706 of their 81,526 templates are that and nothing else, and
not one sends a payload. Yesterday's widening filed 35,383 of them as proofs
of concept, which is a claim the files do not support.
Judge the template rather than the repository it came from. A template whose
every request reads readme.txt, style.css or a changelog, and which never
sends a body or a payload, is a version probe and is skipped wherever it
comes from. That keeps the 382 real templates Wordfence does publish and
drops the same shape out of the community sets, which were re-hosting them.
39,973 templated CVEs becomes 4,929 and 19,504 entries give up a nuclei
section they should never have been given. The index returns to 104,184
CVEs with PoCs from an inflated 117,373.
ProjectDiscovery templates what the whole industry cares about, which leaves
most of the WordPress plugin CVEs that dominate this index with no runnable
check at all. Wordfence publishes one for nearly all of them and updates
daily; a second community set fills a further gap.
The filename pattern was the barrier. ProjectDiscovery names a template for
its CVE and nothing else, so an exact match worked; Wordfence appends the
plugin slug and an advisory uuid, and all 36,529 of its templates were being
skipped by a regex anchored to the end of the name.
4,420 CVEs templated becomes 39,973, of which 19,945 land on entries this
index carries. Ratings follow the links, so CVEs carrying a severity and
CVSS go from 3,952 to 23,897. ProjectDiscovery keeps precedence where it has
a template; the rest are read only for CVEs it does not cover.
The design asked for published and modified in the result column and the
frontend has been rendering them since the reskin, against a field nothing
supplied. Seeded from the CVE Program's daily release: 154,160 of 154,166
CVEs, so coverage on the indexed set is total.
The 590 MB archive is a one-off. Every record the delta log hands the daily
sync afterwards already carries its own dates, so update_cves keeps the file
current from work it was doing anyway.
Costs 0.86 MB gzipped on the index.
A module's CVE list comes from its own declared references, so an exploit
that chains two flaws should be linked under both. A scanner is different:
"SSL/TLS Version Detection" cites BEAST, RC4, POODLE and Logjam because it
reports on them, and CVE-2014-3566 was carrying it as exploit code. So were
"SSH Username Enumeration" and "JBoss Vulnerability Scanner".
Keep auxiliary and post modules only where they name a single CVE, which is
the shape of an auxiliary module that is itself the exploit; exploit modules
keep every CVE they declare. 3,542 links become 3,339, 86 entries lose a
Metasploit section entirely and 51 lose part of one.
Metasploit also ships CVE-1012-5358 for CVE-2012-5358. The regex accepted
it; an impossible year is now rejected before it can invent a 1012/
directory.
Four gaps, measured against the index before any of this was written.
ExploitDB and Metasploit are folded in the way nuclei templates were. ExploitDB
covers 25,066 CVEs and adds 30,327 links across 17,237 entries; Metasploit
covers 3,178 and adds 3,520 across 2,550. Together with the templates the index
goes from 101,809 CVEs carrying a PoC to 103,945. Neither archive skews recent,
which is why nuclei went first, but both are the difference between an old CVE
having nothing and having something that runs. Each keeps its own section and
its own field, and each leads the list ahead of the repositories: a module, an
entry and a template all run as they stand, without reading somebody's code.
EPSS now covers the index rather than the 4,274 CVEs a template happened to
rate. FIRST's daily feed scores 103,783 of 103,945, so a result can say how
likely a flaw is to be exploited in the next thirty days, not only that code
exists for it. It is fetched during the site build and never tracked here: a
daily rescore of 366,000 rows would be nothing but churn.
The trending tables also stop hiding what they exist to show. The star floor
excluded 1,059 of the 1,312 2026 PoC repositories pushed this quarter, and a
repository published this morning has no stars, so the newest row on the front
page was six days old. A separate lane drops the floor entirely and pays for it
with a ten day window, a cap of ten rows, and the same paperwork check the rest
of the table uses. The newest row is now hours old.
A nuclei template is a runnable check for one CVE, which makes it a proof of
concept whether or not anyone published a repository for it. Measured against
the index: 3,952 CVEs gain a template link and 858 of those had no PoC at all,
584 of the additions falling in 2024 or later. That last figure is the point.
ExploitDB carries more CVEs overall and Metasploit is better weaponised, but
both skew historical; nuclei is where coverage of a recent CVE actually shows
up first.
Templates also carry the ratings the entries never had. Severity, CVSS with
its vector, EPSS with its percentile and CWE now sit in nuclei.json for the
site to read: 4,193 of 4,418 templates state a CVSS score and 4,274 an EPSS
score. A result can finally say how bad a flaw is and how likely it is to be
used, rather than only that somebody wrote code for it.
Templates are kept in their own section and their own field. They are not
somebody's repository and should not be ranked, starred or credited as one,
but they do lead the PoC list, being the one link that runs as it stands.
The index files are also revalidated rather than reused from cache. They are
rebuilt hourly and a returning visitor was holding whatever it downloaded
first, which cost a stale index here until the copy was thrown away by hand.
KEV reached the site three days ago but nothing in the markdown said a
vulnerability was under active attack, and the repository is what most
people actually read. 1,595 entries now carry a red CISA KEV shield beside
their product and vulnerability badges, marked for ransomware use where
CISA records it.
update_cves writes the badge on entries it creates, and the weekly audit
reconciles the whole set against the refreshed catalogue, so an entry CISA
adds gains the badge and one it withdraws loses it. Re-running the sync is
a no-op.
Every colour is now a Primer dark_default token. The header band, its four
capability pills and the heading above the trending table are gone, so the
search field is the first thing on the page and the switcher labels its own
table. Rows tighten to 10px, descriptions clamp to two lines, and the PoC
list loses its box for a hairline rule.
Blue is the only thing you can click. Amber means 500 stars or more and red
means known-exploited, and nothing else carries a hue: the star glyph is
dimmed at every value so the column reads as numbers rather than symbols.
The handoff's placeholder wanted an em dash. It reads "Search CVE, vendor,
product or keyword, exclude with -windows" with the middot the count line
already uses, and the six em dashes I had left in code comments are gone
too.
The first full pass over all 60,039 reference URLs: 1,566 answer 404 or
410, which removes 2,663 links from 2,419 entries and 2,445 lines from
references.txt. Anything that timed out, refused a HEAD or answered 403
kept its link.
The weekly job takes it from here on a five-week cycle, so this only had to
be paid once.
A timeout kept the link either way, but the checker still followed a failed
HEAD with a GET to the same dead host, so every unreachable URL cost two
full timeouts. Return on the first network failure and drop the timeout to
eight seconds: a measured 1,500 references went from roughly three URLs a
second to fifteen.
That pass removed 98 links across 86 inventory lines, and the weekly budget
rises to 12,000 references, which sweeps the corpus every five weeks.
The audit only ever asked GitHub about repositories, leaving 60,112
reference URLs unchecked since the corpus was inherited. Sampling put hard
404s at 4%, and one host accounts for a fifth of that on its own: CIS
retired the OVAL Repository, so all 942 links to it are permanently gone
and are removed in the next commit.
The rest is diffuse, so it needs sweeping rather than a rule. Check an
eighth of the URLs each week alongside the repositories, ordered
round-robin by host so a slice does not arrive at one server as a burst.
Only a 404 or 410 removes a link: a timeout, a refused HEAD or a 403 keeps
it, and update_cves already applies the same test to references it
discovers, so this holds inherited links to the standard new ones meet.
An advisory copied from a GitHub Security Advisory opens with "### Impact"
or "### Summary". The section parser treated any heading as a boundary, so
those descriptions ended themselves and reached the site empty. Only the
four headings an entry actually uses end a section now, and leading heading
markers are stripped when the text is rendered.
A trailing slash made exploit-db.com/exploits/41034 and .../41034/ two
different links, which put 53 duplicate rows in front of readers.
github.com also serves attachments and advisories, and 103 of those were
being parsed as owner/repo -- rendering "user-attachments / files" with an
empty star column instead of the PoC archive they point to.
CISA publishes a catalogue of vulnerabilities observed being exploited in
the wild, and 1,111 of its 1,685 entries are CVEs this index already holds
PoCs for. That is the one fact a reader most wants before opening an
exploit, and it was nowhere on the page.
The weekly audit stores the catalogue, the build trims it to the CVEs the
index carries, and a result row shows a KEV chip -- with the ransomware
flag where CISA records one, which covers 352 of them. 37 KB served.
A failed fetch keeps the stored catalogue rather than dropping the badge
off every entry. The chip reuses the header pill's treatment: the palette
still spends green on things you can click and amber on star counts only.
The PoC vocabulary was English-only, so a repository describing itself as
"漏洞利用" — an exploit — read as evidence that it was not one. Chinese is
the second language of this corpus by a wide margin, and the same gap
covered Spanish, Portuguese, French, Russian, Japanese and Korean.
NON_POC_RE also vetoed "prueba", which is the proof in "prueba de
concepto", inverting the signal for Spanish repositories entirely.
On the 600-repository discovery sample this admits two more genuine PoCs
and nothing else.
The audit already asks GitHub about every linked repository, so the same
query can carry back a star count and a last-push date for free. Results go
to repo_meta.json, which lets a search result say which of two hundred
linked PoCs is worth opening.
2.1 MB for 45,008 repositories, refreshed weekly.