Last active
August 13, 2025 03:25
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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