293 lines
12 KiB
Python
Executable File
293 lines
12 KiB
Python
Executable File
"""
|
|
utils/embeds.py — cyberpunk / hacker-aesthetic embed builders.
|
|
|
|
Every embed in VulnForge is built here so styling stays consistent:
|
|
dark backdrops, neon severity colours, monospace code blocks, and tidy
|
|
mobile + desktop formatting. Functions accept normalised dataclasses from
|
|
utils.api and return ready-to-send nextcord.Embed objects.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime, timezone
|
|
|
|
import nextcord
|
|
|
|
from config import settings
|
|
from utils.api import CVERecord, KEVEntry
|
|
from utils.enricher import cve_links
|
|
from utils.helpers import (
|
|
discord_timestamp,
|
|
severity_color,
|
|
severity_emoji,
|
|
truncate,
|
|
)
|
|
|
|
# Visual furniture reused across embeds.
|
|
DIVIDER = "▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰▰"
|
|
BRAND = "VulnForge // Security Intelligence"
|
|
BRAND_ICON = "https://www.cisa.gov/profiles/cisad8_gov/themes/custom/gesso/logo.svg"
|
|
|
|
|
|
def cve_pivots(cve_id: str, *, compact: bool = False) -> str:
|
|
"""One-line markdown pivot links for a CVE — PoC search / exploit-db / etc.
|
|
|
|
Used to enrich every CVE-bearing embed (cve, weekly, kev) with quick links
|
|
to GitHub PoCs, exploit-db, Nuclei templates, trickest/cve and Vulncheck.
|
|
Pass `compact=True` for tighter rendering in list-style embeds.
|
|
"""
|
|
links = {name: url for name, url in cve_links(cve_id)}
|
|
if compact:
|
|
order = ("GitHub PoC search", "exploit-db", "Nuclei templates")
|
|
labels = {"GitHub PoC search": "PoC", "exploit-db": "ExDB", "Nuclei templates": "Nuclei"}
|
|
else:
|
|
order = ("GitHub PoC search", "exploit-db", "Nuclei templates",
|
|
"trickest/cve", "Vulncheck", "GitHub Advisory")
|
|
labels = {
|
|
"GitHub PoC search": "GH PoC",
|
|
"exploit-db": "Exploit-DB",
|
|
"Nuclei templates": "Nuclei",
|
|
"trickest/cve": "trickest/cve",
|
|
"Vulncheck": "Vulncheck",
|
|
"GitHub Advisory": "GH Advisory",
|
|
}
|
|
return " · ".join(f"[{labels[k]}]({links[k]})" for k in order if k in links)
|
|
|
|
|
|
def _footer(embed: nextcord.Embed) -> nextcord.Embed:
|
|
embed.set_footer(text=f"⛓ {BRAND}")
|
|
embed.timestamp = datetime.now(timezone.utc)
|
|
return embed
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────
|
|
# CVE detail embed
|
|
# ──────────────────────────────────────────────────────────────
|
|
def build_cve_embed(
|
|
cve: CVERecord,
|
|
kev: KEVEntry | None,
|
|
*,
|
|
show_details: bool = True,
|
|
show_kev: bool = True,
|
|
show_references: bool = True,
|
|
show_cwe: bool = True,
|
|
notes: list[dict] | None = None,
|
|
) -> nextcord.Embed:
|
|
"""Build the rich, option-driven CVE embed returned by /cve."""
|
|
sev = cve.cvss_severity
|
|
color = settings.color_kev if kev else severity_color(sev)
|
|
exploited = " ☣ ACTIVELY EXPLOITED" if kev else ""
|
|
|
|
embed = nextcord.Embed(
|
|
title=f"{severity_emoji(sev)} {cve.cve_id}{exploited}",
|
|
url=cve.nvd_url,
|
|
description=truncate(cve.description, 1024) if show_details else None,
|
|
color=color,
|
|
)
|
|
|
|
# Score line — always shown; it's the headline metric.
|
|
score_str = f"`{cve.cvss_score}`" if cve.cvss_score is not None else "`N/A`"
|
|
ver = f" (CVSS v{cve.cvss_version})" if cve.cvss_version else ""
|
|
embed.add_field(
|
|
name="◈ Severity",
|
|
value=f"{severity_emoji(sev)} **{sev}**{ver}",
|
|
inline=True,
|
|
)
|
|
embed.add_field(name="◈ Base Score", value=score_str, inline=True)
|
|
embed.add_field(name="◈ Status", value=f"`{cve.status}`", inline=True)
|
|
|
|
if show_details:
|
|
embed.add_field(
|
|
name="◈ Published",
|
|
value=discord_timestamp(cve.published, "D"),
|
|
inline=True,
|
|
)
|
|
embed.add_field(
|
|
name="◈ Modified",
|
|
value=discord_timestamp(cve.last_modified, "R"),
|
|
inline=True,
|
|
)
|
|
if cve.cvss_vector:
|
|
embed.add_field(
|
|
name="◈ Vector",
|
|
value=f"```{truncate(cve.cvss_vector, 240)}```",
|
|
inline=False,
|
|
)
|
|
|
|
if show_cwe and cve.cwes:
|
|
embed.add_field(
|
|
name="◈ Weaknesses (CWE)",
|
|
value=" ".join(f"`{c}`" for c in cve.cwes[:8]),
|
|
inline=False,
|
|
)
|
|
|
|
# KEV block — the most security-critical signal.
|
|
if show_kev:
|
|
if kev:
|
|
ransom = kev.ransomware if kev.ransomware else "Unknown"
|
|
embed.add_field(name=DIVIDER, value="**☣ CISA KEV — ACTIVE EXPLOITATION**", inline=False)
|
|
embed.add_field(name="▸ Vendor / Product", value=f"`{kev.vendor}` / `{kev.product}`", inline=True)
|
|
embed.add_field(name="▸ Added to KEV", value=discord_timestamp(kev.date_added_dt, "D"), inline=True)
|
|
embed.add_field(name="▸ Remediation Due", value=f"`{kev.due_date or 'N/A'}`", inline=True)
|
|
embed.add_field(name="▸ Ransomware Use", value=f"`{ransom}`", inline=True)
|
|
if kev.required_action:
|
|
embed.add_field(
|
|
name="▸ Required Action",
|
|
value=truncate(kev.required_action, 1024),
|
|
inline=False,
|
|
)
|
|
else:
|
|
embed.add_field(
|
|
name="◈ CISA KEV Status",
|
|
value="✅ Not currently listed in the Known Exploited Vulnerabilities catalog.",
|
|
inline=False,
|
|
)
|
|
|
|
if show_references and cve.references:
|
|
links = "\n".join(f"• [{truncate(u, 70)}]({u})" for u in cve.references[:6])
|
|
embed.add_field(name="◈ References", value=truncate(links, 1024), inline=False)
|
|
|
|
if notes:
|
|
rendered = "\n".join(
|
|
f"• <@{n['user_id']}>: {truncate(n['note'], 150)}" for n in notes[:5]
|
|
)
|
|
embed.add_field(name="◈ Researcher Notes", value=truncate(rendered, 1024), inline=False)
|
|
|
|
pivots = cve_pivots(cve.cve_id)
|
|
if pivots:
|
|
embed.add_field(name="◈ Recon Pivots", value=pivots, inline=False)
|
|
|
|
embed.add_field(
|
|
name="◈ Links",
|
|
value=f"[🛰 NVD]({cve.nvd_url}) • [🛡 CISA KEV]({settings.cisa_kev_web})",
|
|
inline=False,
|
|
)
|
|
return _footer(embed)
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────
|
|
# Weekly digest embed
|
|
# ──────────────────────────────────────────────────────────────
|
|
def build_weekly_embed(
|
|
*,
|
|
total_new: int,
|
|
in_kev_count: int,
|
|
newly_added_kev: list[KEVEntry],
|
|
top_cves: list[CVERecord],
|
|
days: int = 7,
|
|
) -> nextcord.Embed:
|
|
"""Build the automated weekly threat-intelligence digest embed."""
|
|
embed = nextcord.Embed(
|
|
title="☣ VULNFORGE WEEKLY THREAT DIGEST",
|
|
description=(
|
|
f"```ansi\n\u001b[1;31m> Intelligence sweep — last {days} days\u001b[0m\n```\n"
|
|
f"Aggregated from **NVD** and the **CISA KEV** catalog."
|
|
),
|
|
color=settings.color_critical if newly_added_kev else settings.color_info,
|
|
url=settings.cisa_kev_web,
|
|
)
|
|
|
|
# Headline metrics block.
|
|
embed.add_field(name="🆕 New CVEs", value=f"**{total_new:,}**", inline=True)
|
|
embed.add_field(name="☣ Already in KEV", value=f"**{in_kev_count:,}**", inline=True)
|
|
embed.add_field(name="⚠ Newly Added to KEV", value=f"**{len(newly_added_kev):,}**", inline=True)
|
|
|
|
embed.add_field(name=DIVIDER, value="**▶ TOP CRITICAL VULNERABILITIES**", inline=False)
|
|
|
|
if top_cves:
|
|
for cve in top_cves:
|
|
sev = cve.cvss_severity
|
|
score = cve.cvss_score if cve.cvss_score is not None else "N/A"
|
|
pivots = cve_pivots(cve.cve_id, compact=True)
|
|
link_line = f"[NVD]({cve.nvd_url})"
|
|
if pivots:
|
|
link_line += f" · {pivots}"
|
|
embed.add_field(
|
|
name=f"{severity_emoji(sev)} {cve.cve_id} — {score}",
|
|
value=f"{truncate(cve.description, 140)}\n{link_line}",
|
|
inline=False,
|
|
)
|
|
else:
|
|
embed.add_field(
|
|
name="—",
|
|
value="No scored CVEs found in the window.",
|
|
inline=False,
|
|
)
|
|
|
|
if newly_added_kev:
|
|
kev_lines = "\n".join(
|
|
f"• `{e.cve_id}` — {truncate(e.product, 38)} ({e.vendor}) — {cve_pivots(e.cve_id, compact=True)}"
|
|
for e in newly_added_kev[:8]
|
|
)
|
|
embed.add_field(
|
|
name="☣ Newly Weaponized (Added to KEV)",
|
|
value=truncate(kev_lines, 1024),
|
|
inline=False,
|
|
)
|
|
|
|
embed.add_field(
|
|
name="◈ Sources",
|
|
value=(
|
|
"[🛰 NVD Feed](https://nvd.nist.gov/vuln) • "
|
|
f"[🛡 CISA KEV]({settings.cisa_kev_web})"
|
|
),
|
|
inline=False,
|
|
)
|
|
return _footer(embed)
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────
|
|
# KEV list embed
|
|
# ──────────────────────────────────────────────────────────────
|
|
def build_kev_list_embed(entries: list[KEVEntry], limit: int) -> nextcord.Embed:
|
|
"""Build the embed for the /kev command (latest KEV additions)."""
|
|
embed = nextcord.Embed(
|
|
title="☣ CISA KEV — LATEST KNOWN EXPLOITED VULNERABILITIES",
|
|
description=(
|
|
"```ansi\n\u001b[1;31m> Actively exploited in the wild\u001b[0m\n```"
|
|
f"Showing the **{min(limit, len(entries))}** most recent additions."
|
|
),
|
|
color=settings.color_kev,
|
|
url=settings.cisa_kev_web,
|
|
)
|
|
for e in entries[:limit]:
|
|
pivots = cve_pivots(e.cve_id, compact=True)
|
|
link_line = f"[NVD]({settings.nvd_cve_web}/{e.cve_id})"
|
|
if pivots:
|
|
link_line += f" · {pivots}"
|
|
embed.add_field(
|
|
name=f"🩸 {e.cve_id} — {truncate(e.product, 50)}",
|
|
value=truncate(
|
|
f"**Vendor:** {e.vendor}\n"
|
|
f"**Added:** {discord_timestamp(e.date_added_dt, 'D')} • "
|
|
f"**Ransomware:** `{e.ransomware}`\n"
|
|
f"{truncate(e.short_description, 140)}\n"
|
|
f"{link_line}",
|
|
1024,
|
|
),
|
|
inline=False,
|
|
)
|
|
if not entries:
|
|
embed.description = "No KEV entries could be retrieved right now."
|
|
return _footer(embed)
|
|
|
|
|
|
# ──────────────────────────────────────────────────────────────
|
|
# Error / utility embeds
|
|
# ──────────────────────────────────────────────────────────────
|
|
def build_error_embed(title: str, message: str) -> nextcord.Embed:
|
|
embed = nextcord.Embed(
|
|
title=f"⛔ {title}",
|
|
description=message,
|
|
color=settings.color_critical,
|
|
)
|
|
return _footer(embed)
|
|
|
|
|
|
def build_info_embed(title: str, message: str) -> nextcord.Embed:
|
|
embed = nextcord.Embed(
|
|
title=f"✅ {title}",
|
|
description=message,
|
|
color=settings.color_success,
|
|
)
|
|
return _footer(embed) |