class

EPSS::Client

Inherits Reference < Object

High-level client for the FIRST EPSS REST API (https://api.first.org/data/v1/epss).

client = EPSS::Client.new
resp = client.fetch(EPSS::Query.new(cves: ["CVE-2022-27225"]))
resp.scores.first.epss # => 0.001870

Convenience helpers cover the common cases without constructing a Query:

client.score("CVE-2022-27225")       # => EPSS::Score?
client.scores(["CVE-1", "CVE-2"])    # => Array(EPSS::Score)
client.each_score(query) { |s| ... } # paginated stream

Constants

DEFAULT_BASE_URI = URI.parse("https://api.first.org/data/v1/epss")
DEFAULT_USER_AGENT = "epss.cr/#{EPSS::VERSION} (+https://github.com/hahwul/epss.cr)"
MAX_BACKOFF = 30.seconds

Upper bound for a single exponential-backoff sleep. Without a cap the doubling delay grows unbounded and can strand a fiber for minutes; 30s is plenty to let a transient outage clear.

Constructors

new(base_uri : URI = DEFAULT_BASE_URI, user_agent : String = DEFAULT_USER_AGENT, max_retries : Int32 = 3, retry_backoff : Time::Span = 500.milliseconds, transport : Transport = HTTPTransport.new)
Source

Instance methods

all_scores(query : Query = Query.new, *, page_size : Int32 = 1000) : Array(Score)

Materialize every result matching query into a single Array. Convenience wrapper around #each_score โ€” be aware that an unfiltered query can be hundreds of thousands of rows.

Source
backoff_delay(attempt : Int32) : Time::Span

Exponential backoff with a hard cap and decorrelated jitter: base * 2^(attempt-1), clamped to MAX_BACKOFF, plus up to 10% jitter.

The cap keeps a long retry chain from sleeping for minutes, and the jitter spreads concurrent retries so they don't all wake at once. This is library runtime code (not a deterministic workflow script), so a random source is acceptable here.

Public so the cap/jitter bounds can be unit-tested without sleeping.

Source
base_uri
Source
build_uri(query : Query) : URI

Compose the absolute URI for a query against this client's base.

Source
each_score(query : Query = Query.new, *, page_size : Int32 = 1000, & : Score -> ) : Nil

Iterate every score matching query, transparently fetching subsequent pages while results remain. Uses the API's limit / offset parameters; the iteration order matches the server's response order (controlled by query.order).

Source
fetch(query : Query = Query.new) : Response

Issue a single request and return the decoded Response. Does not iterate pages โ€” use #each_score or #all_scores for that.

Source
fetch_feed(date : Time, *, host : String = CSV::FEED_HOST) : CSV::Feed

Download and parse the daily EPSS feed for date using this client's transport, retry policy, User-Agent, and timeouts. The feed is the second canonical EPSS distribution channel; routing it through the same pipeline as the JSON API means a slow network surfaces as APIError instead of an unbounded hang.

feed = client.fetch_feed(Time.utc(2026, 5, 18))
feed.scores.size # => 240000+
Source
max_retries
Source
retry_backoff
Source
score(cve : String, *, date : Time | Nil = nil) : Score | Nil

Return the (at most one) score for a single CVE on the latest day, or nil if FIRST has no published score for it.

Source
scores(cves : Enumerable(String), *, date : Time | Nil = nil, batch_size : Int32 = 100) : Array(Score)

Look up multiple CVEs in one call. The API caps the URL length, so this helper batches into chunks of batch_size (default 100) and concatenates the results.

Source
time_series(cve : String) : Array(Score)

Fetch the full 30-day EPSS time-series for one CVE. Returns a flat list of Scores โ€” one per day, sorted oldest-first.

series = client.time_series("CVE-2022-27225")
series.first.date # => 30 days ago
series.last.date  # => today (or most recent publication)
Source
transport
Source
user_agent
Source