mirror of
https://github.com/0xMarcio/cve.git
synced 2026-10-27 16:51:03 +00:00
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.
This commit is contained in:
1 parent
66a8c7d36b
commit
2ed7386028
10 files changed
+729
-142
No files matched your search
+180
-14
@@ -33,6 +33,13 @@ USER_AGENT = "0xMarcio-cve-trending"
|
||||
ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), os.pardir)
|
||||
README = os.path.join(ROOT, "README.md")
|
||||
TRENDING = os.path.join(ROOT, "trending.json")
|
||||
KEV = os.path.join(ROOT, "kev.json")
|
||||
STATS = os.path.join(ROOT, "docs", "stats.json")
|
||||
DESC_LIMIT = 110 # a table cell, not a paragraph
|
||||
FOLDED_AFTER = 2 # years past the newest two are collapsed behind a summary
|
||||
CVE_ID = re.compile(r"CVE[-_](\d{4})[-_](\d{4,7})", re.IGNORECASE)
|
||||
SLUG = "0xMarcio/cve"
|
||||
HERO_URL = f"https://raw.githubusercontent.com/{SLUG}/main/docs/hero.svg"
|
||||
|
||||
|
||||
def search_year(year: int, since: str) -> tuple[int, list[dict]]:
|
||||
@@ -178,23 +185,157 @@ def code_pushed(full_names: list[str], token: str) -> dict[str, str]:
|
||||
def time_ago(timestamp: str) -> str:
|
||||
moment = datetime.strptime(timestamp, "%Y-%m-%dT%H:%M:%SZ").replace(tzinfo=timezone.utc)
|
||||
delta = datetime.now(timezone.utc) - moment
|
||||
for amount, unit in ((delta.days, "day"), (delta.seconds // 3600, "hour"), (delta.seconds // 60, "minute")):
|
||||
for amount, unit in ((delta.days, "d"), (delta.seconds // 3600, "h"), (delta.seconds // 60, "m")):
|
||||
if amount > 0:
|
||||
return f"{amount} {unit}{'s' if amount > 1 else ''} ago"
|
||||
return f"{amount}{unit} ago"
|
||||
return "just now"
|
||||
|
||||
|
||||
def cell(value: str) -> str:
|
||||
"""Keep repository text on a single markdown table cell."""
|
||||
return " ".join(str(value or "").split()).replace("|", "/")
|
||||
text = " ".join(str(value or "").split()).replace("|", "/")
|
||||
return text.replace("\u2014", "-").replace("\u2013", "-")
|
||||
|
||||
|
||||
|
||||
def known_exploited() -> set[str]:
|
||||
"""CVE ids CISA lists as exploited in the wild."""
|
||||
try:
|
||||
with open(KEV, encoding="utf-8") as handle:
|
||||
return {str(key).upper() for key in json.load(handle)}
|
||||
except (OSError, ValueError):
|
||||
return set()
|
||||
|
||||
|
||||
def cve_of(repo: dict) -> str:
|
||||
match = CVE_ID.search(f"{repo.get('name') or ''} {repo.get('description') or ''}")
|
||||
return f"CVE-{match.group(1)}-{match.group(2)}" if match else ""
|
||||
|
||||
|
||||
def shorten(text: str) -> str:
|
||||
text = cell(text)
|
||||
if len(text) <= DESC_LIMIT:
|
||||
return text
|
||||
return text[:DESC_LIMIT].rsplit(" ", 1)[0] + "\u2026"
|
||||
|
||||
|
||||
def figures() -> dict:
|
||||
"""Index totals, written by hero.py from the same count the banner shows."""
|
||||
try:
|
||||
with open(STATS, encoding="utf-8") as handle:
|
||||
return json.load(handle)
|
||||
except (OSError, ValueError):
|
||||
return {}
|
||||
|
||||
|
||||
def shields(text: str) -> str:
|
||||
"""Escape one field of a shields badge path.
|
||||
|
||||
The path is split on single hyphens, so a hyphen inside the text has to be
|
||||
doubled before it is percent-encoded or the badge loses everything after it.
|
||||
"""
|
||||
return parse.quote(text.replace("-", "--").replace("_", "__"), safe="")
|
||||
|
||||
|
||||
def pill(label: str, value: str, colour: str, link: str) -> str:
|
||||
"""A shields badge whose text is baked in at build time.
|
||||
|
||||
Anything describing the index is fixed here rather than resolved when the
|
||||
page is viewed: the figure is then exactly the one the banner was drawn
|
||||
from, and it cannot degrade to "resource not found" when a lookup fails.
|
||||
"""
|
||||
return (f"[}-{shields(value)}-{colour}"
|
||||
f"?style=flat-square&labelColor=161b22)]({link})")
|
||||
|
||||
|
||||
def live_pill(path: str, label: str, colour: str, link: str, stamp: str) -> str:
|
||||
"""A shields badge that has to be resolved at view time.
|
||||
|
||||
The stamp is ignored by shields but changes the URL every rebuild, so
|
||||
GitHub's image proxy fetches a fresh badge instead of replaying a cached one.
|
||||
"""
|
||||
return (f"[}&color={colour}"
|
||||
f"&labelColor=161b22&_={stamp})]({link})")
|
||||
|
||||
|
||||
def header(stamp: str, synced: str) -> list[str]:
|
||||
"""Everything above the tables. The banner carries a cache-busting stamp
|
||||
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/"
|
||||
badges = [
|
||||
pill("last sync", synced, "2f81f7", f"https://github.com/{SLUG}/commits/main"),
|
||||
live_pill("actions/workflow/status/" + SLUG + "/hot_cves.yml", "sweep", "2f81f7",
|
||||
f"https://github.com/{SLUG}/actions/workflows/hot_cves.yml", stamp),
|
||||
]
|
||||
if counts.get("with_pocs"):
|
||||
badges.append(pill("CVEs with PoCs", f"{counts['with_pocs']:,}", "2f81f7", home))
|
||||
if counts.get("kev"):
|
||||
badges.append(pill("known exploited", f"{counts['kev']:,}", "f85149", home))
|
||||
badges.append(live_pill("stars/" + SLUG, "stars", "e3b341",
|
||||
f"https://github.com/{SLUG}/stargazers", stamp))
|
||||
return [
|
||||
'<div align="center">',
|
||||
"",
|
||||
f'<a href="{home}"><img src="{HERO_URL}?v={stamp}" alt="CVE Radar" width="100%"></a>',
|
||||
"",
|
||||
" ".join(badges),
|
||||
"",
|
||||
"</div>",
|
||||
"",
|
||||
]
|
||||
|
||||
|
||||
FOOTER = """
|
||||
|
||||
## Data
|
||||
|
||||
Every file is plain JSON on the CDN. No key, no rate limit.
|
||||
|
||||
```bash
|
||||
curl -s https://cve.codepwn.win/CVE_list.json | jq '.[] | select(.cve=="CVE-2026-75604")'
|
||||
curl -s https://cve.codepwn.win/kev.json | jq 'keys | length'
|
||||
curl -s https://cve.codepwn.win/repo_meta.json | jq '."sfewer-r7/CVE-2026-55040"'
|
||||
curl -s https://cve.codepwn.win/trending_poc.json | jq '{total_cves, with_pocs}'
|
||||
```
|
||||
|
||||
| Endpoint | Holds |
|
||||
| --- | --- |
|
||||
| [`CVE_list.json`](https://cve.codepwn.win/CVE_list.json) | Every indexed CVE, its description and its PoC links |
|
||||
| [`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 |
|
||||
| [`trending_poc.json`](https://cve.codepwn.win/trending_poc.json) | Trending repositories plus index totals |
|
||||
| [`2026/CVE-2026-75604.md`](2026/CVE-2026-75604.md) | Markdown copy of one CVE, one directory per year |
|
||||
|
||||
## Build
|
||||
|
||||
| Job | Cadence | Picks up |
|
||||
| --- | --- | --- |
|
||||
| [PoC sweep](.github/workflows/hot_cves.yml) | hourly | New and updated exploit repositories |
|
||||
| [CVE sync](.github/workflows/sync_cve_pocs.yml) | daily | New CVEs and references from NVD and MITRE |
|
||||
| [Link audit](.github/workflows/audit_poc_links.yml) | weekly | Repositories that went dead, dropped from the index |
|
||||
|
||||
A repository counts as updated only when a commit touches something other than
|
||||
paperwork, so a README tweak cannot pass a year-old exploit off as recent.
|
||||
|
||||
## Contributing
|
||||
|
||||
Missing PoC, wrong link, dead repository: open an issue with the CVE id and the
|
||||
repository URL.
|
||||
"""
|
||||
|
||||
|
||||
def main() -> int:
|
||||
now = datetime.now(timezone.utc)
|
||||
since = (now - timedelta(days=WINDOW_DAYS)).date().isoformat()
|
||||
current_year = now.year
|
||||
lines = ['<h1 align="center">Recently updated Proof-of-Concepts</h1>']
|
||||
kev = known_exploited()
|
||||
items: list[dict] = []
|
||||
sections: list[list[str]] = []
|
||||
flagged = 0
|
||||
|
||||
token = github_token()
|
||||
for year in range(current_year, current_year - YEARS, -1):
|
||||
@@ -210,18 +351,23 @@ def main() -> int:
|
||||
repositories.sort(key=lambda repo: repo["_shipped"], reverse=True)
|
||||
repositories = repositories[:PER_YEAR]
|
||||
print(f"CVE-{year}: {total} pushed since {since}, "
|
||||
f"{stale - len([r for r in repositories])} dropped as paperwork-only")
|
||||
f"{stale - len(repositories)} dropped as paperwork-only")
|
||||
if not repositories:
|
||||
continue
|
||||
lines.append(f"\n\n## {year}\n")
|
||||
lines.append(f"### Latest {len(repositories)} of {total} Repositories\n")
|
||||
lines.append("| Stars | Updated | Name | Description |")
|
||||
lines.append("| --- | --- | --- | --- |")
|
||||
block = [
|
||||
f"## {year} · newest {len(repositories)} of {total}",
|
||||
"",
|
||||
"| Stars | Updated | Repository | Description |",
|
||||
"| --- | --- | --- | --- |",
|
||||
]
|
||||
for repo in repositories:
|
||||
# The last commit that touched anything but paperwork. pushed_at counts
|
||||
# a README tweak, which had the table claiming a year-old exploit was
|
||||
# updated twenty-one hours ago.
|
||||
pushed = repo["_shipped"]
|
||||
cve = cve_of(repo)
|
||||
exploited = cve in kev
|
||||
flagged += exploited
|
||||
items.append({
|
||||
"year": year,
|
||||
"stars": int(repo.get("stargazers_count") or 0),
|
||||
@@ -230,18 +376,38 @@ def main() -> int:
|
||||
"desc": cell(repo.get("description")),
|
||||
"pushed": pushed,
|
||||
"created": str(repo.get("created_at") or ""),
|
||||
"cve": cve,
|
||||
"kev": exploited,
|
||||
})
|
||||
lines.append(
|
||||
f"| {repo.get('stargazers_count', 0)}⭐ | {time_ago(pushed)} "
|
||||
f"| [{cell(repo.get('name'))}]({repo.get('html_url')}) | {cell(repo.get('description'))} |"
|
||||
block.append(
|
||||
f"| {repo.get('stargazers_count', 0)}\u2b50 | {time_ago(pushed)} "
|
||||
f"| {'\U0001f525 ' if exploited else ''}[{cell(repo.get('name'))}]({repo.get('html_url')}) "
|
||||
f"| {shorten(repo.get('description'))} |"
|
||||
)
|
||||
sections.append(block)
|
||||
|
||||
stamp = now.strftime("%Y%m%d%H%M")
|
||||
# No hyphens: they are the field separator in a shields badge path.
|
||||
synced = now.strftime("%d %b %Y %H:%M UTC")
|
||||
lines = header(stamp, synced)
|
||||
lines.append(f"Exploit repositories with a commit in the last {WINDOW_DAYS} days, "
|
||||
f"newest first. \U0001f525 marks a CVE on the CISA known exploited list.")
|
||||
lines.append("")
|
||||
for index, block in enumerate(sections):
|
||||
if index == FOLDED_AFTER and len(sections) > FOLDED_AFTER:
|
||||
years = ", ".join(section[0][3:7] for section in sections[FOLDED_AFTER:])
|
||||
lines.append(f"<details>\n<summary>{years}</summary>\n")
|
||||
lines.extend(block)
|
||||
lines.append("")
|
||||
if len(sections) > FOLDED_AFTER:
|
||||
lines.append("</details>")
|
||||
|
||||
with open(README, "w", encoding="utf-8") as handle:
|
||||
handle.write("\n".join(lines))
|
||||
handle.write("\n".join(lines).rstrip() + FOOTER)
|
||||
with open(TRENDING, "w", encoding="utf-8") as handle:
|
||||
json.dump(items, handle, ensure_ascii=False, indent=1)
|
||||
handle.write("\n")
|
||||
print(f"Wrote {README} and {TRENDING} ({len(items)} repositories)")
|
||||
print(f"Wrote {README} and {TRENDING} ({len(items)} repositories, {flagged} known-exploited)")
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Render the CVE Radar 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
|
||||
would drop anything it has to resolve afterwards.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
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"
|
||||
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")
|
||||
|
||||
CANVAS = "#0d1117"
|
||||
SUBTLE = "#161b22"
|
||||
BORDER = "#30363d"
|
||||
FG = "#e6edf3"
|
||||
MUTED = "#8b949e"
|
||||
NEUTRAL = "#6e7681"
|
||||
ACCENT = "#2f81f7"
|
||||
DANGER = "#f85149"
|
||||
MONO = "ui-monospace,SFMono-Regular,Menlo,Consolas,monospace"
|
||||
|
||||
|
||||
def corpus_counts() -> dict | None:
|
||||
"""Count the index the same way the site build does.
|
||||
|
||||
The banner and the site header have to agree, so this reuses build_site's
|
||||
own parser instead of a second guess at what counts as a PoC. Roughly
|
||||
fifteen seconds over the whole corpus.
|
||||
"""
|
||||
try:
|
||||
spec = importlib.util.spec_from_file_location("build_site", BUILD_SITE)
|
||||
build_site = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(build_site)
|
||||
entries, total = build_site.build_cve_list(build_site.load_blacklist())
|
||||
except Exception as problem:
|
||||
print(f"corpus count failed: {problem}")
|
||||
return None
|
||||
if not total:
|
||||
return None
|
||||
return {
|
||||
"total_cves": total,
|
||||
"with_pocs": len(entries),
|
||||
"kev": len(build_site.build_kev(entries)),
|
||||
}
|
||||
|
||||
|
||||
def live_stats() -> dict | None:
|
||||
"""What the deployed site is currently serving.
|
||||
|
||||
Only reached when the corpus itself will not parse, which on a normal
|
||||
checkout means something is broken rather than something is missing.
|
||||
"""
|
||||
try:
|
||||
with request.urlopen(LIVE_STATS, timeout=20) as response:
|
||||
data = json.load(response)
|
||||
total = int(data.get("total_cves") or 0)
|
||||
pocs = int(data.get("with_pocs") or 0)
|
||||
except Exception as problem:
|
||||
print(f"live stats failed: {problem}")
|
||||
return None
|
||||
if not total or not pocs:
|
||||
return None
|
||||
try:
|
||||
# the site's copy, already narrowed to CVEs the index carries, so this
|
||||
# path reports the same figure the corpus count would
|
||||
with request.urlopen(LIVE_KEV, timeout=20) as response:
|
||||
kev = len(json.load(response))
|
||||
except Exception:
|
||||
kev = 0
|
||||
return {"total_cves": total, "with_pocs": pocs, "kev": kev}
|
||||
|
||||
|
||||
def group(value: int) -> str:
|
||||
return f"{value:,}"
|
||||
|
||||
|
||||
def sweep(cx: float, cy: float, radius: float, opacity: float) -> str:
|
||||
"""Radar rings with a hand rotating over them once every eight seconds."""
|
||||
rings = "".join(
|
||||
f'<circle cx="{cx}" cy="{cy}" r="{radius * scale:.1f}" fill="none" '
|
||||
f'stroke="{ACCENT}" stroke-width="1" opacity="{opacity * fade:.3f}"/>'
|
||||
for scale, fade in ((0.35, 0.9), (0.62, 0.65), (0.85, 0.45), (1.0, 0.3))
|
||||
)
|
||||
return f"""<g>
|
||||
{rings}
|
||||
<line x1="{cx}" y1="{cy}" x2="{cx}" y2="{cy - radius:.1f}" stroke="{ACCENT}"
|
||||
stroke-width="1.5" stroke-linecap="round" opacity="{opacity * 1.6:.3f}">
|
||||
<animateTransform attributeName="transform" type="rotate"
|
||||
from="0 {cx} {cy}" to="360 {cx} {cy}"
|
||||
dur="8s" repeatCount="indefinite"/>
|
||||
</line>
|
||||
</g>"""
|
||||
|
||||
|
||||
def stat_block(x: float, y: float, value: str, label: str, colour: str, size: int) -> str:
|
||||
return f"""<text x="{x}" y="{y}" font-family="{MONO}" font-size="{size}" font-weight="700"
|
||||
fill="{colour}" letter-spacing="-0.5">{value}</text>
|
||||
<text x="{x}" y="{y + size * 0.62:.0f}" font-family="{MONO}" font-size="{size * 0.34:.0f}"
|
||||
fill="{NEUTRAL}" letter-spacing="1.6">{label}</text>"""
|
||||
|
||||
|
||||
def hero(counts: dict) -> str:
|
||||
blocks = "".join(
|
||||
stat_block(x, 96, value, label, colour, 34)
|
||||
for x, value, label, colour in (
|
||||
(620, group(counts["total_cves"]), "CVES INDEXED", FG),
|
||||
(840, group(counts["with_pocs"]), "WITH POCS", ACCENT),
|
||||
(1010, group(counts["kev"]), "KNOWN EXPLOITED", DANGER),
|
||||
)
|
||||
)
|
||||
return f"""<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="180" viewBox="0 0 1200 180"
|
||||
role="img" aria-label="CVE Radar, {group(counts['with_pocs'])} CVEs with public proof-of-concept exploits">
|
||||
<clipPath id="card"><rect x="1" y="1" width="1198" height="178" rx="12"/></clipPath>
|
||||
<rect x="0.5" y="0.5" width="1199" height="179" rx="12" fill="{CANVAS}" stroke="{BORDER}"/>
|
||||
<g clip-path="url(#card)" opacity="0.6">{sweep(1090, 90, 160, 0.3)}</g>
|
||||
|
||||
<text x="56" y="70" font-family="{MONO}" font-size="44" font-weight="700"
|
||||
fill="{FG}" letter-spacing="2">CVE RADAR</text>
|
||||
<text x="56" y="104" font-family="{MONO}" font-size="15" fill="{MUTED}">
|
||||
Public proof-of-concept exploits for every CVE.</text>
|
||||
<text x="56" y="132" font-family="{MONO}" font-size="15" fill="{ACCENT}">cve.codepwn.win</text>
|
||||
|
||||
{blocks}
|
||||
</svg>
|
||||
"""
|
||||
|
||||
|
||||
def social(counts: dict) -> str:
|
||||
"""1280x640 card for the repository social preview and the site og:image."""
|
||||
blocks = "".join(
|
||||
stat_block(x, 430, value, label, colour, 46)
|
||||
for x, value, label, colour in (
|
||||
(96, group(counts["total_cves"]), "CVES INDEXED", FG),
|
||||
(500, group(counts["with_pocs"]), "WITH POCS", ACCENT),
|
||||
(880, group(counts["kev"]), "KNOWN EXPLOITED", DANGER),
|
||||
)
|
||||
)
|
||||
return f"""<svg xmlns="http://www.w3.org/2000/svg" width="1280" height="640" viewBox="0 0 1280 640"
|
||||
role="img" aria-label="CVE Radar">
|
||||
<clipPath id="card"><rect x="56" y="56" width="1168" height="528" rx="16"/></clipPath>
|
||||
<rect width="1280" height="640" fill="{CANVAS}"/>
|
||||
<g clip-path="url(#card)" opacity="0.6">{sweep(1000, 300, 220, 0.26)}</g>
|
||||
<rect x="56" y="56" width="1168" height="528" rx="16" fill="none" stroke="{BORDER}"/>
|
||||
|
||||
<text x="96" y="215" font-family="{MONO}" font-size="76" font-weight="700"
|
||||
fill="{FG}" letter-spacing="3">CVE RADAR</text>
|
||||
<text x="96" y="272" font-family="{MONO}" font-size="24" fill="{MUTED}">
|
||||
Public proof-of-concept exploits for every CVE.</text>
|
||||
|
||||
<rect x="96" y="344" width="1088" height="1" fill="{BORDER}"/>
|
||||
{blocks}
|
||||
|
||||
<text x="96" y="532" font-family="{MONO}" font-size="22" fill="{ACCENT}"
|
||||
letter-spacing="1">cve.codepwn.win</text>
|
||||
</svg>
|
||||
"""
|
||||
|
||||
|
||||
def render(counts: dict) -> None:
|
||||
"""Draw both cards and leave the figures behind for the README build."""
|
||||
for path, markup in ((HERO, hero(counts)), (SOCIAL, social(counts))):
|
||||
with open(path, "w", encoding="utf-8") as handle:
|
||||
handle.write(markup)
|
||||
# No timestamp: this file should only change when the figures do, so the
|
||||
# README build can read the same numbers the banner is drawn from.
|
||||
with open(STATS, "w", encoding="utf-8") as handle:
|
||||
json.dump(counts, handle, indent=1, sort_keys=True)
|
||||
handle.write("\n")
|
||||
|
||||
|
||||
def main() -> int:
|
||||
counts = corpus_counts() or live_stats()
|
||||
if not counts:
|
||||
# Drawing zeros would be worse than leaving the previous banner in place.
|
||||
print("no usable counts, leaving the existing banner alone")
|
||||
return 1
|
||||
render(counts)
|
||||
print(f"Wrote {HERO}, {SOCIAL} and {STATS}: {group(counts['total_cves'])} CVEs, "
|
||||
f"{group(counts['with_pocs'])} with PoCs, {group(counts['kev'])} known exploited")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -3,7 +3,7 @@ name: Hot CVEs List
|
||||
on:
|
||||
workflow_dispatch:
|
||||
schedule:
|
||||
- cron: '0 */6 * * *'
|
||||
- cron: '7 * * * *'
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -24,6 +24,16 @@ jobs:
|
||||
with:
|
||||
python-version: '3.13'
|
||||
|
||||
- name: Redraw the banner and recount the index
|
||||
run: |
|
||||
# Counts the corpus with the site build's own parser, so the banner can
|
||||
# never disagree with the site, and drops the figures in docs/stats.json
|
||||
# for the README badges to quote.
|
||||
python3 .github/hero.py
|
||||
# og:image has to be a raster: Twitter, Slack and Discord all ignore SVG
|
||||
sudo apt-get install -y --no-install-recommends librsvg2-bin >/dev/null
|
||||
rsvg-convert -w 1280 -h 640 docs/social-card.svg -o docs/social-card.png
|
||||
|
||||
- name: Rebuild the trending PoC table
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
@@ -33,7 +43,7 @@ jobs:
|
||||
run: |
|
||||
git config user.name 'CVE-BOT'
|
||||
git config user.email '[email protected]'
|
||||
git add -- README.md trending.json
|
||||
git add -- README.md trending.json docs/hero.svg docs/social-card.svg docs/social-card.png docs/stats.json
|
||||
if git diff --cached --quiet; then
|
||||
echo "No changes to commit"
|
||||
exit 0
|
||||
|
||||
Reference in new issue
Block a user