diff --git a/.github/getTrending.py b/.github/getTrending.py index 00960039ba6..66106eec039 100644 --- a/.github/getTrending.py +++ b/.github/getTrending.py @@ -45,6 +45,7 @@ USER_AGENT = "0xMarcio-cve-trending" ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir) sys.path.insert(0, os.path.join(ROOT, "scripts")) +from brand import BRAND, SITE from update_cves import load_blacklist, qualifying_repo_cves README = os.path.join(ROOT, "README.md") @@ -506,11 +507,11 @@ def refresh_count_header() -> int: badges = { "CVEs with PoCs": pill( "CVEs with PoCs", f"{counts['with_pocs']:,}", "2f81f7", - "https://cve.codepwn.win/", + f"{SITE}/", ), "known exploited": pill( "known exploited", f"{counts['kev']:,}", "f85149", - "https://cve.codepwn.win/", + f"{SITE}/", ), } badge_replacements = 0 @@ -536,7 +537,7 @@ def header(stamp: str, synced: str) -> list[str]: because GitHub proxies README images and would otherwise serve a stale copy of a file that is redrawn whenever the index moves.""" counts = figures() - home = "https://cve.codepwn.win/" + home = f"{SITE}/" badges = [ pill("last sync", synced, "2f81f7", f"https://github.com/{SLUG}/commits/main"), live_pill("actions/workflow/status/" + SLUG + "/hot_cves.yml", "CI", "2f81f7", @@ -551,11 +552,11 @@ def header(stamp: str, synced: str) -> list[str]: return [ '
', "", - f'CVE Radar', + f'{BRAND}', "", " ".join(badges), "", - f'Search CVE Radar', + f'Search {BRAND}', "", "
", "", @@ -570,25 +571,25 @@ Every file is plain JSON on the CDN. No key, no rate limit. ```bash # everything the index knows about one CVE -curl -s https://cve.codepwn.win/CVE_list.json \\ +curl -s @SITE@/CVE_list.json \\ | jq '.[] | select(.cve == "CVE-2021-44228") | {cve, poc: (.poc | length), nuclei, msf, edb, vulhub, collections}' # every published CVSS assessment plus vetted advisory links -curl -s https://cve.codepwn.win/cve_metadata.json | jq '."CVE-2021-44228"' +curl -s @SITE@/cve_metadata.json | jq '."CVE-2021-44228"' # likelihood of exploitation in the next 30 days -curl -s https://cve.codepwn.win/epss.json | jq '."CVE-2021-44228"' +curl -s @SITE@/epss.json | jq '."CVE-2021-44228"' # stars and last push for one PoC repository; repository keys are lowercased -curl -s https://cve.codepwn.win/repo_meta.json | jq '."sfewer-r7/cve-2026-55040"' +curl -s @SITE@/repo_meta.json | jq '."sfewer-r7/cve-2026-55040"' ``` What CISA says is being exploited, that also has a PoC here, ranked by how likely each is to be used next: ```bash -curl -s https://cve.codepwn.win/kev.json -o kev.json -curl -s https://cve.codepwn.win/epss.json -o epss.json +curl -s @SITE@/kev.json -o kev.json +curl -s @SITE@/epss.json -o epss.json jq -n --slurpfile kev kev.json --slurpfile epss epss.json \\ '[$kev[0] | keys[] | select($epss[0][.]) | {cve: ., epss: $epss[0][.][0]}] | sort_by(-.epss) | .[:10]' @@ -596,13 +597,13 @@ jq -n --slurpfile kev kev.json --slurpfile epss epss.json \\ | Endpoint | Holds | | --- | --- | -| [`CVE_list.json`](https://cve.codepwn.win/CVE_list.json) | Every CVE with a linked PoC, its description and its `poc`, `nuclei`, `msf`, `edb`, `vulhub` and `collections` links | -| [`cve_metadata.json`](https://cve.codepwn.win/cve_metadata.json) | NVD CVSS v2.0, v3.0, v3.1 and v4.0 assessments with vectors and vetted advisory links | -| [`epss.json`](https://cve.codepwn.win/epss.json) | Exploitation probability and percentile, for nearly every CVE indexed | -| [`nuclei.json`](https://cve.codepwn.win/nuclei.json) | Template metadata for the CVEs covered by a runnable Nuclei check | -| [`kev.json`](https://cve.codepwn.win/kev.json) | CISA known exploited, keyed by CVE id | -| [`repo_meta.json`](https://cve.codepwn.win/repo_meta.json) | Stars and last push date per PoC repository, keys lowercased | -| [`trending_poc.json`](https://cve.codepwn.win/trending_poc.json) | Trending repositories plus index totals | +| [`CVE_list.json`](@SITE@/CVE_list.json) | Every CVE with a linked PoC, its description and its `poc`, `nuclei`, `msf`, `edb`, `vulhub` and `collections` links | +| [`cve_metadata.json`](@SITE@/cve_metadata.json) | NVD CVSS v2.0, v3.0, v3.1 and v4.0 assessments with vectors and vetted advisory links | +| [`epss.json`](@SITE@/epss.json) | Exploitation probability and percentile, for nearly every CVE indexed | +| [`nuclei.json`](@SITE@/nuclei.json) | Template metadata for the CVEs covered by a runnable Nuclei check | +| [`kev.json`](@SITE@/kev.json) | CISA known exploited, keyed by CVE id | +| [`repo_meta.json`](@SITE@/repo_meta.json) | Stars and last push date per PoC repository, keys lowercased | +| [`trending_poc.json`](@SITE@/trending_poc.json) | Trending repositories plus index totals | | [`cves/2026/CVE-2026-68138.md`](cves/2026/CVE-2026-68138.md) | Markdown copy of one CVE, one directory per year | CVSS rows are `[version, score, severity, vector, source, assessment type]`. @@ -641,7 +642,7 @@ Advisory rows are `[URL, NVD reference tags]`. Missing PoC, wrong link, dead repository: open an issue with the CVE id and the repository URL. -""" +""".replace("@SITE@", SITE) def main() -> int: diff --git a/.github/hero.py b/.github/hero.py index a86d6b3362f..395e81b3153 100644 --- a/.github/hero.py +++ b/.github/hero.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Render the CVE Radar hero banner and social card as self-contained SVG. +"""Render the PoC Index hero banner and social card as self-contained SVG. Both cards are plain SVG with no external font, image or stylesheet reference: GitHub proxies README images through camo, which fetches the file on its own and @@ -11,12 +11,17 @@ from __future__ import annotations import importlib.util import json import os +import sys from urllib import request ROOT = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir)) BUILD_SITE = os.path.join(ROOT, "scripts", "build_site.py") -LIVE_STATS = "https://cve.codepwn.win/trending_poc.json" -LIVE_KEV = "https://cve.codepwn.win/kev.json" +sys.path.insert(0, os.path.join(ROOT, "scripts")) + +from brand import BRAND, SITE, SUBTITLE, host + +LIVE_STATS = f"{SITE}/trending_poc.json" +LIVE_KEV = f"{SITE}/kev.json" HERO = os.path.join(ROOT, "docs", "hero.svg") SOCIAL = os.path.join(ROOT, "docs", "social-card.svg") STATS = os.path.join(ROOT, "docs", "stats.json") @@ -122,16 +127,16 @@ def hero(counts: dict) -> str: ) ) return f""" + role="img" aria-label="{BRAND}, {group(counts['with_pocs'])} CVEs with public proof-of-concept exploits"> {sweep(1090, 90, 160, 0.3)} CVE RADAR + fill="{FG}" letter-spacing="2">{BRAND} - Public proof-of-concept exploits, indexed by CVE. - cve.codepwn.win + {SUBTITLE} + {host()} {blocks} @@ -149,22 +154,22 @@ def social(counts: dict) -> str: ) ) return f""" + role="img" aria-label="{BRAND}"> {sweep(1000, 300, 220, 0.26)} CVE RADAR + fill="{FG}" letter-spacing="3">{BRAND} - Public proof-of-concept exploits, indexed by CVE. + {SUBTITLE} {blocks} cve.codepwn.win + letter-spacing="1">{host()} """ diff --git a/.github/workflows/audit_poc_links.yml b/.github/workflows/audit_poc_links.yml index 329ca96e4bb..9e89e108439 100644 --- a/.github/workflows/audit_poc_links.yml +++ b/.github/workflows/audit_poc_links.yml @@ -40,7 +40,7 @@ jobs: - name: Check for changes and commit run: | git config user.name 'CVE-BOT' - git config user.email 'cve@codepwn.win' + git config user.email 'bot@pocindex.io' git add --all -- \ .github/link_audit_state.json \ diff --git a/.github/workflows/hot_cves.yml b/.github/workflows/hot_cves.yml index 3abcbe13fad..4baa24ec039 100644 --- a/.github/workflows/hot_cves.yml +++ b/.github/workflows/hot_cves.yml @@ -47,7 +47,7 @@ jobs: - name: Check for changes and commit run: | git config user.name 'CVE-BOT' - git config user.email 'cve@codepwn.win' + git config user.email 'bot@pocindex.io' git add --all -- \ README.md \ index/trending.json \ diff --git a/.github/workflows/site.yml b/.github/workflows/site.yml index 59e7bf31575..8e72d1aa676 100644 --- a/.github/workflows/site.yml +++ b/.github/workflows/site.yml @@ -1,4 +1,4 @@ -name: Build & Deploy CVE Radar +name: Build & Deploy PoC Index on: schedule: @@ -32,6 +32,9 @@ jobs: - name: Build site data run: python3 scripts/build_site.py + - name: Build robots.txt and sitemap.xml + run: python3 scripts/build_seo.py + - name: Verify published counts run: | python3 .github/hero.py diff --git a/.github/workflows/sync_collections.yml b/.github/workflows/sync_collections.yml index 8b975ba4c05..cd40a1815a6 100644 --- a/.github/workflows/sync_collections.yml +++ b/.github/workflows/sync_collections.yml @@ -40,7 +40,7 @@ jobs: - name: Check for changes and commit run: | git config user.name 'CVE-BOT' - git config user.email 'cve@codepwn.win' + git config user.email 'bot@pocindex.io' git add --all -- \ README.md \ docs/hero.svg \ diff --git a/.github/workflows/sync_cve_pocs.yml b/.github/workflows/sync_cve_pocs.yml index 2258a39e702..b7675033b50 100644 --- a/.github/workflows/sync_cve_pocs.yml +++ b/.github/workflows/sync_cve_pocs.yml @@ -42,7 +42,7 @@ jobs: - name: Check for changes and commit run: | git config user.name 'CVE-BOT' - git config user.email 'cve@codepwn.win' + git config user.email 'bot@pocindex.io' git add --all -- \ .github/cve_sync_state.json \ diff --git a/.github/workflows/sync_exploits.yml b/.github/workflows/sync_exploits.yml index a70a38f4513..55fa3a990ca 100644 --- a/.github/workflows/sync_exploits.yml +++ b/.github/workflows/sync_exploits.yml @@ -40,7 +40,7 @@ jobs: - name: Check for changes and commit run: | git config user.name 'CVE-BOT' - git config user.email 'cve@codepwn.win' + git config user.email 'bot@pocindex.io' git add --all -- \ README.md \ docs/hero.svg \ diff --git a/.github/workflows/sync_metadata.yml b/.github/workflows/sync_metadata.yml index e2bf9d2b661..26289848af4 100644 --- a/.github/workflows/sync_metadata.yml +++ b/.github/workflows/sync_metadata.yml @@ -57,7 +57,7 @@ jobs: - name: Check for changes and commit run: | git config user.name 'CVE-BOT' - git config user.email 'cve@codepwn.win' + git config user.email 'bot@pocindex.io' git add -- \ README.md \ cves/ \ diff --git a/.github/workflows/sync_nuclei.yml b/.github/workflows/sync_nuclei.yml index fa89c1e4c35..4ecb526d6fb 100644 --- a/.github/workflows/sync_nuclei.yml +++ b/.github/workflows/sync_nuclei.yml @@ -40,7 +40,7 @@ jobs: - name: Check for changes and commit run: | git config user.name 'CVE-BOT' - git config user.email 'cve@codepwn.win' + git config user.email 'bot@pocindex.io' git add --all -- \ README.md \ docs/hero.svg \ diff --git a/.github/workflows/sync_pocingithub.yml b/.github/workflows/sync_pocingithub.yml index d4fc61c2559..58d1626a75a 100644 --- a/.github/workflows/sync_pocingithub.yml +++ b/.github/workflows/sync_pocingithub.yml @@ -46,7 +46,7 @@ jobs: - name: Check for changes and commit run: | git config user.name 'CVE-BOT' - git config user.email 'cve@codepwn.win' + git config user.email 'bot@pocindex.io' git add --all -- \ README.md \ docs/hero.svg \ diff --git a/.gitignore b/.gitignore index 5a53b5e2b91..11fbe576a69 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,8 @@ docs/nuclei.json docs/cve_metadata.json docs/epss.json docs/api/v1/cve/ +docs/robots.txt +docs/sitemap.xml __pycache__/ # Assistant tooling. None of it belongs in the repository. diff --git a/README.md b/README.md index ed4d97a4fb7..af74d844ef4 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@
-CVE Radar +PoC Index -[![last sync](https://img.shields.io/badge/last%20sync-01%20Sep%202026%2023%3A15%20UTC-2f81f7?style=flat-square&labelColor=161b22)](https://github.com/0xMarcio/cve/commits/main) [![CI](https://img.shields.io/github/actions/workflow/status/0xMarcio/cve/hot_cves.yml?style=flat-square&label=CI&color=2f81f7&labelColor=161b22&_=202609012315)](https://github.com/0xMarcio/cve/actions/workflows/hot_cves.yml) [![CVEs with PoCs](https://img.shields.io/badge/CVEs%20with%20PoCs-82%2C409-2f81f7?style=flat-square&labelColor=161b22)](https://cve.codepwn.win/) [![known exploited](https://img.shields.io/badge/known%20exploited-1%2C181-f85149?style=flat-square&labelColor=161b22)](https://cve.codepwn.win/) [![stars](https://img.shields.io/github/stars/0xMarcio/cve?style=flat-square&label=stars&color=e3b341&labelColor=161b22&_=202609012315)](https://github.com/0xMarcio/cve/stargazers) +[![last sync](https://img.shields.io/badge/last%20sync-01%20Sep%202026%2023%3A15%20UTC-2f81f7?style=flat-square&labelColor=161b22)](https://github.com/0xMarcio/cve/commits/main) [![CI](https://img.shields.io/github/actions/workflow/status/0xMarcio/cve/hot_cves.yml?style=flat-square&label=CI&color=2f81f7&labelColor=161b22&_=202609012315)](https://github.com/0xMarcio/cve/actions/workflows/hot_cves.yml) [![CVEs with PoCs](https://img.shields.io/badge/CVEs%20with%20PoCs-82%2C409-2f81f7?style=flat-square&labelColor=161b22)](https://pocindex.io/) [![known exploited](https://img.shields.io/badge/known%20exploited-1%2C181-f85149?style=flat-square&labelColor=161b22)](https://pocindex.io/) [![stars](https://img.shields.io/github/stars/0xMarcio/cve?style=flat-square&label=stars&color=e3b341&labelColor=161b22&_=202609012315)](https://github.com/0xMarcio/cve/stargazers) -Search CVE Radar +Search PoC Index
@@ -107,25 +107,25 @@ Every file is plain JSON on the CDN. No key, no rate limit. ```bash # everything the index knows about one CVE -curl -s https://cve.codepwn.win/CVE_list.json \ +curl -s https://pocindex.io/CVE_list.json \ | jq '.[] | select(.cve == "CVE-2021-44228") | {cve, poc: (.poc | length), nuclei, msf, edb, vulhub, collections}' # every published CVSS assessment plus vetted advisory links -curl -s https://cve.codepwn.win/cve_metadata.json | jq '."CVE-2021-44228"' +curl -s https://pocindex.io/cve_metadata.json | jq '."CVE-2021-44228"' # likelihood of exploitation in the next 30 days -curl -s https://cve.codepwn.win/epss.json | jq '."CVE-2021-44228"' +curl -s https://pocindex.io/epss.json | jq '."CVE-2021-44228"' # stars and last push for one PoC repository; repository keys are lowercased -curl -s https://cve.codepwn.win/repo_meta.json | jq '."sfewer-r7/cve-2026-55040"' +curl -s https://pocindex.io/repo_meta.json | jq '."sfewer-r7/cve-2026-55040"' ``` What CISA says is being exploited, that also has a PoC here, ranked by how likely each is to be used next: ```bash -curl -s https://cve.codepwn.win/kev.json -o kev.json -curl -s https://cve.codepwn.win/epss.json -o epss.json +curl -s https://pocindex.io/kev.json -o kev.json +curl -s https://pocindex.io/epss.json -o epss.json jq -n --slurpfile kev kev.json --slurpfile epss epss.json \ '[$kev[0] | keys[] | select($epss[0][.]) | {cve: ., epss: $epss[0][.][0]}] | sort_by(-.epss) | .[:10]' @@ -133,13 +133,13 @@ jq -n --slurpfile kev kev.json --slurpfile epss epss.json \ | Endpoint | Holds | | --- | --- | -| [`CVE_list.json`](https://cve.codepwn.win/CVE_list.json) | Every CVE with a linked PoC, its description and its `poc`, `nuclei`, `msf`, `edb`, `vulhub` and `collections` links | -| [`cve_metadata.json`](https://cve.codepwn.win/cve_metadata.json) | NVD CVSS v2.0, v3.0, v3.1 and v4.0 assessments with vectors and vetted advisory links | -| [`epss.json`](https://cve.codepwn.win/epss.json) | Exploitation probability and percentile, for nearly every CVE indexed | -| [`nuclei.json`](https://cve.codepwn.win/nuclei.json) | Template metadata for the CVEs covered by a runnable Nuclei check | -| [`kev.json`](https://cve.codepwn.win/kev.json) | CISA known exploited, keyed by CVE id | -| [`repo_meta.json`](https://cve.codepwn.win/repo_meta.json) | Stars and last push date per PoC repository, keys lowercased | -| [`trending_poc.json`](https://cve.codepwn.win/trending_poc.json) | Trending repositories plus index totals | +| [`CVE_list.json`](https://pocindex.io/CVE_list.json) | Every CVE with a linked PoC, its description and its `poc`, `nuclei`, `msf`, `edb`, `vulhub` and `collections` links | +| [`cve_metadata.json`](https://pocindex.io/cve_metadata.json) | NVD CVSS v2.0, v3.0, v3.1 and v4.0 assessments with vectors and vetted advisory links | +| [`epss.json`](https://pocindex.io/epss.json) | Exploitation probability and percentile, for nearly every CVE indexed | +| [`nuclei.json`](https://pocindex.io/nuclei.json) | Template metadata for the CVEs covered by a runnable Nuclei check | +| [`kev.json`](https://pocindex.io/kev.json) | CISA known exploited, keyed by CVE id | +| [`repo_meta.json`](https://pocindex.io/repo_meta.json) | Stars and last push date per PoC repository, keys lowercased | +| [`trending_poc.json`](https://pocindex.io/trending_poc.json) | Trending repositories plus index totals | | [`cves/2026/CVE-2026-68138.md`](cves/2026/CVE-2026-68138.md) | Markdown copy of one CVE, one directory per year | CVSS rows are `[version, score, severity, vector, source, assessment type]`. diff --git a/docs/CNAME b/docs/CNAME index 4034a05272f..fc709dce326 100644 --- a/docs/CNAME +++ b/docs/CNAME @@ -1 +1 @@ -cve.codepwn.win \ No newline at end of file +pocindex.io diff --git a/docs/hero.svg b/docs/hero.svg index 39f1d244a13..71e12417e98 100644 --- a/docs/hero.svg +++ b/docs/hero.svg @@ -1,5 +1,5 @@ + role="img" aria-label="PoC Index, 82,409 CVEs with public proof-of-concept exploits"> @@ -13,10 +13,10 @@ CVE RADAR + fill="#e6edf3" letter-spacing="2">PoC Index Public proof-of-concept exploits, indexed by CVE. - cve.codepwn.win + pocindex.io 170,946 diff --git a/docs/index.html b/docs/index.html index 03996b7b286..c8d7e2d777b 100644 --- a/docs/index.html +++ b/docs/index.html @@ -3,22 +3,46 @@ - CVE PoC Hub - + + PoC Index: Search CVE Proof-of-Concept Exploits + + - + - - + + - - + + - + - + + + @@ -35,7 +59,7 @@
-

CVE RADAR

+

PoC Index

Public proof-of-concept exploits, indexed by CVE.

@@ -108,6 +132,9 @@ sources: nvd · cve program · github · github advisories · cisa vulnrichment · cisa kev · first epss + diff --git a/docs/logic.js b/docs/logic.js index e48174bda74..a81ea9d101f 100644 --- a/docs/logic.js +++ b/docs/logic.js @@ -702,7 +702,38 @@ function idleStatus() { return ''; } +/* ---- url state --------------------------------------------------------- */ + +// A result was unlinkable: every search lived in memory, so nobody could cite +// one and no crawler ever saw a second page. The query rides in ?q= now, which +// is also the route the SearchAction in index.html declares. +function readURLState() { + const params = new URLSearchParams(location.search); + const query = (params.get('q') || '').trim(); + if (query) { + state.query = query; + el.input.value = query; + } + if (params.get('kev') === '1') state.kevOnly = true; +} + +// replaceState, not pushState: typing a query should not bury the back button +// under one history entry per keystroke. +let urlTimer = null; +function syncURL() { + clearTimeout(urlTimer); + urlTimer = setTimeout(() => { + const params = new URLSearchParams(); + if (state.query.length >= MIN_QUERY) params.set('q', state.query); + if (state.kevOnly) params.set('kev', '1'); + const search = params.toString(); + const next = search ? `${location.pathname}?${search}` : location.pathname; + if (next !== location.pathname + location.search) history.replaceState(null, '', next); + }, 300); +} + function render() { + syncURL(); if (state.query.length < MIN_QUERY && !hasActiveFilters()) { el.status.textContent = state.ready ? idleStatus() : 'loading index…'; renderTrending(); @@ -914,6 +945,8 @@ async function loadJSON(url, options) { } catch (err) { console.warn(err.message); } + readURLState(); + syncFilterControls(); paintHeroStats(); render(); diff --git a/docs/search.svg b/docs/search.svg index 8689ba54b65..8f996b13c83 100644 --- a/docs/search.svg +++ b/docs/search.svg @@ -1,5 +1,5 @@ + role="img" aria-label="Search PoC Index"> diff --git a/docs/social-card.svg b/docs/social-card.svg index 1133f78ee8d..92f1a07c7ec 100644 --- a/docs/social-card.svg +++ b/docs/social-card.svg @@ -1,5 +1,5 @@ + role="img" aria-label="PoC Index"> @@ -14,7 +14,7 @@ CVE RADAR + fill="#e6edf3" letter-spacing="3">PoC Index Public proof-of-concept exploits, indexed by CVE. @@ -31,5 +31,5 @@ fill="#6e7681" letter-spacing="1.6">KNOWN EXPLOITED cve.codepwn.win + letter-spacing="1">pocindex.io diff --git a/docs/style.css b/docs/style.css index e2e8cdf7048..722ed96749a 100644 --- a/docs/style.css +++ b/docs/style.css @@ -1,4 +1,4 @@ -/* CVE PoC Hub. GitHub Primer dark_default tokens, per the refit handoff. */ +/* PoC Index. GitHub Primer dark_default tokens, per the refit handoff. */ :root { --canvas: #0d1117; --canvas-subtle: #161b22; @@ -520,7 +520,9 @@ a.chip:hover { color: var(--fg); border-color: var(--fg-muted); } font: 400 11px/1.6 var(--mono); color: var(--fg-muted); } -.site-footer span:last-child { margin-left: auto; } +.site-footer .container:first-child span:last-child { margin-left: auto; } +.site-legal { padding-top: 8px; } +.site-legal span { margin-left: 0; opacity: .72; } /* ---- narrow viewports --------------------------------------------------- */ diff --git a/scripts/brand.py b/scripts/brand.py new file mode 100644 index 00000000000..f84e5c52b39 --- /dev/null +++ b/scripts/brand.py @@ -0,0 +1,46 @@ +"""Every user-facing name and URL, in one place. + +The site header, the browser title, the social card, the README banner and the +GitHub About box all have to say the same thing. When they disagree Google picks +its own site name, and mentions stop compounding into one brand. Change a string +here and every generated surface follows. +""" + +BRAND = "PoC Index" +# Short line under the wordmark, on the site and on both cards. +SUBTITLE = "Public proof-of-concept exploits, indexed by CVE." +TITLE = f"{BRAND}: Search CVE Proof-of-Concept Exploits" +# og:description and twitter:description. Shorter than DESCRIPTION because a +# social card truncates far earlier than a search result does. +SOCIAL_DESCRIPTION = ( + f"{SUBTITLE} Searchable, refreshed hourly, no signup." +) +DESCRIPTION = ( + "Search 82,000+ public CVE proof-of-concept exploits from GitHub, Nuclei, " + "ExploitDB, Metasploit and Vulhub. Updated hourly. Free JSON API, no signup." +) +# Shown under the GitHub About box and in search results for the repository. +# Deliberately unlike the upstream wording: 167 forks carry the old description +# verbatim, so a generic line ranks the copies alongside the original. +REPO_DESCRIPTION = ( + "Search public CVE proof-of-concept exploits from GitHub, Nuclei, ExploitDB, " + "Metasploit and Vulhub. 82,000+ CVEs with PoCs, updated hourly with CVSS, KEV " + "and EPSS. Free JSON API, no signup." +) + +# Flip to https://pocindex.io only after DNS resolves and GitHub Pages has issued +# the certificate. A canonical pointing at a host that does not answer yet drops +# the page out of the index, so this trails the DNS cutover, it does not lead it. +SITE = "https://pocindex.io" +SLUG = "0xMarcio/cve" + +# CVE is a MITRE trademark, so it stays out of the brand and carries a notice. +LEGAL = ( + "CVE is a trademark of The MITRE Corporation. " + f"{BRAND} is not affiliated with or endorsed by MITRE or the CVE Program." +) + + +def host() -> str: + """SITE without the scheme, for the places that display a bare domain.""" + return SITE.split("://", 1)[-1].rstrip("/") diff --git a/scripts/build_seo.py b/scripts/build_seo.py new file mode 100644 index 00000000000..f4b0367bd23 --- /dev/null +++ b/scripts/build_seo.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 +"""Write docs/robots.txt and docs/sitemap.xml. + +Both files were 404 before this, so nothing told a crawler where the site ends +or how often it changes. Everything here is derived from scripts/brand.py, so a +domain move is one edit rather than a hunt through the tree. +""" + +from __future__ import annotations + +import os +import sys +from datetime import datetime, timezone + +ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir) +sys.path.insert(0, os.path.join(ROOT, "scripts")) + +from brand import SITE + +DOCS = os.path.join(ROOT, "docs") +ROBOTS = os.path.join(DOCS, "robots.txt") +SITEMAP = os.path.join(DOCS, "sitemap.xml") + +# Googlebot renders the page before it indexes it, so anything the first paint +# needs has to stay crawlable. These three are small and carry the only server +# rendered content on the page, the trending table and the header counts. +RENDER_CRITICAL = ("trending_poc.json", "kev.json", "stats.json") + +# These only feed the search box, which a crawler never types into. Together they +# are over 13 MB on the wire, which is crawl budget spent on nothing. Blocking +# them here does not touch curl: robots.txt binds crawlers, not clients. +BULK_PAYLOADS = ( + "CVE_list.json", + "cve_metadata.json", + "epss.json", + "repo_meta.json", + "nuclei.json", +) + + +def robots() -> str: + lines = ["User-agent: *", "Allow: /"] + lines += [f"Disallow: /{name}" for name in BULK_PAYLOADS] + lines += ["", f"Sitemap: {SITE}/sitemap.xml", ""] + return "\n".join(lines) + + +def sitemap(urls: list[tuple[str, str]]) -> str: + body = "".join( + f" \n {loc}\n {lastmod}\n \n" + for loc, lastmod in urls + ) + return ( + '\n' + '\n' + f"{body}" + "\n" + ) + + +def main() -> int: + today = datetime.now(timezone.utc).strftime("%Y-%m-%d") + urls = [(f"{SITE}/", today)] + + os.makedirs(DOCS, exist_ok=True) + with open(ROBOTS, "w", encoding="utf-8") as handle: + handle.write(robots()) + with open(SITEMAP, "w", encoding="utf-8") as handle: + handle.write(sitemap(urls)) + + print(f"Wrote {ROBOTS} and {SITEMAP} ({len(urls)} URL) for {SITE}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main())