299 lines
11 KiB
PHP
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;'>⚠ {$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/'
|
|
);
|
|
}
|
|
}
|
|
}
|