Commit Graph
64 Commits
Author SHA1 Message Date
0xMarcio 89fbf68e21 Fix workflow queues and index consistency 2026-10-06 21:09:05 +02:00
0xMarcio c8f213af17 Find this year's PoCs within the hour and stop indexing bare shells
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.
2026-09-04 03:46:23 +02:00
0xMarcio 5f918199ab Keep the pacemaker queue to one link
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.
2026-09-04 02:45:40 +02:00
0xMarcio 46b5acf052 Tighten the writers: timeouts, a shorter audit, fewer bytes in git
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.
2026-09-04 00:36:16 +02:00
0xMarcio e039cf8b62 Name the repository when the pacemaker dispatches
The pacemaker never checks the repository out, so gh had no git context to
infer the repository from and the first link failed after its sleep.
2026-09-03 23:05:44 +02:00
0xMarcio 82936168a5 Keep the hourly refresh on GitHub with a pacemaker chain
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.
2026-09-03 23:02:52 +02:00
0xMarcio c15ee7212c Make the hourly Hot CVEs trigger permanent
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.
2026-09-03 22:55:32 +02:00
0xMarcio 3041d9b90e Dispatch the site build after every bot commit
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.
2026-09-03 22:21:48 +02:00
0xMarcio 0e5e8d7e8c Fix correctness, security and consistency bugs across the generated site
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.
2026-09-02 17:17:28 +02:00
0xMarcio 9b9957ea7f Give every CVE its own page at the site root
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.
2026-09-02 04:00:28 +02:00
0xMarcio 31da22381c Rebrand to PoC Index and move to pocindex.io
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.
2026-09-02 03:36:22 +02:00
0xMarcio aad1789a71 Keep published counts synchronized 2026-09-02 00:54:09 +02:00
0xMarcio 8fe2bf2d04 Index verified advisory PoCs and expand CVSS sources 2026-09-01 23:17:04 +02:00
0xMarcio e534a2d0d9 Avoid duplicate Pages deployments 2026-09-01 20:01:14 +02:00
0xMarcio 796d813803 Persist CVE discovery state 2026-09-01 19:08:30 +02:00
0xMarcio 81017c9871 Expand CVE PoC coverage and metadata 2026-09-01 19:01:07 +02:00
0xMarcio f140e13160 Repair the README pipeline after the move
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.
2026-09-01 15:50:08 +02:00
0xMarcio 2821985dd9 Reach the PoCs published before the search window opens
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.
2026-09-01 15:21:53 +02:00
0xMarcio bafbb8d4d6 Scan five years of CVEs for new PoCs, not two
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.
2026-09-01 14:43:39 +02:00
0xMarcio a7449da61b Move the year directories out of the repository root
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.
2026-09-01 14:43:38 +02:00
0xMarcio a150d3d1b9 Give every entry its publication dates
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.
2026-09-01 13:44:47 +02:00
0xMarcio 84da5a5fc6 Index ExploitDB and Metasploit, score everything with EPSS, and stop hiding new PoCs
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.
2026-09-01 05:24:39 +02:00
0xMarcio ff11c84652 Stage the nuclei sync with a pathspec git accepts
git would not expand [12][0-9][0-9][0-9] as a pathspec, so the job did its
work and then died on the commit. Everything it touches is already tracked
apart from nuclei.json, so stage tracked changes and name that one file.
2026-09-01 05:12:57 +02:00
0xMarcio 35cb1e6962 Index nuclei templates as PoCs and take their ratings with them
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.
2026-09-01 05:11:31 +02:00
0xMarcio de770d6c6f Bring the site up to the README and fix the unreadable text
The page and the README had drifted into looking like two projects. The site
now opens on the same card the README banner draws, built as markup rather
than a second copy of the image, so it fills itself from the index totals the
page already loads and cannot go stale.

Contrast was the real problem. --neutral sat at 4.12:1 against the canvas,
under the 4.5 floor, and it carried the search placeholder, the star glyph and
every separator; descriptions were rendered in the muted token at 6.15:1 while
doing the work of body copy. Descriptions move to a subtle token at 12.26:1,
muted and neutral both come up, and links go to the brighter accent at 7.49:1.
The column headers were 9.5px mono under a tenth of an em of tracking, which
no contrast ratio was going to rescue.

Known-exploited CVEs are marked in the trending table too, not only inside a
result, since which row to read first is the one thing the table could not
say. The README swaps the fire emoji for the same KEV pill, served from the
repository so every flagged row points at one cached file.

The search field no longer repeats the totals now that the hero carries them,
and keeps its slot for the match timing.
2026-09-01 04:46:30 +02:00
0xMarcio 2ed7386028 Give the README a banner and cut it back to the useful parts
The README opened on a bare heading and five year tables, said nothing about
what the project is, and never linked the site. Anyone landing on the repo had
to guess. It now leads with a generated banner carrying the live index totals,
a badge row, and tables trimmed to what fits a table cell.

hero.py draws the banner and the social card as self-contained SVG, since
GitHub proxies README images and drops anything the file has to fetch for
itself. It counts the corpus through build_site's own parser rather than a
second guess at what counts as a PoC, so the banner cannot drift from the site
header, and leaves the figures in docs/stats.json for the README build to
quote. If the count fails it falls back to the deployed site and, failing
that, writes nothing at all: a banner reading zero is worse than yesterday's.

