October 31, 2026
Building a Security Advisory Bulletin Automation: From 25000 CVEs to One Email per Client
Every day new security vulnerabilities are published. A security team cannot send all of them to every client. Most would not apply and…
By Khalilrouissi
3 min read
Every day new security vulnerabilities are published. A security team cannot send all of them to every client. Most would not apply and clients would stop reading the emails.
So I built a small project that sends each client only the vulnerabilities that affect the products they really use. To test it I created three fictional clients.
There is no AI in it. Every decision follows a simple rule so I can always explain why a vulnerability was sent or skipped.
In short version we input public vulnerability data plus each client list of products. Next we process the data and match rank and write email per client with only the relevant iteams
The problem I had is that each client uses different products. In my demo Client A runs FortiOS and Windows Server 2019. Client B runs PAN-OS and Ubuntu. There are eight product in total across the three clients.
A new Fortinet vulnerability should reach Client A and not Client B because of the product and version deference. so we need to check the product and version first then decide who gets the alert.
SO how all of this works? The project is made of six small Python scripts. I run them one after another by hand.
1- Fetch: download the latest data from NVD and CISA KEV 2- Normalize: pull out the vendor, product and version or each affected product 3- Match: compare those products with each client list 4- Prioritize: keep the important recent items that were not send before 5- Generate: write one bulletin per client from template 6- Send: email it to each client
In run the matcher found 296 CVEs for client A, 12 for Client B and 14 for Client C. After the rules the three emails contained 149,2 and 4 CVEs.
Client A has the most because Windows Server 2019 gets many fixes every month.
Two time limits are used. The fetch step asks NVD for CVEs changed in the last 7 days. The selection step then keeps CVEs from the last 30 days except KEV ones which are always kept
The downloaded files are saved. If they already exist, the script reuses them instead of downloading again
Now how a match is decided. First we take one product in a client list for example vendor paloaltonetworks and the product pan-os and the version 10.2.4. NVD names the same product with CPE string and my code read three parts from it the vendor, product and version
Then it make two checks. First the vendor and the product name must be written exactly the same. Second the client version must be the same as the affected versions
This is a simple version check not a full one. If NVD leaves the version blank my code still counts it as a possible match which can be a false alarm. I want to test and improve this.
Now after matching how do we rand the items?
The ranking uses fixed rules with no hidden score. A CVE on the KEV list is URGENT. Otherwise the CVSS score decides 9.0 or more is CRITICAL 7.0 or more is HIGH and anything lower is skipped.
After that the script uses Jinja2 templates to create an HTML and a plaintext bulletin for each client that has items Each item shows the CVE its priority the affected product what to do and patch links when available.
The bulletin step writes a file called outbox.json with who gets what. When I run send.py it sends each email over SMTP and then saves the CVE IDs in sent.json.
In my test three bulletins were sent with 149, 2 and 4 CVEs. I then ran the scripts again and got nothing new for every client because those CVEs were already in sent.json. This is the duplicate protection working. It also means no empty nothing to report emails are sent.
And finally the github repo for the project