Zeek and Auditing of Cryptographic Connections
Hi !
Although the previously mentioned github repository “Zeek/PQC” gives good results, they are not the ones I wanted. What I want is to precisely identify which IT assets are present on my network. Then, assuming they use cryptographic algorithms, which ones are they, and more importantly, are they secure with respect to PQC?
As usual, let’s put Zeek to work because if he is well positioned, he should be able to give us the information we need. I have to say that during the script’s development, I had to adapt to the different naming conventions.
# Lowercase with underscores (Zeek standard)
"x25519_mlkem768",
"secp256r1_mlkem768",
"x25519_kyber768",
"secp256r1_kyber768",
# Uppercase / Compact formats (OpenSSL, BoringSSL, Wireshark variants)
"X25519MLKEM768",
"SECP256R1MLKEM768",
"X25519KYBER768",
"SECP256R1KYBER768",
"x25519mlkem768",
"secp256r1mlkem768"
Therefore, instead of relying on rigid, hardcoded string equality, the script converts KEX names to lowercase, strips formatting characters (_, -, spaces), and uses a pattern matcher. This automatically recognizes all client and vendor variants.
Here is the script “pqc_cnx_audit.zeek”
module PQC_Cnx;
export {
# 1. Extend the global Zeek Log::ID enum for PQC logging
redef enum Log::ID += { LOG };
# 2. Define the log record structure
type Info: record {
ts: time &log;
uid: string &log;
id: conn_id &log;
proto: string &log; # "SSL" or "SSH"
server_name: string &log &optional; # TLS SNI or SSH Server Banner
kex_algorithm: string &log &optional; # TLS Curve / Named Group or SSH KEX
cipher_suite: string &log &optional; # Symmetric Cipher
is_pqc: bool &log; # T if Key Exchange is PQC / Hybrid
};
}
# Pattern matching known Post-Quantum Cryptography algorithm families
const pqc_pattern = /mlkem|kyber|sntrup761|frodo|bike|hqc|mceliece|dlx/;
# Helper function: Normalizes KEX string and checks for PQC compliance
function is_pqc_kex(kex_name: string): bool
{
local normalized = to_lower(kex_name);
# Strip formatting variations (underscores, hyphens, spaces)
gsub(normalized, /_|-| /, "");
if ( pqc_pattern in normalized )
return T;
return F;
}
event zeek_init()
{
# Create the logging stream for pqc_connections.log
Log::create_stream(PQC_Cnx::LOG, [$columns=Info, $path="pqc_connections"]);
}
# -----------------------------------------------------------------------------
# 1. SSL/TLS Connection Audit
# -----------------------------------------------------------------------------
event ssl_established(c: connection)
{
# Filter connections to process only local network IPs
if ( ! Site::is_local_addr(c$id$orig_h) && ! Site::is_local_addr(c$id$resp_h) )
return;
if ( ! c?$ssl )
return;
local is_pqc_safe = F;
local used_kex = "";
if ( c$ssl?$curve )
{
used_kex = c$ssl$curve;
if ( is_pqc_kex(used_kex) )
is_pqc_safe = T;
}
local rec: PQC_Cnx::Info = [
$ts=network_time(),
$uid=c$uid,
$id=c$id,
$proto="SSL",
$is_pqc=is_pqc_safe
];
if ( c$ssl?$server_name )
rec$server_name = c$ssl$server_name;
if ( c$ssl?$cipher )
rec$cipher_suite = c$ssl$cipher;
if ( used_kex != "" )
rec$kex_algorithm = used_kex;
Log::write(PQC_Cnx::LOG, rec);
}
# -----------------------------------------------------------------------------
# 2. SSH Connection Audit
# -----------------------------------------------------------------------------
event SSH::log_ssh(rec_ssh: SSH::Info)
{
# Filter connections to process only local network IPs
if ( ! Site::is_local_addr(rec_ssh$id$orig_h) && ! Site::is_local_addr(rec_ssh$id$resp_h) )
return;
local is_pqc_safe = F;
local used_kex = "";
if ( rec_ssh?$kex_alg )
{
used_kex = rec_ssh$kex_alg;
if ( is_pqc_kex(used_kex) )
is_pqc_safe = T;
}
local rec: PQC_Cnx::Info = [
$ts=rec_ssh$ts,
$uid=rec_ssh$uid,
$id=rec_ssh$id,
$proto="SSH",
$is_pqc=is_pqc_safe
];
if ( rec_ssh?$server )
rec$server_name = rec_ssh$server;
if ( rec_ssh?$cipher_alg )
rec$cipher_suite = rec_ssh$cipher_alg;
if ( used_kex != "" )
rec$kex_algorithm = used_kex;
Log::write(PQC_Cnx::LOG, rec);
}
Here is an example of the file resulting from the analysis.
#separator \x09
#set_separator ,
#empty_field (empty)
#unset_field -
#path pqc_connections
#open 2026-09-25-14-01-17
#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p proto server_name kex_algorithm cipher_suite is_pqc
#types time string addr port addr port string string string string bool
1790362826.753344 CAVKvg4PAB9FgUq767 18.192.166.72 33142 162.212.157.188 443 SSL www.openmon.org X25519MLKEM768 TLS_CHACHA20_POLY1305_SHA256 T
1790362847.865459 CugxMg3LvNNNSWZNNc 162.212.157.188 3413 8.8.4.4 853 SSL - X25519MLKEM768 TLS_CHACHA20_POLY1305_SHA256 T
1790362848.140136 CHuxrC4ILDdwpZJ6vg 162.212.157.188 2502 142.251.150.119 443 SSL www.google.com x25519 TLS_CHACHA20_POLY1305_SHA256 F
1790362848.195039 CANhgv4mVg2l0NkZj7 162.212.157.188 24093 142.251.157.119 443 SSL www.google.com x25519 TLS_CHACHA20_POLY1305_SHA256 F
1790362848.198440 CO3Zrx3rAFNKmIINb7 162.212.157.188 26836 9.9.9.9 443 SSL - x25519 TLS_CHACHA20_POLY1305_SHA256 F
1790362874.011921 CRwJQd2Jmeduh2B84h 76.65.144.199 36864 162.212.157.188 443 SSL sunset.openmon.org x25519 TLS_CHACHA20_POLY1305_SHA256 F
1790362885.340102 CtygO44Bk7HEDxCa0i 162.212.157.188 12441 8.8.8.8 853 SSL - X25519MLKEM768 TLS_CHACHA20_POLY1305_SHA256 T
1790363016.856218 CIKpUe3VmKPc7FEV03 3.212.205.90 25806 162.212.157.188 443 SSL www.openmon.org secp256r1 TLS_AES_256_GCM_SHA384 F
1790363134.208916 ChE1Vj3kpIZtQroME3 172.203.190.142 11371 162.212.157.188 443 SSL www.openmon.org x25519 TLS_CHACHA20_POLY1305_SHA256 F
1790363209.618070 CRNG976j4ZxyDmye2 66.249.66.35 45919 162.212.157.188 443 SSL www.openmon.org x25519 TLS_CHACHA20_POLY1305_SHA256 F
1790363215.165605 CR5YV3bif8bQZ4G12 66.249.66.36 38594 162.212.157.188 443 SSL www.openmon.org x25519 TLS_CHACHA20_POLY1305_SHA256 F
To complete the picture, let’s visualize this (after all, you must be starting to realize that I prefer to “see” things rather than just log lines). So, I ended up coding a program in Perl that will transform the lines created by Zeek into HTML format. Here’s the result: PQC connections
In the next article, I will talk about PQC and explain the reason for taking an inventory of cryptographic algorithms.
Cheers.