I recently changed my at home monitoring of the CISA Known Exploited Vulnerabilities feed (More information on KEV here) to make the alerting more accessible. While I monitor some products we use at work this is not a business critical service.

This quick and dirty script uses Mojolicious to hit this CISA KEV API, read a local file to match against target vendors, products or both, logging matches to a local cache so we don't keep reporting on the same thing and sending a notification to my local gotify instance (clients for web, Android etc.).

#!/usr/bin/env perl use strict; use warnings; use feature 'say'; use Mojo::Log; use Mojo::File; use Mojo::JSON qw(decode_json encode_json); use Mojo::UserAgent; # cisa-kev-mon # monitor the CISA KEV feed # * Send notifications to gotify # * Log them locally so we don't keep reporting on the same thing. # For further info on CISA Known Exploited Vulnerablities visit: # https://www.cisa.gov/known-exploited-vulnerabilities # logging my $log = Mojo::Log->new( path => 'cve_mon.log', level => 'debug' ); # read list software we're interested in unless ( -e 'targets.json' ){ $log->fatal( 'no targets.json' ); die 'No targets.json'; } # get targets my @targets = @{ decode_json( Mojo::File->new( 'targets.json' )->slurp + ) }; # gotify notification server config unless ( $ENV{GOTIFY_URL} && $ENV{GOTIFY_TOKEN} ){ $log->fatal( 'Check Gotify env vars, GOTIFY_URL & GOTIFY_TOKEN' ); die 'Fail: Check Gotify env vars, GOTIFY_URL & GOTIFY_TOKEN'; } my $gotify_url = $ENV{GOTIFY_URL}; my $gotify_token = $ENV{GOTIFY_TOKEN}; # cve.org base url my $cve_url = 'https://www.cve.org/CVERecord?id='; # cisa recent json feed my $cisa_url = 'https://www.cisa.gov/sites/default/files/feeds/known +_exploited_vulnerabilities.json'; # local cache so we don't report on already seen CVEs my $cve_cache = Mojo::File->new( 'seen_cves.json' ); # Fetch JSON my $ua = Mojo::UserAgent->new; my $res = $ua->get( $cisa_url )->result; # die if we can't get the JSON feed unless ( $res->is_success ){ $log->error( 'Failed to fetch CISA feed: ' . $res->message ); die 'Failed to fetch CISA feed: ' . $res->message; } my $data = decode_json( $res->body ); my @vulns = @{ $data->{vulnerabilities} }; # Load existing CVEs from local cache my %seen_cves; if ( -e $cve_cache ){ %seen_cves = %{ decode_json( $cve_cache->slurp ) }; } # Filter existing & collect new CVEs my @new_cves; foreach my $vuln ( @vulns ) { my $vendor = $vuln->{vendorProject}; my $product = $vuln->{product}; my $cve_id = $vuln->{cveID}; # skip if the CVE has been logged before next if $seen_cves{$cve_id}; # for each target for my $target( @targets ){ my $matches = 0; # Match both vendor and product if (defined $target->{vendor} && defined $target->{product}) { + $matches = ($vendor =~ /\Q$target->{vendor}\E/i && $produc +t =~ /\Q$target->{product}\E/i); } # match vendor elsif (defined $target->{vendor}) { $matches = ($vendor =~ /\Q$target->{vendor}\E/i); } # match product elsif (defined $target->{product}) { $matches = ($product =~ /\Q$target->{product}\E/i); } if ($matches) { # post to gotify my $res = $ua->post( $gotify_url => { 'X-Gotify-Key' => $gotify_token } => form => { title => 'cisa KEV CVE alert', message => "New CVE: $vendor - $product $cve_url$c +ve_id", priority => 5, } )->result; unless ( $res->is_success ){ $log->fatal( 'Failed to post to gotify: ' . $res->code + . ' - ' . $res->message ); die 'Failed to post to gotify: ' . $res->code . ' - ' +. $res->message; } # add to local cache push @new_cves, $vuln; $seen_cves{$cve_id} = 1; last; } } } # Output if ( @new_cves ) { say 'New vulnerabilities found:'; foreach my $cve ( @new_cves ) { say "[$cve->{cveID}] $cve->{vendorProject} $cve->{product}: $c +ve->{vulnerabilityName} (Added: $cve->{dateAdded})"; } } else { say 'No new vulnerabilities for your monitored vendors/products.'; } # Save updated seen CVEs to local file Mojo::File->new( 'seen_cves.json' )->spew( encode_json( \%seen_cves ) +);

targets.json example:

[ { "vendor": "Microsoft", "product": "SharePoint" }, { "vendor": "Microsoft", "product": "Windows 2012" }, { "vendor": "Microsoft", "product": "Windows 10" }, { "vendor": "Microsoft", "product": "CoPilot" }, { "vendor": "Microsoft", "product": "Teams" }, { "vendor": "Microsoft", "product": "Edge" }, { "vendor": "Oracle", "product": "Solaris" } { "vendor": "Example Vendor"}, { "product": "Example Product"} ]

Example output if you bother to leave that in:

New vulnerabilities found: [CVE-2025-49704] Microsoft SharePoint: Microsoft SharePoint Code Injec +tion Vulnerability (Added: 2025-07-22) [CVE-2025-49706] Microsoft SharePoint: Microsoft SharePoint Improper A +uthentication Vulnerability (Added: 2025-07-22) [CVE-2025-53770] Microsoft SharePoint: Microsoft SharePoint Deserializ +ation of Untrusted Data Vulnerability (Added: 2025-07-20)

Working in large multi vendor organisations, many of whom are outsourced, we don't always hear about things promptly, if at all. Forewarned is forearmed as the adage goes. Screenshot of the Gotify Android app output.