Case study
VirusTotal enrichment for Cloudflare alerts
The Python script runs from cron. It pulls the IP address from Cloudflare alerts in the SIEM, checks it in the public VirusTotal database and writes the result - country, provider, reputation and the number of vendors flagging the address as malicious - straight into the alert.
- Technologies
- Python
- requests
- VirusTotal API v3
- Cloudflare
- Query DSL
- cron
Context
The SIEM receives alerts from Cloudflare. An alert shows which rule fired and which IP address the request came from, but says nothing about that address’s reputation in public sources.
Problem
Analysts used to look up the addresses from Cloudflare alerts in VirusTotal by hand, which meant repetitive work for every alert.
My role
I wrote the whole script on my own, in Python, without AI.
Data and tools
- Input
- new alerts from Cloudflare rules in the SIEM; the client IP address from the alert body
- Context source
- VirusTotal API v3 -
/ip_addresses/{ip} - Output
- fields with the VirusTotal result added to the alert document
- Libraries
requests,python-dotenv,ipaddress,logging
How it works
Fetch new alerts
Every minute (cron) the script sends a Query DSL request to the SIEM for the newest alerts from the latest time window that don’t have a VirusTotal result yet. The per-run alert limit is matched to the actual alert volume in the SIEM.
Pick the Cloudflare alerts
Only alerts from Cloudflare rules are kept, and the client address is taken from their body.
Public addresses only
Only public addresses are sent to VirusTotal (
ipaddress.ip_address(ip).is_global).Query VirusTotal
From the response the script takes the country, AS owner, ASN, network range, reputation and the last analysis stats, and when the address is flagged as malicious, also the list of engines that flagged it.
Write back to the alert
The result goes into the alert in the SIEM via
_update: provider, country, reputation, the malicious count and, if any, the engines behind it.Step and error logs
Each step logs its own duration and the running total, and errors go to a separate, detailed log.
{
"size": <limit>,
"query": {
"bool": {
"must": [{ "range": { "@timestamp": { "gte": "now-<okno>" } } }],
"must_not": [{ "exists": { "field": "<pole_wyniku>" } }]
}
},
"sort": [{ "@timestamp": { "order": "desc" } }]
}if ipaddress.ip_address(ip).is_global:
response = requests.get(url, headers={"X-Apikey": VT_key})
attributes = response.json()["data"]["attributes"]
stats = attributes["last_analysis_stats"]
result = {
"country": attributes.get("country"),
"isp_name": attributes.get("as_owner"),
"reputation": attributes.get("reputation"),
"malicious": stats.get("malicious"),
}{
"virustotal": {
"ISP_name": "LAB-EXAMPLE-AS",
"Country": "ZZ",
"Reputation": -12,
"Malicious": 7,
"FlaggedBy": "EngineA,EngineB,EngineC"
}
}Key decisions
- Each alert checked once
- The query skips alerts that already have a VirusTotal result, so no alert is checked twice.
- Public addresses only
- Private and internal addresses never leave the organization.
- A limit matched to the volume
- The number of alerts fetched per run is calibrated to the actual volume of Cloudflare alerts in the SIEM, with the script running every minute.
- Secrets outside the code
- The VirusTotal key and SIEM credentials live in
.env, not in the code. - The result in the alert itself
- The analyst sees the address reputation right in the alert, without opening VirusTotal.
Status
The script runs in production: cron starts it every minute, and the results go straight into the alerts in the SIEM, which the whole SOC team uses.
Limitations
- The enrichment covers only alerts from Cloudflare rules and only public IP addresses.
- The VirusTotal reputation is context for the analyst, not a verdict: the decision on the alert stays with a person.