180 lines
5.7 KiB
JavaScript
180 lines
5.7 KiB
JavaScript
const logIpToFile = require('../logIpToFile.js');
|
|
const tailFile = require('../services/tailFile.js');
|
|
const logger = require('../logger.js');
|
|
const resolvePath = require('../pathResolver.js');
|
|
const { HONEYTRAP_LOG_FILE, SERVER_ID } = require('../../config.js').MAIN;
|
|
const { FLAGS, createFlagCollection } = require('../flags.js');
|
|
|
|
const LOG_FILE = resolvePath(HONEYTRAP_LOG_FILE);
|
|
const HEADER_PRIORITY = ['user-agent', 'accept', 'accept-language', 'accept-encoding'];
|
|
|
|
let lastFlushTime = Date.now();
|
|
const attackBuffer = new Map();
|
|
|
|
const capitalizeHeader = header => header.split('-').map(w => w[0].toUpperCase() + w.slice(1)).join('-');
|
|
|
|
const parseHttpRequest = (ascii, dpt) => {
|
|
const lines = ascii.replace(/\r\n|\r/g, '\n').trim().split('\n');
|
|
|
|
const requestLineRaw = lines.shift()?.trim() || '';
|
|
const protocol = requestLineRaw.match(/HTTP\/[0-9.]+/i)?.[0]?.toUpperCase() || 'HTTP';
|
|
const requestLine = requestLineRaw.replace(/\s*HTTP\/[0-9.]+$/i, '');
|
|
|
|
const headers = {};
|
|
const body = [];
|
|
let parsingBody = false;
|
|
|
|
for (const line of lines) {
|
|
if (parsingBody) {
|
|
body.push(line);
|
|
continue;
|
|
}
|
|
|
|
if (!line.trim()) {
|
|
parsingBody = true;
|
|
continue;
|
|
}
|
|
|
|
const [key, ...value] = line.split(':');
|
|
if (value.length && key.toLowerCase() !== 'host') headers[key.trim().toLowerCase()] = value.join(':').trim();
|
|
}
|
|
|
|
const formattedHeaders = HEADER_PRIORITY
|
|
.filter(h => headers[h])
|
|
.map(h => `${capitalizeHeader(h)}: ${headers[h]}`)
|
|
.join('\n');
|
|
|
|
let output = `${protocol} request on ${dpt}\n\n${requestLine}`;
|
|
if (formattedHeaders) output += `\n${formattedHeaders}`;
|
|
|
|
if (requestLineRaw.startsWith('POST')) {
|
|
const bodyContent = body.join('\n').trim();
|
|
if (bodyContent) output += `\nPOST Data: ${bodyContent}`;
|
|
}
|
|
|
|
return output;
|
|
};
|
|
|
|
const getReportDetails = (entry, dpt) => {
|
|
const proto = entry?.attack_connection?.protocol || 'unknown';
|
|
const hex = entry?.attack_connection?.payload?.data_hex || '';
|
|
const ascii = Buffer.from(hex, 'hex').toString('utf8');
|
|
const simplifiedAscii = ascii.replace(/\s+/g, ' ').toLowerCase();
|
|
const payloadLen = entry?.attack_connection?.payload?.length || 0;
|
|
|
|
const flags = createFlagCollection();
|
|
let comment;
|
|
|
|
switch (true) {
|
|
case payloadLen === 0:
|
|
flags.add(FLAGS.PORT_SCAN);
|
|
comment = `Empty payload on ${dpt}/${proto} (likely service probe)`;
|
|
break;
|
|
case payloadLen > 1000:
|
|
flags.add(FLAGS.BAD_WEB_BOT);
|
|
comment = `Large payload (${payloadLen} bytes) on ${dpt}/${proto}`;
|
|
break;
|
|
case (/HTTP\/(0\.9|1\.0|1\.1|2|3)/i).test(simplifiedAscii):
|
|
flags.add(FLAGS.HTTP, FLAGS.BAD_WEB_BOT);
|
|
comment = parseHttpRequest(ascii, dpt);
|
|
break;
|
|
case (/\bssh\b/).test(simplifiedAscii):
|
|
flags.add(FLAGS.SSH);
|
|
comment = `SSH handshake/banner on ${dpt}/${proto} (${payloadLen} bytes of payload)`;
|
|
break;
|
|
case simplifiedAscii.includes('cookie:'):
|
|
flags.add(FLAGS.HTTP, FLAGS.BAD_WEB_BOT);
|
|
comment = `HTTP header with cookie on ${dpt}/${proto}`;
|
|
break;
|
|
case (/(admin|root|wget|curl|bash|eval|php|bin)/).test(simplifiedAscii):
|
|
flags.add(FLAGS.COMMAND_INJECTION);
|
|
comment = `Suspicious payload on ${dpt}/${proto} (possible command injection)`;
|
|
break;
|
|
default:
|
|
flags.add(FLAGS.PORT_SCAN);
|
|
comment = `Unauthorized traffic on ${dpt}/${proto} (${payloadLen} bytes of payload)`;
|
|
break;
|
|
}
|
|
|
|
return { proto, baseComment: comment, categories: flags.toString(), timestamp: entry?.['@timestamp'] };
|
|
};
|
|
|
|
const flushBuffer = async reportIp => {
|
|
if (!attackBuffer.size) return;
|
|
|
|
for (const [srcIp, ports] of attackBuffer.entries()) {
|
|
const sortedPorts = Array.from(ports.entries())
|
|
.sort(([, a], [, b]) => b.count - a.count)
|
|
.slice(0, 6);
|
|
|
|
const proto = sortedPorts[0][1].proto || 'tcp';
|
|
const timestamp = sortedPorts[0][1].timestamp;
|
|
const baseComment = sortedPorts[0][1].baseComment;
|
|
const categories = sortedPorts[0][1].categories;
|
|
|
|
const portSummary = sortedPorts.map(([port, data]) => `${port} [${data.count}]`).join(', ');
|
|
const comment = `Honeypot ${SERVER_ID ? `[${SERVER_ID}]` : 'hit'}: ${baseComment.replace(/ on \d+\/\w+/, '')}; ${portSummary} ${proto.toUpperCase()}`;
|
|
|
|
await reportIp('HONEYTRAP', { srcIp, dpt: sortedPorts[0][0], proto, timestamp }, categories, comment);
|
|
await logIpToFile(srcIp, { honeypot: 'HONEYTRAP', comment });
|
|
}
|
|
|
|
logger.success(`HONEYTRAP -> Flushed ${attackBuffer.size} IPs`);
|
|
attackBuffer.clear();
|
|
};
|
|
|
|
module.exports = reportIp => {
|
|
tailFile(LOG_FILE, async line => {
|
|
if (!line.length) return;
|
|
|
|
let entry;
|
|
try {
|
|
entry = JSON.parse(line);
|
|
} catch (err) {
|
|
return logger.error(`HONEYTRAP -> JSON parse error: ${err.message}\nFaulty line: ${JSON.stringify(line)}`);
|
|
}
|
|
|
|
try {
|
|
const srcIp = entry?.attack_connection?.remote_ip;
|
|
const dpt = entry?.attack_connection?.local_port;
|
|
if (!srcIp || !dpt) return;
|
|
|
|
let ipData = attackBuffer.get(srcIp);
|
|
if (!ipData) {
|
|
ipData = new Map();
|
|
attackBuffer.set(srcIp, ipData);
|
|
}
|
|
|
|
const { proto, timestamp, categories, baseComment } = getReportDetails(entry, dpt);
|
|
let portData = ipData.get(dpt);
|
|
if (portData) {
|
|
portData.count++;
|
|
} else {
|
|
portData = { count: 1, proto, timestamp, categories, baseComment };
|
|
ipData.set(dpt, portData);
|
|
}
|
|
|
|
logger.info(`HONEYTRAP -> ${srcIp} hit ${dpt} | x${portData.count}`);
|
|
} catch (err) {
|
|
logger.error(err);
|
|
}
|
|
}, { label: 'HONEYTRAP' }).catch(err => logger.error(err));
|
|
|
|
// Clean buffer
|
|
const cleanupInterval = setInterval(async () => {
|
|
if (Date.now() >= lastFlushTime + 5 * 60 * 1000) {
|
|
await flushBuffer(reportIp);
|
|
lastFlushTime = Date.now();
|
|
}
|
|
}, 60 * 1000);
|
|
|
|
logger.success('🛡️ HONEYTRAP » Watcher initialized');
|
|
return {
|
|
flush: () => flushBuffer(reportIp),
|
|
cleanup: () => {
|
|
clearInterval(cleanupInterval);
|
|
attackBuffer.clear();
|
|
},
|
|
};
|
|
};
|