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.
GitHub queues runs blocked by the concurrency group in order rather than
cancelling the older one, so every cron restart added a link that waited
its turn behind the chain and the queue grew by about five a day. A link
that starts and finds another already waiting now steps aside.
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.
GitHub's cron scheduler creates this repository's scheduled runs four to
five hours late and drops the slots in between, so the hourly Hot CVEs
refresh cannot come from a cron. A workflow_dispatch call creates a run
within seconds, so a pacemaker workflow now sleeps until the next :05,
fires the refresh and its own successor, and keeps time by itself. Its
cron only restarts the chain if a link fails.
Hot CVEs loses its cron and the gate that was there to coexist with an
external trigger; nothing outside GitHub is involved any more.
GitHub's cron scheduler created the hourly run about five times a day,
four to five hours late. A systemd timer on the VPS now dispatches it every
hour. The cron stays as fallback and watchdog: a gate job skips it while
the trigger is alive, lets it run when the trending table is over 55
minutes old, and fails the run once it is over three hours old so the
failure email names the cause.
Also stage docs/kev.svg in every writer that redraws it, verify the index
in the link audit before committing like the other writers do, and refresh
the apt index before installing librsvg.
Commits pushed with the workflow token raise no push event, so site.yml
only ran on its 06:20 schedule and the site trailed the index by up to a
day. workflow_dispatch is exempt from that rule, so each writer now
triggers it after a successful push.
The push retry rebased onto the fetched main and, on a conflict, left HEAD
there; the next push then succeeded as a no-op and the run went green with
its changes discarded. Abort the rebase and fail instead.
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.
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.
Moving the data files into index/ left three references behind in .github,
and the hourly job has been failing to stage its own output since. The
README has been advertising 104,016 CVEs with PoCs while the site served
89,125, and every KEV marker had dropped out of its table because the
catalogue was being read from a path that no longer exists.
Figures come from the same count build_site publishes, so the badges, the
hero card and the site cannot disagree again.
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.
Discovery only looked at the current year and the one before it, so a PoC
published this week for a 2023 CVE was never found. Exploits for older
vulnerabilities keep appearing, and the daily run has budget to spare: the
window is still three days, so the cost is five sets of search terms instead
of two against the same short window.
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.