onyx

onyx --method

Method

Methodology behind the trending score and the signals surfaced on the dashboard.

Each CVE carries a trend score (higher = more urgent to look at). Rows from different sources are merged per CVE before scoring:

core = w_kev        * (in CISA KEV ? 1 : 0)
     + w_epss       * epss
     + w_poc        * (public PoC or nuclei template ? 1 : 0)
     + w_template   * (nuclei template ? 1 : 0)
     + w_severity   * (cvss / 10)

buzz = w_advisory * min(1, advisory_feeds / 2)
     + w_chatter  * min(1, log2(1 + mentions) / log2(21))
     + w_momentum * acceleration            (mentions/day, last 3 d vs the 4 before)

surge = w_epss_delta * max(0, epss_change_7d) * 10   (24 h until a week of history exists)

score = core * 2^(-core_age / half_life) + buzz * 2^(-buzz_age / half_life) + surge

core_age counts from the CVE’s most recent dated signal — publication or KEV addition — so an old CVE that was just added to KEV still ranks. buzz_age counts from the newest advisory or mention, so an old CVE discussed today climbs back on its buzz points without one citation reviving its full KEV / EPSS weight. The EPSS surge is measured over the last 7 days, so it is current by definition and is not decayed.

History

Every render stores a daily snapshot of each CVE (score, rank, EPSS, signals) and each tech; EPSS history is backfilled from FIRST (~30 days) on a CVE’s first run. That feeds the timeline on each CVE page, Climbing fastest on the dashboard, each tech’s exploitation speed (median days from publication to KEV), and the signal check below.

Current weights:

  • KEV: 30
  • EPSS: 20
  • EPSS delta (24 h): 5
  • Public PoC (or nuclei template): 25
  • Nuclei template bonus: 8
  • Momentum (chatter accelerating): 5
  • Severity: 1
  • Advisories (Tier 1): 10
  • Community chatter (Tier 2): 15
  • Half-life: 30 days (a score halves every 30 days)
  • Social window: mentions from the last 7 days count

Tune in onyx.yaml (trending.weights.*, trending.decay_days, trending.social_window_days).

Signals

  • KEV — listed in the CISA Known Exploited Vulnerabilities catalog: exploited in the wild.
  • PoC — public exploit code: vulnx’s is_poc flag, or a citation to Exploit-DB, Packet Storm, Metasploit, Nuclei templates or an exploit repository.
  • NUCLEI — a nuclei template exists for the CVE. It counts as public exploit code (a template is a verified, triggering check) and adds a bonus on top: internet-wide scanning for the bug is one command away, for defenders and attackers alike. A newly published template shows up under What changed and in the RSS feed.
  • EPSS — FIRST’s probability of exploitation in the next 30 days; the delta compares with the previous day’s score.
  • ADV (Tier 1) — the number of distinct CERT / vendor advisory feeds citing the CVE this week.
  • Buzz (Tier 2) — community mentions this week: Hacker News stories and comments, Reddit posts, new GitHub repositories named after the CVE and, with a token, GitHub code search hits.

Sources

  • vulnx (ProjectDiscovery) — primary CVE feed, queried per watchlist entry by CPE vendor/product, newest first.
  • CISA KEV — matched to the watchlist on the catalog’s vendor/product fields.
  • FIRST EPSS — daily exploitation-probability scores for every tracked CVE.
  • nuclei-templates (ProjectDiscovery) — the CVE template index, downloaded each run; vulnx’s is_template flag fills in until a template lands in the index.
  • Advisory feeds (Tier 1) — CISA, CERT-EU, CERT-FR (alertes), CERT-FR (avis), Fortinet PSIRT, Cisco PSIRT, GitLab security releases, Drupal security, Node.js vulnerabilities, Django weblog, Ivanti security advisories, Kubernetes CVE feed, Wordfence (WordPress).
  • Hacker News (Tier 2) — Algolia search, looked up per CVE: those already mentioned elsewhere, then the top of the ranking.
  • Reddit (Tier 2) — newest posts of r/netsec, r/cybersecurity, r/sysadmin, r/blueteamsec (public RSS).
  • GitHub (Tier 2) — new repositories named or described after a CVE; code search for the top CVEs when GITHUB_TOKEN is set.

CVEs that advisories or the community discuss but that no watchlist tech matches appear in Discussed, not on the watchlist on the dashboard.

Without a PDCP_API_KEY, vulnx rate-limits after a handful of queries; Onyx then resumes from where it stopped on the next run, so full coverage takes a few hourly cycles.

Cadence

Default cadence: hourly. scripts/ingest.sh and scripts/render.sh are idempotent. The site shows a warning when its data is more than two hours old.

Signal check

Which signals actually point at exploitation, measured on the 2912 tracked CVEs: precision is the share carrying the signal that CISA lists in KEV; lift compares it with the 4.9% base rate. Lead is the median number of days the signal appeared before the KEV listing, where both are dated (mentions are kept 30 days, so lead samples stay small until history builds up).

Signal CVEs In KEV Precision Lift Lead before KEV
Nuclei template 47 47 100.0% ×20.4 —
Community chatter (HN / Reddit / GitHub) 9 6 66.7% ×13.6 —
EPSS ≥ 10 % 299 115 38.5% ×7.8 —
CVSS ≥ 9.0 246 17 6.9% ×1.4 254 d n=17
Public PoC 666 44 6.6% ×1.4 —
CERT / vendor advisory 138 8 5.8% ×1.2 —

Read with care: the tracked set is biased — it is the newest ~100 CVEs per tech (often too new for KEV or a template) plus older CVEs that reached Onyx because they are in KEV. Signals common on old KEV entries (nuclei templates, high EPSS) therefore look stronger here than they would on a random sample. Lead times need dated history and fill in over weeks; medians from fewer than 3 CVEs are hidden.

Use this to tune trending.weights in onyx.yaml: a signal with high lift and a positive lead is an early warning worth weighting up; one with lift near ×1 is noise.