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.
Posts are HTML formatted. Put <p> </p> tags around your paragraphs. Put <code> </code> tags around your code and data!
Titles consisting of a single word are discouraged, and in most cases are disallowed outright.
Read Where should I post X? if you're not absolutely sure you're posting in the right place.
Please read these before you post! —
Posts may use any of the Perl Monks Approved HTML tags:
- a, abbr, b, big, blockquote, br, caption, center, col, colgroup, dd, del, details, div, dl, dt, em, font, h1, h2, h3, h4, h5, h6, hr, i, ins, li, ol, p, pre, readmore, small, span, spoiler, strike, strong, sub, summary, sup, table, tbody, td, tfoot, th, thead, tr, tt, u, ul, wbr
You may need to use entities for some characters, as follows. (Exception: Within code tags, you can put the characters literally.)
| |
For: |
|
Use: |
| & | | & |
| < | | < |
| > | | > |
| [ | | [ |
| ] | | ] |
Link using PerlMonks shortcuts! What shortcuts can I use for linking?
See Writeup Formatting Tips and other pages linked from there for more info.
|
|