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 = "

⚠ {$action}

Module{$moduleName}
Action{$action}
Employee{$employeeInfo}
Employee Email{$employeeEmail}
IP Address{$ip}
User Agent{$userAgent}
Date" . date('Y-m-d H:i:s') . "
Shop{$shopUrl}
"; $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 = ""; $body .= "

⚠ File Changes Detected

"; $body .= "

Date: " . date('Y-m-d H:i:s') . "

"; if (!empty($changes['new'])) { $body .= "

New files (" . count($changes['new']) . ")

"; } if (!empty($changes['modified'])) { $body .= "

Modified files (" . count($changes['modified']) . ")

"; } if (!empty($changes['deleted'])) { $body .= "

Deleted files (" . count($changes['deleted']) . ")

"; } $body .= ""; $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/' ); } } }