System design

Architecture overview

CAT is a fully client-side single-page application. There is no back-end server, no database, and no user accounts. All data fetching, parsing, and rendering is performed in the user's browser using vanilla JavaScript.

๐ŸŒ Browser
โ†’
๐Ÿ“ก CORS Proxy
Workers
โ†’
๐Ÿ”— Source APIs
Public sources
โ†’
๐Ÿ“ฆ Raw data
JSON / HTML
โ†’
โš™ Parser
in-browser
โ†’
๐Ÿ–ฅ DOM render

CORS proxy

Browsers enforce the Same-Origin Policy, which prevents JavaScript from fetching resources from domains it didn't originate from. Since CAT needs to reach different external origins, all requests are routed through a Cloudflare Worker proxy at:

The Worker fetches the target URL server-side and returns the response with permissive Access-Control-Allow-Origin: * headers, bypassing the browser CORS restriction. The Worker is stateless and does not store any request data.

โš ๏ธ If the proxy worker is unavailable, all source dots will show black/white (network error). The proxy is the only blocking point that can prevent proper data retrieval.

Fetch flow

For each CVE, fetches are organised in two phases:

Phase 1 (awaited): CVEList and NVD are fetched simultaneously. These are the primary sources; their data is needed to build the initial card DOM โ€” assigner, date, base CVSS, initial CWEs, and initial references. The card is inserted into the page once both resolve (or time out).

Phase 2 (parallel): All other sources are fetched simultaneously. Each source updates the card independently via refreshCard(). This allows the page to be useful before all secondary sources finish.

A 400 ms delay is added between cards when processing a batch search, avoiding request storms. The spinning โŸณ icon on the References row disappears once all Phase 2 queries have settled.

Landing page

Landing curtain

The landing curtain is a collapsible panel rendered before any CVE search. It aggregates live data from four independent feeds, renders a unified scrolling carousel. All processing is client-side; no dedicated endpoint is involved beyond the CORS proxy.

Data feeds

Four asynchronous fetches are launched in parallel on page load. Each resolves independently and triggers a debounced carousel rebuild (60 ms) to avoid redundant DOM operations when multiple sources settle simultaneously.

NVD Latest NVD REST API v2 with a sliding pubDate window (3 โ†’ 7 โ†’ 14 days), sorted client-side by published descending. Returns up to 5 CVEs. Top EPSS api.first.org/data/v1/epss?order=!epss&limit=5 โ€” top 5 by EPSS score. Each CVE is then individually enriched via NVD (score, description, CWE) with a 300 ms inter-request delay to stay within rate limits. CISA KEV The 5 most recently added entries (sorted by dateAdded) are shown. ENISA Critical euvdservices.enisa.europa.eu/api/criticalvulnerabilities โ€” up to 5 entries. A parallel request to the most critical new vulnerabilities.

Session cache

Once all feeds have loaded, the complete dataset is saved to sessionStorage with a one-hour TTL. On subsequent page loads within that window, the banner renders instantly from the cache โ€” no network requests are made. The cache is cleared when the tab is closed.

Cards from all active feeds are interleaved in round-robin order and duplicated to produce a seamless infinite scroll. The scroll speed is kept constant regardless of how many cards are displayed, and the animation pauses on hover. CWE names are resolved from the local database and injected into every chip โ€” including the duplicated half of the track โ€” without any additional network request.

Toggling a feed filter chip instantly rebuilds the carousel from already-loaded data, with no new fetch triggered.

Data sources

Data sources โ€” detailed reference

CVEList MITRE CVEList v5 JSON (GitHub raw)
URL pattern https://raw.githubusercontent.com/CVEProject/cvelistV5/main/cves/{YEAR}/{BUCKET}xxx/{CVE-ID}.json Method GET โ†’ JSON Auth None (public GitHub repo)

The bucket is obtained by taking the first digits depending on the size of the id (e.g. CVE-2024-12345 โ†’ bucket 12xxx or CVE-2024-1345 โ†’ bucket 1xxx).

Extracted fields: Assigner, Published date, Descriptions, Score CVSS, CWE, References urls.

This is the primary source. A null response (error network 404 from GitHub) means the CVE has been reserved but not yet published โ€” the card shows a "Not in CVEList" warning.

NVD NIST National Vulnerability Database REST API v2 (JSON)
URL pattern https://services.nvd.nist.gov/rest/json/cves/2.0?cveId={CVE-ID} Method GET โ†’ JSON Auth None (public API, optional API key for higher rate limits)

Extracted fields : Description, Score CVSS, CWE, References urls.

NVD can lag behind CVEList by hours to weeks for newly published CVEs.

