LinkedIn Sourceforge

Vincent's Blog

Pleasure in the job puts perfection in the work (Aristote)

Tuning log2table rules with real logs, and with an AI looking over my shoulder

Posted on 2026-10-24 17:43:00 from Vincent in OpenBSD Log2Table

Give a shoutout to Zach M on unsplash.com log2table is a small daemon I wrote: it watches log files, recognizes attack patterns with regular expressions, and puts the offending IP addresses in a PF table so the firewall blocks them. The whole behavior is driven by one file: parameters.py.

Writing rules is easy. Knowing whether they are good rules is harder: do they catch what is really hitting my server today? Do they ban innocent visitors? Do two rules count the same attack twice?

This post explains the new version of parameters.py in the repository, its built-in checker that runs against your real log files, and how I used an AI (Claude, through Claude Code) to read the checker's output and propose improvements.


1. parameters.py in a nutshell

PARAMS lists, for each log file, rules made of a regex and a weight. The regex must capture the attacker's address in a named group (?P<ip>...). Each match adds the rule's weight to that IP's score, and the IP is blocked once the score reaches the threshold. One principle matters for the rest of this post: one line, one rule. If a line matches two rules, the same attempt is counted twice.

For the details, see the log2table posts and the repository.


2. The checker: running your rules against real logs

The bottom of parameters.py is no longer a toy test on a hand-written test.txt. It replays your rules against the files they are meant for:

python3 parameters.py            # last 1000 lines of each log file in PARAMS
python3 parameters.py 5000       # last 5000 lines
python3 parameters.py -f test.txt  # a test file, against all rules

Every line comes back with a label:

RULE ['ssh_root']: Oct 23 04:22:06 myhost sshd-session[85005]: Failed password for root from 192.0.2.10 port 52138 ssh2
NOTMATCH: Oct 23 04:22:06 myhost sshd-session[85005]: Connection closed by authenticating user root 192.0.2.10 port 52138 [preauth]
MULTIPLE ['rule_a', 'rule_b']: ...
  • RULE: exactly one rule matched. This is what you want for attacks.
  • NOTMATCH: no rule matched. Most of these are normal traffic, but new attacks hide here.
  • MULTIPLE: several rules matched, so that line is double counted. Fix the regexes.

At the end, a count per rule is printed on stderr, so you can still pipe the lines:

/var/log/authlog: ssh_root=114, ssh_others=0, ssh_invaliduser=47, NOTMATCH=548, MULTIPLE=0
python3 parameters.py 5000 | grep NOTMATCH | less

Missing log files are reported and skipped, so the same file works on a machine without, say, a web server.


3. The hard part: reading thousands of NOTMATCH lines

On my server, one run over the last 1000 lines of ten log files produced 10,010 lines: about 5,400 RULE and 4,600 NOTMATCH. Nobody reads 4,600 lines carefully. That is where I asked an AI to help.

I ran the checker, saved its output (python3 parameters.py > log2table_parameters.out), and asked Claude Code, in the folder holding both files, a simple question: "Review this output: should I add new rules to block attackers?"

Here is what it did, step by step. It is worth describing, because it is exactly the method a careful admin would follow, just faster.

Step 1: group, don't read

Instead of reading lines one by one, it replaced the variable parts (IP addresses, ports, process ids, numbers) with placeholders and counted the remaining shapes:

150  sshd-session: WARNING: connection from IP port N is not using a post-quantum key exchange algorithm
118  sshd-session: Received disconnect from IP port N:N: Bye Bye [preauth]
 98  sshd-session: Disconnected from authenticating user root IP port N [preauth]
...

For web logs it did the same on the request path and status code. Thousands of lines became a few dozen patterns.

Step 2: is this IP already caught anyway?

An unmatched line is not automatically a missing rule. For each SSH pattern, it checked whether the IPs in those lines were already scored by other rules:

98 lines  98 ips  score0=0  under100=0  Disconnected from authenticating user root