Badges describing the index are baked in at build time so they always match
the banner and cannot degrade to "resource not found"; the two that describe
live GitHub state carry a stamp that defeats the image proxy cache.

The sweep now runs hourly instead of every six hours. It takes under two
minutes and the repository shows up in "recently updated" far more often.

Also adds the OpenGraph tags the site never had, so a shared link renders a
card instead of a bare URL.
2026-09-01 04:28:33 +02:00
0xMarcio b6d3f380ca Stop paying twice for an unreachable host
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.
2026-08-31 01:45:23 +02:00
0xMarcio 66a4f41227 Check reference links for rot, not just repositories
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.
2026-08-31 00:27:34 +02:00
0xMarcio 1aa51fa852 Mark the CVEs attackers are actually using
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.
2026-08-31 00:13:01 +02:00
0xMarcio 576b0e4735 Collect stars and last push during the weekly audit
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.
2026-08-30 21:53:20 +02:00
0xMarcio 5452bc8353 Report when a PoC repository last shipped code
The trending table sorted and displayed updated_at, which also moves when a
repository merely gains a star, so a four-month-old exploit was claiming it
had been updated three hours ago. Record pushed_at and created_at and let
the page decide: last commit for the age it shows, repository age for the
trending rank.

The job now writes trending.json alongside the README table, so the site
reads the data instead of regex-parsing markdown the same job generated.
2026-08-30 21:53:19 +02:00
0xMarcio 6d65ae36bb Audit PoC links against GitHub weekly
Sampling the corpus put 17% of the repositories it links to behind a 404,
so readers were following links to repositories that no longer exist. The
previous audit script was 630 lines, never wired to anything, and kept its
state under a gitignored directory.

Replace it with a check that batches repository lookups through GraphQL
and removes a link only when GitHub answers NOT_FOUND for it, so a
throttle or a network error can never drop one. A quarter of the corpus
runs each week, which stays inside the token's hourly budget and sweeps
everything within the month.
2026-08-30 21:09:35 +02:00
0xMarcio 68d5c9b8f4 Replay bot commits when a push is rejected
The two scheduled jobs write disjoint paths but pin their checkout when
they are queued, so a commit landing in between throws away a completed
run. Rebase onto the current main and retry instead, deepening the clone
only on the rare occasion it is needed.
2026-08-30 17:04:04 +02:00
0xMarcio f937e5595c Authenticate the trending search and keep the token out of git
The search API was called anonymously on a 10 requests/minute limit while
the workflow already had a token; send it as an Authorization header,
never in the URL, and never print it. Dropping the token from the origin
remote also stops it being written into .git/config on the runner, since
actions/checkout already provides push credentials.

Rewrite on urllib so the job installs nothing, retry rate limits and 5xx,
and fail loudly instead of silently publishing a README with a year
missing. Repository text is flattened so a pipe cannot break the table.
2026-08-30 16:59:42 +02:00
0xMarcio f9fc5ae28d Stop tracking generated site data
Pages builds from the workflow artifact, so the committed CVE_list.json
was never served while adding a fresh 53 MB blob to history every day.
Generate it during the Pages build instead and drop the duplicate
generator; scripts/build_site.py is now the only thing that writes it.

The jinja2 templates re-rendered a trending table that logic.js replaces
on load, so the page shell goes back to being a plain static file and the
Pages build no longer installs anything. Shallow-clone the sync job too:
it never reads git history, and the three-day lookback already recovers
a skipped day.
2026-08-30 16:59:31 +02:00
0xMarcio c8fbbc40b2 Clean and verify CVE PoC sources 2026-08-29 04:29:03 +02:00
0xMarcio 189a0f24df Update Pages actions 2026-08-29 00:32:01 +02:00
0xMarcio d2dc891620 Deploy site after CVE sync 2026-08-29 00:27:01 +02:00
0xMarcio a62cda8ec8 Automate CVE PoC ingestion 2026-08-29 00:21:06 +02:00
0xMarcio 5c78e585a1 Simplify site to search + trending 2025-12-18 07:13:08 +01:00
0xMarcio 5cb3a28aeb Restore README trending tables and auto-build site 2025-12-17 21:39:16 +01:00
0xMarcio 722d7261ba Filter trending PoCs to current-year updates 2025-12-17 21:06:33 +01:00
0xMarcio 23be2e0751 Fix trending PoCs rendering and simplify frontend 2025-12-17 20:54:34 +01:00
0xMarcio 1f0cd8e78b Add PoC pipeline with blacklist filtering and Pages build 2025-12-17 15:53:37 +01:00
0xMarcio 02a23e8ef5 Add scheduled Pages build for CVE radar 2025-12-17 13:59:51 +01:00
0xMarcio 0c3e517733 Automate CVE sync and daily PoC refresh 2025-09-19 00:48:01 +02:00
Marc 856c5475f6 Update hot_cves.yml 2025-02-03 10:19:08 +01:00
Marc ceaaf6cfdb Update hot_cves.yml 2024-09-27 14:58:03 +02:00
Marc aba83e1e34 deployment workflow 2024-06-10 14:58:02 +02:00
Marc 410e68ca9d Update hot_cves.yml 2024-06-10 10:48:26 +02:00