Skip to content

Instantly share code, notes, and snippets.

@ChenYingChou
Last active July 23, 2021 08:26
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ChenYingChou/0e0605bd10e7144c4c913affd05cccc6 to your computer and use it in GitHub Desktop.
Save ChenYingChou/0e0605bd10e7144c4c913affd05cccc6 to your computer and use it in GitHub Desktop.
Check system logs and add DNS and SSH attackers to the firewall blacklist
#!/usr/bin/env node
//@ts-check
const fs = require('fs')
const path = require('path')
const dns = require('dns')
const util = require('util')
const dateformat = require('dateformat')
const Tail = require('tail').Tail
const resolve4 = util.promisify(dns.resolve4)
const writeFile = util.promisify(fs.writeFile)
function log_msg() {
const stime = dateformat(new Date(), 'mm-dd HH:MM:ss.l')
console.log(util.format(stime, ...arguments))
}
// https://nodejs.org/api/util.html#util_util_inspect_object_options
Object.assign(util.inspect.defaultOptions, {
breakLength: Infinity,
maxArrayLength: 50,
depth: 10,
})
// change to directory of program
process.chdir(path.dirname(process.argv[1]))
process.title = 'watch-log'
/**
* @typedef {Object} Config
* @property {string} watchList - Add the blocked IP to the file, default `/root/watch-blacklist.txt`
* @property {string} blacklist - Blacklist of iptables recent module, default `/proc/net/xt_recent/blacklist`
* @property {number} minIPSize - Minimum size of `ipSet`, default `100`
* @property {number} maxIPSize - maximum size of `ipSet`, default `1000`
* @property {number} lastBucketSize - Read `ipSet` from last bucket size of `watchList`, default `2000` bytes
* @property {Object<string, Object<string, string>>} targets
*/
/** @type {Config} */
const cfg = Object.assign({
watchList: '/root/watch-blacklist.txt',
blacklist: '/proc/net/xt_recent/blacklist',
minIPSize: 100,
maxIPSize: 1000,
lastBucketSize: 2000,
}, require('./watch-log.json'))
/** @type {Set<string>} */
let ipSet = (filename => {
let fd
let lines
try {
fd = fs.openSync(filename, 'r')
const stat = fs.fstatSync(fd)
const position = Math.max(stat.size - cfg.lastBucketSize, 0)
const size = stat.size - position
const buffer = Buffer.alloc(size)
const bytes = fs.readSync(fd, buffer, 0, size, position)
lines = Buffer.from(buffer, 0, bytes).toString().trim().split(/\s*\n\s*/)
lines = lines.length > cfg.minIPSize ? lines.slice(-cfg.minIPSize) : lines.slice(1)
} catch (error) {
if (fd) log_msg(`!! ${error}`)
}
if (fd) fs.closeSync(fd)
return new Set(lines)
})(cfg.watchList)
/**
* @param {string} ip
*/
function add_ip(ip) {
if (!ip || ipSet.has(ip)) return false
ipSet.add(ip)
if (ipSet.size > cfg.maxIPSize) {
const list = Array.from(ipSet)
ipSet = new Set(list.slice(-cfg.minIPSize))
}
return true
}
/**
* @param {string} hostname - IP4 or hostname
* @returns {Promise<string>|string} IP4 address or `null`
*/
function query_ip(hostname) {
if (/^\d.*\d$/.test(hostname)) return hostname // IP4 address
if (add_ip(hostname) === false) return null // `hostname` already resolved
return resolve4(hostname).then(addressList => addressList[0]).catch(err => {
log_msg(`!! query_ip> ${err}`)
return ''
})
}
let watchStream = fs.createWriteStream(cfg.watchList, { 'flags': 'a' })
const tailList = ((targets) => {
/** @type {Tail[]} */
let result = []
Object.entries(targets).forEach(([filename, obj]) => {
const keys = Object.keys(obj)
if (keys.length === 0) return
const rxList = Object.entries(obj).map(([key, value]) => {
return new RegExp(`${key}\\[\\d+\\]: ${value}`)
})
const tail = new Tail(filename)
tail.on('line', /** @param {string} data */(data) => {
rxList.forEach(async (rx, index) => {
const mx = data.match(rx)
if (mx) {
const ip = await query_ip(mx[1])
if (add_ip(ip) === false) return
const key = keys[index]
log_msg(`${key}: ${ip}`)
watchStream.write(`${ip}\n`)
writeFile(cfg.blacklist, `+${ip}`, {encoding:'ascii', flag:'a'}).catch(err => {
log_msg(`!! ${cfg.blacklist}> ${err}`)
})
}
})
})
tail.on('error', (error) => {
log_msg(`!! ${filename}> ${error}`)
})
result.push(tail)
})
return result
})(cfg.targets)
function finish() {
watchStream.end()
for (const tail of tailList) {
tail.unwatch()
}
}
if (tailList.length === 0) {
log_msg('!! No watched targets')
finish()
}
process.on('SIGINT', finish)
process.on('SIGQUIT', finish)
process.on('SIGTERM', finish)
process.on('SIGHUP', () => {
if (watchStream) {
watchStream.end()
watchStream = fs.createWriteStream(cfg.watchList, {flags: 'a'})
}
})
{
"watchList": "/root/ip-blacklist.txt",
"blacklist": "/proc/net/xt_recent/blacklist",
"minIPSize": 100,
"maxIPSize": 1000,
"lastBucketSize": 2000,
"targets": {
"/var/log/messages": {
"named": "client(?: @0x[0-9a-f]+)? (\\d+\\.\\d+\\.\\d+\\.\\d+)#\\d+.* denied"
},
"/var/log/secure": {
"sshd": ".* [Ii]nvalid user .*? from (\\d+\\.\\d+\\.\\d+\\.\\d+)",
"vsftpd": ".* authentication failure .* rhost=([\\w\\.-]+)"
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment