Skip to content

Instantly share code, notes, and snippets.

@doriantaylor
Last active August 13, 2025 03:25
Show Gist options
  • Select an option

  • Save doriantaylor/7427efc4532df296fc8f39b02751f4dd to your computer and use it in GitHub Desktop.

Select an option

Save doriantaylor/7427efc4532df296fc8f39b02751f4dd to your computer and use it in GitHub Desktop.
an "IDS" that you can chuck into a cronjob to update a list of IPs that can be read by the firewall
#!/usr/bin/env ruby
require 'zlib'
require 'ipaddr'
require 'pathname'
require 'tempfile'
require 'set'
require 'digest'
class CheapAssIDS
def initialize table: nil, verbose: false
@verbose = !!verbose
@state = Set[]
if table
table = @table = Pathname(table).expand_path
raise ArgumentError, "#{table} not a writable file" unless
(table.exist? and table.file? and table.writable?) or
(table.parent.exist? and table.parent.directory? and
table.parent.writable?)
if table.exist?
@digest = Digest::SHA256.new
# read it in
@state = table.readlines.map do |ip|
@digest << ip
ip = ip.gsub(/\s*#.*/, '').strip
next if ip.empty?
IPAddr.new(ip) rescue nil
end.compact.to_set
end
end
end
def run *files
files.each do |fn|
fn = Pathname(fn).expand_path
next unless fn.exist? and fn.file? and fn.readable?
process_one fn
end
fh = if @table
mode = @table.stat.mode & 0777
Tempfile.create('.ip-table-', @table.parent.to_s, mode: mode)
else
$stdout
end
test = Digest::SHA256.new
@state.to_a.sort.each do |ip|
line = ip.to_s + $/
test << line
fh << line
end
if @table
# close the temp file
fh.close
# get the path back
tmp = Pathname(fh.path)
warn "old: #{@digest.hexdigest} new: #{test.hexdigest}" if @verbose
if test == @digest
warn "deleting…" if @verbose
tmp.unlink
else
warn "renaming…" if @verbose
tmp.rename @table
end
end
end
private
RE = /sshd\[\d+\]: Failed password for (?:invalid user.*|user root )from (\d+(?:\.\d+){3})/
def process_one path
fh = if path.extname.downcase == '.gz'
Zlib::GzipReader.new(path.open)
else
path.open
end
fh.each do |line|
if m = RE.match(line)
@state << IPAddr.new(m.captures.first)
end
end
end
end
if $0 == __FILE__
require 'optparse'
options = {}
OptionParser.new do |parser|
parser.banner = "Usage: #{$0} [-t FILE] LOGFILE [LOGFILE...]"
parser.on("-t FILE", "--table FILE", 'the IP state table') do |v|
options[:table] = Pathname(v)
end
parser.on("-v", "--verbose", 'increase verbosity') do |v|
options[:verbose] = true
end
end.parse!
CheapAssIDS.new(**options).run(*ARGV)
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment