update
This commit is contained in:
22
modules/securitywatcher/cron_check.php
Normal file
22
modules/securitywatcher/cron_check.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
/**
|
||||
* Security Watcher - Cron file check
|
||||
* Run via cron every 15 minutes:
|
||||
* *\/15 * * * * php /path/to/prestashop/modules/securitywatcher/cron_check.php
|
||||
*/
|
||||
|
||||
// Bootstrap PrestaShop
|
||||
$dir = dirname(__FILE__);
|
||||
$psRoot = realpath($dir . '/../../');
|
||||
|
||||
require_once $psRoot . '/config/config.inc.php';
|
||||
|
||||
$module = Module::getInstanceByName('securitywatcher');
|
||||
|
||||
if ($module && $module->active) {
|
||||
$changes = $module->cronCheckFiles();
|
||||
$total = count($changes['new']) + count($changes['modified']) + count($changes['deleted']);
|
||||
echo date('Y-m-d H:i:s') . " - Check complete. Changes: {$total}\n";
|
||||
} else {
|
||||
echo "SecurityWatcher module not active.\n";
|
||||
}
|
||||
1
modules/securitywatcher/mails/en/securitywatcher.html
Normal file
1
modules/securitywatcher/mails/en/securitywatcher.html
Normal file
@@ -0,0 +1 @@
|
||||
{content}
|
||||
1
modules/securitywatcher/mails/en/securitywatcher.txt
Normal file
1
modules/securitywatcher/mails/en/securitywatcher.txt
Normal file
@@ -0,0 +1 @@
|
||||
{content}
|
||||
1
modules/securitywatcher/mails/pl/securitywatcher.html
Normal file
1
modules/securitywatcher/mails/pl/securitywatcher.html
Normal file
@@ -0,0 +1 @@
|
||||
{content}
|
||||
1
modules/securitywatcher/mails/pl/securitywatcher.txt
Normal file
1
modules/securitywatcher/mails/pl/securitywatcher.txt
Normal file
@@ -0,0 +1 @@
|
||||
{content}
|
||||
1
modules/securitywatcher/security.log
Normal file
1
modules/securitywatcher/security.log
Normal file
@@ -0,0 +1 @@
|
||||
2026-04-02 09:50:44 | MODULE UNINSTALLED | pagecache | Jacek Pyziak (ID: 13) | biuro@project-pro.pl | 91.189.216.43
|
||||
298
modules/securitywatcher/securitywatcher.php
Normal file
298
modules/securitywatcher/securitywatcher.php
Normal file
@@ -0,0 +1,298 @@
|
||||
<?php
|
||||
|
||||
if (!defined('_PS_VERSION_')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
class SecurityWatcher extends Module
|
||||
{
|
||||
private $alertEmail = 'biuro@project-pro.pl';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->name = 'securitywatcher';
|
||||
$this->tab = 'administration';
|
||||
$this->version = '1.0.0';
|
||||
$this->author = 'Project-Pro';
|
||||
$this->need_instance = 0;
|
||||
$this->bootstrap = true;
|
||||
|
||||
parent::__construct();
|
||||
|
||||
$this->displayName = $this->l('Security Watcher');
|
||||
$this->description = $this->l('Monitors module installations and file changes, sends email alerts.');
|
||||
}
|
||||
|
||||
public function install()
|
||||
{
|
||||
return parent::install()
|
||||
&& $this->registerHook('actionModuleInstallAfter')
|
||||
&& $this->registerHook('actionModuleUninstallAfter')
|
||||
&& $this->registerHook('actionModuleInstallBefore')
|
||||
&& $this->registerHook('actionObjectModuleAddAfter')
|
||||
&& $this->createSnapshotTable()
|
||||
&& $this->takeSnapshot();
|
||||
}
|
||||
|
||||
public function uninstall()
|
||||
{
|
||||
return parent::uninstall();
|
||||
}
|
||||
|
||||
private function createSnapshotTable()
|
||||
{
|
||||
$sql = 'CREATE TABLE IF NOT EXISTS `' . _DB_PREFIX_ . 'securitywatcher_snapshot` (
|
||||
`id` INT AUTO_INCREMENT PRIMARY KEY,
|
||||
`path` VARCHAR(512) NOT NULL,
|
||||
`hash` VARCHAR(64) NOT NULL,
|
||||
`size` INT NOT NULL,
|
||||
`mtime` INT NOT NULL,
|
||||
`snapshot_date` DATETIME NOT NULL,
|
||||
INDEX (`path`)
|
||||
) ENGINE=' . _MYSQL_ENGINE_ . ' DEFAULT CHARSET=utf8';
|
||||
|
||||
return Db::getInstance()->execute($sql);
|
||||
}
|
||||
|
||||
public function takeSnapshot()
|
||||
{
|
||||
Db::getInstance()->execute('TRUNCATE TABLE `' . _DB_PREFIX_ . 'securitywatcher_snapshot`');
|
||||
|
||||
$dirs = [
|
||||
_PS_MODULE_DIR_,
|
||||
_PS_ROOT_DIR_ . '/themes/',
|
||||
];
|
||||
|
||||
foreach ($dirs as $dir) {
|
||||
$this->scanDir($dir);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function scanDir($dir)
|
||||
{
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
RecursiveIteratorIterator::SELF_FIRST
|
||||
);
|
||||
|
||||
$batch = [];
|
||||
$count = 0;
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isFile() && preg_match('/\.(php|js|tpl|html|css)$/i', $file->getFilename())) {
|
||||
$path = str_replace(_PS_ROOT_DIR_, '', $file->getPathname());
|
||||
$batch[] = '('
|
||||
. "'" . pSQL($path) . "',"
|
||||
. "'" . pSQL(md5_file($file->getPathname())) . "',"
|
||||
. (int)$file->getSize() . ','
|
||||
. (int)$file->getMTime() . ','
|
||||
. "NOW()"
|
||||
. ')';
|
||||
$count++;
|
||||
|
||||
if ($count % 500 === 0) {
|
||||
$this->insertBatch($batch);
|
||||
$batch = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($batch)) {
|
||||
$this->insertBatch($batch);
|
||||
}
|
||||
}
|
||||
|
||||
private function insertBatch($batch)
|
||||
{
|
||||
$sql = 'INSERT INTO `' . _DB_PREFIX_ . 'securitywatcher_snapshot` (path, hash, size, mtime, snapshot_date) VALUES ' . implode(',', $batch);
|
||||
Db::getInstance()->execute($sql);
|
||||
}
|
||||
|
||||
public function hookActionModuleInstallBefore($params)
|
||||
{
|
||||
$this->sendModuleAlert('INSTALLATION ATTEMPT', $params);
|
||||
}
|
||||
|
||||
public function hookActionModuleInstallAfter($params)
|
||||
{
|
||||
$this->sendModuleAlert('MODULE INSTALLED', $params);
|
||||
}
|
||||
|
||||
public function hookActionModuleUninstallAfter($params)
|
||||
{
|
||||
$this->sendModuleAlert('MODULE UNINSTALLED', $params);
|
||||
}
|
||||
|
||||
public function hookActionObjectModuleAddAfter($params)
|
||||
{
|
||||
if (isset($params['object'])) {
|
||||
$this->sendModuleAlert('MODULE ADDED TO DB', $params);
|
||||
}
|
||||
}
|
||||
|
||||
private function sendModuleAlert($action, $params)
|
||||
{
|
||||
$moduleName = 'unknown';
|
||||
if (isset($params['module']) && is_object($params['module'])) {
|
||||
$moduleName = $params['module']->name;
|
||||
} elseif (isset($params['object']) && is_object($params['object'])) {
|
||||
$moduleName = $params['object']->name;
|
||||
}
|
||||
|
||||
$employee = Context::getContext()->employee;
|
||||
$employeeInfo = 'Not logged in / CLI';
|
||||
$employeeEmail = 'N/A';
|
||||
if ($employee && $employee->id) {
|
||||
$employeeInfo = $employee->firstname . ' ' . $employee->lastname . ' (ID: ' . $employee->id . ')';
|
||||
$employeeEmail = $employee->email;
|
||||
}
|
||||
|
||||
$ip = Tools::getRemoteAddr();
|
||||
$userAgent = isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : 'N/A';
|
||||
$shopUrl = Tools::getShopDomainSsl(true);
|
||||
|
||||
$subject = "[SECURITY] {$action}: {$moduleName} @ " . Configuration::get('PS_SHOP_NAME');
|
||||
|
||||
$body = "
|
||||
<html><body style='font-family: Arial, sans-serif;'>
|
||||
<h2 style='color: #cc0000;'>⚠ {$action}</h2>
|
||||
<table style='border-collapse:collapse; width:100%;'>
|
||||
<tr><td style='padding:8px; border:1px solid #ddd; font-weight:bold;'>Module</td><td style='padding:8px; border:1px solid #ddd;'>{$moduleName}</td></tr>
|
||||
<tr><td style='padding:8px; border:1px solid #ddd; font-weight:bold;'>Action</td><td style='padding:8px; border:1px solid #ddd;'>{$action}</td></tr>
|
||||
<tr><td style='padding:8px; border:1px solid #ddd; font-weight:bold;'>Employee</td><td style='padding:8px; border:1px solid #ddd;'>{$employeeInfo}</td></tr>
|
||||
<tr><td style='padding:8px; border:1px solid #ddd; font-weight:bold;'>Employee Email</td><td style='padding:8px; border:1px solid #ddd;'>{$employeeEmail}</td></tr>
|
||||
<tr><td style='padding:8px; border:1px solid #ddd; font-weight:bold;'>IP Address</td><td style='padding:8px; border:1px solid #ddd;'>{$ip}</td></tr>
|
||||
<tr><td style='padding:8px; border:1px solid #ddd; font-weight:bold;'>User Agent</td><td style='padding:8px; border:1px solid #ddd;'>{$userAgent}</td></tr>
|
||||
<tr><td style='padding:8px; border:1px solid #ddd; font-weight:bold;'>Date</td><td style='padding:8px; border:1px solid #ddd;'>" . date('Y-m-d H:i:s') . "</td></tr>
|
||||
<tr><td style='padding:8px; border:1px solid #ddd; font-weight:bold;'>Shop</td><td style='padding:8px; border:1px solid #ddd;'>{$shopUrl}</td></tr>
|
||||
</table>
|
||||
</body></html>";
|
||||
|
||||
$this->sendAlert($subject, $body);
|
||||
|
||||
$logFile = _PS_MODULE_DIR_ . 'securitywatcher/security.log';
|
||||
$logLine = date('Y-m-d H:i:s') . " | {$action} | {$moduleName} | {$employeeInfo} | {$employeeEmail} | {$ip}\n";
|
||||
file_put_contents($logFile, $logLine, FILE_APPEND | LOCK_EX);
|
||||
}
|
||||
|
||||
public function cronCheckFiles()
|
||||
{
|
||||
$dirs = [
|
||||
_PS_MODULE_DIR_,
|
||||
_PS_ROOT_DIR_ . '/themes/',
|
||||
];
|
||||
|
||||
$changes = ['new' => [], 'modified' => [], 'deleted' => []];
|
||||
|
||||
foreach ($dirs as $dir) {
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS)
|
||||
);
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isFile() && preg_match('/\.(php|js|tpl|html|css)$/i', $file->getFilename())) {
|
||||
$path = str_replace(_PS_ROOT_DIR_, '', $file->getPathname());
|
||||
$hash = md5_file($file->getPathname());
|
||||
|
||||
$existing = Db::getInstance()->getRow(
|
||||
'SELECT hash FROM `' . _DB_PREFIX_ . 'securitywatcher_snapshot` WHERE path = "' . pSQL($path) . '"'
|
||||
);
|
||||
|
||||
if (!$existing) {
|
||||
$changes['new'][] = $path;
|
||||
} elseif ($existing['hash'] !== $hash) {
|
||||
$changes['modified'][] = $path;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$snapshotFiles = Db::getInstance()->executeS(
|
||||
'SELECT path FROM `' . _DB_PREFIX_ . 'securitywatcher_snapshot`'
|
||||
);
|
||||
|
||||
foreach ($snapshotFiles as $row) {
|
||||
if (!file_exists(_PS_ROOT_DIR_ . $row['path'])) {
|
||||
$changes['deleted'][] = $row['path'];
|
||||
}
|
||||
}
|
||||
|
||||
$totalChanges = count($changes['new']) + count($changes['modified']) + count($changes['deleted']);
|
||||
|
||||
if ($totalChanges > 0) {
|
||||
$subject = "[SECURITY] {$totalChanges} file change(s) detected @ " . Configuration::get('PS_SHOP_NAME');
|
||||
|
||||
$body = "<html><body style='font-family: Arial, sans-serif;'>";
|
||||
$body .= "<h2 style='color: #cc0000;'>⚠ File Changes Detected</h2>";
|
||||
$body .= "<p>Date: " . date('Y-m-d H:i:s') . "</p>";
|
||||
|
||||
if (!empty($changes['new'])) {
|
||||
$body .= "<h3 style='color: #cc6600;'>New files (" . count($changes['new']) . ")</h3><ul>";
|
||||
foreach (array_slice($changes['new'], 0, 50) as $f) {
|
||||
$body .= "<li>" . htmlspecialchars($f) . "</li>";
|
||||
}
|
||||
if (count($changes['new']) > 50) {
|
||||
$body .= "<li>... and " . (count($changes['new']) - 50) . " more</li>";
|
||||
}
|
||||
$body .= "</ul>";
|
||||
}
|
||||
|
||||
if (!empty($changes['modified'])) {
|
||||
$body .= "<h3 style='color: #cc0000;'>Modified files (" . count($changes['modified']) . ")</h3><ul>";
|
||||
foreach (array_slice($changes['modified'], 0, 50) as $f) {
|
||||
$body .= "<li>" . htmlspecialchars($f) . "</li>";
|
||||
}
|
||||
if (count($changes['modified']) > 50) {
|
||||
$body .= "<li>... and " . (count($changes['modified']) - 50) . " more</li>";
|
||||
}
|
||||
$body .= "</ul>";
|
||||
}
|
||||
|
||||
if (!empty($changes['deleted'])) {
|
||||
$body .= "<h3 style='color: #999;'>Deleted files (" . count($changes['deleted']) . ")</h3><ul>";
|
||||
foreach (array_slice($changes['deleted'], 0, 50) as $f) {
|
||||
$body .= "<li>" . htmlspecialchars($f) . "</li>";
|
||||
}
|
||||
if (count($changes['deleted']) > 50) {
|
||||
$body .= "<li>... and " . (count($changes['deleted']) - 50) . " more</li>";
|
||||
}
|
||||
$body .= "</ul>";
|
||||
}
|
||||
|
||||
$body .= "</body></html>";
|
||||
|
||||
$this->sendAlert($subject, $body);
|
||||
|
||||
$logFile = _PS_MODULE_DIR_ . 'securitywatcher/security.log';
|
||||
$logLine = date('Y-m-d H:i:s') . " | FILE CHANGES | new:" . count($changes['new']) . " mod:" . count($changes['modified']) . " del:" . count($changes['deleted']) . "\n";
|
||||
file_put_contents($logFile, $logLine, FILE_APPEND | LOCK_EX);
|
||||
}
|
||||
|
||||
$this->takeSnapshot();
|
||||
|
||||
return $changes;
|
||||
}
|
||||
|
||||
private function sendAlert($subject, $body)
|
||||
{
|
||||
$emails = array_map('trim', explode(',', $this->alertEmail));
|
||||
|
||||
foreach ($emails as $email) {
|
||||
@Mail::Send(
|
||||
(int)Configuration::get('PS_LANG_DEFAULT'),
|
||||
'securitywatcher',
|
||||
$subject,
|
||||
['{content}' => $body],
|
||||
$email,
|
||||
null,
|
||||
Configuration::get('PS_SHOP_EMAIL'),
|
||||
Configuration::get('PS_SHOP_NAME'),
|
||||
null,
|
||||
null,
|
||||
_PS_MODULE_DIR_ . 'securitywatcher/mails/'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user