diff --git a/ajax/export.php b/ajax/export.php index e69de29..2b9f0c6 100644 --- a/ajax/export.php +++ b/ajax/export.php @@ -0,0 +1,28 @@ +getFromDB($id)) { + http_response_code(404); + exit('Configuration not found'); +} + +$content = $config->getDecryptedContent(); + +// Opzionale: mascheramento credenziali +$content = preg_replace('/(?<=password\s|secret\s|enable\s)[^\s\r\n]+/i', '***REDACTED***', $content); + +header('Content-Disposition: attachment; filename="netconfig_' . $id . '_' . date('Ymd_His') . '.txt"'); +echo $content; +exit; diff --git a/composer.json b/composer.json index e69de29..348032b 100644 --- a/composer.json +++ b/composer.json @@ -0,0 +1,24 @@ +{ + "name": "glpi-plugin/netconfig", + "description": "Backup e versioning configurazioni di rete per GLPI 11", + "type": "glpi-plugin", + "minimum-stability": "stable", + "prefer-stable": true, + "require": { + "php": ">=8.2", + "symfony/diff": "^6.4", + "guzzlehttp/guzzle": "^7.8" + }, + "autoload": { + "psr-4": { + "PluginNetconfig\\": "src/", + "PluginNetconfig\\Api\\": "src/Api/" + } + }, + "config": { + "optimize-autoloader": true, + "allow-plugins": { + "php-http/discovery": true + } + } +} diff --git a/hook.php b/hook.php index e69de29..c7c96ad 100644 --- a/hook.php +++ b/hook.php @@ -0,0 +1,17 @@ +doQuery($sql); + return true; +} + +function plugin_netconfig_uninstall() { + $db = DBConnection::getReadConnection(); + $db->doQuery("DROP TABLE IF EXISTS `glpi_plugin_netconfig_configs`"); + return true; +} diff --git a/setup.php b/setup.php index e69de29..2087c0b 100644 --- a/setup.php +++ b/setup.php @@ -0,0 +1,52 @@ + '>=11.0']; + + // Integrazione menu GLPI + $PLUGIN_HOOKS['menu_toadd']['netconfig'] = [ + 'tools' => \PluginNetconfig\Config::class, + ]; + + // Registrazione endpoint API per l'agent + $PLUGIN_HOOKS['add_api_endpoint']['netconfig'] = [ + '/plugin/netconfig/agent' => 'PluginNetconfig\Agent\Receive', + ]; + + // Attivazione massive actions + $PLUGIN_HOOKS['use_massive_actions']['netconfig'] = true; + + // Registrazione automatica dei diritti profilo + if (class_exists('\PluginNetconfig\Config')) { + $PLUGIN_HOOKS['add_javascript']['netconfig'][] = 'netconfig.js'; // Opzionale + } +} + +function plugin_version_netconfig() { + return [ + 'name' => 'Network Config Backup', + 'version' => PLUGIN_NETCONFIG_VERSION, + 'author' => 'Your Name / Team', + 'license' => 'GPL-3.0-or-later', + 'homepage' => '', + 'requirements' => [ + 'glpi' => ['min' => '11.0.0', 'max' => '11.99.99'], + 'php' => ['min' => '8.2.0'], + ], + ]; +} + +function plugin_install_netconfig() { + return true; +} + +function plugin_uninstall_netconfig() { + return true; +} diff --git a/src/Agent/Task/Backup.php b/src/Agent/Task/Backup.php index e69de29..35af413 100644 --- a/src/Agent/Task/Backup.php +++ b/src/Agent/Task/Backup.php @@ -0,0 +1,30 @@ +namespace PluginNetconfig\Agent\Task; + +use PluginNetconfig\Config; +use Glpi\Toolbox\Encryption; +use Session; + +class Backup +{ + public static function handleReceived(array $input): bool + { + if (!Config::canCreate()) return false; + + $hash = hash('sha256', $input['config']); + $latest = Config::getLastByDevice($input['device_id']); + + if ($latest && $latest->fields['config_hash'] === $hash) { + return true; // Nessuna modifica + } + + $config = new Config(); + $data = [ + 'networkdevices_id' => $input['device_id'], + 'config_content' => Encryption::encrypt($input['config']), + 'config_hash' => $hash, + 'users_id' => Session::getLoginUserID() ?: 0, // 0 = agent/system + 'created_at' => $_SESSION['glpi_currenttime'] + ]; + return $config->add($data) !== false; + } +} diff --git a/src/Config.php b/src/Config.php index e69de29..7d11479 100644 --- a/src/Config.php +++ b/src/Config.php @@ -0,0 +1,143 @@ + __('Read'), + UPDATE => __('Update'), + CREATE => __('Create'), + PURGE => __('Delete permanently'), + ]; + } + + public static function canView(): bool { return Session::haveRight(self::$rightname, READ); } + public static function canCreate(): bool { return Session::haveRight(self::$rightname, CREATE); } + public static function canUpdate(): bool { return Session::haveRight(self::$rightname, UPDATE); } + public static function canPurge(): bool { return Session::haveRight(self::$rightname, PURGE); } + + public function getTabNameForItem(CommonGLPI $item, $withtemplate = 0): string + { + if ($item->getType() === 'NetworkEquipment' && self::canView()) { + return __('Config History', 'netconfig') . ' (' . countElementsInTable(self::getTable(), ['networkdevices_id' => $item->getID()]) . ')'; + } + return ''; + } + + public static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtemplate = 0): bool + { + if ($item->getType() !== 'NetworkEquipment') return true; + + $configs = (new self())->find( + ['networkdevices_id' => $item->getID()], + 'created_at DESC' + ); + + $twig = new TemplateRenderer(); + echo $twig->render('@netconfig/config_tab.html.twig', [ + 'configs' => $configs, + 'can_export' => self::canView(), + 'can_create' => self::canCreate() + ]); + return true; + } + + public static function getLastConfigByDevice(int $networkdevices_id): ?array + { + $db = DBConnection::getReadConnection(); + $stmt = $db->prepare("SELECT * FROM " . self::getTable() . " WHERE networkdevices_id = ? ORDER BY created_at DESC LIMIT 1"); + $stmt->bind_param('i', $networkdevices_id); + $stmt->execute(); + return $stmt->get_result()->fetch_assoc() ?: null; + } + + public static function saveConfig(array $data): bool + { + $config = new self(); + $hash = hash('sha256', $data['content']); + $last = self::getLastConfigByDevice($data['networkdevices_id']); + + if ($last && $last['config_hash'] === $hash) { + return true; // Nessuna modifica rilevata + } + + $input = [ + 'networkdevices_id' => $data['networkdevices_id'], + 'config_content' => Encryption::encrypt($data['content']), + 'config_hash' => $hash, + 'users_id' => $data['users_id'] ?? Session::getLoginUserID() ?: 0, + 'is_encrypted' => 1, + ]; + + return $config->add($input) !== false; + } + + public function getDecryptedContent(): string + { + return $this->fields['is_encrypted'] + ? Encryption::decrypt($this->fields['config_content']) + : $this->fields['config_content']; + } + + public function getDiffFromPrevious(): string + { + $last = self::getLastConfigByDevice($this->fields['networkdevices_id']); + if (!$last || $last['id'] === $this->fields['id']) return ''; + + $prev = new self(); + $prev->getFromDB($last['id']); + $differ = new Differ(new HtmlOutput()); + return $differ->diff($prev->getDecryptedContent(), $this->getDecryptedContent()); + } + + // MASSIVE ACTIONS + public function getSpecificMassiveActions($checkitem = null): array + { + $actions = parent::getSpecificMassiveActions($checkitem); + if (self::canCreate()) { + $actions[self::class . ':BackupNow'] = __('Force backup now', 'netconfig'); + } + if (self::canView()) { + $actions[self::class . ':ExportSelected'] = __('Export selected', 'netconfig'); + } + return $actions; + } + + public static function processMassiveActionsForOneItemtype(MassiveAction $ma, CommonDBTM $item, array $ids): void + { + switch ($ma->getAction()) { + case self::class . ':BackupNow': + foreach ($ids as $id) { + // Qui puoi integrare coda RabbitMQ, webhook o chiamata diretta all'agent + $ma->itemDone($item::getType(), $id, MassiveAction::ACTION_OK); + } + break; + case self::class . ':ExportSelected': + foreach ($ids as $id) { + // Redirect to export script + $ma->itemDone($item::getType(), $id, MassiveAction::ACTION_OK); + } + break; + } + } +}