<?php
/**
* "System Health and Security Probe" is a program that runs periodically to
* investigate general system health and security problems and to send report
* emails to admins. It's designed to be used as part of RED SCARF Suite.
* It requires at least an abridged version of RED SCARF Suite and Postfix,
* installed and configured on a Debian server as described in the Complete
* Guide to a Complete Linux Server. For more details, please see the
* README.txt file.
*
* Copyright (C) 2024 Double Bastion LLC <www.doublebastion.com>
*
* This file is part of "System Health and Security Probe".
*
* "System Health and Security Probe" is free software: you can redistribute
* it and/or modify it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 3 of the License,
* or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
if (file_exists(dirname(__FILE__) . '/shsp-config.php')) {
require(dirname(__FILE__) . '/shsp-config.php');
if (preg_match('/^[0-9]+$/', $time_in_hours)) { $runinterval = $time_in_hours * 3600; } else { exit("Error. 'System Health and Security Probe' needs a valid \$time_in_hours parameter.\n"); }
if (!empty($db_name)) { $bannedipsdb = $db_name; } else { exit("Error. 'System Health and Security Probe' needs a valid \$db_name parameter.\n"); }
if (!empty($db_user)) { $bannedipsuser = $db_user; } else { exit("Error. 'System Health and Security Probe' needs a valid \$db_user parameter.\n"); }
if (!empty($db_password)) { $bannedipspswd = $db_password; } else { exit("Error. 'System Health and Security Probe' needs a valid \$db_password parameter.\n"); }
if (!empty($report_from)) { $emailsender = $report_from; } else { exit("Error. 'System Health and Security Probe' needs a valid \$report_from parameter.\n"); }
if (!empty($report_to)) {
$emailto = str_replace(" ", "", $report_to);
$emailtoarr = explode(",", $emailto);
$emailtofirst = $emailtoarr[0];
} else { exit("Error. 'System Health and Security Probe' needs a valid \$report_to parameter.\n"); }
if (!empty($report_subject)) { $subject = $report_subject; } else { $subject = "System Health and Security Report"; }
if (!empty($phpmyadmin_log)) { $phpmyadminlog = $phpmyadmin_log; $phpmyadminlogsec = $phpmyadminlog . ".1"; } else { $phpmyadminlog = ""; $phpmyadminlogsec = ""; }
if (!empty($mailman_log)) { $mailmanlog = $mailman_log; $mailmanlogsec = $mailmanlog . ".1"; } else { $mailmanlog = ""; $mailmanlogsec = ""; }
if (!empty($dolibarr_log)) { $dolibarrlog = $dolibarr_log; $dolibarrlogsec = $dolibarrlog . ".1"; } else { $dolibarrlog = ""; $dolibarrlogsec = ""; }
if (!empty($phplist_log)) { $phplistlog = $phplist_log; $phplistlogsec = $phplistlog . ".1"; } else { $phplistlog = ""; $phplistlogsec = ""; }
if (!empty($roundcube_log)) { $roundcubelog = $roundcube_log; $roundcubelogsec = $roundcubelog . ".1"; } else { $roundcubelog = ""; $roundcubelogsec = ""; }
if (!empty($matomo_log)) { $matomolog = $matomo_log; $matomologsec = $matomolog . ".1"; } else { $matomolog = ""; $matomologsec = ""; }
if (!empty($looladmin_log)) { $looladminlog = $looladmin_log; $looladminlogsec = $looladminlog . ".1"; } else { $looladminlog = ""; $looladminlogsec = ""; }
if (!empty($wordpress_log)) {
$wprexpl = explode(",", $wordpress_log);
$wordpresslog = array_filter(array_map('trim', $wprexpl));
$wordpresslogsec = array_map(function($wplog) { return $wplog . ".1"; }, $wordpresslog);
} else { $wordpresslog = []; $wordpresslogsec = []; }
if (!empty($asterisk_log)) { $asterisklog = $asterisk_log; $asterisklogsec = $asterisklog . ".1"; } else { $asterisklog = ""; $asterisklogsec = ""; }
if (!empty($bind_log)) { $bindlog = $bind_log; $bindlogsec = $bindlog . ".0"; } else { $bindlog = ""; $bindlogsec = ""; }
if (!empty($nextcloud_log)) { $nextcloudlog = $nextcloud_log; $nextcloudlogsec = $nextcloudlog . ".1"; } else { $nextcloudlog = ""; $nextcloudlogsec = ""; }
if (!empty($dovecot_log)) { $dovecotlog = $dovecot_log; $dovecotlogsec = $dovecotlog . ".1"; } else { $dovecotlog = ""; $dovecotlogsec = ""; }
if (!empty($postfix_log)) { $postfixlog = $postfix_log; $postfixlogsec = $postfixlog . ".1"; } else { $postfixlog = ""; $postfixlogsec = ""; }
if (!empty($postfix_sasl_log)) { $postfixsasllog = $postfix_sasl_log; $postfixsasllogsec = $postfixsasllog . ".1"; } else { $postfixsasllog = ""; $postfixsasllogsec = ""; }
if (!empty($proftpd_log)) { $proftpdlog = $proftpd_log; $proftpdlogsec = $proftpdlog . ".1"; } else { $proftpdlog = ""; $proftpdlogsec = ""; }
if (!empty($sshd_log)) { $sshdlog = $sshd_log; $sshdlogsec = $sshdlog . ".1"; } else { $sshdlog = ""; $sshdlogsec = ""; }
if (!empty($openvpn_log)) { $openvpnlog = $openvpn_log; $openvpnlogsec = $openvpnlog . ".1"; } else { $openvpnlog = ""; $openvpnlogsec = ""; }
if (!empty($postfix_admin_log)) { $postfixadminlog = $postfix_admin_log; $postfixadminlogsec = $postfixadminlog . ".1"; } else { $postfixadminlog = ""; $postfixadminlogsec = ""; }
if (!empty($roundpin_log)) { $roundpinlog = $roundpin_log; $roundpinlogsec = $roundpinlog . ".1"; } else { $roundpinlog = ""; $roundpinlogsec = ""; }
if (!empty($mybb_log)) { $mybblog = $mybb_log; $mybblogsec = $mybblog . ".1"; } else { $mybblog = ""; $mybblogsec = ""; }
if (!empty($friendica_log)) { $friendicalog = $friendica_log; $friendicalogsec = $friendicalog . ".1"; } else { $friendicalog = ""; $friendicalogsec = ""; }
if (!empty($redscarfsuite_panel_log)) { $redscarfsuitepanellog = $redscarfsuite_panel_log; $redscarfsuitepanellogsec = $redscarfsuitepanellog . ".1"; } else { $redscarfsuitepanellog = ""; $redscarfsuitepanellogsec = ""; }
if (!empty($disk_threshold)) { $diskthreshold = $disk_threshold; } else { $diskthreshold = "3145728"; }
if (!empty($clamav_report_dir)) {
if (mb_substr($clamav_report_dir, -1) == "/") {
$clamavreportdir = substr($clamav_report_dir, 0, -1);
} else { $clamavreportdir = $clamav_report_dir; }
} else { $clamavreportdir = ""; }
if (!empty($backup_directory)) {
if (mb_substr($backup_directory, -1) == "/") {
$backupdirectory = substr($backup_directory, 0, -1);
} else { $backupdirectory = $backup_directory; }
} else { $backupdirectory = ""; }
if (!empty($automatic_emails_to_isp)) { $automaticemail = $automatic_emails_to_isp; } else { $automaticemail = "no"; }
if (!empty($excluded_jails)) { $excludedjails = $excluded_jails; } else { $excludedjails = []; }
if (!empty($excluded_ips)) { $excludedips = str_replace(" ", "", $excluded_ips); } else { $excludedips = ""; }
if (!empty($sysadmin_name)) { $sysadminname = $sysadmin_name; } else { $sysadminname = ""; }
if (!empty($abuse_reports_to_admin)) { $abusereportstoadmin = $abuse_reports_to_admin; } else { $abusereportstoadmin = "no"; }
if (count($wordpresslog) > 1) {
$wordpressfirst = implode(' or ', $wordpresslog);
$wordpresssec = implode(' or ', $wordpresslogsec);
} else {
$wordpressfirst = $wordpresslog[0];
$wordpresssec = $wordpresslogsec[0];
}
// Log paths
$jaillogarr = ['asterisk' => $asterisklog, 'named-refused' => $bindlog, 'dovecot' => $dovecotlog, 'looladmin' => $looladminlog, 'mailman' => $mailmanlog,
'nextcloud' => $nextcloudlog, 'dolibarr' => $dolibarrlog, 'phplist' => $phplistlog, 'phpmyadmin' => $phpmyadminlog,
'postfix' => $postfixlog, 'postfix-sasl' => $postfixsasllog, 'proftpd' => $proftpdlog, 'roundcube' => $roundcubelog, 'sshd' => $sshdlog,
'openvpn' => $openvpnlog, 'postfixadmin' => $postfixadminlog, 'roundpin' => $roundpinlog, 'mybb' => $mybblog, 'matomo' => $matomolog,
'friendica' => $friendicalog, 'redscarfsuitepanel' => $redscarfsuitepanellog,'wordpress' => [$wordpressfirst, $wordpresssec]];
$jaillogarrsec = ['asterisk' => $asterisklogsec, 'named-refused' => $bindlogsec, 'dovecot' => $dovecotlogsec, 'looladmin' => $looladminlogsec,
'mailman' => $mailmanlogsec, 'nextcloud' => $nextcloudlogsec, 'dolibarr' => $dolibarrlogsec, 'phplist' => $phplistlogsec,
'phpmyadmin' => $phpmyadminlogsec, 'postfix' => $postfixlogsec, 'postfix-sasl' => $postfixsasllogsec,
'proftpd' => $proftpdlogsec, 'roundcube' => $roundcubelogsec, 'matomo' => $matomologsec, 'sshd' => $sshdlogsec, 'openvpn' => $openvpnlogsec,
'postfixadmin' => $postfixadminlogsec, 'roundpin' => $roundpinlogsec, 'mybb' => $mybblogsec, 'friendica' => $friendicalogsec,
'redscarfsuitepanel' => $redscarfsuitepanellogsec];
// Jail ports
$jailsports = ['asterisk' => '5060', 'named-refused' => '53', 'dovecot' => '143', 'looladmin' => '443', 'mailman' => '443', 'nextcloud' => '443', 'dolibarr' => '443',
'phplist' => '443', 'phpmyadmin' => '443', 'postfix' => '25', 'postfix-sasl' => '25', 'proftpd' => '21', 'roundcube' => '443',
'matomo' => '443', 'sshd' => '22', 'openvpn' => '1194', 'postfixadmin' => '443', 'roundpin' => '443', 'wordpress' => '443'];
// Connect to the database
mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);
$mysqli = new mysqli('localhost', $bannedipsuser, $bannedipspswd, $bannedipsdb);
if ($mysqli->connect_error) { exit('Error connecting to the database !'); }
$mysqli->set_charset("utf8mb4");
$query0 = "SHOW TABLES LIKE 'bannedipstable';";
$result0 = $mysqli->query($query0);
// Create the 4 tables if it's the first run of this script
if (mysqli_num_rows($result0) == 0) {
// Create the table for the IPs banned by Fail2ban
$query1 = " CREATE TABLE IF NOT EXISTS bannedipstable (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
IP VARCHAR (400) DEFAULT NULL,
jail VARCHAR (300) DEFAULT NULL,
first_ban DATETIME DEFAULT NULL,
last_ban DATETIME DEFAULT NULL,
ban_number INT DEFAULT NULL,
emails_isp INT DEFAULT 0
); ";
$result1 = $mysqli->query($query1);
// Create the table for the last sent email
$query2 = " CREATE TABLE IF NOT EXISTS lastsentemail (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
old_message LONGTEXT DEFAULT NULL
); ";
$result2 = $mysqli->query($query2);
/**
* Create the table for the last sent Nextcloud and mail log lines containing virus detections ('mail_loglines' and 'nextcloud_loglines'), and the last
* read log lines ('lastmailline' and 'lastnextline')
*/
$query3 = " CREATE TABLE IF NOT EXISTS sentreadlines (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
mail_loglines TEXT DEFAULT NULL,
nextcloud_loglines TEXT DEFAULT NULL,
lastmailline TEXT DEFAULT NULL,
lastnextline TEXT DEFAULT NULL,
lastpersdetmail TEXT DEFAULT NULL,
lastpersdetnext TEXT DEFAULT NULL
); ";
$result3 = $mysqli->query($query3);
// Create the table to store the jails and their respective banned IPs sent in the last email or banned in the past
$query4 = " CREATE TABLE IF NOT EXISTS jailsandips (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
jail VARCHAR (300) DEFAULT NULL,
bannedips LONGTEXT DEFAULT NULL
); ";
$result4 = $mysqli->query($query4);
}
// Search for failed processes and format the data
$getfailedservinit = shell_exec('systemctl --failed --all');
$getfailedservsec = str_replace("To show all installed unit files use 'systemctl list-unit-files'.", "", $getfailedservinit);
$failedservsplit = preg_split('/\r\n|\r|\n/', $getfailedservsec);
$countlines = count($failedservsplit);
$getfailedservbr = implode("<br>", $failedservsplit);
$getfailedservtert = $failedservsplit[$countlines - 3];
// If there are failed processes, list them in a paragraph in the email to be sent
if ($getfailedservtert[0] != 0) {
$getfailedserv = "<ul><li style='color:#225aad;font-weight:700;'>The following services are in failed state:</li><br><br>" . $getfailedservbr . "</ul>";
} else { $getfailedserv = null; }
/**
* Read the periodic virus scanning reports to see if there were any detections during the last scan
*/
// Create the ClamAV reports directory if it hasn't been already created
if (!is_dir($clamavreportdir)) {
exec("mkdir -p " . $clamavreportdir);
}
$maildetectrep = $clamavreportdir . "/clamav_mail_report";
$nextdetectrep = $clamavreportdir . "/clamav_nextcloud_report";
$targetstr = "Infected files: ";
// Search for the infected files lines in the mail scan report
if (is_file($maildetectrep)) {
$readmaildetrep = file_get_contents($maildetectrep);
$infectedfl = strstr($readmaildetrep, $targetstr);
$txtbreakline = preg_split('/\r\n|\r|\n/', $infectedfl);
$nbofinfectfls = explode("Infected files: ", $txtbreakline[0]);
$nbinfctdflsmail = $nbofinfectfls[1];
if ($nbinfctdflsmail != 0) {
$getdetlines = explode("\n", $readmaildetrep);
$getdetmaillnarr = [];
$gosel = false;
$p = 0;
foreach ($getdetlines as $lnkey => $lnvalue) {
if (strpos($lnvalue, "SCAN SUMMARY") !== false) {
$gosel = false;
}
if ($gosel) {
$getdetmaillnarr[$p] = $lnvalue;
$p++;
}
if (strpos($lnvalue, "---------------------------") !== false) {
$gosel = true;
}
}
$maildetlnbr = implode("<br><br>", $getdetmaillnarr);
$maildetlnbr_enc = base64_encode($maildetlnbr);
} else { $maildetlnbr_enc = null; }
} else {
$nbinfctdflsmail = 0;
$maildetlnbr_enc = null;
}
// Search for the infected files lines in the Nextcloud scan report
if (is_file($nextdetectrep)) {
$readnextdetrep = file_get_contents($nextdetectrep);
$infectedfltxt = strstr($readnextdetrep, $targetstr);
$txtbreakln = preg_split('/\r\n|\r|\n/', $infectedfltxt);
$nbofinfectedinit = explode("Infected files: ", $txtbreakln[0]);
$nbinfctdflsnext = $nbofinfectedinit[1];
if ($nbinfctdflsnext != 0) {
$getdetnextlines = explode("\n", $readnextdetrep);
$getdetnextlinesarr = [];
$goselnext = false;
$b = 0;
foreach ($getdetnextlines as $lnextkey => $lnextvalue) {
if (strpos($lnextvalue, "SCAN SUMMARY") !== false) {
$goselnext = false;
}
if ($goselnext) {
$getdetnextlinesarr[$b] = $lnextvalue;
$b++;
}
if (strpos($lnextvalue, "---------------------------") !== false) {
$goselnext = true;
}
}
$nextdetlnbr = implode("<br><br>", $getdetnextlinesarr);
$nextdetlnbr_enc = base64_encode($nextdetlnbr);
} else { $nextdetlnbr_enc = null; }
} else {
$nbinfctdflsnext = 0;
$nextdetlnbr_enc = null;
}
// Get the last sent Nextcloud log lines and mail log lines containing virus detections, the last mail log line and Nextcloud log line read by the script in the previous run
// and the last sent mail virus detections and Nextcloud virus detections
$queryvr = "SELECT id, mail_loglines, nextcloud_loglines, lastmailline, lastnextline, lastpersdetmail, lastpersdetnext FROM sentreadlines WHERE id = 1;";
$resultvr = $mysqli->query($queryvr);
$result_fetchvr = $resultvr->fetch_array();
$old_maildetections_enc = $result_fetchvr[1];
$old_nextdetections_enc = $result_fetchvr[2];
$lastreadmaillineinit = $result_fetchvr[3];
$lastreadnextlineinit = $result_fetchvr[4];
$old_mailvirdetect_enc = $result_fetchvr[5];
$old_nextvirdetect_enc = $result_fetchvr[6];
$lastreadmailline = base64_decode($lastreadmaillineinit);
$lastreadnextline = base64_decode($lastreadnextlineinit);
if ($nbinfctdflsmail != 0 && $nbinfctdflsnext != 0 && $maildetlnbr_enc != $old_mailvirdetect_enc && $nextdetlnbr_enc != $old_nextvirdetect_enc) {
$clamavperiodicdetect = "<ul><li style='color:#225aad;font-weight:700;'>ClamAV antivirus has made the following detections by periodic scanning:</li><br>
<br><ul><li><b>Viruses detected in emails directories (You should remove them manually):</b><br><br>" . $maildetlnbr . "</li></ul><br>
<ul><li><b>Viruses detected in Nextcloud directories (You should remove them manually):</b><br><br>" . $nextdetlnbr . "</li></ul></ul><br>";
} elseif ($nbinfctdflsmail != 0 && $nbinfctdflsnext == 0 && $maildetlnbr_enc != $old_mailvirdetect_enc) {
$clamavperiodicdetect = "<ul><li style='color:#225aad;font-weight:700;'>ClamAV antivirus has made the following detections by periodic scanning:</li><br>
<br><ul><li><b>Viruses detected in emails directories (You should remove them manually):</b><br><br>" . $maildetlnbr . "</li></ul></ul><br>";
} elseif ($nbinfctdflsmail == 0 && $nbinfctdflsnext != 0 && $nextdetlnbr_enc != $old_nextvirdetect_enc) {
$clamavperiodicdetect = "<ul><li style='color:#225aad;font-weight:700;'>ClamAV antivirus has made the following detections by periodic scanning:</li><br>
<br><ul><li><b>Viruses detected in Nextcloud directories (You should remove them manually):</b><br><br>" . $nextdetlnbr . "</li></ul></ul><br>";
} else { $clamavperiodicdetect = null; }
// Get the log lines that contain virus detections in emails
$mailstring = ' milter-hold: ';
$maillog1init = $postfixlog;
$maillog2init = $postfixlogsec;
if (is_file($maillog1init)) {
$maillog1sec = file_get_contents($maillog1init);
} else { $maillog1sec = ''; }
if (is_file($maillog2init)) {
$maillog2sec = file_get_contents($maillog2init);
} else { $maillog2sec = ''; }
if ($lastreadmaillineinit != '') {
if (strpos($maillog1sec, $lastreadmailline) !== false) {
$maillog1tert = strstr($maillog1sec, $lastreadmailline);
$maillog1fourth = preg_split('/\r\n|\r|\n/', $maillog1tert);
$maillogexplall = $maillog1fourth;
array_pop($maillog1fourth);
$endoflstrdmlln = end($maillog1fourth);
$reset1 = reset($maillog1fourth);
$newlastreadmailline = base64_encode($endoflstrdmlln);
} else {
$maillog2tert = strstr($maillog2sec, $lastreadmailline);
$maillog2fourth = preg_split('/\r\n|\r|\n/', $maillog2tert);
$maillog2fifth = implode("\n", $maillog2fourth);
$maillog2sixth = $maillog2fifth . $maillog1sec;
$maillog2expl = preg_split('/\r\n|\r|\n/', $maillog2sixth);
$maillogexplall = $maillog2expl;
array_pop($maillog2expl);
$endoflstrdmlln2 = end($maillog2expl);
$reset2 = reset($maillog2expl);
$newlastreadmailline = base64_encode($endoflstrdmlln2);
}
} else {
if ($maillog1sec != '' || $maillog2sec != '') {
$maillogconc = $maillog2sec . $maillog1sec;
$maillogexplall = preg_split('/\r\n|\r|\n/', $maillogconc);
if ($maillogexplall[0] == '') {
$newlastreadinit = $maillogexplall[1];
} else { $newlastreadinit = $maillogexplall[0]; }
$newlastreadmailline = base64_encode($newlastreadinit);
} else { $newlastreadmailline = ''; $maillogexplall = []; }
}
$mailloglines = [];
foreach($maillogexplall as $key => $value) {
if (strpos($value, $mailstring) !== false) {
$mailloglines[] = htmlentities($value);
}
}
$maildetections = implode("<br><br>", $mailloglines);
$maildetections_enc = base64_encode($maildetections);
// Get the log lines that contain virus detections in files uploaded to Nextcloud
$nextstring = 'Infected file deleted';
if (is_file($nextcloudlog)) {
$nextlog1sec = file_get_contents($nextcloudlog);
} else { $nextlog1sec = ''; }
if (is_file($nextcloudlogsec)) {
$nextlog2sec = file_get_contents($nextcloudlogsec);
} else { $nextlog2sec = ''; }
if ($lastreadnextlineinit != '') {
if (strpos($nextlog1sec, $lastreadnextline) !== false) {
$nextlog1tert = strstr($nextlog1sec, $lastreadnextline);
$nextlog1fourth = preg_split('/\r\n|\r|\n/', $nextlog1tert);
$nextlogexplall = $nextlog1fourth;
array_pop($nextlog1fourth);
$endoflstrdmlln = end($nextlog1fourth);
$newlastreadnextline = base64_encode($endoflstrdmlln);
} else {
$nextlog2tert = strstr($nextlog2sec, $lastreadnextline);
$nextlog2fourth = preg_split('/\r\n|\r|\n/', $nextlog2tert);
$nextlog2fifth = implode("\n", $nextlog2fourth);
$nextlog2sixth = $nextlog2fifth . $nextlog1sec;
$nextlog2expl = preg_split('/\r\n|\r|\n/', $nextlog2sixth);
$nextlogexplall = $nextlog2expl;
array_pop($nextlog2expl);
$endoflstrdmlln2 = end($nextlog2expl);
$newlastreadnextline = base64_encode($endoflstrdmlln2);
}
} else {
if ($nextlog1sec != '' || $nextlog2sec != '') {
$nextlogconc = $nextlog2sec . $nextlog1sec;
$nextlogexplall = preg_split('/\r\n|\r|\n/', $nextlogconc);
if ($nextlogexplall[0] == '') {
$newlastreadinit = $nextlogexplall[1];
} else {
$newlastreadinit = $nextlogexplall[0];
}
$newlastreadnextline = base64_encode($newlastreadinit);
} else {
$newlastreadnextline = ''; $nextlogexplall = [];
}
}
$nextloglines = [];
foreach ($nextlogexplall as $key2 => $value2) {
if (strpos($value2, $nextstring) !== false) {
$nextloglines[] = htmlentities($value2);
}
}
$nextdetections = implode("<br><br>", $nextloglines);
$nextdetections_enc = base64_encode($nextdetections);
// Decide how the virus detections paragraph will look like
if (count($mailloglines) != 0 && count($nextloglines) != 0 && $maildetections_enc != $old_maildetections_enc && $nextdetections_enc != $old_nextdetections_enc) {
$clamavdetections = "<ul><li><span style='color:#225aad;font-weight:700;'>The logs show the following ClamAV recent detections:</span></span></li><br>
<br><ul><li><b>Viruses detected in emails (they have been already removed):</b><br><br>" . $maildetections . "</li></ul><br>
<ul><li><b>Viruses detected in files uploaded to Nextcloud (they have been already removed):</b><br><br>" . $nextdetections . "<br>
</li></ul></ul><br>";
} elseif (count($mailloglines) != 0 && count($nextloglines) == 0 && $maildetections_enc != $old_maildetections_enc) {
$clamavdetections = "<ul><li><span style='color:#225aad;font-weight:700;'>The logs show the following ClamAV recent detections:</span></span></li><br>
<br><ul><li><b>Viruses detected in emails (they have been already removed):</b><br><br>" . $maildetections . "<br>
</li></ul></ul><br>";
} elseif (count($mailloglines) == 0 && count($nextloglines) != 0 && $nextdetections_enc != $old_nextdetections_enc) {
$clamavdetections = "<ul><li><span style='color:#225aad;font-weight:700;>The logs show the following ClamAV recent detections:</span></span></li><br>
<br><ul><li><b>Viruses detected in files uploaded to Nextcloud (they have been already removed):</b><br><br>" . $nextdetections . "<br>
</li></ul></ul><br>";
} else { $clamavdetections = null; }
// Get all the IPs banned by Fail2ban and their respective jails and clean the data
$bannedipslist = ` fail2ban-client status | grep "Jail list:" | sed "s/ //g" | awk '{split($2,a,",");for(i in a) system("fail2ban-client status " a[i])}' | grep "Status\|IP list" `;
$bannedipslistesc = escapeshellarg($bannedipslist);
$getblockedipsfirst = str_replace("'", "", $bannedipslistesc);
$getblockedipssec = str_replace("Status for the jail:", "@", $getblockedipsfirst);
$getblockedipthird = str_replace(array(" `- Banned IP list:"), "", $getblockedipssec);
$getblockedipfourth = str_replace(array("\r\n", "\r", "\n", "\t", " ", " ", " "), " ", $getblockedipthird);
$getblockedipfifth = str_replace(" ", " ", $getblockedipfourth);
$getblockedipsixth = explode("@", $getblockedipfifth);
array_shift($getblockedipsixth);
// Collect the banned IPs
$jailsarr = [];
$allipsintotal = [];
$allipsperjail = [];
foreach ($getblockedipsixth as $key => $value) {
$expldval = explode(" ", $value);
if (count($expldval) > 3) {
$jailsarr[] = $expldval[1];
for ($j = 2; $j < count($expldval)-1; $j++) {
$allipsperjail[] = $expldval[$j];
}
$allipsintotal[] = array($expldval[1] => $allipsperjail);
}
unset($expldval, $allipsperjail);
}
// Get the hostname
$nameofhost = exec("hostname");
// Loop through all the currently banned IPs and get all the necessary details about each, so that they can be inserted into the database and in the email report
$blockedipsrows = [];
$ipsfreqban = [];
$whoisinfo = [];
$ipstimespans = [];
$finwhoisdata = [];
$banipschck = 0;
$abreportsentcheck = 0;
// Set the default timezone for the date/time functions in this script
$systemTime = trim(file_get_contents("/etc/timezone"));
if ($systemTime == 'Etc/UTC') { $systemTime = 'UTC'; }
date_default_timezone_set($systemTime);
if (count($jailsarr) != 0) {
$j = 1;
$date1 = date("Y-m-d H:i:s");
// Get the data from the Fail2ban logs
$logonefl = explode("\n", file_get_contents("/var/log/fail2ban.log"));
$logtwofl = explode("\n", file_get_contents("/var/log/fail2ban.log.1"));
$log1data = array_reverse($logonefl);
$log2data = array_reverse($logtwofl);
foreach ($allipsintotal as $key => $value) {
if (is_array($value)) {
foreach ($value as $key2 => $value2) {
$ipsforjail = [];
$restartcheck = 0;
$getjail = $key2;
// Get the current jail ban time, as it is set in '/etc/fail2ban/jail.local'
$jail = "[" . $getjail . "]";
$procjail = "@" . $jail;
$logdata = file_get_contents('/etc/fail2ban/jail.local');
if (strpos($logdata, $jail) !== false) {
$preproc = str_replace("\n", "@", $logdata);
$firstsplit = explode("@# JAILS", $preproc);
$splitdata = explode($procjail, $firstsplit[1]);
$splitdatasec = strstr($splitdata[1], "@bantime");
$splitdatatert = explode("@", $splitdatasec);
$jailbantime = filter_var($splitdatatert[1], FILTER_SANITIZE_NUMBER_INT);
} else { $jailbantime = null; }
// Get the last date when Fail2ban was restarted, from the logs
$lastrestart = '';
foreach ($log1data as $key => $value) {
if (strpos($value, "Stopping all jails") !== false) {
$restartlinesplit = explode(",", $value);
$lastrestart = $restartlinesplit[0];
break 1;
}
}
if ($lastrestart == '') {
foreach ($log2data as $keylog => $valuelog) {
if (strpos($valuelog, "Stopping all jails") !== false) {
$restartlinesplit2 = explode(",", $valuelog);
$lastrestart = $restartlinesplit2[0];
break 1;
}
}
}
// Calculate the timespan (in seconds) between the date of the last Fail2ban restart as found in logs and the current date
if ($lastrestart != '') {
$lastresdate = new DateTime($lastrestart);
$currentdate = new DateTime($date1);
$datesubtr = $currentdate->getTimestamp() - $lastresdate->getTimestamp();
$restartspan = round($datesubtr);
} else { $restartspan = 10 * $jailbantime; }
/**
* Decide if the last Fail2ban restart happened recently enough to trigger a false positive when updating the database with the total number
* of bans for each IP
*/
if ($restartspan < $jailbantime) {
$restartcheck = 1;
}
// Get the data from the current jail log files
$getlogdata1 = [];
$getlogdata1sec = [];
$getlogdata2 = [];
$getlogdata2sec = [];
if ($getjail == 'wordpress') {
foreach ($wordpresslog as $wkey => $wvalue) {
if (is_file($wvalue)) {
$getwplogarr = explode("\n", file_get_contents("$wvalue"));
$getlogdata1[] = array_reverse($getwplogarr);
}
}
foreach ($wordpresslogsec as $wkeysec => $wvaluesec) {
if (is_file($wvaluesec)) {
$getwplogarrsec = explode("\n", file_get_contents("$wvaluesec"));
$getlogdata1sec[] = array_reverse($getwplogarrsec);
}
}
} else {
if (is_file($jaillogarr[$getjail])) {
$getjaillogarr = explode("\n", file_get_contents($jaillogarr[$getjail]));
$getlogdata2 = array_reverse($getjaillogarr);
}
if (is_file($jaillogarrsec[$getjail])) {
$getjaillogarrsec = explode("\n", file_get_contents($jaillogarrsec[$getjail]));
$getlogdata2sec = array_reverse($getjaillogarrsec);
}
}
if (is_array($value2)) {
foreach ($value2 as $key3 => $value3) {
$getip = $value3;
$ipsforjail[] = $value3;
/**
* Check if the current IP associated to the current jail is already in the database and get the first ban date, the last ban date and the total
* number of bans for this IP from the database
*/
$queryipj = $mysqli->prepare("SELECT id, IP, jail, first_ban, last_ban FROM bannedipstable WHERE IP = ? AND jail = ?");
$queryipj->bind_param("ss", $getip, $getjail);
$queryipj->execute();
$fetchipj = $queryipj->get_result();
$fetchipjres = $fetchipj->fetch_row();
$queryipj->close();
if ($fetchipjres) {
$ipisindatabase = $fetchipjres[0];
$firstbandate = $fetchipjres[3];
$lastbandate = $fetchipjres[4];
} else {
$ipisindatabase = '';
$firstbandate = '';
$lastbandate = '';
}
// Get the last ban date from '/var/log/fail2ban.log' or '/var/log/fail2ban.log.1', for the current IP
$date_last_b = '';
foreach ($log1data as $key => $value) {
if (str_contains($value, "[". $getjail ."] Ban ". $getip) && (str_ends_with($value, $getip))) {
$lastbanline = $value;
$banlinesplit = explode(",", $lastbanline);
$date_last_b = $banlinesplit[0];
break 1;
}
}
if ($date_last_b == '') {
foreach ($log2data as $key2 => $value2) {
if (str_contains($value2, "[". $getjail ."] Ban ". $getip) && (str_ends_with($value2, $getip))) {
$lastbanline2 = $value2;
$banlinesplit2 = explode(",", $lastbanline2);
$date_last_b = $banlinesplit2[0];
break 1;
}
}
}
if (($date_last_b == '') && ($lastbandate != '')) {
$date_last_b = $lastbandate;
}
if (($date_last_b == '') && ($lastbandate == '')) {
$date_last_b = $date1;
}
if ($firstbandate == '') {
$first_ban_proc = $date_last_b;
} else { $first_ban_proc = $firstbandate; }
// Calculate the timespan (in seconds) between the last ban date found in logs and the current date
$datedifference2 = strtotime($date1) - strtotime($date_last_b);
$logspantime = intval($datedifference2);
$ipstimespans[] = $logspantime;
// If the IP is not in the database, insert it with all the necessary details
if ($ipisindatabase == '') {
$crid = 1;
$queryinsip = $mysqli->prepare("INSERT INTO bannedipstable (IP, jail, first_ban, last_ban, ban_number) VALUES (?, ?, ?, ?, ?);");
$queryinsip->bind_param("ssssi", $getip, $getjail, $date_last_b, $date_last_b, $crid);
$queryinsip->execute();
$queryinsip->close();
/**
* If the IP is already in the database, update its record with the number of bans and the last ban date, but only if the current IP has been
* banned in the last run interval and only if the timespan between the last Fail2ban restart and the current time is greater than the current
* jail ban time. Otherwise, the IPs record shouldn't be updated because it means that either the current IP has been banned before the last
* script run, so, the ban has been already processed and the IP's record has been already updated, or that the IP is currently banned only
* because Fail2ban has been restarted in the recent past, since by default Fail2ban bans again all the IPs that were banned at the time of restart.
*/
} elseif (($ipisindatabase != '') && ($logspantime <= $runinterval) && ($logspantime != 0) && ($restartcheck == 0) && ($date_last_b != $firstbandate)) {
$upbandt = $mysqli->prepare("UPDATE bannedipstable SET ban_number = ban_number + 1, last_ban = ? WHERE IP = ? AND jail = ?");
$upbandt->bind_param("sss", $date_last_b, $getip, $getjail);
$upbandt->execute();
$upbandt->close();
}
// Get the total number of bans, the first ban date and the last ban date for the current IP, as they are after updating the database
$getbnbfl = $mysqli->prepare("SELECT IP, jail, first_ban, last_ban, ban_number FROM bannedipstable WHERE IP = ? AND jail = ?");
$getbnbfl->bind_param("ss", $getip, $getjail);
$getbnbfl->execute();
$fetchbnfl = $getbnbfl->get_result();
$resfetchbnfl = $fetchbnfl->fetch_row();
$firstbandatesec = $resfetchbnfl[2];
$lastbandatesec = $resfetchbnfl[3];
$totalbanspr = $resfetchbnfl[4];
$getbnbfl->close();
// Calculate the timespan (in days) between the first ban date and the last ban date as found in the database, for the current IP
$first_ban_date = new DateTime($firstbandatesec);
$last_ban_date = new DateTime($lastbandatesec);
$datediff = $last_ban_date->getTimestamp() - $first_ban_date->getTimestamp();
$timespanall = round($datediff / (60 * 60 * 24));
// Write the paragraph with information about the current IP, if it has been banned more than once
if ($totalbanspr > 1) {
$loglinearrcount = 0;
/**
* Get the last 20 log lines containing the failed log in attempts of the current IP from all the WordPress, Nextcloud, ProFTPD, Asterisk,
* Bind or OpenVPN logs
*/
if ($getjail == 'wordpress' || $getjail == 'nextcloud' || $getjail == 'proftdp' || $getjail == 'asterisk' || $getjail == 'named-refused' || $getjail == 'openvpn') {
/**
* Set the regex expressions that will allow correct identification of failed log in attepts against WordPress, Nextcloud, ProFTPD,
* Asterisk, Bind and OpenVPN
*/
if ($getjail == 'wordpress') {
$regpattern = '/^(?=.*?\b'.$getip.'\b)(?=.*?\b(POST)\b).*$/';
} elseif ($getjail == 'nextcloud') {
$regpattern = '/^(?=.*?\b'.$getip.'\b)(?=.*?\b(Login failed)\b).*$/';
} elseif ($getjail == 'proftdp') {
$regpattern = '/^(?=.*?\b'.$getip.'\b)(?=.*?\b(no such user found|Login failed|SECURITY VIOLATION|Maximum login attempts)\b).*$/';
} elseif ($getjail == 'asterisk') {
$regpattern = '/^(?=.*?\b'.$getip.'\b)(?=.*?\b(failed for|PJSIP syntax error)\b).*$/';
} elseif ($getjail == 'named-refused') {
$regpattern = '/^(?=.*?\b'.$getip.'\b)(?=.*?\b(denied)\b).*$/';
} elseif ($getjail == 'openvpn') {
$regpattern = '/^(?=.*?\b'.$getip.'\b)(?=.*?\b(Connection reset, restarting|TLS Error: TLS handshake failed|Fatal TLS error|VERIFY ERROR|WARNING: Bad encapsulated packet length)\b).*$/';
}
if ($getjail == 'wordpress') {
$loglinearr1 = [];
$reslinesarr = [];
foreach ($getlogdata1 as $getwkey => $getwvalue) {
if (is_array($getwvalue)) {
foreach ($getwvalue as $lkey1 => $lvalue1) {
preg_match_all($regpattern, $lvalue1, $reslinesarr);
if (isset($reslinesarr[0][0]) && $reslinesarr[0][0] != '') {
$loglinearr1[] = $reslinesarr[0][0];
}
}
}
}
} elseif ($getjail == 'nextcloud' || $getjail == 'proftdp' || $getjail == 'asterisk' || $getjail == 'named-refused' || $getjail == 'openvpn') {
$loglinearr1 = [];
$reslinesarr = [];
foreach ($getlogdata2 as $lkey2 => $lvalue2) {
preg_match_all($regpattern, $lvalue2, $reslinesarr);
if (isset($reslinesarr[0][0]) && $reslinesarr[0][0] != '') {
$loglinearr1[] = $reslinesarr[0][0];
}
}
}
// If at least 20 log entries were found for the current IP, collect the last 20 log entries and don't search in the rotated log file
if (count($loglinearr1) > 19) {
$loglinearrauxinit = array_slice($loglinearr1, 0, 20);
$loglinearraux = array_reverse($loglinearrauxinit);
$loglinearrfin = implode("<br></b>", $loglinearraux);
$loglinearrfinpt = implode("\n ", $loglinearraux);
// If less than 20 log entries were found in the current log file, search for the current IP in the rotated log file
} else {
if ($getjail == 'wordpress') {
$resloglsec = [];
foreach ($getlogdata1sec as $getwkeysec => $getwvaluesec) {
if (is_array($getwvaluesec)) {
foreach ($getwvaluesec as $lkey1sec => $lvalue1sec) {
preg_match_all($regpattern, $lvalue1sec, $resloglsec);
if (isset($resloglsec[0][0]) && $resloglsec[0][0] != '') {
$loglinearr1[] = $resloglsec[0][0];
}
}
}
}
} elseif ($getjail == 'nextcloud' || $getjail == 'proftdp' || $getjail == 'asterisk' || $getjail == 'named-refused' || $getjail == 'openvpn') {
$resloglsec = [];
foreach ($getlogdata2sec as $getwkeysec => $getwvaluesec) {
preg_match_all($regpattern, $getwvaluesec, $resloglsec);
if (isset($resloglsec[0][0]) && $resloglsec[0][0] != '') {
$loglinearr1[] = $resloglsec[0][0];
}
}
}
// Collect only the last 20 entries if there are more
if (count($loglinearr1) > 19) {
$loglinearrauxinit = array_slice($loglinearr1, 0, 20);
$loglinearraux = array_reverse($loglinearrauxinit);
$loglinearrfin = implode("<br></b>", $loglinearraux);
$loglinearrfinpt = implode("\n ", $loglinearraux);
} else {
$loglinearrsec = array_reverse($loglinearr1);
$loglinearrfin = implode("<br></b>", $loglinearrsec);
$loglinearrfinpt = implode("\n ", $loglinearrsec);
}
}
$loglinearrcount = count($loglinearr1);
// If the current jail is different from 'wordpress', 'nextcloud', 'proftpd', 'asterisk', 'named-refused' or 'openvpn', get the last 20 log entries containing the current IP
} else {
$loglinearr2 = [];
foreach ($getlogdata2 as $lkey2 => $lvalue2) {
if (strpos($lvalue2, $getip) !== false) {
$loglinearr2[] = $lvalue2;
}
}
if (count($loglinearr2) > 19) {
$loglinearruninit = array_slice($loglinearr2, 0, 20);
$loglinearrun = array_reverse($loglinearruninit);
$loglinearrfin = implode("<br></b>", $loglinearrun);
$loglinearrfinpt = implode("\n ", $loglinearrun);
// If less than 20 log entries were found in the current log file, search for the current IP in the rotated log file
} else {
foreach ($getlogdata2sec as $lkey2sec => $lvalue2sec) {
if (strpos($lvalue2sec, $getip) !== false) {
$loglinearr2[] = $lvalue2sec;
}
}
// Collect only the last 20 entries and remove the rest if there are more
if (count($loglinearr2) > 19) {
$loglinearrun = array_slice($loglinearr2, 0, 20);
$loglinearrot = array_reverse($loglinearrun);
$loglinearrfin = implode("<br></b>", $loglinearrot);
$loglinearrfinpt = implode("\n ", $loglinearrot);
} else {
$loglinearruts = array_reverse($loglinearr2);
$loglinearrfin = implode("<br></b>", $loglinearruts);
$loglinearrfinpt = implode("\n ", $loglinearruts);
}
}
$loglinearrcount = count($loglinearr2);
}
// Get the 'whois' data for the current IP
$whoisinfo = shell_exec("whois " . $getip);
$whoisexpl = preg_split('/\r\n|\r|\n/', $whoisinfo);
// Extract all the email addresses from the 'whois' data
$whoisimplinit = implode(" ", $whoisexpl);
$emregpattern = "/[\._a-zA-Z0-9-]+@[\._a-zA-Z0-9-]+/i";
preg_match_all($emregpattern, $whoisinfo, $ematches);
$allemailsarr = [];
foreach ($ematches[0] as $thisemail) {
$allemailsarr[] = $thisemail;
}
$finemailarray = array_unique($allemailsarr);
$finemails = array_values($finemailarray);
$whoisimpl = implode("<br></b>", $whoisexpl);
// Search for the 'abuse' email address in the 'whois' data
$foundabusemail = '';
foreach ($whoisexpl as $whkey => $whvalue) {
if (($foundabusemail == '') && (strpos($whvalue, 'Abuse contact for') !== false)) {
$wfirstextr = strstr($whvalue, 'Abuse contact for');
$wlinesplit = explode("'", $wfirstextr);
$whotrimmed = array_filter(array_map('trim', $wlinesplit));
$whoreverse = array_reverse($whotrimmed);
if ($whoreverse[0] != 'abuse@ripe.net' && $whoreverse[0] != 'no-email@apnic.net') {
$foundabusemail = $whoreverse[0];
}
break;
}
}
if (($foundabusemail == '') && (strpos($whoisinfo, 'abuse-mailbox') !== false)) {
$wfirstextr = strstr($whoisinfo, 'abuse-mailbox');
$wextsplit = preg_split('/\r\n|\r|\n/', $wfirstextr);
$wlinesplit = explode(' ', $wextsplit[0]);
$whotrimmed = array_filter(array_map('trim', $wlinesplit));
$whoreverse = array_reverse($whotrimmed);
if ($whoreverse[0] != 'abuse@ripe.net' && $whoreverse[0] != 'no-email@apnic.net') {
$foundabusemail = $whoreverse[0];
}
}
if (($foundabusemail == '') && (strpos($whoisinfo, 'OrgAbuseEmail') !== false)) {
$wfirstextr = strstr($whoisinfo, 'OrgAbuseEmail');
$wextsplit = preg_split('/\r\n|\r|\n/', $wfirstextr);
$wlinesplit = explode(' ', $wextsplit[0]);
$whotrimmed = array_filter(array_map('trim', $wlinesplit));
$whoreverse = array_reverse($whotrimmed);
if ($whoreverse[0] != 'abuse@ripe.net' && $whoreverse[0] != 'no-email@apnic.net') {
$foundabusemail = $whoreverse[0];
}
}
if ($foundabusemail == '') {
foreach ($finemails as $finkey => $finvalue) {
if (strpos($finvalue, "abuse@") !== false && $finvalue != "abuse@ripe.net" && $finvalue != "no-email@apnic.net") {
$foundabusemail = $finvalue;
break 1;
}
}
}
// If no log entries were found for the current IP, include an explanation in the email
if ($loglinearrcount != 0) {
$foundlglnchck = null;
} else {
$logdirectoryinit = explode("/", $jaillogarr[$getjail]);
array_pop($logdirectoryinit);
$logdirectory = implode("/", $logdirectoryinit);
$foundlglnchck = "(No log entries were included in the draft email because no entries could be found for this IP
in the current log and in the most recent rotated log file. If you want to send an abuse report for
this IP, you will have to search for these log entries manually in the archived log files located
in '" . $logdirectory . "'.)";
}
/**
* Get the ban moment that will be displayed in the draft abuse report email. It's better to take this timestamp from the last
* found log line containing the current IP, which will also be listed at the end of the email.
*/
$gmtindinit = "UTC " . shell_exec('date +\'%:z\''); // UTC time offset
$gmtind = str_replace("\n", "", $gmtindinit);
// Extract the timestamp differently, according to the log format
if ($loglinearrcount != 0) {
if ($getjail == 'postfix' || $getjail == 'postfix-sasl' || $getjail == 'sshd') {
$exploglinesinit = explode("<br></b>", $loglinearrfin);
$exploglines = array_reverse($exploglinesinit);
$firstlinelg = $exploglines[0];
$getdatexpinit = explode(".", $firstlinelg);
$getdatefrlinit = array_filter(array_map('trim', $getdatexpinit));
$getdatefrl = array_values($getdatefrlinit);
$getdatexp = explode("T", $getdatefrl[0]);
$lastbandateproc = $getdatexp[0] . ", " . $getdatexp[1] . ", " . $gmtind;
} elseif ($getjail == 'dovecot') {
$exploglinesinit = explode("<br></b>", $loglinearrfin);
$exploglines = array_reverse($exploglinesinit);
$firstlinelg = $exploglines[0];
$getdatexp = explode(" ", $firstlinelg);
$getdatefrlinit = array_filter(array_map('trim', $getdatexp));
$getdatefrl = array_values($getdatefrlinit);
$lastbandateproc = $getdatefrl[0] . ", " . $getdatefrl[1] . ", " . $gmtind;
} elseif ($getjail == 'named-refused') {
$exploglinesinit = explode("<br></b>", $loglinearrfin);
$exploglines = array_reverse($exploglinesinit);
$firstlinelg = $exploglines[0];
$getdatexp = explode(" ", $firstlinelg);
$getdatefrlinit = array_filter(array_map('trim', $getdatexp));
$getdatefrl = array_values($getdatefrlinit);
$lastbandateproc = $getdatefrl[0] . ", " . $getdatefrl[1] . ", " . $gmtind;
} elseif ($getjail == 'wordpress' || $getjail == 'mailman' || $getjail == 'dolibarr' || $getjail == 'phplist' ||
$getjail == 'phpmyadmin' || $getjail == 'looladmin' || $getjail == 'matomo') {
$exploglinesinit = explode("<br></b>", $loglinearrfin);
$exploglines = array_reverse($exploglinesinit);
$firstlinelg = $exploglines[0];
$getdatefrl = explode("[", $firstlinelg);
$getdatefrlsec = explode("]", $getdatefrl[1]);
$getdatefrlthird = explode(" ", $getdatefrlsec[0]);
$lastbandateproc = $getdatefrlthird[0] . ", " . $gmtind;
} elseif ($getjail == 'asterisk') {
$exploglinesinit = explode("<br></b>", $loglinearrfin);
$exploglines = array_reverse($exploglinesinit);
$firstlinelg = $exploglines[0];
$getdatefrl = explode("[", $firstlinelg);
$getdatefrlsec = explode("]", $getdatefrl[1]);
$lastbandateproc = $getdatefrlsec[0] . ", " . $gmtind;
} elseif ($getjail == 'roundcube') {
$exploglinesinit = explode("<br></b>", $loglinearrfin);
$exploglines = array_reverse($exploglinesinit);
$firstlinelg = $exploglines[0];
$getdatefrl = explode("[", $firstlinelg);
$getdatefrlsec = explode("]", $getdatefrl[1]);
$getdatefrlthird = explode(" ", $getdatefrlsec[0]);
$lastbandateproc = $getdatefrlthird[0] . ", " . $getdatefrlthird[1] . ", " . $gmtind;
} elseif ($getjail == 'nextcloud') {
$exploglinesinit = explode("<br></b>", $loglinearrfin);
$exploglines = array_reverse($exploglinesinit);
$firstlinelg = $exploglines[0];
$getdatefrl = explode("\"time\":", $firstlinelg);
$getdatefrlsec = explode("\"", $getdatefrl[1]);
$getdatefrlthird = explode("+", $getdatefrlsec[1]);
$getdatefrlfourth = explode("T", $getdatefrlthird[0]);
$lastbandateproc = $getdatefrlfourth[0] . ", " . $getdatefrlfourth[1] . ", " . $gmtind;
} elseif ($getjail == 'proftpd') {
$exploglinesinit = explode("<br></b>", $loglinearrfin);
$exploglines = array_reverse($exploglinesinit);
$firstlinelg = $exploglines[0];
$getdatefrl = explode(",", $firstlinelg);
$getdatefrlsec = explode(" ", $getdatefrl[0]);
$lastbandateproc = $getdatefrlsec[0] . ", " . $getdatefrlsec[1] . ", " . $gmtind;
} elseif ($getjail == 'openvpn') {
$exploglinesinit = explode("<br></b>", $loglinearrfin);
$exploglines = array_reverse($exploglinesinit);
$firstlinelg = $exploglines[0];
$getdatefrl = explode(" us=", $firstlinelg);
$getdatefrlsec = explode(" ", $getdatefrl[0]);
$lastbandateproc = $getdatefrlsec[0] . ", " . $getdatefrlsec[1] . ", " . $gmtind;
}
} else $lastbandateproc = $lastbandatesec;
// Add a warning about the possibility of disclosing sensitive information in the log lines included in the abuse report email
if ($loglinearrcount != 0) {
$senswarn = "It's recommended to check if the log lines included in the draft email from below contain sensitive information, such as custom
WordPress login URLs, usernames used for HTTP authentication, etc. If such information exists, you can replace it with asterisks or
other placeholders before sending the email.";
} else { $senswarn = null; }
// If the 'abuse' email address has been found in the 'whois' database, include a warning text in the email
if ($foundabusemail != '') {
$foundornotinfo = "The 'abuse' email address for this IP has been found in the data output of the 'whois ip' command. It is:<br><br><b>" .
$foundabusemail . "</b><br><br>This address was found automatically and <b>may be incorrect</b>. You can verify it
against the 'whois' data from above before sending the abuse notification.<br>" . $foundlglnchck . "<br>" . $senswarn . "<br>";
// If no 'abuse' email address has been found, include an explanation in the email
} else {
$foundornotinfo = "System Health and Security Probe couldn't find the 'abuse' email address for this IP. You can look again at the
'whois' data for this IP from above, and try to identify the 'abuse' email address which may have a different
format than the standard 'abuse@...'. <br>" . $foundlglnchck . "<br>" . $senswarn . "<br>";
}
// HTML version of the email that can be sent to the 'abuse' email address for the current IP
$abusereportemail = "<!DOCTYPE html>
<html>
<head>
<meta http-equiv='content-type' content='text/html; charset=UTF-8'>
</head>
<body text='#000000' bgcolor='#FFFFFF'>
<br>Hello,<br><br>
The machine with the IP <b>" . $getip . "</b> has attacked our server on the service " . $getjail . ", <br>
on port " . $jailsports[$getjail] . ", on " . $lastbandateproc . " . The IP was blocked automatically.<br><br>
Please check the machine with the IP <b>" . $getip . "</b> to fix this problem.<br><br>
Below you can find the log lines showing the failed log in attempts on our system.<br><br>
Lines containing <b>" . $getip . "</b> :<br><br>" . $loglinearrfin . "<br><br>
You can contact us at " . $emailtofirst . "<br><br>
Thank you,<br>
" . $sysadminname . "<br>
The System Administrator <br>
</body>
</html>";
// Plain text version of the email that can be sent to the 'abuse' email address for the current IP
$abusereportemailpt = "\n Hello,\n\n The machine with the IP " . $getip . " has attacked our server on the service " . $getjail . ", \n on port " . $jailsports[$getjail] . ", on " . $lastbandateproc . ". The IP was blocked automatically. \n\n Please check the machine with the IP " . $getip . " to fix this problem. \n\n Below you can find the log lines showing the failed log in attempts on our system. \n\n Lines containing " . $getip . " : \n\n " . $loglinearrfinpt . " \n\n You can contact us at " . $emailtofirst . " \n\n Thank you, \n " . $sysadminname . " \n The System Administrator \n";
// Write the paragraph containing the email that can be sent to the 'abuse' email address for the current IP
if ($getjail != 'wordpress') {
if ($getjail != 'named-refused') {
$jaillognamefirst = $jaillogarr[$getjail];
$jaillognamesec = $jaillognamefirst . ".1";
} else {
$jaillognamefirst = $jaillogarr['named-refused'];
$jaillognamesec = $jaillognamefirst . ".0";
}
} else {
$jaillognamefirst = $jaillogarr['wordpress'][0];
$jaillognamesec = $jaillogarr['wordpress'][1];
}
if ($loglinearrcount != 0) {
$variantabmail = "</b><br>
If you haven't enabled automatic sending of abuse reports, you can send manually an abuse report to the entity that owns
this offending IP. Below is a draft of the abuse report that you can send for this IP. " . $foundornotinfo . "</b><br>
<p style='border:2px; border-style:solid; border-color:#19aa64; padding: 1em; word-wrap: break-word;'> " . $abusereportemail . "</p></b><br>";
} else {
$variantabmail = "Unfortunately, the repeated attacks of " . $getip . " couldn't be found in the most recent log files: '" . $jaillognamefirst . "'
and '" . $jaillognamesec . "'. It seems that these attacks took place some time ago and the log files in which they were
recorded have been already archived.<br><br>";
}
// Write the 'information about IP' paragraph
$finwhoisdata[] = "<b><h3>Information about " . $getip . "</h3></b><b><br><br>" . $getip . " has been banned by Fail2ban " . $totalbanspr .
" times in the timespan of " . $timespanall . " days (between " . $firstbandatesec . " and " . $lastbandatesec . ")
due to repeated failed log in attempts against '" . $getjail . "'. As a rule, these attempts can be found in '" . $jaillognamefirst . "' or
in '" . $jaillognamesec . "'.<br><br>'Whois' data for " . $getip . " :</b><br><br>
<p style='border:2px; border-style:solid; border-color:#0393bf; padding: 1em; word-wrap: break-word;'>" . $whoisimpl . "</p><br><br>" . $variantabmail;
// If the settings in the 'config' file require it, send the abuse report email to the ISP of the current offending IP
$automaticemailproc = strtolower($automaticemail);
$excludedipsarr = explode(",", $excludedips);
if ($automaticemailproc == 'yes' && $foundabusemail != '' && $loglinearrcount != 0) {
if (($ipisindatabase != '') && ($logspantime <= $runinterval) && ($restartcheck == 0)) {
if (!in_array($jaillogarr[$getjail], $excludedjails)) {
if (!in_array($getip, $excludedipsarr)) {
if ($abusereportstoadmin == "yes") {
$toisp = $foundabusemail . "," . implode(",", $emailtoarr);
} else { $toisp = $foundabusemail; }
$subjectisp = 'Abuse Report';
// Create a unique alphanumeric string to use as a boundary
$msgboundary = bin2hex(openssl_random_pseudo_bytes(16));
// Add the headers
$headersisp = "MIME-Version: 1.0" . "\r\n";
$headersisp .= "From: " . $emailsender . "\r\n";
$headersisp .= "To: " . $toisp . "\r\n";
$headersisp .= "Content-Type: multipart/alternative;boundary=" . $msgboundary . "\r\n";
$messageisp = "This is a MIME encoded message." . "\r\n\r\n";
$messageisp .= "--". $msgboundary ."\r\n";
$messageisp .= "Content-type: text/plain;charset=utf-8" . "\r\n";
$messageisp .= "Content-Transfer-Encoding: 7bit" . "\r\n\r\n";
// Plain text message body
$messageisp .= $abusereportemailpt . "\r\n\r\n";
$messageisp .= "--". $msgboundary ."\r\n";
$messageisp .= "Content-Type: text/html;charset=UTF-8" . "\r\n";
$messageisp .= "Content-Transfer-Encoding: 7bit" . "\r\n\r\n";
// HTML message body
$messageisp .= $abusereportemail . "\r\n\r\n";
$messageisp .= "--". $msgboundary ."--";
// Send the abuse report email
mail('', $subjectisp, $messageisp, $headersisp, "-f " . $emailsender . "");
$abreportsentcheck = 1;
// Increment the number of sent abuse report emails for the current IP, in the 'bannedipstable'
$updateabno = $mysqli->prepare("UPDATE bannedipstable SET emails_isp = emails_isp + 1 WHERE IP = ? AND jail = ?");
$updateabno->bind_param("ss",$getip, $getjail);
$updateabno->execute();
$updateabno->close();
}
}
}
}
}
// Color the total number of bans for the current IP in red if it exceeds 1
if ($totalbanspr == 1) {
$totalbanscolored = "<td align='center' style='color:#000000;'><b>1</b></td>";
} else $totalbanscolored = "<td align='center'><b><font style='color:#ff0000;'>" . $totalbanspr . "</font></b></td>";
// Get from the database the number of abuse report emails that were sent to the ISP of the current IP
$getispemailsno = $mysqli->prepare("SELECT IP, jail, emails_isp FROM bannedipstable WHERE IP = ? AND jail = ?");
$getispemailsno->bind_param("ss", $getip, $getjail);
$getispemailsno->execute();
$resgetispml = $getispemailsno->get_result();
$fetchresispml = $resgetispml->fetch_row();
$totalispemails = $fetchresispml[2];
$getispemailsno->close();
// Add a row corresponding to the current IP and jail, to the blocked IPs table
$blockedipsrows[] = "<tr>
<td>" . $j . "</td>
<td style='padding-left: 7px;'>" . $getip . "</td>
<td style='padding-left: 4px;'>" . $getjail . "</td>
<td style='padding-left: 4px;'>" . $first_ban_proc . "</td>
<td style='padding-left: 4px;'>" . $lastbandatesec . "</td>
<td align='center'>" . $timespanall . "</td>" . $totalbanscolored . "<td align='center'>" . $totalispemails . "</td>
</tr>";
$j++;
}
unset($jailbantime, $totalbanscolored, $loglinearrfin, $loglinearrfinpt);
}
// Get from the database all the banned IPs for the current jail that were banned when the script ran last time
$getbrl = $mysqli->prepare("SELECT id, jail, bannedips FROM jailsandips WHERE jail = ?");
$getbrl->bind_param("s", $getjail);
$getbrl->execute();
$resgetbrl = $getbrl->get_result();
$fetchgetbrl = $resgetbrl->fetch_row();
$lastsentipsperjail = $fetchgetbrl[2];
$lastsentipsarr = explode("|", $lastsentipsperjail);
$getbrl->close();
/**
* Compare the IPs that were banned last time the script ran with the IPs that are banned now. If the IPs banned now are among those banned last
* time, then the check counter will remain as it is, otherwise the counter will be increased to signal that new IPs were banned since the last
* script run
*/
if ($lastsentipsperjail != '') {
$arrintersect = array_intersect($lastsentipsarr, $ipsforjail);
$countarrintersect = count($arrintersect);
$countipsforjail = count($ipsforjail);
if ($countarrintersect != $countipsforjail) {
$banipschck++;
}
} else { $banipschck++; }
// Insert into the 'jailsandips' table all the banned IPs for the current jail
$ipsforjailimpl = implode("|", $ipsforjail);
if ($fetchgetbrl) {
$upbanips = $mysqli->prepare("UPDATE jailsandips SET bannedips = ? WHERE jail = ?");
$upbanips->bind_param("ss", $ipsforjailimpl, $getjail);
$upbanips->execute();
$upbanips->close();
} else {
$insbanips = "INSERT INTO jailsandips (jail, bannedips) VALUES (?, ?);";
$insbanips->bind_param("ss", $getjail, $ipsforjailimpl);
$insbanips->execute();
$insbanips->close();
}
}
}
}
// List the whois data for the IPs that were banned more than once, in the appropriate paragraph of the email
if (count($finwhoisdata) != 0) {
$whoisdataforips = implode("<br><br>", $finwhoisdata);
$ipsbmrthrtms = "<br><br><b><h2>Information about the IPs that were banned more than once</h2></b><br>
If you consider sending abuse reports to the organizations that own the IPs that are currently banned and were also banned
at least once in the past, you can find draft emails under the 'whois' information for each IP, below:<br><br>" . $whoisdataforips;
} else { $ipsbmrthrtms = null; }
// Create the blocked IPs paragraph of the email
$blockedipstable = implode("", $blockedipsrows);
$getblockedips = "<ul><li style='color:#225aad;font-weight:700;'>The following IP addresses <u>are currently banned</u> by Fail2ban due to repeated failed log in
attempts against the services displayed in the 'Jail' column:</li><br><br>
<table bgcolor='#f1f9ff' border='1' style='border-collapse:collapse;' width='100%' cellpadding='2'>
<tr>
<th style='text-align:center;'>No.</th>
<th style='text-align:center;'>IP</th>
<th style='text-align:center;'>Jail</th>
<th style='text-align:center;'>First<br>Ban Date</th>
<th style='text-align:center;'>Last<br>Ban Date</th>
<th style='text-align:center;'>Days<br>between<br>first and<br>last ban</th>
<th style='text-align:center;'>Total Number<br>of Bans</th>
<th style='text-align:center;'>Total Number<br>of Emails<br>Automatically<br>Sent to ISP</th>
</tr>" . $blockedipstable . "</table>" . $ipsbmrthrtms . "</ul>";
} else { $getblockedips = null; }
// Get the total free storage space (in kibibytes) and if it is below the threshold specified in the 'config' file (in kibibytes), include a warning in the email
$freedisk = exec("df -x tmpfs --total | awk '{print $4}' | tail -1");
$totalspace = exec("df -x tmpfs --total | awk '{print $2}' | tail -1");
$freediskgb = round($freedisk / 1048576);
$totaldiskgb = round($totalspace / 1048576);
if ($freedisk <= $diskthreshold){
$diskwarn = "<ul><li style='color:#225aad;font-weight:700;'>The total free storage space on all partitions of your system is
(approximately): " . $freediskgb . " gibibytes from a total of " . $totaldiskgb . " gibibytes.</li> You may consider
removing unnecessary files and directories, optimizing data storage processes, upgrading your server's storage or upgrading your server.</ul><br><br>";
} else { $diskwarn = null; }
// Include an info line about the free storage space in the email
$infodisk = "• The total free storage space on all partitions of your system is (approximately): " . $freediskgb . " gibibytes from a total
of " . $totaldiskgb . " gibibytes.</b>";
// Get the system uptime and the average CPU load in the last 15 minutes
$uptimeinit = exec("uptime | awk '{print $3,$4}'");
$uptimesplit = explode(" ", $uptimeinit);
if ($uptimesplit[1] == 'days,') {
$uptimefin = $uptimesplit[0] . " days.";
} elseif ($uptimesplit[1] == 'min,') {
$uptimefin = $uptimesplit[0] . " min.";
} elseif ($uptimesplit[1] == 'day,') {
$uptimefin = $uptimesplit[0] . " day.";
} elseif (strpos($uptimeinit, ':') !== false) {
$uptimesec = explode(":", $uptimesplit[0]);
$uptimefin = $uptimesec[0] . " hours.";
}
$updaystext = "• The system has been up for " . $uptimefin;
$averageload = exec("awk '{print $3}'< /proc/loadavg");
$cpunumber = exec("cat /proc/cpuinfo | awk '/^processor/{print $3}' | wc -l");
/**
* If the average CPU load in the last 15 minutes is greater than 1 x (the number of CPU's), or if there are any other problems detected,
* list the average load in a paragraph of the email
*/
$sortedbanspans = sort($ipstimespans);
if ($averageload > $cpunumber) {
$averageloadwarn = "<ul><li style='color:#225aad;font-weight:700;'>• The average CPU load in the last 15 minutes is: " . $averageload . " (The average
CPU load is considered high if it stays constantly over " . $cpunumber . " )</li></ul>";
} else { $averageloadwarn = null; }
// Include an info line about the average CPU load in the last 15 minutes in the 'General System Information' paragraph of the email
$averageloadgen = "• The average CPU load in the last 15 minutes is: " . $averageload . " (The average CPU load is considered high only if it stays
constantly over " . $cpunumber . ")";
// Get the base64 encoded logo
$base64Logo = file_get_contents(dirname(__FILE__) . "/img/base64LogoImg.txt");
// Create a boundary string for the email report
$separator = bin2hex(openssl_random_pseudo_bytes(16));
// Set the email headers
$headers = "MIME-Version: 1.0" . "\r\n";
$headers .= "From: " . $emailsender . "\r\n";
$headers .= "Return-Path: " . $emailsender . "\r\n";
$headers .= "Reply-To: " . $emailsender . "\r\n";
$headers .= "Content-Type: multipart/related;" . "\r\n";
$headers .= " boundary=------------". $separator . "\r\n";
$headers .= "--------------". $separator . "\r\n";
$headers .= "Content-Type: text/html; charset=UTF-8" . "\r\n";
$headers .= "Content-Transfer-Encoding: 7bit" . "\r\n";
// Write the email to report the problems found
$message = "
<!DOCTYPE html>
<html>
<head>
<title>System Health and Security Report</title>
</head>
<body bgcolor='#f1f9ff'>
<div style='background-color:#f1f9ff;padding:4px 8px;'>
<div style='display: block; margin: 6px auto; width: 229px; height: 170px;'><img src='cid:logo-". $separator ."'></div>
<p>Hello,</p>
<p>This is an automated message sent by System Health and Security Probe v. 1.0.0. Host: " . $nameofhost . ".<br><br>
System Health and Security Probe runs regularly and investigates if any service running on the server is in failed state, if ClamAV
antivirus has detected any virus in the incoming emails or in the files uploaded to Nextcloud, if any IP address has been banned due to repeated
failed log in attempts against one of the applications monitored by Fail2ban, if the free storage space on all partitions is under a certain
threshold, and if the average CPU load in the last 15 minutes exceeds 100% utilization of all the CPU cores. <br><br>
The problems currently detected are listed below:<br><br></p>
<p>$getfailedserv</p>
<p>$clamavperiodicdetect</p>
<p>$clamavdetections</p>
<p>$getblockedips</p>
<p>$diskwarn</p>
<p>$averageloadwarn</p>
<h4>General System Information:</h4>
<p>$infodisk</p>
<p>$averageloadgen</p>
<p>$updaystext</p>
<br>Yours,<br>
System Health and Security Probe<br>
Host: " . $nameofhost . "<br><br>
</div>
</body>
</html>
--------------". $separator ."
Content-Type: image/svg+xml; name='k9zg7dfQsjhcrp1lr.svg'
Content-Disposition: inline; filename='k9zg7dfQsjhcrp1lr.svg'
Content-Id: <logo-". $separator .">
Content-Transfer-Encoding: base64
". $base64Logo ."
--------------". $separator ."--
";
/**
* Extract a fragment from the newly created email, excluding some elements which almost always change from one script run to the other. This fragment will be stored in the
* database to be compared with every similar fragment in the future emails prepared to be sent. If the fragment in the new email is identical with the one stored in the
* database, in certain situations the new message will not be sent because all the data it contains has been already included in the previous email(s). The new message will
* be always sent if the average CPU load in the last 15 minutes exceeds 100% utilization of all the CPU cores, or if the free storage space is below the critical threshold.
*/
$message_light = "
<p>$getfailedserv</p>
<p>$clamavperiodicdetect</p>
<p>$clamavdetections</p>
<p>$diskwarn</p>
<p>$averageloadwarn</p>
";
$new_message_enc = base64_encode($message_light);
// Get the last sent email from the 'lastsentemail' table
$getlsml = "SELECT id, old_message FROM lastsentemail WHERE id = 1;";
$resgetlsml = $mysqli->query($getlsml);
$resfetchlsml = $resgetlsml->fetch_array();
$old_message_enc = $resfetchlsml[1];
/**
* Send the current email if any new IP has been banned since the last run, or if the average CPU load in the last 15 minutes exceeds 100% utilization of all the
* CPU cores, or if the free storage space is below the critical threshold, or if ClamAV detected any viruses in emails or in the files uploaded to Nextcloud.
* If the timespan between the last IP ban and the current time is longer than the run interval and the banned IPs are the only problem the system has, then the
* email shouldn't be sent, because the report about all the currently banned IPs has been already sent on the previous script run.
*/
if (($banipschck != 0) || ($averageload > $cpunumber) || ($freedisk <= $diskthreshold) || ($abreportsentcheck != 0) || ((strcmp($new_message_enc, $old_message_enc) !== 0) &&
(($getfailedservtert[0] != 0) || (($nbinfctdflsmail != 0) && ($maildetlnbr_enc != $old_mailvirdetect_enc)) || (($nbinfctdflsnext != 0) &&
($nextdetlnbr_enc != $old_nextvirdetect_enc)) || (count($mailloglines) != 0) || (count($nextloglines) != 0) || ((count($jailsarr) != 0) && ($ipstimespans[0] <= $runinterval))))) {
mail($emailto, $subject, $message, $headers, "-f " . $emailsender . "");
// Store the 'light' version of the last sent email in the 'lastsentemail' table
$nmid = 1;
$rplnmenc = $mysqli->prepare("REPLACE INTO lastsentemail (id, old_message) VALUES (?, ?);");
$rplnmenc->bind_param("is", $nmid, $new_message_enc);
$rplnmenc->execute();
$rplnmenc->close();
/**
* From the email that has just been sent, take the log lines and report lines containing virus detections, take the last read log lines and store them
* in the appropriate fields, in the 'sentreadlines' table
*/
$strlglns = $mysqli->prepare("REPLACE INTO sentreadlines (id, mail_loglines, nextcloud_loglines, lastmailline, lastnextline, lastpersdetmail, lastpersdetnext) VALUES
(?, ?, ?, ?, ?, ?, ?);");
$strlglns->bind_param("issssss", $nmid, $maildetections_enc, $newlastreadmailline, $nextdetections_enc, $newlastreadnextline, $maildetlnbr_enc, $nextdetlnbr_enc);
$strlglns->execute();
$strlglns->close();
}
$getlsml = "SELECT id, old_message FROM lastsentemail WHERE id = 1;";
$resgetlsml = $mysqli->query($getlsml);
$resfetchlsml = $resgetlsml->fetch_array();
$old_message_enc = $resfetchlsml[1];
// Update the last read log lines at every script run, even if the current email hasn't been sent because it was identical with the last sent one
$chcklstl = "SELECT * FROM sentreadlines;";
$reschcklstl = $mysqli->query($chcklstl);
$fetchchcklstl = $reschcklstl->fetch_array();
if ($fetchchcklstl) {
$lnid = 1;
$upnlmnln = $mysqli->prepare("UPDATE sentreadlines SET lastmailline = ?, lastnextline = ? WHERE id = ?");
$upnlmnln->bind_param("ssi", $newlastreadmailline, $newlastreadnextline, $lnid);
$upnlmnln->execute();
$upnlmnln->close();
}
/**
* If the 'bannedipstable' table has more than 100000000 rows, first backup the current database, then remove the first 5000000 rows, so as to avoid working with an
* excessively large database. If you want to save the backup in a directory different from '/srv/shas-probe-db-backup', then specify it in the 'config' file, in the
* $backup_directory parameter
*/
$bptcount = "SELECT COUNT(id) FROM bannedipstable";
$resbptcount = $mysqli->query($bptcount);
$fetchbpt = $resbptcount->fetch_array();
$rownb = $fetchbpt[0];
if ($rownb > 100000000) {
$date2 = date("Y-m-d");
if (!is_dir($backupdirectory)) {
exec("mkdir -p " . $backupdirectory);
}
$backupname = "bannedipsdb-" . $date2 . ".sql";
$dbbackup = 'mysqldump --user=' . $bannedipsuser . ' --password=' . $bannedipspswd . ' --databases ' . $bannedipsdb . ' > ' . $backupdirectory . '/' . $backupname . '';
$runcommand = exec($dbbackup);
$dlrows = "DELETE FROM bannedipstable ORDER BY id ASC limit 5000000";
$resdlrows = $mysqli->query($dlrows);
}
} else { exit("Error. No configuration file."); }
?>