Beefy Boxes and Bandwidth Generously Provided by pair Networks
good chemistry is complicated,
and a little bit messy -LW
 
PerlMonks  

CISA Known Exploited Vulnerabilities monitoring & notifications

by marto (Cardinal)
on Jul 27, 2025 at 15:28 UTC ( [id://11165805]=CUFP: print w/replies, xml ) Need Help??

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.

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: CUFP [id://11165805]
Front-paged by davies
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others cooling their heels in the Monastery: (2)
As of 2026-04-14 19:23 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found

    Notices?
    hippoepoptai's answer Re: how do I set a cookie and redirect was blessed by hippo!
    erzuuliAnonymous Monks are no longer allowed to use Super Search, due to an excessive use of this resource by robots.