โš ๏ธ Currently, all extracted scores are automatically marked as coming from NVD, while in reality NVD aggregates and republishes scores from other providers. This means that the NVD label does not guarantee that the score was calculated by NVD itself.
RedHat Red Hat Security Advisory CSAF VEX (JSON) + Legacy REST API
Primary URL https://security.access.redhat.com/data/csaf/v2/vex/{YEAR}/cve-{id}.json Fallback URL https://access.redhat.com/hydra/rest/securitydata/cve/{CVE-ID}.json Format CSAF VEX (primary) / Red Hat proprietary JSON (fallback)

Two requests are made in parallel (CSAF VEX and legacy API).

Information extracted : Description, Score CVSS, CWE, References urls.

The legacy API and the CSAF JSON work in a complementary way (if information is not found in one then we check in the other).

SUSE SUSE Linux Enterprise CSAF VEX (JSON via FTP)
URL pattern https://ftp.suse.com/pub/projects/security/csaf-vex/{cve-id-lowercase}.json Format CSAF VEX 2.0

Extraction follows the same CSAF VEX logic as Red Hat: Description, Score CVSS, References urls.

โš ๏ธ I've noticed a discrepancy between the CVSS score provided on the CSAF and the score displayed on the SUSE website. Therefore, I assume that the unique CVSS score in the CSAF file reflects SUSE's analysis.
Debian Debian Security Tracker HTML scraping
URL pattern https://security-tracker.debian.org/tracker/{CVE-ID} Method GET HTML โ†’ DOMParser

The page is parsed to pick up the description. The HTML is also checked to detect if debian's product is concerns by this vulnerability.

