#!/usr/bin/php -q
<?php
/**
* HostBlock v.0.1
*
* Simple utility that parses log files and updates access files to deny access
* to suspicious hosts.
*
* @author Rolands Kusiņš
* @license GPL
*/
// Allow execution only from console
if(php_sapi_name() !== "cli"){
echo "This script can be run only from console!";
exit(1);
}
// Define allowed command line arguments
$shortopts = "hslctaep:y:r:d";
$longopts = array(
'help',// usage
'statistics',// statistics
'list',// ip list (with count and/or ip)
'count',// show activity count with list
'time',// show last activity time with list
'parse-apache-log',// parse Apache access log file
'parse-ssh-log',// parse SSHd log file
'path:',// path to log file for manual parsing
'year:',// year for SSHd log parsing - this log file has time without year, to parse older log files this option was introduced
'test',// run parsing as test - do not update IP data
'remove:',// remove IP address from data file
'daemon',// run script as daemon
);
if(isset($argv)){
// Init some variables
$showUsage = false;
// Parse command line arguments
$opts = getopt($shortopts,$longopts);
// Include needed classes, config, initialize needed variables and objects
if(isset($opts['statistics']) || isset($opts['s']) || isset($opts['list'])
|| isset($opts['l']) || isset($opts['parse-apache-log']) || isset($opts['a'])
|| isset($opts['parse-ssh-log']) || isset($opts['h'])
|| isset($opts['remove'])|| isset($opts['r']) || isset($opts['daemon'])
|| isset($opts['d'])){
include_once "hostblock/dist-cfg.php";
include_once "hostblock/Log.php";
$log = new Log();
$log->logDirectory = LOGDIR_PATH;
include_once "hostblock/Stats.php";
include_once "hostblock/ApacheAccessLogParser.php";
include_once "hostblock/AccessUpdate.php";
include_once "hostblock/SshdLogParser.php";
$config = parse_ini_file(CONFIG_PATH);
if(isset($config['datetimeformat'])) $log->dateTimeFormat = $config['datetimeformat'];
// Suspicious entry match count
if(!isset($config['suspiciousentrymatchcount'])) $config['suspiciousentrymatchcount'] = 10;
else $config['suspiciousentrymatchcount'] = (int)$config['suspiciousentrymatchcount'];
// How long must a IP be kept in blacklist
if(!isset($config['blacklisttime'])) $config['blacklisttime'] = 0;
else $config['blacklisttime'] = (int)$config['blacklisttime'];
// Whitelist
if(!isset($config['whitelist'])) $config['whitelist'] = null;
// Permanent blacklist
if(!isset($config['blacklist'])) $config['blacklist'] = null;
// Timezone
if(!isset($config['timezone'])) $config['timezone'] = "UTC";
date_default_timezone_set($config['timezone']);
// Init stats object
$stats = new Stats();
$stats->suspiciousIpsPath = WORKDIR_PATH."/suspicious_ips";
$stats->log = $log;
$stats->suspiciousEntryMatchCount = $config['suspiciousentrymatchcount'];
$stats->blacklistTime = $config['blacklisttime'];
$stats->permanentBlacklistFile = $config['blacklist'];
$stats->permanentWhitelistFile = $config['whitelist'];
if(isset($config['datetimeformat'])) $stats->dateTimeFormat = $config['datetimeformat'];
$stats->loadBlacklist();
}
if(isset($opts['statistics']) || isset($opts['s'])){
// Output statistics
$log->write("Preparing statistics...");
// Get data from file
$stats->load();
// Calculate
$stats->calculate();
// Output data (formatted for console)
$stats->output();
$log->write("Statistics calculated");
exit(0);
} elseif(isset($opts['list']) || isset($opts['l'])){
// Output blacklisted IP addresses
$log->write("Preparing list of blacklisted IP addresses...");
$count = false;
$time = false;
if(isset($opts['count']) || isset($opts['c'])) $count = true;
if(isset($opts['time']) || isset($opts['t'])) $time = true;
// Get data from file
$stats->load();
// Output data (each IP in new line)
$stats->outputBlacklist($count, $time);
$log->write("Blacklist returned");
exit(0);
} elseif(isset($opts['parse-apache-log']) || isset($opts['a'])){
if(isset($opts['path']) || isset($opts['p'])){
// Parse Apache access log file
$path = null;
if(isset($opts['path'])) $path = $opts['path'];
if(isset($opts['p'])) $path = $opts['p'];
if(!file_exists($path)){
echo "Log file doesn't exist!\n";
$log->write("Log file doesn't exist!","error");
exit(1);
}
if(!isset($config['apacheaccesspaterns'])){
$config['apacheaccesspaterns'] = array();
}
// Init Apache access log file parser
$apacheAccessLogParser = new ApacheAccessLogParser();
$apacheAccessLogParser->log = $log;
$apacheAccessLogParser->suspiciousPatterns = $config['apacheaccesspaterns'];
// Load info about suspicious IPs
$ipInfo = array();
$data = @file_get_contents(WORKDIR_PATH."/suspicious_ips");
if($data != false){
$ipInfo = unserialize($data);
$log->write("Suspicious IP data loaded!");
}
$stats->ipInfo = $ipInfo;
echo "Suspicious IP addresses before processing: ".count($stats->ipInfo)."\n";
echo "Blacklisted IP addresses before processing: ".$stats->getBlacklistedIpCount()."\n";
// Parse file
$apacheAccessLogFile['path'] = $path;
$apacheAccessLogFile['offset'] = 0;
$updateHostData = false;
$updateOffsets = false;
$matchCount = $apacheAccessLogParser->parseFile($apacheAccessLogFile, $ipInfo, $updateHostData, $updateOffsets);
// Update IP data
$stats->ipInfo = $ipInfo;
if(!isset($opts['test'])){
$data = serialize($ipInfo);
@file_put_contents(WORKDIR_PATH."/suspicious_ips", $data);
}
echo "Pattern match count: ".$matchCount."\n";
echo "Suspicious IP addresses after processing: ".count($stats->ipInfo)."\n";
echo "Blacklisted IP addresses after processing: ".$stats->getBlacklistedIpCount()."\n";
$log->write("Apache access log file parsing finished.");
if(!isset($opts['test'])){
echo "IP address data updated! Please wait for daemon to reload IP data and update access files if needed.\n";
$log->write("IP address data updated! Please wait for daemon to reload IP data and update access files if needed.");
}
exit(0);
} else{
echo "Path to log file not provided!\n";
$showUsage = true;
}
} elseif(isset($opts['parse-ssh-log']) || isset($opts['e'])){
if(isset($opts['path']) || isset($opts['p'])){
// Parse SSHd log file
$path = null;
if(isset($opts['path'])) $path = $opts['path'];
if(isset($opts['p'])) $path = $opts['p'];
if(!file_exists($path)){
echo "Log file doesn't exist!\n";
$log->write("Log file doesn't exist!","error");
exit(1);
}
$year = date("Y");
if(isset($opts['year'])) $year = (int)$opts['year'];
if(isset($opts['y'])) $year = (int)$opts['y'];
// Init SSHd log file parser
$sshdLogParser = new SshdLogParser();
$sshdLogParser->log = $log;
if(isset($config['sshformats']) && count($config['sshformats']) > 0){
$sshdLogParser->formats = $config['sshformats'];
}
if(isset($config['sshrefusedformats']) && count($config['sshrefusedformats']) > 0){
$sshdLogParser->refusedFormats = $config['sshrefusedformats'];
}
/* if(isset($config['sshrefusedformat']) && !empty($config['sshrefusedformat'])){
$sshdLogParser->refusedFormat = $config['sshrefusedformat'];
} */
$sshdLogFile = array();
$sshdLogFile['path'] = $path;
$sshdLogFile['offset'] = 0;
// Info about suspicious IPs
$ipInfo = array();
$data = @file_get_contents(WORKDIR_PATH."/suspicious_ips");
if($data != false){
$ipInfo = unserialize($data);
$log->write("Suspicious IP address data loaded!");
}
$stats->ipInfo = $ipInfo;
echo "Suspicious IP addresses before parsing: ".count($stats->ipInfo)."\n";
echo "Blacklisted IP addresses before parsing: ".$stats->getBlacklistedIpCount()."\n";
echo "Total refused SSH authorization count before parsing: ".$stats->getTotalRefusedConnectCount()."\n";
// Check for entries in SSHd log file
$updateHostData = false;
$updateOffsets = false;
$matchCount = $sshdLogParser->parseFile($sshdLogFile, $ipInfo, $updateHostData, $updateOffsets, $year);
// Update IP data
$stats->ipInfo = $ipInfo;
if(!isset($opts['test'])){
$data = serialize($ipInfo);
@file_put_contents(WORKDIR_PATH."/suspicious_ips", $data);
}
echo "Pattern match count: ".$matchCount."\n";
echo "Suspicious IP addresses after parsing: ".count($stats->ipInfo)."\n";
echo "Blacklisted IP addresses after parsing: ".$stats->getBlacklistedIpCount()."\n";
echo "Total refused SSH authorization count after parsing: ".$stats->getTotalRefusedConnectCount()."\n";
$log->write("SSHd log file parsing finished.");
if(!isset($opts['test'])){
echo "IP address data updated! Please wait for daemon to reload IP data and update access files if needed.\n";
$log->write("IP address data updated! Please wait for daemon to reload IP data and update access files if needed.");
}
exit(0);
} else{
echo "Path to log file not provided!\n";
$showUsage = true;
}
} elseif(isset($opts['remove']) || isset($opts['r'])){
// Remove IP address from data file
$ipToRemove = null;
if(isset($opts['remove'])) $ipToRemove = $opts['remove'];
if(isset($opts['r'])) $ipToRemove = $opts['r'];
// Load data
$stats->load();
if(count($stats->ipInfo) > 0){
if(isset($stats->ipInfo[$ipToRemove])){
// Show some stats if IP address found
echo "Removing IP address from data file.\n";
echo "Suspicious activity count: ".$stats->ipInfo[$ipToRemove]['count']."\n";
echo "Refused SSH authorization count: ";
if(isset($stats->ipInfo[$ipToRemove]['refused'])) echo $stats->ipInfo[$ipToRemove]['refused']."\n";
else echo "0\n";
echo "Last activity: ".date($stats->dateTimeFormat, $stats->ipInfo[$ipToRemove]['lastactivity'])."\n";
// Unset information about this IP address
unset($stats->ipInfo[$ipToRemove]);
// Update data file
$data = serialize($stats->ipInfo);
@file_put_contents(WORKDIR_PATH."/suspicious_ips", $data);
echo "IP address data updated! Please wait for daemon to reload IP data and update access files if needed.\n";
$log->write("IP address data updated! Please wait for daemon to reload IP data and update access files if needed.");
} else{
echo "IP address not found!\n";
$log->write("Trying to remove unknown IP address from data file! IP address: ".$ipToRemove,"error");
exit(1);
}
} else{
echo "No data!\n";
exit(1);
}
exit(0);
} elseif(isset($opts['daemon']) || isset($opts['d'])){
// Start as daemon process
$log->write("Starting daemon process...");
// Check if process is already running
if(file_exists(PID_PATH)){
echo "Another instance of hostblock is already running!\n";
$log->write("Another instance of hostblock is already running!","error");
exit(1);
}
// Fork currently running process
$pid = pcntl_fork();
if($pid == -1){// Fork failed
echo "Failed to fork process!\n";
$log->write("Failed to fork process!","error");
exit(1);
} elseif($pid){// We are parent (pid>0)
// Write PID to file
$f = @fopen(PID_PATH,"w");
if($f){
@fwrite($f,$pid);
@fclose($f);
}
// Fork succeeded, exit
exit(0);
} else{// We are children (pid==0)
// Variable for main loop, will exit loop when false
$running = true;
// tick required for signal handler
declare(ticks = 1);
// Define signal handler
function signal_handler($signalNumber){
global $running;
switch($signalNumber){
case SIGTERM:
// Handle shutdown
$running = false;
break;
}
}
// Install signal handler
$log->write("Registering signal handler...");
pcntl_signal(SIGTERM,"signal_handler");
// Log file parse interval
if(!isset($config['logparseinterval'])) $config['logparseinterval'] = 60;
else $config['logparseinterval'] = (int)$config['logparseinterval'];
// Access file update interval
if(!isset($config['blacklistupdateinterval'])) $config['blacklistupdateinterval'] = 60;
else $config['blacklistupdateinterval'] = (int)$config['blacklistupdateinterval'];
// Get stored log file offsets
$data = @file_get_contents(WORKDIR_PATH."/offsets");
if($data != false){
$offsets = unserialize($data);
$log->write("Log file offset data loaded!");
}
// Apache access log file configuration
$apacheAccessLogFiles = array();
if(!isset($config['apacheaccesslogs'])){
$config['apacheaccesslogs'] = array();
}
if(!isset($config['apacheaccesslogformats'])){
$config['apacheaccesslogformats'] = array();
}
if(count($config['apacheaccesslogs']) > 0){
foreach($config['apacheaccesslogs'] as $k => $v){
$offset = 0;
if(isset($offsets) && isset($offsets[$v])){
$offset = $offsets[$v];
}
$format = "%h %l %u %t \"%r\" %s %b";
if(isset($config['apacheaccesslogformats'][$k]) && !empty($config['apacheaccesslogformats'][$k])){
$format = $config['apacheaccesslogformats'][$k];
}
$apacheAccessLogFiles[] = array(
'path' => $v,
'offset' => $offset,
'format' => $format,
);
}
}
if(!isset($config['apacheaccesspaterns'])){
$config['apacheaccesspaterns'] = array();
}
if(!isset($config['htaccessfiles'])){
$config['htaccessfiles'] = array();
}
// SSHd log file config
$sshdLogFile = array();
if(isset($config['sshlog'])){
$sshdLogFile['path'] = $config['sshlog'];
$sshdLogFile['offset'] = 0;
if(isset($offsets) && isset($offsets[$sshdLogFile['path']])) $sshdLogFile['offset'] = $offsets[$sshdLogFile['path']];
}
// Init Apache access log file parser
$apacheAccessLogParser = new ApacheAccessLogParser();
$apacheAccessLogParser->log = $log;
$apacheAccessLogParser->suspiciousPatterns = $config['apacheaccesspaterns'];
// Init Apache access file updater
$accessUpdater = new AccessUpdate();
$accessUpdater->log = $log;
// Init SSHd log file parser
$sshdLogParser = new SshdLogParser();
$sshdLogParser->log = $log;
if(isset($config['sshformats']) && count($config['sshformats']) > 0){
$sshdLogParser->formats = $config['sshformats'];
}
if(isset($config['sshrefusedformats']) && count($config['sshrefusedformats']) > 0){
$sshdLogParser->refusedFormats = $config['sshrefusedformats'];
}
/* if(isset($config['sshrefusedformat']) && !empty($config['sshrefusedformat'])){
$sshdLogParser->refusedFormat = $config['sshrefusedformat'];
} */
// Info about suspicious IPs
$ipInfo = array();
$data = @file_get_contents(WORKDIR_PATH."/suspicious_ips");
if($data != false){
$ipInfo = unserialize($data);
$log->write("Suspicious IP address data loaded!");
}
$stats->ipInfo = $ipInfo;
// Main loop
$lastParseTime = time()-$config['logparseinterval'];
$lastUpdateTime = time()-$config['blacklistupdateinterval'];
$updateHostData = false;
$updateOffsets = false;
$newMatchCount = 0;
$updateAccessFiles = false;
$blacklistedIpCount = $stats->getBlacklistedIpCount();
$lastFileCheckTime = time();
if(file_exists(WORKDIR_PATH."/suspicious_ips")) $ipInfoMTime = filemtime(WORKDIR_PATH."/suspicious_ips");
if(!is_null($config['blacklist'])) $blacklistMTime = filemtime($config['blacklist']);
if(!is_null($config['whitelist'])) $whitelistMTime = filemtime($config['whitelist']);
while($running){
// Check each 60 seconds if data files are updated and reload if needed
if(time() - $lastFileCheckTime >= 60){
// Suspicious IP data
if(file_exists(WORKDIR_PATH."/suspicious_ips")){
$ipInfoMTimeNew = filemtime(WORKDIR_PATH."/suspicious_ips");
if($ipInfoMTime != $ipInfoMTimeNew){
$log->write("Suspicious IP address data has been changed, reloading data for deamon!");
$data = @file_get_contents(WORKDIR_PATH."/suspicious_ips");
if($data != false){
$ipInfo = unserialize($data);
$log->write("Suspicious IP data loaded!");
}
$stats->ipInfo = $ipInfo;
if($blacklistedIpCount != $stats->getBlacklistedIpCount()){
$updateAccessFiles = true;
$blacklistedIpCount = $stats->getBlacklistedIpCount();
}
$ipInfoMTime = $ipInfoMTimeNew;
}
}
// Blacklist/whitelist
$reloadStatsBlacklist = false;
if(!is_null($config['blacklist'])){
$blacklistMTimeNew = filemtime($config['blacklist']);
if($blacklistMTime != $blacklistMTimeNew){
$reloadStatsBlacklist = true;
$blacklistMTime = $blacklistMTimeNew;
}
}
if(!is_null($config['whitelist'])){
$whitelistMTimeNew = filemtime($config['whitelist']);
if($whitelistMTime != $whitelistMTimeNew){
$reloadStatsBlacklist = true;
$whitelistMTime = $whitelistMTimeNew;
}
}
if($reloadStatsBlacklist){
$log->write("Whitelist/blacklist has been changed, reloading data for deamon!");
$stats->loadBlacklist();
$reloadStatsBlacklist = false;
$updateAccessFiles = true;
$blacklistedIpCount = $stats->getBlacklistedIpCount();
}
$lastFileCheckTime = time();
}
// If it is time to check log files for new entries
if(time() - $lastParseTime >= $config['logparseinterval']){
// Loop through all defined apache log files
if(count($apacheAccessLogFiles) > 0){
foreach($apacheAccessLogFiles as &$apacheAccessLogFile){
// Check for new entries in file
$newMatchCount += $apacheAccessLogParser->parseFile($apacheAccessLogFile, $ipInfo, $updateHostData, $updateOffsets);
}
}
// Check for new entries in SSHd log file
if(isset($sshdLogFile['path'])){
$newMatchCount += $sshdLogParser->parseFile($sshdLogFile, $ipInfo, $updateHostData, $updateOffsets);
}
// Update host data
if($updateHostData == true){
$data = serialize($ipInfo);
@file_put_contents(WORKDIR_PATH."/suspicious_ips", $data);
$ipInfoMTime = filemtime(WORKDIR_PATH."/suspicious_ips");
$updateHostData = false;
// Check if blacklisted IP count differs
// Here might be a bug if we have +1 because of activity and -1 because of time in a same time
$stats->ipInfo = $ipInfo;
if($blacklistedIpCount != $stats->getBlacklistedIpCount()){
$updateAccessFiles = true;
$blacklistedIpCount = $stats->getBlacklistedIpCount();
}
$log->write("Suspicious IP address data updated!");
}
// Update offsets
if($updateOffsets == true){
$offsets = array();
foreach($apacheAccessLogFiles as &$apacheAccessLogFile){
$offsets[$apacheAccessLogFile['path']] = $apacheAccessLogFile['offset'];
}
if(isset($sshdLogFile['path'])){
$offsets[$sshdLogFile['path']] = $sshdLogFile['offset'];
}
$offsets = serialize($offsets);
@file_put_contents(WORKDIR_PATH."/offsets", $offsets);
$updateOffsets = false;
}
// Info in log file if we have new pattern matches
if($newMatchCount > 0){
$log->write("Pattern match count: ".$newMatchCount);
$newMatchCount = 0;
}
// Update last parse time
$lastParseTime = time();
}
// If it is time to check if update to blacklist is needed
if(time() - $lastUpdateTime >= $config['blacklistupdateinterval']){
if($updateAccessFiles == true){
// Get white&black lists
$stats->loadBlacklist();
// Get blacklisted IPs
$blacklistedIps = $stats->getBlacklistedIps();
// If we need to update .htaccess files
if(count($config['htaccessfiles']) > 0){
foreach($config['htaccessfiles'] as &$apacheAccessFile){
$accessUpdater->updateApacheAccessFile($apacheAccessFile, $blacklistedIps);
}
$log->write("Apache access files updated!");
$updateAccessFiles = false;
}
// If we need to update hosts.deny files
if(isset($config['hostsdenyfile']) && !empty($config['hostsdenyfile'])){
$accessUpdater->updateHostsDenyFile($config['hostsdenyfile'], $blacklistedIps);
$log->write("hosts.deny file updated!");
$updateAccessFiles = false;
}
}
// Update last update time
$lastUpdateTime = time();
}
// Sleep half a second before next iteration
usleep(500000);
}
$log->write("Total suspicious IP addresses: ".count($ipInfo));
$log->write("Shutdown");
exit(0);
}
} else{
$showUsage = true;
}
if($showUsage){
echo "HostBlock v.0.1\n\n";
echo "Usage:\n";
echo "hostblock [-h | --help] [-s | --statistics] [-l | --list [-c | --count] [-t | --time]] [-a -p<path> | --parse-apache-access-log --path=<path>] [-e -p<path> -y<year> | --parse-ssh-log --path=<path> --year=<year>] [-r<ip_address> | --remove=<ip_address>] [-d | --daemon]\n";
echo '
--help - show this help information
--statistics - show statistics
--list - show list of blacklisted IP addresses
--list --count - show list of blacklisted IP addresses with suspicious activity count
--list --time - show list of blacklisted IP addresses with last suspicious activity time
--list --count --time - show list of blacklisted IP addresses with suspicious activity count and last suspicious activity time
--parse-apache-log --path=<path> - parse Apache access log file
--parse-ssh-log --path=<path> --year=<year> - parse SSHd log file
--remove=<ip_address> - remove IP address from data file
--daemon - run as daemon
';
}
}
?>
|