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.