No CVSS data is extracted from Debian (they don't publish their own scores).

The link used to extract the data is the same as the page link.

Ubuntu Ubuntu Security CVE Database JSON API
Primary URL https://ubuntu.com/security/cves/{CVE-ID}.json Fallback URL https://ubuntu.com/security/api/v1/cves/{CVE-ID} Format Canonical proprietary JSON

Extracted fields: Description, Score CVSS, References urls.

Like Red Hat, the fallback URL is used to complete the primary URL, if needed.

Microsoft Microsoft Security Response Center CSAF VEX + SUG API + CIRCL
CSAF URL https://msrc.microsoft.com/csaf/vex/{YEAR}/msrc_{cve-lowercase}.json SUG API https://api.msrc.microsoft.com/sug/v2.0/en-US/vulnerability?$filter=cveNumber eq '{CVE-ID}' CIRCL URL https://cve.circl.lu/search?q=msrc_{cve-lowercase}

Multiple sources are queried in parallel. Priority order for description and CVSS: CSAF > SUG API > CIRCL. This strategy was adopted to maximize the retrieval of information on Microsoft.

Extracted fields: Description, Score CVSS, References urls.

Dot logic: Green if any of the three sources returns data. Black/white (network error) only if both CSAF and SUG API return HTTP 0. Red otherwise.

โš ๏ธ I noticed a discrepancy between these sources: Page, CSAF, Sug or CIRCL. So, I chose to retrieve as much information as possible in these sources.

Example : For the vulnerability CVE-2024-42317, the CSAF file and the CIRCL entry are reachable, but the MSRC SUG API and the MSRC Page do not exist.

Amazon Amazon Linux Security Center HTML scraping
URL pattern https://explore.alas.aws.amazon.com/{CVE-ID}.html Method GET HTML โ†’ DOMParser

Extracted fields: Description, Score CVSS.

LibreOffice LibreOffice Security Advisories HTML scraping
URL pattern https://cs.libreoffice.org/about-us/security/advisories/{cve-lowercase}/ Method GET HTML

Only a description is provided by this editor, so it is extracted from HTML tags. There was an issue with https://www.libreoffice.org/about-us/security/advisories/{cve-lowercase}/ domain, maybe some desynchronisation between regional deploy. cs.libreoffice.org will now be use in case of the first link will redirect to https://www.libreoffice.org/security/.

PostgreSQL PostgreSQL Security Information HTML scraping
URL pattern https://www.postgresql.org/support/security/{CVE-ID}/ Method GET HTML โ†’ DOMParser

Extracted fields: Description, Score CVSS.

Oracle Oracle Security Alerts HTML scraping
URL pattern https://www.oracle.com/security-alerts/alert-{cve-lowercase}.html Method GET HTML โ†’ DOMParser

Only a description is extracted from this source. Oracle does not publish machine-readable CVSS data on their individual CVE alert pages.

CISA CISA Known Exploited Vulnerabilities Catalog JSON feed (cached)
Feed URL https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json Format JSON with vulnerabilities[] array

The full CISA KEV feed is fetched once per browser session and cached in a module-level variable. All subsequent CVE cards look up their ID directly in the in-memory cache โ€” no extra network request is made.

Extracted fields: Description, CWEs, Reference urls.

Xen Xen Security Advisories JSON
CVE URL https://xenbits.xen.org/xsa/xsa.json Advisory URL https://xenbits.xen.org/xsa/advisory-${xsaNum}.html Format HMTL + JSON

The strategy is to check whether the CVE is known to Xen; if so, the name of the associated advisory is retrieved to extract the relevant information.

Only the description is extracted by this editor.

ENISA ENISA European Union Vulnerability Database REST API (JSON)
API URL https://euvdservices.enisa.europa.eu/api/enisaid?id={CVE-ID} Chip URL https://euvd.enisa.europa.eu/vulnerability/{EUVD-ID} Method GET โ†’ JSON (single object or single-item array)

It is possible to request the ENISA directly with a CVE because, the CVE can be an alias with an EUVD identifier.

Extracted fields: description, baseScore, references.

โš ๏ธ Currently, ENISA aggregates information and does not invest heavily in enriching each vulnerability entry. As a result, this source may provide data already available from other sources.
Internals

Processing logic

CVSS deduplication

Every time a source returns CVSS data, pushCvss(list, version, data, source) is called. It computes a (version, score, vector) triplet. If an identical triplet already exists in list, the source name is appended to its sources[] array. Otherwise a new entry is created. This means two sources reporting the exact same score and vector are merged into one badge with both source names.

The vector is normalised to a canonical number of components before comparison: 6 for v2.0, 8 for v3.0/v3.1, 11 for v4.0 (extra metrics like environmental and temporal are stripped). Scores are sorted descending by value; ties broken by version (v4.0 > v3.1 > v3.0 > v2.0).

Description grouping

Two descriptions are considered identical if their normalizeForComparison() values match โ€” this function lowercases, collapses whitespace, normalises unicode quotes/dashes/ellipsis, and strips HTML tags.

The result is a list of {text, sources[]} objects rendered one per row in the description table. This correctly handles cases where NVD copies CVEList verbatim, or where a vendor slightly reformats text without changing meaning.

CWE collection

The CWE is information are retrieve in cwe.js.

If any CWE entry is missing a name after collection, one function scrape the CWE detail page from cwe.mitre.org and extract the title from the page's <title> tag.

Reference filtering

Collected references are filtered through two exclusion layers:

(1) Excluded URLs set: All the known source URLs for the current CVE (e.g. the NVD detail page, all sources chip links, all raw data URLs) are pre-computed and excluded. This prevents the References section from being polluted with links that are already clickable via the source chips.

(2) Not-affected domain suppression: If a source is determined to be "not-affected" (e.g. Ubuntu says DNE for all packages), its domain-specific links are filtered out. For example, Ubuntu domain links are suppressed if Ubuntu is not-affected. This avoids noisy references pointing to Ubuntu pages that merely confirm non-affectedness.

EPSS scoring

EPSS scoring is retrieved from first.org/epss. One function queries the official EPSS API and retrieves the latest daily score, then is compare to the score a month ago, depending of evolution observed an indication will show the trend.

Environmental requirements

A calculator holds values for all overridable metrics (CR, IR, AR plus all modified base metrics). It is persisted in localStorage and loaded on startup. Any change recomputes an environmental sub-score.

Score computation per version:

  • CVSS v2.0 : applies CR/IR/AR weights to the base CIA metrics via the NIST adjusted impact formula, then recomputes exploitability. Output rounded to one decimal.
  • CVSS v3.0 / v3.1 : Modified metrics (MAV, MAC, MPR, MUI, MS, MC, MI, MA) override their base counterparts when set. The modified impact sub-score uses CR/IR/AR weights; scope change is respected. Output rounded via the CVSS round-up function.
  • CVSS v4.0 : Computes six severity levels (EQ1โ€“EQ6) from the vector, looks up the base score in a 218-entry table, then interpolates within each level to produce a precise score. Supports all v4.0 modified metrics.

UI โ€” compact panel vs. full modal. The Options panel exposes only the three Security Requirements (CR/IR/AR) plus a ๐Ÿงฎ button that opens the full modal. The modal renders all four groups via : Security Requirements, Modified Base โ€” Common (v3+v4), Modified Base โ€” v3.x, and Modified Base โ€” v4.0. All temporal and unset modified metrics are left as X / ND.

CAPEC associations

CAPEC (Common Attack Pattern Enumeration and Classification) entries are sourced from the local data/cwe.js database. Each CWE entry in the database carries an optional c array of CAPEC IDs that are known to exploit that weakness.

When CWE chips are rendered, the capecs array from the resolved DB entry is read. A single CAPEC is shown as a direct link chip below the CWE row. When multiple CAPECs exist, they are collapsed behind a N CAPECs โ–พ toggle button to keep the UI compact. Each chip links to the corresponding CAPEC detail page on capec.mitre.org. No additional network request is made โ€” all data comes from the pre-built local database.

Client-side state

State management

CAT maintains two layers of client-side state for CVE results: a persistent list of CVEs in localStorage, and in sessionStorage. Neither layer requires a backend.

Data cache

CVE data is stored exclusively in localStorage, with a maximum of 25 CVE IDs in the master list (cat_cves). When a 26th CVE is added, the oldest ID and its associated data entry are evicted. All entries share a 7-day TTL.

The landing curtain feed data (CISA KEV, ENISA lists, NVD latest, EPSS top 5, EPSS batch map) is cached separately in sessionStorage under the key cat_curtain with a 1-hour TTL. On page reload, the curtain renders instantly from this cache โ€” all network requests are skipped until the TTL expires or the tab is closed.

localStorage Up to 25 CVEs โ€” master list + data cache. TTL 7 days. Survives tab close and browser restart. sessionStorage Landing curtain feed data only (cat_curtain) โ€” TTL 1 hour. Cleared when the tab is closed.

LRU eviction

CVE data entries can be large (rich CSAF payloads, full Ubuntu JSON). If localStorage quota is exceeded when saving a card, the Least Recently Used (LRU) strategy evicts the oldest cached entry:

  1. Attempt to write the new entry.
  2. If a QuotaExceededError is caught, scan all cat_cve_* keys in localStorage and find the one with the lowest timestamp (oldest write time).
  3. Remove that entry and retry the write.
  4. Repeat up to 30 times until the write succeeds or no evictable entries remain.
โ„น๏ธ The LRU eviction removes the oldest data entry (cat_cve_*) but never touches the master ID list (cat_cves) โ€” all 25 CVE IDs remain listed even if their cached data was evicted. A fresh fetch is triggered the next time the card is restored.

Cache keys reference

All keys written by CAT:

cat_cves JSON array of up to 25 CVE IDs, ordered oldest โ†’ newest. (localStorage) cat_cve_{ID} Serialised card data (ctx, cvssBase, dotStatuses) โ€” TTL 7 days. (localStorage) cat_curtain Landing feed data: full CISA KEV, ENISA lists, NVD latest, EPSS top 5, EPSS map โ€” TTL 1 hour. (sessionStorage) cat_sort Active sort key โ€” one of none, cvss-desc, cvss-asc, date-desc, date-asc, id-asc, id-desc. (localStorage) cat_filters JSON array of active source names. (localStorage) cat_fields JSON object mapping field keys to booleans. (localStorage) cat_req JSON object with CR, IR, AR keys โ€” Environmental requirement settings. (localStorage) theme "light" or "dark". (localStorage)
Interface

UI architecture

Each CVE card exposes a share button (๐Ÿ”—) inline with the CVE title. Clicking it constructs a URL of the form index.html?cves=CVE-XXXX-XXXXX (comma-separated for multiple IDs) and writes it to the clipboard via the Clipboard API.

When CVEs come from the URL, the localStorage restore is skipped to prevent duplicate cards.

Source chip collapse

A chip whose dot has settled to a non-ok state (dot-fail, dot-notaffected, dot-networkerror) receives data-collapsed="true" and is hidden. A button showing +N โ–พ is injected at the end of the row; clicking it toggles the chips-expanded to show or hide the collapsed chips.

Source filter groups

The "Visible sources" panel is divided into two sub-groups: Databases (CVEList, NVD, ENISA) and Editors (all vendor sources). All sources are fully toggleable.

CVE navigator

The navigator is a dropdown widget embedded directly to the left of the search form. Each CVE in the list carries a colour corresponding to its max CVSS score. The list is rebuilt every time a card is added, deleted, or sorted.

Field visibility

Field visibility is implemented entirely in CSS via body-level classes. When a field is disabled, it masks this section on all cards simultaneously โ€” no manipulation of individual cards is required.

Smart refresh

The โ†ป refresh button re-queries all sources that returned dot-fail (red) or dot-networkerror (black/white). A sessionSave call at the end persists the refreshed state to cache.

Project

File structure

        โ”€โ”€ Project root โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
        shared.css               CSS variables, navigation, buttons โ€” shared across all pages
        app.js                   All JavaScript logic for the main page
        index.html               Primary CVE search interface
        user-guide.html          User guide
        technical-doc.html       Technical documentation
        changelog.html           Version history
        contact.html             Author information

        data/cwec_v4.19.1.xml    Official CWE catalog from cwe.mitre.org
        data/generate_cwe_js.py  Converts a CWE XML catalog (cwec_vX.Y.Z.xml) into data/cwe.js
        data/cwe.js              Formatted CWE dataset used by the application
        โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
      

Splitting into separate files provides several practical benefits: the main page's app.js can be edited without touching HTML; shared.css ensures all pages share the same design tokens and navigation without copy-paste; and the three documentation pages are purely HTML/CSS with no dependency on app.js, so they load immediately.

โ„น๏ธ All files are static โ€” no build step, no bundler, no Node.js required.