GitHub
ESC

Fetching the Live Feed

The KEV::Client class fetches the live CISA feed over HTTPS, parses it, and returns a Catalog. The module-level KEV.fetch is a thin wrapper for one-shot use.

One-shot fetch

require "kev"

catalog = KEV.fetch
puts "#{catalog.size} entries, released #{catalog.date_released}"

The default endpoint is the canonical feed URL:

https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json

For repeated calls — a CI job, a cron sync, a Slack notifier — use a KEV::Client instance so you can short-circuit on unchanged feeds:

client = KEV::Client.new
first  = client.fetch
later  = client.fetch_if_modified  # => nil if the feed has not changed

fetch_if_modified sends If-None-Match (from the last ETag) and If-Modified-Since (from the last Last-Modified header) and returns nil on a 304 Not Modified response. The first call (no validators yet) behaves like a regular fetch.

client.last_etag         # => "\"kev-v1\"" (or nil before first fetch)
client.last_modified     # => "Wed, 15 May 2026 16:55:06 GMT" (or nil)

Configuration

KEV::Client.new(
  url: KEV::Client::DEFAULT_URL,
  user_agent: "my-app/0.1 (+https://example.com)",
  connect_timeout: 5.seconds,
  read_timeout: 15.seconds,
)

A descriptive User-Agent is recommended — CISA may reach out to maintainers if a misbehaving client is identified. The library defaults to kev.cr/<version> (+https://github.com/hahwul/kev.cr).

Error handling

Failure Exception
DNS / connect / TLS errors KEV::FetchError
Non-2xx response KEV::FetchError (status code embedded in the message)
Body present but not JSON JSON::ParseException
JSON valid but schema-malformed KEV::ParseError
begin
  catalog = KEV.fetch
rescue ex : KEV::FetchError
  STDERR.puts "transport error: #{ex.message}"
  exit 1
end

Redirects are not followed

KEV::Client deliberately does not follow 3xx redirects. CISA's feed URL has been stable, and a silent follow could route the client to an attacker-controlled host if the upstream is ever compromised. A 301 / 302 surfaces as a FetchError so you notice and update your configuration.

If you need to point the client at a known mirror or proxy, pass that URL to KEV::Client.new(url: ...) directly.

Persisting between fetches

If you want to keep the conditional-fetch state across process restarts, serialize client.last_etag and client.last_modified and pass them back manually — there is no automatic disk cache (and adding one would couple this library to a filesystem layout).

File.write("kev.etag", client.last_etag.to_s) if client.last_etag

See also