Files
drmaterac.pl/modules/securitywatcher/securitywatcher.php
2026-04-02 09:57:53 +02:00

299 lines
11 KiB
PHP

<?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;'>&#9888; {$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;'>&#9888; 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/'
);
}
}
}