All 98 IPs were already above the threshold thanks to ssh_root. These lines are just the follow-up message of an attempt that was already counted. Adding a rule would only double count. Conclusion: SSH was fine, no new rule needed.

Step 3: separate attacks from normal traffic

On the web side, the unmatched requests were mostly normal (RSS readers, CSS, images, search engines). But in the long tail it found a family of probes my rules didn't know:

  • cloud and deploy files: wrangler.toml, vercel.json, netlify.toml, serverless.yml, railway.json, .amplifyrc, rclone.conf;
  • key files: sa.json, key.json, service_account.json, private-key;
  • quick PHP checks: p.php, i.php, phpversion.php;
  • attacks on AI and low-code tools: langflow, flowise, /api/v1/validate/code.

It also found a bug in an existing pattern: service-?account misses service_account.json (underscore).

An interesting detail: one scanner sent about 30 of these probes, each with a different, respectable-looking user agent (ClaudeBot, GPTBot, Googlebot…). The user agent is not proof of anything.

Step 4: look for false positives too

The most valuable finding was not a missing rule but a rule that was too broad. A rule on the nginx error log matched any line naming a client. But many error lines describe my problems, not the visitor's:

  • Too many open files (the server running out of file descriptors),
  • the backend application refusing or timing out,
  • PHP warnings from an application.

Those lines were adding points to ordinary visitors, my own IP included. The fix was a negative look-ahead excluding server-side errors.

Step 5: test the proposal before proposing it

This is the step that convinced me. Before suggesting anything, the AI applied the modified rules to the same output file and compared:

NEW matches: 67           (each one listed and checked: all were probes)
NO LONGER matched: 83     (all server-side errors)
MULTIPLE: 0

A suggestion that comes with its own regression test is a different thing from a suggestion pulled out of thin air.

Step 6: findings outside the rules

Reading the logs also revealed things a firewall can't fix: an application was serving files that should never be public (a Dockerfile, old configuration backups). Blocking the scanner after it downloaded the file is too late; the right answer is to remove the file or deny it in nginx. A good review tells you when the tool you asked about is not the right tool.


4. What I kept, and what I didn't

The AI also produced a full "repository" version with all the new rules. I didn't keep them all, on purpose. The rules in the repository are examples. Each admin has different sites, applications and tolerance:

  • a path that is a probe on my server can be a legitimate URL on yours;
  • a weight of 20 can be too lenient for one person and too harsh for another;
  • some people have password login disabled, some don't.

What I kept in the repository is the method: the clear structure, the one-line-one-rule principle and, above all, the checker. With it, everyone can see their own NOTMATCH lines and write the rules that fit their own context.


5. Limits and advice if you do the same

  • The AI proposes, you decide. It made one mistake in my session: it told me an application file I didn't recognize was safe to remove. That file turned out to be the application's upload endpoint, and uploads stopped working. Easy to fix, but a good reminder: test after every change, and keep a copy of what you remove.
  • Logs contain personal data. IP addresses are personal data under GDPR. When you hand logs to an AI service, they leave your server. Check what you share; you can anonymize or cut the output first.
  • Run the checker regularly. Attack patterns change every few weeks: yesterday it was WordPress, today it is cloud config files and AI tools.
  • Never forget your own IP. Put it in ignore_ips, or a broken page on your own site will get you blocked from your own server.

Conclusion

Reviewing logs regularely is exactly the kind of work an AI does well. The job is to read thousands of repetitive lines, notice the few that don't fit, and check every idea against the data before trusting it. For a human it is tedious and easy to get wrong after the first hundred lines. For an AI it takes a few minutes, and it doesn't get tired at line 4,000.

The checker in parameters.py provides the material: your rules applied to your real logs, line by line. The AI turns that material into concrete, tested proposals. You keep the decision, because only you know which URLs are legitimate on your sites and how strict you want to be. That split works well: the machine does the reading, and the admin does the judging.



👍 0, 👎 0
displayed: 27



What is the first vowel of the word Python?