From 194322c9fc6cd3687b6fb2c50bef2ab25924bdde Mon Sep 17 00:00:00 2001 From: tips-of-mine <54597409+tips-of-mine@users.noreply.github.com> Date: Sat, 31 May 2025 10:03:48 +0200 Subject: [PATCH] Start repository --- .bolt/config.json | 3 + .bolt/prompt | 5 + .gitignore | 25 + README.md | 1 + ajax/sync_now.php | 66 + eslint.config.js | 28 + front/alert.form.php | 95 + front/alert.php | 20 + front/cvesource.form.php | 88 + front/cvesource.php | 20 + front/inventory.php | 162 ++ hook.php | 171 ++ inc/cve.class.php | 752 ++++++ inc/cvealert.class.php | 414 +++ inc/cveinventory.class.php | 358 +++ inc/cverule.class.php | 497 ++++ inc/cvesource.class.php | 1095 ++++++++ inc/cveticket.class.php | 494 ++++ inc/define.php | 78 + inc/menu.class.php | 273 ++ index.html | 13 + locales/de_DE.po | 330 +++ locales/en_GB.po | 351 +++ locales/es_ES.po | 330 +++ locales/fr_FR.po | 330 +++ locales/it_IT.po | 330 +++ locales/pl_PL.po | 336 +++ locales/pt_PT.po | 330 +++ package-lock.json | 4076 ++++++++++++++++++++++++++++++ package.json | 35 + postcss.config.js | 6 + public/cve_dashboard.html | 488 ++++ public/cve_list.html | 151 ++ scripts/analyze_inventory.php | 57 + scripts/cleanup.php | 57 + scripts/sync_cve.php | 57 + setup.php | 403 +++ src/App.tsx | 79 + src/components/CVEList.tsx | 208 ++ src/components/Dashboard.tsx | 176 ++ src/components/RulesConfig.tsx | 362 +++ src/components/SourcesConfig.tsx | 286 +++ src/css/cve.css | 502 ++++ src/data/mockData.ts | 181 ++ src/index.css | 3 + src/js/cve.js | 417 +++ src/main.tsx | 10 + src/types/cve.ts | 45 + src/vite-env.d.ts | 1 + tailwind.config.js | 8 + tools/extract_template.sh | 22 + tools/generate_mo.sh | 17 + tools/update_po.sh | 18 + tsconfig.app.json | 24 + tsconfig.json | 7 + tsconfig.node.json | 22 + vite.config.ts | 10 + 57 files changed, 14723 insertions(+) create mode 100644 .bolt/config.json create mode 100644 .bolt/prompt create mode 100644 .gitignore create mode 100644 README.md create mode 100644 ajax/sync_now.php create mode 100644 eslint.config.js create mode 100644 front/alert.form.php create mode 100644 front/alert.php create mode 100644 front/cvesource.form.php create mode 100644 front/cvesource.php create mode 100644 front/inventory.php create mode 100644 hook.php create mode 100644 inc/cve.class.php create mode 100644 inc/cvealert.class.php create mode 100644 inc/cveinventory.class.php create mode 100644 inc/cverule.class.php create mode 100644 inc/cvesource.class.php create mode 100644 inc/cveticket.class.php create mode 100644 inc/define.php create mode 100644 inc/menu.class.php create mode 100644 index.html create mode 100644 locales/de_DE.po create mode 100644 locales/en_GB.po create mode 100644 locales/es_ES.po create mode 100644 locales/fr_FR.po create mode 100644 locales/it_IT.po create mode 100644 locales/pl_PL.po create mode 100644 locales/pt_PT.po create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 postcss.config.js create mode 100644 public/cve_dashboard.html create mode 100644 public/cve_list.html create mode 100644 scripts/analyze_inventory.php create mode 100644 scripts/cleanup.php create mode 100644 scripts/sync_cve.php create mode 100644 setup.php create mode 100644 src/App.tsx create mode 100644 src/components/CVEList.tsx create mode 100644 src/components/Dashboard.tsx create mode 100644 src/components/RulesConfig.tsx create mode 100644 src/components/SourcesConfig.tsx create mode 100644 src/css/cve.css create mode 100644 src/data/mockData.ts create mode 100644 src/index.css create mode 100644 src/js/cve.js create mode 100644 src/main.tsx create mode 100644 src/types/cve.ts create mode 100644 src/vite-env.d.ts create mode 100644 tailwind.config.js create mode 100644 tools/extract_template.sh create mode 100644 tools/generate_mo.sh create mode 100644 tools/update_po.sh create mode 100644 tsconfig.app.json create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite.config.ts diff --git a/.bolt/config.json b/.bolt/config.json new file mode 100644 index 0000000..6b6787d --- /dev/null +++ b/.bolt/config.json @@ -0,0 +1,3 @@ +{ + "template": "bolt-vite-react-ts" +} diff --git a/.bolt/prompt b/.bolt/prompt new file mode 100644 index 0000000..ce91b43 --- /dev/null +++ b/.bolt/prompt @@ -0,0 +1,5 @@ +For all designs I ask you to make, have them be beautiful, not cookie cutter. Make webpages that are fully featured and worthy for production. + +By default, this template supports JSX syntax with Tailwind CSS classes, React hooks, and Lucide React for icons. Do not install other packages for UI themes, icons, etc unless absolutely necessary or I request them. + +Use icons from lucide-react for logos. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7ceb59f --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +.env diff --git a/README.md b/README.md new file mode 100644 index 0000000..3335b35 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +GLPI-Plugin-CVE-Prototype diff --git a/ajax/sync_now.php b/ajax/sync_now.php new file mode 100644 index 0000000..8ff873f --- /dev/null +++ b/ajax/sync_now.php @@ -0,0 +1,66 @@ +find(['is_active' => 1]); + +$success = true; +$count = 0; + +foreach ($sources as $data) { + $source->getFromDB($data['id']); + + // Log the start of sync + Toolbox::logInFile('cve_plugin', sprintf('Starting synchronization of source: %s (ID: %d)', + $data['name'], $data['id'])); + + // Update source status to in progress + $source->update([ + 'id' => $data['id'], + 'sync_status' => 'IN_PROGRESS', + ]); + + try { + $result = $source->syncNow($data['id']); + $count += $result ? 1 : 0; + $success = $success && $result; + + // Log the result + Toolbox::logInFile('cve_plugin', sprintf('Sync of source %s %s', + $data['name'], ($result ? 'succeeded' : 'failed'))); + } catch (Exception $e) { + $success = false; + + // Log the error + Toolbox::logInFile('cve_plugin', sprintf('Error during sync of source %s: %s', + $data['name'], $e->getMessage()), true); + + // Update source status to failed + $source->update([ + 'id' => $data['id'], + 'sync_status' => 'FAILED', + 'last_sync' => $_SESSION['glpi_currenttime'] + ]); + } +} + +// Log the overall result +if ($count > 0) { + Toolbox::logInFile('cve_plugin', sprintf('CVE synchronization completed: %d sources processed with %s', + $count, ($success ? 'success' : 'some failures'))); +} + +// No HTML output needed for background task \ No newline at end of file diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..82c2e20 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,28 @@ +import js from '@eslint/js'; +import globals from 'globals'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + { ignores: ['dist'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + } +); diff --git a/front/alert.form.php b/front/alert.form.php new file mode 100644 index 0000000..a727173 --- /dev/null +++ b/front/alert.form.php @@ -0,0 +1,95 @@ +getFromDB($_POST['id']); + + // Process alert to create a ticket + PluginCveCveInventory::processAlert($_POST['id']); + + Html::back(); +} else if (isset($_POST["add"])) { + $alert->check(-1, CREATE, $_POST); + if ($alert->add($_POST)) { + Event::log( + $alert->fields['id'], + "plugin_cve_alert", + 4, + "inventory", + sprintf(__('%1$s adds the vulnerability alert %2$s'), $_SESSION["glpiname"], $_POST["name"]) + ); + } + Html::back(); +} else if (isset($_POST["delete"])) { + $alert->check($_POST["id"], DELETE); + if ($alert->delete($_POST)) { + Event::log( + $_POST["id"], + "plugin_cve_alert", + 4, + "inventory", + sprintf(__('%1$s deletes the vulnerability alert %2$s'), $_SESSION["glpiname"], $_POST["id"]) + ); + } + $alert->redirectToList(); +} else if (isset($_POST["restore"])) { + $alert->check($_POST["id"], DELETE); + if ($alert->restore($_POST)) { + Event::log( + $_POST["id"], + "plugin_cve_alert", + 4, + "inventory", + sprintf(__('%1$s restores the vulnerability alert %2$s'), $_SESSION["glpiname"], $_POST["id"]) + ); + } + $alert->redirectToList(); +} else if (isset($_POST["purge"])) { + $alert->check($_POST["id"], PURGE); + if ($alert->delete($_POST, 1)) { + Event::log( + $_POST["id"], + "plugin_cve_alert", + 4, + "inventory", + sprintf(__('%1$s purges the vulnerability alert %2$s'), $_SESSION["glpiname"], $_POST["id"]) + ); + } + $alert->redirectToList(); +} else if (isset($_POST["update"])) { + $alert->check($_POST["id"], UPDATE); + if ($alert->update($_POST)) { + Event::log( + $_POST["id"], + "plugin_cve_alert", + 4, + "inventory", + sprintf(__('%1$s updates the vulnerability alert %2$s'), $_SESSION["glpiname"], $_POST["id"]) + ); + } + Html::back(); +} else { + Html::header( + PluginCveCveAlert::getTypeName(Session::getPluralNumber()), + $_SERVER['PHP_SELF'], + "tools", + "PluginCveCveMenu", + "alert" + ); + + $id = 0; + if (isset($_GET["id"])) { + $id = $_GET["id"]; + } + + $alert->display(['id' => $id]); + Html::footer(); +} \ No newline at end of file diff --git a/front/alert.php b/front/alert.php new file mode 100644 index 0000000..7cf5d65 --- /dev/null +++ b/front/alert.php @@ -0,0 +1,20 @@ +check(-1, CREATE, $_POST); + if ($source->add($_POST)) { + Event::log( + $source->fields['id'], + "plugin_cve_source", + 4, + "setup", + sprintf(__('%1$s adds the CVE source %2$s'), $_SESSION["glpiname"], $_POST["name"]) + ); + } + Html::back(); +} else if (isset($_POST["delete"])) { + $source->check($_POST["id"], DELETE); + if ($source->delete($_POST)) { + Event::log( + $_POST["id"], + "plugin_cve_source", + 4, + "setup", + sprintf(__('%1$s deletes the CVE source %2$s'), $_SESSION["glpiname"], $_POST["id"]) + ); + } + $source->redirectToList(); +} else if (isset($_POST["restore"])) { + $source->check($_POST["id"], DELETE); + if ($source->restore($_POST)) { + Event::log( + $_POST["id"], + "plugin_cve_source", + 4, + "setup", + sprintf(__('%1$s restores the CVE source %2$s'), $_SESSION["glpiname"], $_POST["id"]) + ); + } + $source->redirectToList(); +} else if (isset($_POST["purge"])) { + $source->check($_POST["id"], PURGE); + if ($source->delete($_POST, 1)) { + Event::log( + $_POST["id"], + "plugin_cve_source", + 4, + "setup", + sprintf(__('%1$s purges the CVE source %2$s'), $_SESSION["glpiname"], $_POST["id"]) + ); + } + $source->redirectToList(); +} else if (isset($_POST["update"])) { + $source->check($_POST["id"], UPDATE); + if ($source->update($_POST)) { + Event::log( + $_POST["id"], + "plugin_cve_source", + 4, + "setup", + sprintf(__('%1$s updates the CVE source %2$s'), $_SESSION["glpiname"], $_POST["id"]) + ); + } + Html::back(); +} else { + Html::header( + PluginCveCveSource::getTypeName(Session::getPluralNumber()), + $_SERVER['PHP_SELF'], + "tools", + "PluginCveCveMenu", + "cvesource" + ); + + $id = 0; + if (isset($_GET["id"])) { + $id = $_GET["id"]; + } + + $source->display(['id' => $id]); + Html::footer(); +} \ No newline at end of file diff --git a/front/cvesource.php b/front/cvesource.php new file mode 100644 index 0000000..698d6d6 --- /dev/null +++ b/front/cvesource.php @@ -0,0 +1,20 @@ +getFromDBbyName('PluginCveCveInventory', 'AnalyzeInventory')) { + $task_id = $task->fields['id']; + + // Execute the task + $result = PluginCveCveInventory::cronAnalyzeInventory($task); + + if ($result) { + Session::addMessageAfterRedirect( + __('Software vulnerability analysis completed successfully.', 'cve'), + true, + INFO + ); + } else { + Session::addMessageAfterRedirect( + __('Software vulnerability analysis completed with no new alerts.', 'cve'), + true, + INFO + ); + } + } else { + Session::addMessageAfterRedirect( + __('Software vulnerability analysis task not found.', 'cve'), + true, + ERROR + ); + } + + Html::redirect($_SERVER['PHP_SELF']); +} else { + $alert = new PluginCveCveAlert(); + + // Get alert statistics + $stats = PluginCveCveAlert::getAlertStats(); + + echo "
"; + echo ""; + echo ""; + + // Show manual scan button + if (Session::haveRight("plugin_cve_inventory", UPDATE)) { + echo ""; + } + + // Show statistics + echo ""; + echo ""; + echo ""; + echo ""; + + echo ""; + echo ""; + echo ""; + echo ""; + + echo ""; + echo ""; + echo ""; + echo ""; + + echo ""; + echo ""; + echo ""; + echo ""; + + echo "
" . __('Software Vulnerability Analysis', 'cve') . "
"; + echo "
"; + echo ""; + Html::closeForm(); + echo "
" . __('Total Vulnerability Alerts', 'cve') . "" . $stats['total'] . "
" . __('New Alerts', 'cve') . "" . $stats['by_status']['NEW'] . "
" . __('Critical Vulnerabilities', 'cve') . "" . $stats['by_severity']['CRITICAL'] . "
" . __('High Vulnerabilities', 'cve') . "" . $stats['by_severity']['HIGH'] . "
"; + echo "
"; + + // Show recent alerts + $alerts = PluginCveCveAlert::getRecentAlerts(10); + + echo "
"; + echo ""; + echo ""; + + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + + if (empty($alerts)) { + echo ""; + } else { + foreach ($alerts as $alert_data) { + echo ""; + + // Software + echo ""; + + // Version + echo ""; + + // CVE ID + echo ""; + + // Severity + echo ""; + + // Status + echo ""; + + // Date + echo ""; + + echo ""; + } + } + + echo "
" . __('Recent Vulnerability Alerts', 'cve') . "
" . __('Software', 'cve') . "" . __('Version', 'cve') . "" . __('CVE ID', 'cve') . "" . __('Severity', 'cve') . "" . __('Status', 'cve') . "" . __('Date', 'cve') . "
" . __('No alerts found', 'cve') . "
"; + echo $alert_data['software_name']; + echo ""; + echo $alert_data['version_name']; + echo ""; + echo ""; + echo $alert_data['cve_id']; + echo ""; + echo ""; + echo ""; + echo $alert_data['severity']; + echo ""; + echo ""; + echo $alert_data['status']; + echo ""; + echo Html::convDateTime($alert_data['date_creation']); + echo "
"; + + // Link to all alerts + echo "
"; + echo "" . __('View all vulnerability alerts', 'cve') . ""; + echo "
"; + + echo "
"; +} + +Html::footer(); \ No newline at end of file diff --git a/hook.php b/hook.php new file mode 100644 index 0000000..90e992f --- /dev/null +++ b/hook.php @@ -0,0 +1,171 @@ +deleteByCriteria(['tickets_id' => $ticket->getID()]); + + // Also remove any alert associations + $alert = new PluginCveCveAlert(); + $alert->deleteByCriteria(['tickets_id' => $ticket->getID()]); +} + +/** + * Create dashboard cards for the plugin + * + * @param array $cards + * @return array + */ +function plugin_cve_dashboard_cards($cards) { + $new_cards = []; + + // Add CVE statistics card + $new_cards['plugin_cve_stats'] = [ + 'widgettype' => 'statscard', + 'title' => __('CVE Statistics', 'cve'), + 'icon' => 'ti ti-shield', + 'provider' => 'PluginCveCve::getCVEStatsDashboard' + ]; + + // Add CVE severity distribution card + $new_cards['plugin_cve_severity'] = [ + 'widgettype' => 'donut', + 'title' => __('CVE Severity Distribution', 'cve'), + 'icon' => 'ti ti-chart-pie', + 'provider' => 'PluginCveCve::getCVESeverityDashboard' + ]; + + // Add recent CVEs card + $new_cards['plugin_cve_recent'] = [ + 'widgettype' => 'table', + 'title' => __('Recent CVEs', 'cve'), + 'icon' => 'ti ti-list', + 'provider' => 'PluginCveCve::getRecentCVEsDashboard' + ]; + + // Add software vulnerability statistics card + $new_cards['plugin_cve_software_vulnerabilities'] = [ + 'widgettype' => 'statscard', + 'title' => __('Software Vulnerability Alerts', 'cve'), + 'icon' => 'ti ti-alert-triangle', + 'provider' => 'PluginCveCveAlert::getAlertStats' + ]; + + return array_merge($cards, $new_cards); +} + +/** + * Create the tables needed to run plugin + * + * @return boolean + */ +function plugin_cve_createTables() { + plugin_cve_install(); + return true; +} + +/** + * Migration function + * + * @param string $version Version to upgrade from + * @return boolean + */ +function plugin_cve_upgradeTables($version) { + // Handle migrations based on version + // This would be implemented with version-specific upgrades + return true; +} + +/** + * Add plugin entries to GLPI's search engine + * + * @param string $itemtype + * @return array + */ +function plugin_cve_getAddSearchOptions($itemtype) { + $options = []; + + if ($itemtype == 'Ticket') { + $options[9100] = [ + 'table' => 'glpi_plugin_cve_tickets', + 'field' => 'id', + 'name' => __('Associated CVEs', 'cve'), + 'massiveaction' => false, + 'joinparams' => [ + 'jointype' => 'child', + 'condition' => "AND `REFTABLE`.`id` = `TABLE`.`tickets_id`" + ] + ]; + } + + if ($itemtype == 'Software') { + $options[9200] = [ + 'table' => 'glpi_plugin_cve_alerts', + 'field' => 'id', + 'name' => __('Vulnerability Alerts', 'cve'), + 'massiveaction' => false, + 'joinparams' => [ + 'jointype' => 'child', + 'condition' => "AND `REFTABLE`.`id` = `TABLE`.`softwares_id`" + ] + ]; + + $options[9201] = [ + 'table' => 'glpi_plugin_cve_alerts', + 'field' => 'severity', + 'name' => __('Vulnerability Severity', 'cve'), + 'massiveaction' => false, + 'joinparams' => [ + 'jointype' => 'child', + 'condition' => "AND `REFTABLE`.`id` = `TABLE`.`softwares_id`" + ] + ]; + } + + return $options; +} + +/** + * Datainjection hook + * + * @param array $data + * @return array + */ +function plugin_datainjection_populate_cve($data) { + // Logic for data injection support + // This would be implemented with actual data mapping logic + return $data; +} \ No newline at end of file diff --git a/inc/cve.class.php b/inc/cve.class.php new file mode 100644 index 0000000..b802d71 --- /dev/null +++ b/inc/cve.class.php @@ -0,0 +1,752 @@ +addDefaultFormTab($tabs); + $this->addStandardTab('PluginCveTicket', $tabs, $options); + $this->addStandardTab('Log', $tabs, $options); + + return $tabs; + } + + /** + * Display the CVE form + * + * @param integer $ID ID of the item + * @param array $options + * @return boolean + */ + function showForm($ID, $options = []) { + global $CFG_GLPI; + + $this->initForm($ID, $options); + $this->showFormHeader($options); + + $canedit = $this->can($ID, UPDATE); + + echo ""; + + // CVE ID + echo "" . __('CVE ID', 'cve') . ""; + echo ""; + echo Html::input('cve_id', ['value' => $this->fields['cve_id'], 'size' => 20]); + echo ""; + + // Severity + echo "" . __('Severity', 'cve') . ""; + echo ""; + $severity_options = [ + 'CRITICAL' => __('Critical', 'cve'), + 'HIGH' => __('High', 'cve'), + 'MEDIUM' => __('Medium', 'cve'), + 'LOW' => __('Low', 'cve') + ]; + Dropdown::showFromArray('severity', $severity_options, + ['value' => $this->fields['severity']]); + echo ""; + + echo ""; + + echo ""; + + // CVSS Score + echo "" . __('CVSS Score', 'cve') . ""; + echo ""; + echo Html::input('cvss_score', ['value' => $this->fields['cvss_score'], 'size' => 5]); + echo ""; + + // CVSS Vector + echo "" . __('CVSS Vector', 'cve') . ""; + echo ""; + echo Html::input('cvss_vector', ['value' => $this->fields['cvss_vector'], 'size' => 40]); + echo ""; + + echo ""; + + echo ""; + + // Published date + echo "" . __('Published Date', 'cve') . ""; + echo ""; + Html::showDateField('published_date', ['value' => $this->fields['published_date']]); + echo ""; + + // Modified date + echo "" . __('Modified Date', 'cve') . ""; + echo ""; + Html::showDateField('modified_date', ['value' => $this->fields['modified_date']]); + echo ""; + + echo ""; + + echo ""; + + // Status + echo "" . __('Status', 'cve') . ""; + echo ""; + $status_options = [ + 'NEW' => __('New', 'cve'), + 'ANALYZED' => __('Analyzed', 'cve'), + 'ASSIGNED' => __('Assigned', 'cve'), + 'RESOLVED' => __('Resolved', 'cve') + ]; + Dropdown::showFromArray('status', $status_options, + ['value' => $this->fields['status']]); + echo ""; + + // Add entity dropdown if needed + echo "" . __('Entity', 'cve') . ""; + echo ""; + Entity::dropdown(['value' => $this->fields['entities_id']]); + echo ""; + + echo ""; + + echo ""; + + // Description + echo "" . __('Description', 'cve') . ""; + echo ""; + echo ""; + echo ""; + + echo ""; + + echo ""; + + // References + echo "" . __('References', 'cve') . ""; + echo ""; + $references = json_decode($this->fields['references'], true) ?: []; + echo ""; + echo "
" . __('Enter one URL per line', 'cve') . ""; + echo ""; + + echo ""; + + echo ""; + + // Affected Products + echo "" . __('Affected Products', 'cve') . ""; + echo ""; + $affected_products = json_decode($this->fields['affected_products'], true) ?: []; + echo ""; + echo "
" . __('Enter one product per line', 'cve') . ""; + echo ""; + + echo ""; + + $this->showFormButtons($options); + + return true; + } + + /** + * Pre-process data before add or update + * + * @param array $input Data to process + * @return array Processed data + */ + function prepareInputForAddOrUpdate($input) { + // Process references from textarea to JSON + if (isset($input['references'])) { + $references = explode("\n", $input['references']); + $references = array_map('trim', $references); + $references = array_filter($references); + $input['references'] = json_encode(array_values($references)); + } + + // Process affected products from textarea to JSON + if (isset($input['affected_products'])) { + $products = explode("\n", $input['affected_products']); + $products = array_map('trim', $products); + $products = array_filter($products); + $input['affected_products'] = json_encode(array_values($products)); + } + + return $input; + } + + /** + * Pre-process input data before adding + * + * @param array $input Input data + * @return array|false Processed input data or false on error + */ + function prepareInputForAdd($input) { + // Set creation date if not provided + if (!isset($input['date_creation'])) { + $input['date_creation'] = $_SESSION['glpi_currenttime']; + } + + // Set default entity if not provided + if (!isset($input['entities_id'])) { + $input['entities_id'] = $_SESSION['glpiactive_entity']; + } + + // Process the input for references and affected products + $input = $this->prepareInputForAddOrUpdate($input); + + return $input; + } + + /** + * Pre-process input data before updating + * + * @param array $input Input data + * @return array|false Processed input data or false on error + */ + function prepareInputForUpdate($input) { + // Set modification date + $input['date_mod'] = $_SESSION['glpi_currenttime']; + + // Process the input for references and affected products + $input = $this->prepareInputForAddOrUpdate($input); + + return $input; + } + + /** + * Get search function for the class + * + * @return array of search options + */ + function rawSearchOptions() { + $tab = []; + + $tab[] = [ + 'id' => 'common', + 'name' => self::getTypeName(2) + ]; + + $tab[] = [ + 'id' => '1', + 'table' => $this->getTable(), + 'field' => 'cve_id', + 'name' => __('CVE ID', 'cve'), + 'datatype' => 'itemlink', + 'massiveaction' => false + ]; + + $tab[] = [ + 'id' => '2', + 'table' => $this->getTable(), + 'field' => 'description', + 'name' => __('Description', 'cve'), + 'datatype' => 'text', + 'massiveaction' => false + ]; + + $tab[] = [ + 'id' => '3', + 'table' => $this->getTable(), + 'field' => 'cvss_score', + 'name' => __('CVSS Score', 'cve'), + 'datatype' => 'decimal', + 'massiveaction' => false + ]; + + $tab[] = [ + 'id' => '4', + 'table' => $this->getTable(), + 'field' => 'severity', + 'name' => __('Severity', 'cve'), + 'datatype' => 'specific', + 'searchtype' => ['equals', 'notequals'] + ]; + + $tab[] = [ + 'id' => '5', + 'table' => $this->getTable(), + 'field' => 'published_date', + 'name' => __('Published Date', 'cve'), + 'datatype' => 'datetime', + 'massiveaction' => false + ]; + + $tab[] = [ + 'id' => '6', + 'table' => $this->getTable(), + 'field' => 'status', + 'name' => __('Status', 'cve'), + 'datatype' => 'specific', + 'searchtype' => ['equals', 'notequals'] + ]; + + $tab[] = [ + 'id' => '16', + 'table' => $this->getTable(), + 'field' => 'date_creation', + 'name' => __('Creation date', 'cve'), + 'datatype' => 'datetime', + 'massiveaction' => false + ]; + + $tab[] = [ + 'id' => '19', + 'table' => $this->getTable(), + 'field' => 'date_mod', + 'name' => __('Last update', 'cve'), + 'datatype' => 'datetime', + 'massiveaction' => false + ]; + + return $tab; + } + + /** + * Check if user has the right to perform an action + * + * @param $action integer ID of the action + * @param $right string|integer Expected right [default READ] + * + * @return boolean + */ + static function canAction($action, $right = READ) { + + // Get the active entity + $active_entity = $_SESSION['glpiactive_entity']; + + // Check if the user can perform the action + return Session::haveRight(self::$rightname, $right); + } + + /** + * Create a ticket from a CVE + * + * @param integer $cves_id ID of the CVE + * @param array $options Additional options + * + * @return boolean|integer ID of the created ticket or false + */ + function createTicket($cves_id, $options = []) { + global $DB; + + // Load the CVE + $cve = new self(); + if (!$cve->getFromDB($cves_id)) { + return false; + } + + // Create a new ticket + $ticket = new Ticket(); + + // Set ticket fields + $ticketData = [ + 'entities_id' => $cve->fields['entities_id'], + 'name' => __('Vulnerability', 'cve') . ' ' . $cve->fields['cve_id'], + 'content' => __('Vulnerability details', 'cve') . ":\n\n" . + $cve->fields['description'] . "\n\n" . + __('References', 'cve') . ":\n" . $cve->fields['references'], + 'status' => Ticket::INCOMING, + 'date' => $_SESSION['glpi_currenttime'], + 'type' => Ticket::INCIDENT_TYPE, + 'urgency' => $this->getCVSStoPriority($cve->fields['cvss_score']), + 'impact' => $this->getCVSStoPriority($cve->fields['cvss_score']), + 'priority' => $this->getCVSStoPriority($cve->fields['cvss_score']), + 'itilcategories_id' => 0, // Default or security category if configured + 'users_id_recipient' => Session::getLoginUserID(), + ]; + + // Apply any custom options + if (count($options)) { + foreach ($options as $key => $val) { + $ticketData[$key] = $val; + } + } + + // Create the ticket + $tickets_id = $ticket->add($ticketData); + + if ($tickets_id) { + // Create the link between CVE and ticket + $cveTicket = new PluginCveTicket(); + $cveTicketData = [ + 'cves_id' => $cves_id, + 'tickets_id' => $tickets_id, + 'creation_type' => 'MANUAL', // Or AUTO if created by rules + 'date_creation' => $_SESSION['glpi_currenttime'] + ]; + + $cveTicket->add($cveTicketData); + + // Update CVE status to ASSIGNED if it was NEW + if ($cve->fields['status'] == 'NEW') { + $cve->update([ + 'id' => $cves_id, + 'status' => 'ASSIGNED' + ]); + } + + return $tickets_id; + } + + return false; + } + + /** + * Convert CVSS score to GLPI priority + * + * @param float $cvss_score CVSS Score + * + * @return integer GLPI priority + */ + private function getCVSStoPriority($cvss_score) { + // Convert CVSS score to GLPI priority (1-5) + if ($cvss_score >= 9) { + return 5; // Very high + } else if ($cvss_score >= 7) { + return 4; // High + } else if ($cvss_score >= 4) { + return 3; // Medium + } else if ($cvss_score >= 1) { + return 2; // Low + } else { + return 1; // Very low + } + } + + /** + * Get the severity class for display + * + * @param string $severity Severity level + * + * @return string CSS class + */ + static function getSeverityClass($severity) { + switch ($severity) { + case 'CRITICAL': + return 'cve-severity-critical'; + case 'HIGH': + return 'cve-severity-high'; + case 'MEDIUM': + return 'cve-severity-medium'; + case 'LOW': + return 'cve-severity-low'; + default: + return ''; + } + } + + /** + * Get the status class for display + * + * @param string $status Status value + * + * @return string CSS class + */ + static function getStatusClass($status) { + switch ($status) { + case 'NEW': + return 'cve-status-new'; + case 'ANALYZED': + return 'cve-status-analyzed'; + case 'ASSIGNED': + return 'cve-status-assigned'; + case 'RESOLVED': + return 'cve-status-resolved'; + default: + return ''; + } + } + + /** + * Get dashboard statistics data + * + * @return array Dashboard data + */ + static function getCVEStatsDashboard() { + global $DB; + + $stats = []; + + // Count by severity + $query = "SELECT severity, COUNT(*) as count + FROM `" . self::getTable() . "` + GROUP BY severity"; + + $result = $DB->query($query); + + $stats['severity'] = [ + 'CRITICAL' => 0, + 'HIGH' => 0, + 'MEDIUM' => 0, + 'LOW' => 0 + ]; + + if ($result) { + while ($data = $DB->fetchAssoc($result)) { + $stats['severity'][$data['severity']] = $data['count']; + } + } + + // Count by status + $query = "SELECT status, COUNT(*) as count + FROM `" . self::getTable() . "` + GROUP BY status"; + + $result = $DB->query($query); + + $stats['status'] = [ + 'NEW' => 0, + 'ANALYZED' => 0, + 'ASSIGNED' => 0, + 'RESOLVED' => 0 + ]; + + if ($result) { + while ($data = $DB->fetchAssoc($result)) { + $stats['status'][$data['status']] = $data['count']; + } + } + + // Get recent CVEs (last 30 days) + $query = "SELECT COUNT(*) as count + FROM `" . self::getTable() . "` + WHERE date_creation > DATE_SUB(NOW(), INTERVAL 30 DAY)"; + + $result = $DB->query($query); + + $stats['recent'] = 0; + + if ($result && $data = $DB->fetchAssoc($result)) { + $stats['recent'] = $data['count']; + } + + return $stats; + } + + /** + * Get severity distribution for dashboard + * + * @return array Dashboard data + */ + static function getCVESeverityDashboard() { + global $DB; + + $data = []; + + // Count by severity + $query = "SELECT severity, COUNT(*) as count + FROM `" . self::getTable() . "` + GROUP BY severity"; + + $result = $DB->query($query); + + $labels = [ + 'CRITICAL' => __('Critical', 'cve'), + 'HIGH' => __('High', 'cve'), + 'MEDIUM' => __('Medium', 'cve'), + 'LOW' => __('Low', 'cve') + ]; + + $colors = [ + 'CRITICAL' => '#d32f2f', + 'HIGH' => '#f57c00', + 'MEDIUM' => '#fbc02d', + 'LOW' => '#2196f3' + ]; + + if ($result) { + $series = []; + $series_labels = []; + + while ($row = $DB->fetchAssoc($result)) { + $series[] = [ + 'name' => $labels[$row['severity']] ?? $row['severity'], + 'data' => [(int)$row['count']], + 'color' => $colors[$row['severity']] ?? '#999999' + ]; + $series_labels[] = $labels[$row['severity']] ?? $row['severity']; + } + + $data = [ + 'labels' => $series_labels, + 'series' => $series + ]; + } + + return $data; + } + + /** + * Get recent CVEs for dashboard + * + * @return array Dashboard data + */ + static function getRecentCVEsDashboard() { + global $DB; + + $data = []; + + // Get recent CVEs + $query = "SELECT id, cve_id, severity, cvss_score, published_date, status + FROM `" . self::getTable() . "` + ORDER BY date_creation DESC + LIMIT 10"; + + $result = $DB->query($query); + + if ($result) { + $data['headers'] = [ + __('CVE ID', 'cve'), + __('Severity', 'cve'), + __('CVSS', 'cve'), + __('Published', 'cve'), + __('Status', 'cve') + ]; + + $data['rows'] = []; + + while ($row = $DB->fetchAssoc($result)) { + $data['rows'][] = [ + 'cve_id' => $row['cve_id'], + 'severity' => $row['severity'], + 'cvss_score' => $row['cvss_score'], + 'published' => $row['published_date'], + 'status' => $row['status'] + ]; + } + } + + return $data; + } + + /** + * Cron task for cleaning old CVEs + * + * @param CronTask $task CronTask object + * @return integer + */ + static function cronCleanOldCVEs($task) { + global $DB; + + // Default to cleaning CVEs older than 1 year + $retention_days = 365; + + // Get retention configuration if exists + $config = new Config(); + $config->getFromDBByCrit(['context' => 'plugin:cve', 'name' => 'retention_days']); + + if (isset($config->fields['value'])) { + $retention_days = (int)$config->fields['value']; + } + + // Only clean resolved CVEs + $query = "DELETE FROM `" . self::getTable() . "` + WHERE `status` = 'RESOLVED' + AND `date_mod` < DATE_SUB(NOW(), INTERVAL $retention_days DAY)"; + + $result = $DB->query($query); + + if ($result) { + $affected = $DB->affectedRows(); + $task->addVolume($affected); + Toolbox::logInFile('cve_plugin', "Cleaned $affected old resolved CVEs older than $retention_days days"); + + return ($affected > 0) ? 1 : 0; + } + + return 0; + } + + /** + * Install the plugin database schema + * + * @return boolean + */ + static function install(Migration $migration) { + global $DB; + + $table = self::getTable(); + + if (!$DB->tableExists($table)) { + $migration->displayMessage("Installing $table"); + + $query = "CREATE TABLE IF NOT EXISTS `$table` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `cve_id` varchar(20) NOT NULL, + `description` text DEFAULT NULL, + `cvss_score` decimal(3,1) DEFAULT NULL, + `cvss_vector` varchar(100) DEFAULT NULL, + `severity` enum('LOW','MEDIUM','HIGH','CRITICAL') DEFAULT NULL, + `published_date` datetime DEFAULT NULL, + `modified_date` datetime DEFAULT NULL, + `status` enum('NEW','ANALYZED','ASSIGNED','RESOLVED') DEFAULT 'NEW', + `references` text DEFAULT NULL, + `affected_products` text DEFAULT NULL, + `entities_id` int(11) NOT NULL DEFAULT '0', + `is_recursive` tinyint(1) NOT NULL DEFAULT '0', + `date_creation` datetime DEFAULT NULL, + `date_mod` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `cve_id` (`cve_id`), + KEY `severity` (`severity`), + KEY `status` (`status`), + KEY `published_date` (`published_date`), + KEY `cvss_score` (`cvss_score`), + KEY `entities_id` (`entities_id`), + KEY `date_creation` (`date_creation`), + KEY `date_mod` (`date_mod`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci"; + + $DB->query($query) or die("Error creating $table " . $DB->error()); + } + + return true; + } + + /** + * Uninstall the plugin database schema + * + * @return boolean + */ + static function uninstall(Migration $migration) { + global $DB; + + $table = self::getTable(); + + if ($DB->tableExists($table)) { + $migration->displayMessage("Uninstalling $table"); + $migration->dropTable($table); + } + + return true; + } +} \ No newline at end of file diff --git a/inc/cvealert.class.php b/inc/cvealert.class.php new file mode 100644 index 0000000..53a1238 --- /dev/null +++ b/inc/cvealert.class.php @@ -0,0 +1,414 @@ +addDefaultFormTab($tabs); + $this->addStandardTab('Log', $tabs, $options); + + return $tabs; + } + + /** + * Display the CVE Alert form + * + * @param integer $ID ID of the item + * @param array $options + * @return boolean + */ + function showForm($ID, $options = []) { + global $CFG_GLPI; + + $this->initForm($ID, $options); + $this->showFormHeader($options); + + $canedit = $this->can($ID, UPDATE); + + echo ""; + + // Software + echo "" . __('Software', 'cve') . ""; + echo ""; + if ($this->fields['softwares_id'] > 0) { + $software = new Software(); + if ($software->getFromDB($this->fields['softwares_id'])) { + echo $software->getLink(); + } else { + echo __('Unknown software', 'cve'); + } + } else { + echo __('N/A', 'cve'); + } + echo ""; + + // Version + echo "" . __('Version', 'cve') . ""; + echo ""; + if ($this->fields['softwareversions_id'] > 0) { + $version = new SoftwareVersion(); + if ($version->getFromDB($this->fields['softwareversions_id'])) { + echo $version->getName(); + } else { + echo __('Unknown version', 'cve'); + } + } else { + echo __('N/A', 'cve'); + } + echo ""; + + echo ""; + + echo ""; + + // CVE + echo "" . __('CVE', 'cve') . ""; + echo ""; + if ($this->fields['cves_id'] > 0) { + $cve = new PluginCveCve(); + if ($cve->getFromDB($this->fields['cves_id'])) { + echo ""; + echo $cve->fields['cve_id']; + echo ""; + } else { + echo __('Unknown CVE', 'cve'); + } + } else { + echo __('N/A', 'cve'); + } + echo ""; + + // Severity + echo "" . __('Severity', 'cve') . ""; + echo ""; + echo ""; + echo $this->fields['severity']; + echo ""; + echo ""; + + echo ""; + + echo ""; + + // Status + echo "" . __('Status', 'cve') . ""; + echo ""; + if ($canedit) { + $status_options = [ + 'NEW' => __('New', 'cve'), + 'PROCESSED' => __('Processed', 'cve'), + 'IGNORED' => __('Ignored', 'cve') + ]; + Dropdown::showFromArray('status', $status_options, + ['value' => $this->fields['status']]); + } else { + echo $this->fields['status']; + } + echo ""; + + // Associated ticket + echo "" . __('Ticket', 'cve') . ""; + echo ""; + if ($this->fields['tickets_id'] > 0) { + $ticket = new Ticket(); + if ($ticket->getFromDB($this->fields['tickets_id'])) { + echo ""; + echo $ticket->fields['name'] . " (" . $this->fields['tickets_id'] . ")"; + echo ""; + } else { + echo __('Unknown ticket', 'cve'); + } + } else { + if ($canedit && $this->fields['status'] == 'NEW') { + echo ""; + } else { + echo __('No ticket associated', 'cve'); + } + } + echo ""; + + echo ""; + + // Add entity dropdown if needed + echo ""; + echo "" . __('Entity', 'cve') . ""; + echo ""; + echo Dropdown::getDropdownName('glpi_entities', $this->fields['entities_id']); + echo ""; + + // Creation date + echo "" . __('Creation Date', 'cve') . ""; + echo "" . Html::convDateTime($this->fields['date_creation']) . ""; + echo ""; + + $this->showFormButtons($options); + + return true; + } + + /** + * Actions done after the PURGE of the item in the database + * + * @return void + */ + function post_purgeItem() { + // Nothing special + } + + /** + * Actions done after the UPDATE of the item in the database + * + * @param integer $history store changes history ? + * @return void + */ + function post_updateItem($history = 1) { + // If status changed to IGNORED, we might want to do something + if (in_array('status', $this->updates) && $this->fields['status'] == 'IGNORED') { + // Add a record to ignore this specific software-CVE combination in the future + // This would be implemented here + } + } + + /** + * Get search function for the class + * + * @return array of search options + */ + function rawSearchOptions() { + $tab = []; + + $tab[] = [ + 'id' => 'common', + 'name' => self::getTypeName(2) + ]; + + $tab[] = [ + 'id' => '1', + 'table' => $this->getTable(), + 'field' => 'id', + 'name' => __('ID', 'cve'), + 'massiveaction' => false, + 'datatype' => 'number' + ]; + + $tab[] = [ + 'id' => '2', + 'table' => 'glpi_softwares', + 'field' => 'name', + 'name' => __('Software', 'cve'), + 'massiveaction' => false, + 'datatype' => 'dropdown', + 'joinparams' => [ + 'jointype' => 'child', + 'condition' => 'AND NEWTABLE.`id` = REFTABLE.`softwares_id`' + ] + ]; + + $tab[] = [ + 'id' => '3', + 'table' => 'glpi_softwareversions', + 'field' => 'name', + 'name' => __('Version', 'cve'), + 'massiveaction' => false, + 'datatype' => 'dropdown', + 'joinparams' => [ + 'jointype' => 'child', + 'condition' => 'AND NEWTABLE.`id` = REFTABLE.`softwareversions_id`' + ] + ]; + + $tab[] = [ + 'id' => '4', + 'table' => 'glpi_plugin_cve_cves', + 'field' => 'cve_id', + 'name' => __('CVE ID', 'cve'), + 'massiveaction' => false, + 'datatype' => 'dropdown', + 'joinparams' => [ + 'jointype' => 'child', + 'condition' => 'AND NEWTABLE.`id` = REFTABLE.`cves_id`' + ] + ]; + + $tab[] = [ + 'id' => '5', + 'table' => $this->getTable(), + 'field' => 'severity', + 'name' => __('Severity', 'cve'), + 'datatype' => 'specific', + 'searchtype' => ['equals', 'notequals'] + ]; + + $tab[] = [ + 'id' => '6', + 'table' => $this->getTable(), + 'field' => 'status', + 'name' => __('Status', 'cve'), + 'datatype' => 'specific', + 'searchtype' => ['equals', 'notequals'] + ]; + + $tab[] = [ + 'id' => '7', + 'table' => 'glpi_tickets', + 'field' => 'name', + 'name' => __('Ticket', 'cve'), + 'massiveaction' => false, + 'datatype' => 'dropdown', + 'joinparams' => [ + 'jointype' => 'child', + 'condition' => 'AND NEWTABLE.`id` = REFTABLE.`tickets_id`' + ] + ]; + + $tab[] = [ + 'id' => '8', + 'table' => 'glpi_entities', + 'field' => 'completename', + 'name' => __('Entity', 'cve'), + 'massiveaction' => false, + 'datatype' => 'dropdown' + ]; + + $tab[] = [ + 'id' => '9', + 'table' => $this->getTable(), + 'field' => 'date_creation', + 'name' => __('Creation date', 'cve'), + 'datatype' => 'datetime', + 'massiveaction' => false + ]; + + $tab[] = [ + 'id' => '10', + 'table' => $this->getTable(), + 'field' => 'date_mod', + 'name' => __('Last update', 'cve'), + 'datatype' => 'datetime', + 'massiveaction' => false + ]; + + return $tab; + } + + /** + * Get dashboard count of alerts by severity + * + * @return array + */ + static function getAlertStats() { + global $DB; + + $stats = []; + + // Count by severity + $query = "SELECT severity, status, COUNT(*) as count + FROM `" . self::getTable() . "` + GROUP BY severity, status"; + + $result = $DB->query($query); + + $stats['by_severity'] = [ + 'CRITICAL' => 0, + 'HIGH' => 0, + 'MEDIUM' => 0, + 'LOW' => 0 + ]; + + $stats['by_status'] = [ + 'NEW' => 0, + 'PROCESSED' => 0, + 'IGNORED' => 0 + ]; + + if ($result) { + while ($data = $DB->fetchAssoc($result)) { + // Add to severity stats + if (isset($stats['by_severity'][$data['severity']])) { + $stats['by_severity'][$data['severity']] += $data['count']; + } + + // Add to status stats + if (isset($stats['by_status'][$data['status']])) { + $stats['by_status'][$data['status']] += $data['count']; + } + } + } + + // Get total count + $query = "SELECT COUNT(*) as total FROM `" . self::getTable() . "`"; + $result = $DB->query($query); + + $stats['total'] = 0; + if ($result && $data = $DB->fetchAssoc($result)) { + $stats['total'] = $data['total']; + } + + return $stats; + } + + /** + * Get alerts for dashboard + * + * @param integer $limit Maximum number of alerts to return + * @return array + */ + static function getRecentAlerts($limit = 10) { + global $DB; + + $alerts = []; + + $query = "SELECT a.*, + c.cve_id, + c.severity AS cve_severity, + s.name AS software_name, + v.name AS version_name + FROM `" . self::getTable() . "` AS a + LEFT JOIN `glpi_plugin_cve_cves` AS c ON c.id = a.cves_id + LEFT JOIN `glpi_softwares` AS s ON s.id = a.softwares_id + LEFT JOIN `glpi_softwareversions` AS v ON v.id = a.softwareversions_id + ORDER BY a.date_creation DESC + LIMIT $limit"; + + $result = $DB->query($query); + + if ($result) { + while ($data = $DB->fetchAssoc($result)) { + $alerts[] = $data; + } + } + + return $alerts; + } +} \ No newline at end of file diff --git a/inc/cveinventory.class.php b/inc/cveinventory.class.php new file mode 100644 index 0000000..6041b99 --- /dev/null +++ b/inc/cveinventory.class.php @@ -0,0 +1,358 @@ +log("Starting software vulnerability analysis"); + $task->setVolume(0); + + // Get all active entities + $entity = new Entity(); + $entities = $entity->find(['is_recursive' => 1]); + $entity_ids = array_column($entities, 'id'); + + // Get all software from inventory + $software = new Software(); + $software_versions = new SoftwareVersion(); + + $matched_count = 0; + $alert_count = 0; + + // For each entity, process software inventory + foreach ($entity_ids as $entity_id) { + $task->log("Processing entity ID: $entity_id"); + + // Get software in this entity + $software_list = $software->find(['entities_id' => $entity_id]); + + foreach ($software_list as $sw) { + // Get versions of this software + $versions = $software_versions->find(['softwares_id' => $sw['id']]); + + foreach ($versions as $version) { + // Search for vulnerabilities for this software/version + $vulnerabilities = self::findVulnerabilities($sw['name'], $version['name']); + + if (!empty($vulnerabilities)) { + $matched_count += count($vulnerabilities); + + // Process each vulnerability + foreach ($vulnerabilities as $cve_id) { + // Create alert if it doesn't already exist + if (self::createAlert($sw['id'], $version['id'], $cve_id, $entity_id)) { + $alert_count++; + } + } + } + } + } + } + + $task->setVolume($matched_count); + $task->log("Analysis completed. Found $matched_count potential vulnerabilities, created $alert_count new alerts"); + + return ($alert_count > 0) ? 1 : 0; + } + + /** + * Find vulnerabilities for a given software and version + * + * @param string $software_name Software name + * @param string $version_name Version string + * @return array Array of matching CVE IDs + */ + private static function findVulnerabilities($software_name, $version_name) { + global $DB; + + $matches = []; + + // Normalize software name for better matching + $normalized_name = strtolower(trim($software_name)); + $normalized_version = trim($version_name); + + // Search in affected_products field of CVEs + $query = "SELECT id, cve_id, affected_products + FROM `glpi_plugin_cve_cves` + WHERE `status` != 'RESOLVED'"; + + $result = $DB->query($query); + + if ($result) { + while ($row = $DB->fetchAssoc($result)) { + $affected_products = json_decode($row['affected_products'], true) ?: []; + + foreach ($affected_products as $product) { + // Simple matching for demonstration + // In a real implementation, this would use CPE matching or more sophisticated algorithms + if (self::matchesSoftware($normalized_name, $normalized_version, $product)) { + $matches[] = $row['id']; + break; // Found a match for this CVE + } + } + } + } + + return $matches; + } + + /** + * Check if software and version match an affected product string + * + * @param string $name Normalized software name + * @param string $version Normalized version + * @param string $product Affected product string + * @return boolean True if matches + */ + private static function matchesSoftware($name, $version, $product) { + // Normalize product string + $product = strtolower(trim($product)); + + // Check if product string contains both software name and version + // This is a simple implementation and would need to be more sophisticated in real use + if (strpos($product, $name) !== false) { + // If version is part of an affected range + if (strpos($product, ' < ' . $version) !== false || + strpos($product, '<=' . $version) !== false || + strpos($product, $version) !== false) { + return true; + } + } + + return false; + } + + /** + * Create a vulnerability alert + * + * @param integer $software_id Software ID + * @param integer $version_id Version ID + * @param integer $cve_id CVE ID + * @param integer $entity_id Entity ID + * @return boolean True if new alert was created + */ + private static function createAlert($software_id, $version_id, $cve_id, $entity_id) { + global $DB; + + // Check if alert already exists + $query = "SELECT id FROM `glpi_plugin_cve_alerts` + WHERE `softwares_id` = $software_id + AND `softwareversions_id` = $version_id + AND `cves_id` = $cve_id"; + + $result = $DB->query($query); + + if ($result && $DB->numrows($result) > 0) { + // Alert already exists + return false; + } + + // Get CVE details + $cve = new PluginCveCve(); + $cve->getFromDB($cve_id); + + // Create new alert + $alert = new PluginCveCveAlert(); + $alert_id = $alert->add([ + 'softwares_id' => $software_id, + 'softwareversions_id' => $version_id, + 'cves_id' => $cve_id, + 'entities_id' => $entity_id, + 'status' => 'NEW', + 'severity' => $cve->fields['severity'], + 'date_creation' => $_SESSION['glpi_currenttime'] + ]); + + if ($alert_id) { + // Process the alert according to rules + self::processAlert($alert_id); + return true; + } + + return false; + } + + /** + * Process a vulnerability alert based on rules + * + * @param integer $alert_id Alert ID + * @return boolean Success + */ + private static function processAlert($alert_id) { + // Get alert details + $alert = new PluginCveCveAlert(); + if (!$alert->getFromDB($alert_id)) { + return false; + } + + // Get associated CVE + $cve = new PluginCveCve(); + if (!$cve->getFromDB($alert->fields['cves_id'])) { + return false; + } + + // Get software details + $software = new Software(); + if (!$software->getFromDB($alert->fields['softwares_id'])) { + return false; + } + + $version = new SoftwareVersion(); + if (!$version->getFromDB($alert->fields['softwareversions_id'])) { + return false; + } + + // Apply rules based on severity + if ($alert->fields['severity'] == 'CRITICAL' || $alert->fields['severity'] == 'HIGH') { + // Create a ticket for high/critical vulnerabilities + $ticket = new Ticket(); + + $content = __('A vulnerability has been detected in your software inventory', 'cve') . "\n\n"; + $content .= __('Software', 'cve') . ': ' . $software->fields['name'] . ' ' . $version->fields['name'] . "\n"; + $content .= __('CVE', 'cve') . ': ' . $cve->fields['cve_id'] . "\n"; + $content .= __('Severity', 'cve') . ': ' . $cve->fields['severity'] . "\n"; + $content .= __('CVSS Score', 'cve') . ': ' . $cve->fields['cvss_score'] . "\n\n"; + $content .= __('Description', 'cve') . ":\n" . $cve->fields['description'] . "\n\n"; + + $affected_products = json_decode($cve->fields['affected_products'], true) ?: []; + if (!empty($affected_products)) { + $content .= __('Affected Products', 'cve') . ":\n" . implode("\n", $affected_products) . "\n\n"; + } + + $references = json_decode($cve->fields['references'], true) ?: []; + if (!empty($references)) { + $content .= __('References', 'cve') . ":\n" . implode("\n", $references) . "\n"; + } + + $ticket_id = $ticket->add([ + 'name' => __('Vulnerability', 'cve') . ' ' . $cve->fields['cve_id'] . ' - ' . $software->fields['name'], + 'content' => $content, + 'status' => Ticket::INCOMING, + 'priority' => ($alert->fields['severity'] == 'CRITICAL') ? 5 : 4, + 'urgency' => ($alert->fields['severity'] == 'CRITICAL') ? 5 : 4, + 'impact' => ($alert->fields['severity'] == 'CRITICAL') ? 5 : 4, + 'entities_id' => $alert->fields['entities_id'], + 'date' => $_SESSION['glpi_currenttime'], + 'itilcategories_id' => 0, // Default or security category if configured + 'type' => Ticket::INCIDENT_TYPE + ]); + + if ($ticket_id) { + // Link the CVE to the ticket + $cveTicket = new PluginCveCveTicket(); + $cveTicket->add([ + 'cves_id' => $alert->fields['cves_id'], + 'tickets_id' => $ticket_id, + 'creation_type' => 'AUTO', + 'date_creation' => $_SESSION['glpi_currenttime'] + ]); + + // Update alert status + $alert->update([ + 'id' => $alert_id, + 'status' => 'PROCESSED', + 'tickets_id' => $ticket_id + ]); + } + } else { + // For medium/low severity, just mark as processed without ticket + $alert->update([ + 'id' => $alert_id, + 'status' => 'PROCESSED' + ]); + } + + return true; + } + + /** + * Install the plugin database schema + * + * @return boolean + */ + static function install(Migration $migration) { + global $DB; + + $table = 'glpi_plugin_cve_alerts'; + + if (!$DB->tableExists($table)) { + $migration->displayMessage("Installing $table"); + + $query = "CREATE TABLE IF NOT EXISTS `$table` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `softwares_id` int(11) NOT NULL, + `softwareversions_id` int(11) NOT NULL, + `cves_id` int(11) NOT NULL, + `entities_id` int(11) NOT NULL DEFAULT '0', + `status` enum('NEW','PROCESSED','IGNORED') DEFAULT 'NEW', + `severity` enum('LOW','MEDIUM','HIGH','CRITICAL') DEFAULT NULL, + `tickets_id` int(11) DEFAULT NULL, + `date_creation` datetime DEFAULT NULL, + `date_mod` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `unique_alert` (`softwares_id`, `softwareversions_id`, `cves_id`), + KEY `softwares_id` (`softwares_id`), + KEY `softwareversions_id` (`softwareversions_id`), + KEY `cves_id` (`cves_id`), + KEY `entities_id` (`entities_id`), + KEY `status` (`status`), + KEY `severity` (`severity`), + KEY `tickets_id` (`tickets_id`), + KEY `date_creation` (`date_creation`), + KEY `date_mod` (`date_mod`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci"; + + $DB->query($query) or die("Error creating $table " . $DB->error()); + } + + return true; + } + + /** + * Uninstall the plugin database schema + * + * @return boolean + */ + static function uninstall(Migration $migration) { + global $DB; + + $table = 'glpi_plugin_cve_alerts'; + + if ($DB->tableExists($table)) { + $migration->displayMessage("Uninstalling $table"); + $migration->dropTable($table); + } + + return true; + } +} \ No newline at end of file diff --git a/inc/cverule.class.php b/inc/cverule.class.php new file mode 100644 index 0000000..2d6d7e9 --- /dev/null +++ b/inc/cverule.class.php @@ -0,0 +1,497 @@ +addDefaultFormTab($tabs); + $this->addStandardTab('Log', $tabs, $options); + + return $tabs; + } + + /** + * Display the CVE Rule form + * + * @param integer $ID ID of the item + * @param array $options + * @return boolean + */ + function showForm($ID, $options = []) { + global $CFG_GLPI; + + $this->initForm($ID, $options); + $this->showFormHeader($options); + + $canedit = $this->can($ID, UPDATE); + + echo ""; + + // Rule Name + echo "" . __('Rule Name', 'cve') . ""; + echo ""; + echo Html::input('name', ['value' => $this->fields['name'], 'size' => 40]); + echo ""; + + // Priority + echo "" . __('Priority', 'cve') . ""; + echo ""; + echo Html::input('priority', ['value' => $this->fields['priority'], 'size' => 5, 'type' => 'number', 'min' => 1]); + echo "
" . __('Lower numbers are processed first', 'cve') . ""; + echo ""; + + echo ""; + + echo ""; + + // Criteria - Severity + echo "" . __('Severity', 'cve') . ""; + echo ""; + $criteria = json_decode($this->fields['criteria'], true) ?: []; + $severity = $criteria['severity'] ?? 'CRITICAL'; + + $severity_options = [ + 'CRITICAL' => __('Critical', 'cve'), + 'HIGH' => __('High', 'cve'), + 'MEDIUM' => __('Medium', 'cve'), + 'LOW' => __('Low', 'cve') + ]; + Dropdown::showFromArray('criteria_severity', $severity_options, + ['value' => $severity]); + echo ""; + + // Status + echo "" . __('Status', 'cve') . ""; + echo ""; + $status_options = [ + 'NEW' => __('New', 'cve'), + 'ANALYZED' => __('Analyzed', 'cve'), + 'ASSIGNED' => __('Assigned', 'cve'), + 'RESOLVED' => __('Resolved', 'cve') + ]; + Dropdown::showFromArray('rule_status', $status_options, + ['value' => $this->fields['status'] ?? 'NEW']); + echo ""; + + echo ""; + + echo ""; + + // Actions - Create Ticket + echo "" . __('Create Ticket', 'cve') . ""; + echo ""; + $actions = json_decode($this->fields['actions'], true) ?: []; + $create_ticket = isset($actions['create_ticket']) ? $actions['create_ticket'] : true; + + Dropdown::showYesNo('actions_create_ticket', $create_ticket); + echo ""; + + // Ticket Priority + echo "" . __('Ticket Priority', 'cve') . ""; + echo ""; + $ticket_priority = $actions['ticket_priority'] ?? 'NORMAL'; + + $priority_options = [ + 'VERY HIGH' => __('Very High', 'cve'), + 'HIGH' => __('High', 'cve'), + 'NORMAL' => __('Normal', 'cve'), + 'LOW' => __('Low', 'cve') + ]; + Dropdown::showFromArray('actions_ticket_priority', $priority_options, + ['value' => $ticket_priority]); + echo ""; + + echo ""; + + echo ""; + + // Actions - Notify Admins + echo "" . __('Notify Administrators', 'cve') . ""; + echo ""; + $notify_admins = isset($actions['notify_admins']) ? $actions['notify_admins'] : false; + + Dropdown::showYesNo('actions_notify_admins', $notify_admins); + echo ""; + + // Actions - Add to Report + echo "" . __('Add to Vulnerability Report', 'cve') . ""; + echo ""; + $add_to_report = isset($actions['add_to_report']) ? $actions['add_to_report'] : false; + + Dropdown::showYesNo('actions_add_to_report', $add_to_report); + echo ""; + + echo ""; + + echo ""; + + // Is Active + echo "" . __('Active Rule', 'cve') . ""; + echo ""; + Dropdown::showYesNo('is_active', $this->fields['is_active']); + echo ""; + + echo ""; + + echo ""; + + $this->showFormButtons($options); + + return true; + } + + /** + * Process a CVE with rules + * + * @param PluginCveCve $cve CVE to process + * @return boolean True if any rule was applied + */ + static function processCVE(PluginCveCve $cve) { + $rule = new self(); + + // Get active rules sorted by priority + $rules = $rule->find(['is_active' => 1], ['priority' => 'ASC']); + + $rule_applied = false; + + foreach ($rules as $rule_data) { + // Load the rule + $rule->getFromDB($rule_data['id']); + + // Check if the CVE matches the criteria + if (self::matchesCriteria($cve, $rule_data)) { + // Apply the actions + self::applyActions($cve, $rule_data); + $rule_applied = true; + } + } + + return $rule_applied; + } + + /** + * Check if a CVE matches rule criteria + * + * @param PluginCveCve $cve CVE to check + * @param array $rule_data Rule data + * @return boolean True if the CVE matches the criteria + */ + private static function matchesCriteria(PluginCveCve $cve, array $rule_data) { + $criteria = json_decode($rule_data['criteria'], true) ?: []; + + // Check severity + if (isset($criteria['severity'])) { + // Get the numeric values for comparison + $severity_values = [ + 'CRITICAL' => 4, + 'HIGH' => 3, + 'MEDIUM' => 2, + 'LOW' => 1 + ]; + + $rule_severity = $severity_values[$criteria['severity']] ?? 0; + $cve_severity = $severity_values[$cve->fields['severity']] ?? 0; + + // Match if CVE severity is greater than or equal to rule severity + if ($cve_severity < $rule_severity) { + return false; + } + } + + // Check affected products if specified + if (isset($criteria['affected_products']) && !empty($criteria['affected_products'])) { + $cve_products = json_decode($cve->fields['affected_products'], true) ?: []; + $rule_products = $criteria['affected_products']; + + $found = false; + foreach ($rule_products as $product) { + if (in_array($product, $cve_products)) { + $found = true; + break; + } + } + + if (!$found) { + return false; + } + } + + // Check CVSS score range if specified + if (isset($criteria['cvss_min']) && $cve->fields['cvss_score'] < $criteria['cvss_min']) { + return false; + } + + if (isset($criteria['cvss_max']) && $cve->fields['cvss_score'] > $criteria['cvss_max']) { + return false; + } + + // All criteria matched or no criteria specified + return true; + } + + /** + * Apply rule actions to a CVE + * + * @param PluginCveCve $cve CVE to process + * @param array $rule_data Rule data + * @return boolean True if actions were applied successfully + */ + private static function applyActions(PluginCveCve $cve, array $rule_data) { + $actions = json_decode($rule_data['actions'], true) ?: []; + + // Create a ticket if needed + if (isset($actions['create_ticket']) && $actions['create_ticket']) { + $ticket_options = []; + + // Set ticket priority + if (isset($actions['ticket_priority'])) { + switch ($actions['ticket_priority']) { + case 'VERY HIGH': + $ticket_options['priority'] = 5; + break; + case 'HIGH': + $ticket_options['priority'] = 4; + break; + case 'NORMAL': + $ticket_options['priority'] = 3; + break; + case 'LOW': + $ticket_options['priority'] = 2; + break; + default: + $ticket_options['priority'] = 3; // Default to normal + } + } + + // Create the ticket + $cve->createTicket($cve->getID(), $ticket_options); + } + + // Send notifications if needed + if (isset($actions['notify_admins']) && $actions['notify_admins']) { + // This would implement the notification logic + // For example: NotificationEvent::raiseEvent('new_cve', $cve); + } + + // Add to report if needed + if (isset($actions['add_to_report']) && $actions['add_to_report']) { + // This would implement the reporting logic + } + + return true; + } + + /** + * Get search function for the class + * + * @return array of search options + */ + function rawSearchOptions() { + $tab = []; + + $tab[] = [ + 'id' => 'common', + 'name' => self::getTypeName(2) + ]; + + $tab[] = [ + 'id' => '1', + 'table' => $this->getTable(), + 'field' => 'name', + 'name' => __('Rule Name', 'cve'), + 'datatype' => 'itemlink', + 'massiveaction' => false + ]; + + $tab[] = [ + 'id' => '2', + 'table' => $this->getTable(), + 'field' => 'priority', + 'name' => __('Priority', 'cve'), + 'datatype' => 'number', + 'min' => 1, + 'massiveaction' => true + ]; + + $tab[] = [ + 'id' => '3', + 'table' => $this->getTable(), + 'field' => 'criteria', + 'name' => __('Criteria', 'cve'), + 'datatype' => 'text', + 'massiveaction' => false + ]; + + $tab[] = [ + 'id' => '4', + 'table' => $this->getTable(), + 'field' => 'actions', + 'name' => __('Actions', 'cve'), + 'datatype' => 'text', + 'massiveaction' => false + ]; + + $tab[] = [ + 'id' => '5', + 'table' => $this->getTable(), + 'field' => 'is_active', + 'name' => __('Active', 'cve'), + 'datatype' => 'bool', + 'massiveaction' => true + ]; + + $tab[] = [ + 'id' => '19', + 'table' => $this->getTable(), + 'field' => 'date_mod', + 'name' => __('Last update', 'cve'), + 'datatype' => 'datetime', + 'massiveaction' => false + ]; + + $tab[] = [ + 'id' => '16', + 'table' => $this->getTable(), + 'field' => 'date_creation', + 'name' => __('Creation date', 'cve'), + 'datatype' => 'datetime', + 'massiveaction' => false + ]; + + return $tab; + } + + /** + * Install the plugin database schema + * + * @return boolean + */ + static function install(Migration $migration) { + global $DB; + + $table = self::getTable(); + + if (!$DB->tableExists($table)) { + $migration->displayMessage("Installing $table"); + + $query = "CREATE TABLE IF NOT EXISTS `$table` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(100) NOT NULL, + `criteria` json DEFAULT NULL, + `actions` json DEFAULT NULL, + `priority` int(11) NOT NULL DEFAULT '1', + `is_active` tinyint(1) NOT NULL DEFAULT '0', + `status` varchar(20) DEFAULT 'NEW', + `date_creation` datetime DEFAULT NULL, + `date_mod` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `name` (`name`), + KEY `is_active` (`is_active`), + KEY `priority` (`priority`), + KEY `date_creation` (`date_creation`), + KEY `date_mod` (`date_mod`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci"; + + $DB->query($query) or die("Error creating $table " . $DB->error()); + + // Add default rules + $default_rules = [ + [ + 'name' => 'Critical Vulnerabilities - Immediate Ticket', + 'criteria' => json_encode(['severity' => 'CRITICAL']), + 'actions' => json_encode([ + 'create_ticket' => true, + 'ticket_priority' => 'VERY HIGH', + 'notify_admins' => true + ]), + 'priority' => 1, + 'is_active' => 1, + 'status' => 'NEW', + 'date_creation' => $_SESSION['glpi_currenttime'], + 'date_mod' => $_SESSION['glpi_currenttime'] + ], + [ + 'name' => 'High Risk Vulnerabilities - Create Ticket', + 'criteria' => json_encode(['severity' => 'HIGH']), + 'actions' => json_encode([ + 'create_ticket' => true, + 'ticket_priority' => 'HIGH', + 'notify_admins' => false + ]), + 'priority' => 2, + 'is_active' => 1, + 'status' => 'NEW', + 'date_creation' => $_SESSION['glpi_currenttime'], + 'date_mod' => $_SESSION['glpi_currenttime'] + ], + [ + 'name' => 'Medium Risk - Add to Report', + 'criteria' => json_encode(['severity' => 'MEDIUM']), + 'actions' => json_encode([ + 'create_ticket' => false, + 'add_to_report' => true + ]), + 'priority' => 3, + 'is_active' => 1, + 'status' => 'NEW', + 'date_creation' => $_SESSION['glpi_currenttime'], + 'date_mod' => $_SESSION['glpi_currenttime'] + ] + ]; + + $rule = new self(); + foreach ($default_rules as $rule_data) { + $rule->add($rule_data); + } + } + + return true; + } + + /** + * Uninstall the plugin database schema + * + * @return boolean + */ + static function uninstall(Migration $migration) { + global $DB; + + $table = self::getTable(); + + if ($DB->tableExists($table)) { + $migration->displayMessage("Uninstalling $table"); + $migration->dropTable($table); + } + + return true; + } +} \ No newline at end of file diff --git a/inc/cvesource.class.php b/inc/cvesource.class.php new file mode 100644 index 0000000..4efbbd5 --- /dev/null +++ b/inc/cvesource.class.php @@ -0,0 +1,1095 @@ +addDefaultFormTab($tabs); + $this->addStandardTab('Log', $tabs, $options); + + return $tabs; + } + + /** + * Display the CVE Source form + * + * @param integer $ID ID of the item + * @param array $options + * @return boolean + */ + function showForm($ID, $options = []) { + global $CFG_GLPI; + + $this->initForm($ID, $options); + $this->showFormHeader($options); + + $canedit = $this->can($ID, UPDATE); + + echo ""; + + // Source Name + echo "" . __('Source Name', 'cve') . ""; + echo ""; + echo Html::input('name', ['value' => $this->fields['name'], 'size' => 40]); + echo ""; + + // Active + echo "" . __('Active', 'cve') . ""; + echo ""; + Dropdown::showYesNo('is_active', $this->fields['is_active']); + echo ""; + + echo ""; + + echo ""; + + // API URL + echo "" . __('API URL', 'cve') . ""; + echo ""; + echo Html::input('url', ['value' => $this->fields['url'], 'size' => 60]); + echo ""; + + // Sync Frequency + echo "" . __('Sync Frequency (hours)', 'cve') . ""; + echo ""; + echo Html::input('sync_frequency', ['value' => $this->fields['sync_frequency'], 'type' => 'number', 'min' => 1, 'max' => 168]); + echo ""; + + echo ""; + + echo ""; + + // API Key + echo "" . __('API Key', 'cve') . ""; + echo ""; + echo Html::input('api_key', ['value' => $this->fields['api_key'], 'size' => 40, 'type' => 'password']); + echo "
" . __('Leave empty to keep current value', 'cve') . ""; + echo ""; + + // Last Sync + echo "" . __('Last Sync', 'cve') . ""; + echo ""; + if (empty($this->fields['last_sync'])) { + echo __('Never', 'cve'); + } else { + echo Html::convDateTime($this->fields['last_sync']); + } + echo ""; + + echo ""; + + echo ""; + + // Sync Status + echo "" . __('Sync Status', 'cve') . ""; + echo ""; + $status_options = [ + 'SUCCESS' => __('Success', 'cve'), + 'FAILED' => __('Failed', 'cve'), + 'IN_PROGRESS' => __('In Progress', 'cve'), + 'PENDING' => __('Pending', 'cve') + ]; + + $sync_status = $this->fields['sync_status'] ?? 'PENDING'; + + echo $status_options[$sync_status] ?? __('Unknown', 'cve'); + echo ""; + + // Source Type + echo "" . __('Source Type', 'cve') . ""; + echo ""; + $type_options = [ + 'NVD' => 'National Vulnerability Database (NVD)', + 'MITRE' => 'MITRE CVE Database', + 'CISA' => 'CISA Known Exploited Vulnerabilities (KEV)', + 'CUSTOM' => __('Custom', 'cve') + ]; + + Dropdown::showFromArray('source_type', $type_options, + ['value' => $this->fields['source_type'] ?? 'CUSTOM']); + echo ""; + + echo ""; + + echo ""; + + // Source Format + echo "" . __('Data Format', 'cve') . ""; + echo ""; + $format_options = [ + 'JSON' => 'JSON', + 'XML' => 'XML', + 'CSV' => 'CSV' + ]; + + Dropdown::showFromArray('data_format', $format_options, + ['value' => $this->fields['data_format'] ?? 'JSON']); + echo ""; + + // Description + echo "" . __('Description', 'cve') . ""; + echo ""; + echo ""; + echo ""; + + echo ""; + + $this->showFormButtons($options); + + // Add a Sync Now button if we're editing an existing source + if ($ID > 0 && $canedit) { + echo "
"; + echo ""; + echo Html::hidden('id', ['value' => $ID]); + Html::showSecurityToken(); + echo ""; + Html::closeForm(); + echo "
"; + } + + return true; + } + + /** + * Pre-process input data before adding + * + * @param array $input Input data + * @return array|false Processed input data or false on error + */ + function prepareInputForAdd($input) { + // Set creation date if not provided + if (!isset($input['date_creation'])) { + $input['date_creation'] = $_SESSION['glpi_currenttime']; + } + + // Set default values + if (!isset($input['is_active'])) { + $input['is_active'] = 0; + } + + if (!isset($input['sync_frequency'])) { + $input['sync_frequency'] = 24; // Default: once a day + } + + if (!isset($input['sync_status'])) { + $input['sync_status'] = 'PENDING'; + } + + // Validate required fields + if (empty($input['name'])) { + Session::addMessageAfterRedirect( + __('Source name is required', 'cve'), + true, + ERROR + ); + return false; + } + + if (empty($input['url'])) { + Session::addMessageAfterRedirect( + __('API URL is required', 'cve'), + true, + ERROR + ); + return false; + } + + // Validate URL format + if (!filter_var($input['url'], FILTER_VALIDATE_URL)) { + Session::addMessageAfterRedirect( + __('Invalid URL format', 'cve'), + true, + ERROR + ); + return false; + } + + return $input; + } + + /** + * Pre-process input data before updating + * + * @param array $input Input data + * @return array|false Processed input data or false on error + */ + function prepareInputForUpdate($input) { + // Set modification date + $input['date_mod'] = $_SESSION['glpi_currenttime']; + + // Don't overwrite API key if empty (keep the old one) + if (isset($input['api_key']) && empty($input['api_key'])) { + unset($input['api_key']); + } + + // Validate required fields if they're being updated + if (isset($input['name']) && empty($input['name'])) { + Session::addMessageAfterRedirect( + __('Source name is required', 'cve'), + true, + ERROR + ); + return false; + } + + if (isset($input['url']) && empty($input['url'])) { + Session::addMessageAfterRedirect( + __('API URL is required', 'cve'), + true, + ERROR + ); + return false; + } + + // Validate URL format if it's being updated + if (isset($input['url']) && !filter_var($input['url'], FILTER_VALIDATE_URL)) { + Session::addMessageAfterRedirect( + __('Invalid URL format', 'cve'), + true, + ERROR + ); + return false; + } + + return $input; + } + + /** + * Get search function for the class + * + * @return array of search options + */ + function rawSearchOptions() { + $tab = []; + + $tab[] = [ + 'id' => 'common', + 'name' => self::getTypeName(2) + ]; + + $tab[] = [ + 'id' => '1', + 'table' => $this->getTable(), + 'field' => 'name', + 'name' => __('Source Name', 'cve'), + 'datatype' => 'itemlink', + 'massiveaction' => false + ]; + + $tab[] = [ + 'id' => '2', + 'table' => $this->getTable(), + 'field' => 'url', + 'name' => __('API URL', 'cve'), + 'datatype' => 'string', + 'massiveaction' => false + ]; + + $tab[] = [ + 'id' => '3', + 'table' => $this->getTable(), + 'field' => 'is_active', + 'name' => __('Active', 'cve'), + 'datatype' => 'bool', + 'massiveaction' => true + ]; + + $tab[] = [ + 'id' => '4', + 'table' => $this->getTable(), + 'field' => 'sync_frequency', + 'name' => __('Sync Frequency (hours)', 'cve'), + 'datatype' => 'number', + 'min' => 1, + 'max' => 168, + 'step' => 1, + 'toadd' => [0 => __('Never', 'cve')], + 'massiveaction' => true + ]; + + $tab[] = [ + 'id' => '5', + 'table' => $this->getTable(), + 'field' => 'last_sync', + 'name' => __('Last Sync', 'cve'), + 'datatype' => 'datetime', + 'massiveaction' => false + ]; + + $tab[] = [ + 'id' => '6', + 'table' => $this->getTable(), + 'field' => 'sync_status', + 'name' => __('Sync Status', 'cve'), + 'datatype' => 'specific', + 'searchtype' => ['equals', 'notequals'] + ]; + + $tab[] = [ + 'id' => '7', + 'table' => $this->getTable(), + 'field' => 'source_type', + 'name' => __('Source Type', 'cve'), + 'datatype' => 'specific', + 'searchtype' => ['equals', 'notequals'] + ]; + + $tab[] = [ + 'id' => '19', + 'table' => $this->getTable(), + 'field' => 'date_mod', + 'name' => __('Last update', 'cve'), + 'datatype' => 'datetime', + 'massiveaction' => false + ]; + + $tab[] = [ + 'id' => '121', + 'table' => $this->getTable(), + 'field' => 'date_creation', + 'name' => __('Creation date', 'cve'), + 'datatype' => 'datetime', + 'massiveaction' => false + ]; + + return $tab; + } + + /** + * Synchronize CVE data from a specific source + * + * @param integer $source_id ID of the source to sync + * @return boolean Success status + */ + function syncNow($source_id) { + global $DB; + + // Get source information + $this->getFromDB($source_id); + + // Check if source is active + if (!$this->fields['is_active']) { + Toolbox::logInFile('cve_plugin', sprintf('Sync skipped for source %s (ID: %d) because it is inactive', + $this->fields['name'], $source_id)); + return false; + } + + // Update the sync status to in progress + $this->update([ + 'id' => $source_id, + 'sync_status' => 'IN_PROGRESS', + ]); + + try { + // Process based on source type + switch ($this->fields['source_type']) { + case 'NVD': + $success = $this->syncFromNVD(); + break; + case 'MITRE': + $success = $this->syncFromMITRE(); + break; + case 'CISA': + $success = $this->syncFromCISA(); + break; + case 'CUSTOM': + default: + $success = $this->syncFromCustomURL(); + break; + } + + // Update the sync status + $this->update([ + 'id' => $source_id, + 'sync_status' => $success ? 'SUCCESS' : 'FAILED', + 'last_sync' => $_SESSION['glpi_currenttime'] + ]); + + return $success; + } catch (Exception $e) { + // Log the error + Toolbox::logInFile('cve_plugin', sprintf('Error during sync of source %s (ID: %d): %s', + $this->fields['name'], $source_id, $e->getMessage()), true); + + // Update the sync status to failed + $this->update([ + 'id' => $source_id, + 'sync_status' => 'FAILED', + 'last_sync' => $_SESSION['glpi_currenttime'] + ]); + + return false; + } + } + + /** + * Sync from NVD API + * + * @return boolean Success + */ + private function syncFromNVD() { + // Example NVD API: https://services.nvd.nist.gov/rest/json/cves/2.0 + // Limited to demonstration - would need proper API key and pagination handling + $url = $this->fields['url']; + $api_key = $this->fields['api_key']; + + // Construct API request with API key if provided + $options = [ + 'http' => [ + 'method' => 'GET', + 'header' => [ + 'User-Agent: GLPI-CVE-Plugin/1.0', + ] + ] + ]; + + if (!empty($api_key)) { + $options['http']['header'][] = 'apiKey: ' . $api_key; + } + + $context = stream_context_create($options); + $result = file_get_contents($url, false, $context); + + if ($result === false) { + Toolbox::logInFile('cve_plugin', 'Failed to retrieve data from NVD API', true); + return false; + } + + // Parse JSON response + $data = json_decode($result, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + Toolbox::logInFile('cve_plugin', 'Error parsing JSON from NVD API: ' . json_last_error_msg(), true); + return false; + } + + // Process the results - this would be more complex in a real implementation + if (isset($data['vulnerabilities'])) { + return $this->processCVEData($data['vulnerabilities']); + } + + return false; + } + + /** + * Sync from MITRE API + * + * @return boolean Success + */ + private function syncFromMITRE() { + // Simplified for demonstration + $url = $this->fields['url']; + + $options = [ + 'http' => [ + 'method' => 'GET', + 'header' => [ + 'User-Agent: GLPI-CVE-Plugin/1.0', + 'Accept: application/json' + ] + ] + ]; + + $context = stream_context_create($options); + $result = file_get_contents($url, false, $context); + + if ($result === false) { + Toolbox::logInFile('cve_plugin', 'Failed to retrieve data from MITRE API', true); + return false; + } + + // Parse JSON response + $data = json_decode($result, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + Toolbox::logInFile('cve_plugin', 'Error parsing JSON from MITRE API: ' . json_last_error_msg(), true); + return false; + } + + // Process the results - format would depend on actual MITRE API structure + if (isset($data['cveItems'])) { + return $this->processCVEData($data['cveItems']); + } + + return false; + } + + /** + * Sync from CISA KEV Catalog + * + * @return boolean Success + */ + private function syncFromCISA() { + // Example for CISA Known Exploited Vulnerabilities catalog + $url = $this->fields['url']; + + $options = [ + 'http' => [ + 'method' => 'GET', + 'header' => 'User-Agent: GLPI-CVE-Plugin/1.0' + ] + ]; + + $context = stream_context_create($options); + $result = file_get_contents($url, false, $context); + + if ($result === false) { + Toolbox::logInFile('cve_plugin', 'Failed to retrieve data from CISA KEV Catalog', true); + return false; + } + + // Parse JSON response + $data = json_decode($result, true); + + if (json_last_error() !== JSON_ERROR_NONE) { + Toolbox::logInFile('cve_plugin', 'Error parsing JSON from CISA KEV Catalog: ' . json_last_error_msg(), true); + return false; + } + + // Process the results - format would depend on actual CISA API structure + if (isset($data['vulnerabilities'])) { + return $this->processCVEData($data['vulnerabilities'], true); // Mark as actively exploited + } + + return false; + } + + /** + * Sync from a custom URL + * + * @return boolean Success + */ + private function syncFromCustomURL() { + $url = $this->fields['url']; + $api_key = $this->fields['api_key']; + $format = $this->fields['data_format'] ?? 'JSON'; + + // Construct API request + $options = [ + 'http' => [ + 'method' => 'GET', + 'header' => 'User-Agent: GLPI-CVE-Plugin/1.0' + ] + ]; + + // Add API key if provided + if (!empty($api_key)) { + if (strpos($url, '?') !== false) { + // URL already has parameters + $url .= '&apiKey=' . urlencode($api_key); + } else { + // URL has no parameters yet + $url .= '?apiKey=' . urlencode($api_key); + } + } + + $context = stream_context_create($options); + $result = file_get_contents($url, false, $context); + + if ($result === false) { + Toolbox::logInFile('cve_plugin', 'Failed to retrieve data from custom URL: ' . $url, true); + return false; + } + + // Process based on format + switch (strtoupper($format)) { + case 'JSON': + $data = json_decode($result, true); + if (json_last_error() !== JSON_ERROR_NONE) { + Toolbox::logInFile('cve_plugin', 'Error parsing JSON from custom URL: ' . json_last_error_msg(), true); + return false; + } + break; + + case 'XML': + libxml_use_internal_errors(true); + $xml = simplexml_load_string($result); + if ($xml === false) { + $errors = libxml_get_errors(); + $error_msg = ''; + foreach ($errors as $error) { + $error_msg .= $error->message . "\n"; + } + libxml_clear_errors(); + Toolbox::logInFile('cve_plugin', 'Error parsing XML from custom URL: ' . $error_msg, true); + return false; + } + $data = json_decode(json_encode($xml), true); + break; + + case 'CSV': + // Simple CSV parsing - would need more robust handling in production + $lines = explode("\n", $result); + $headers = str_getcsv(array_shift($lines)); + $data = []; + foreach ($lines as $line) { + if (empty(trim($line))) continue; + $row = array_combine($headers, str_getcsv($line)); + if ($row) { + $data[] = $row; + } + } + break; + + default: + Toolbox::logInFile('cve_plugin', 'Unsupported format: ' . $format, true); + return false; + } + + // Process the data - this would need custom mapping based on the data structure + return $this->processCustomData($data); + } + + /** + * Process CVE data from standard sources + * + * @param array $vulnerabilities Array of vulnerability data + * @param bool $actively_exploited Whether these vulnerabilities are actively exploited + * @return boolean Success + */ + private function processCVEData($vulnerabilities, $actively_exploited = false) { + global $DB; + + // Initialize counters for logging + $count_added = 0; + $count_updated = 0; + + $cve = new PluginCveCve(); + + foreach ($vulnerabilities as $vuln) { + // Extract data based on source structure + // This is a simplified example - real implementation would need to handle various formats + + // Basic info we expect to find in most sources + $cve_id = $vuln['cveId'] ?? $vuln['id'] ?? $vuln['cve_id'] ?? null; + $description = $vuln['description'] ?? $vuln['summary'] ?? $vuln['desc'] ?? ''; + $cvss_score = $vuln['cvssScore'] ?? $vuln['impact'] ?? $vuln['baseScore'] ?? null; + $cvss_vector = $vuln['cvssVector'] ?? $vuln['attackVector'] ?? ''; + + // Try to determine severity if not directly provided + $severity = $vuln['severity'] ?? ''; + if (empty($severity) && !empty($cvss_score)) { + if ($cvss_score >= 9.0) $severity = 'CRITICAL'; + else if ($cvss_score >= 7.0) $severity = 'HIGH'; + else if ($cvss_score >= 4.0) $severity = 'MEDIUM'; + else $severity = 'LOW'; + } + + // Dates + $published_date = $vuln['publishedDate'] ?? $vuln['published'] ?? null; + $modified_date = $vuln['lastModifiedDate'] ?? $vuln['modified'] ?? null; + + // References and products + $references = $vuln['references'] ?? []; + $affected_products = $vuln['affectedProducts'] ?? $vuln['products'] ?? []; + + // Skip if we don't have a valid CVE ID + if (empty($cve_id)) { + continue; + } + + // Prepare the data + $cve_data = [ + 'cve_id' => $cve_id, + 'description' => $description, + 'cvss_score' => $cvss_score, + 'cvss_vector' => $cvss_vector, + 'severity' => $severity, + 'status' => 'NEW', // Default for newly imported CVEs + 'entities_id' => 0, // Root entity + 'is_recursive' => 1 // Available in all sub-entities + ]; + + // Add dates if available + if (!empty($published_date)) { + $cve_data['published_date'] = date('Y-m-d H:i:s', strtotime($published_date)); + } + + if (!empty($modified_date)) { + $cve_data['modified_date'] = date('Y-m-d H:i:s', strtotime($modified_date)); + } + + // Add references and affected products as JSON + if (!empty($references)) { + $cve_data['references'] = json_encode($references); + } + + if (!empty($affected_products)) { + $cve_data['affected_products'] = json_encode($affected_products); + } + + // Mark as actively exploited if from CISA KEV + if ($actively_exploited) { + $cve_data['status'] = 'ANALYZED'; // Increase priority + $cve_data['is_exploited'] = 1; + } + + // Check if this CVE already exists + $existing_id = $cve->getFromDBbyRequest([ + 'WHERE' => ['cve_id' => $cve_id] + ]); + + if ($existing_id) { + // Update existing CVE + $cve_data['id'] = $cve->getID(); + + // Only update certain fields, preserve status if already processed + unset($cve_data['status']); + + if ($cve->update($cve_data)) { + $count_updated++; + } + } else { + // Add new CVE + if ($cve->add($cve_data)) { + $count_added++; + + // Process the new CVE with rules + PluginCveCveRule::processCVE($cve); + } + } + } + + // Log results + Toolbox::logInFile('cve_plugin', sprintf('Source sync completed: %d CVEs added, %d CVEs updated', + $count_added, $count_updated)); + + return true; + } + + /** + * Process custom format data + * + * @param array $data Array of custom data + * @return boolean Success + */ + private function processCustomData($data) { + global $DB; + + // This is a placeholder for processing custom data formats + // Real implementation would need to map custom data structure to CVE fields + + Toolbox::logInFile('cve_plugin', 'Processing custom data format'); + + // Initialize counters for logging + $count_added = 0; + $count_updated = 0; + + $cve = new PluginCveCve(); + + // Example mapping function - would need customization per source + foreach ($data as $item) { + // Try to map fields based on common patterns + $cve_id = null; + + // Look for CVE ID pattern in various fields + foreach ($item as $key => $value) { + if (is_string($value) && preg_match('/CVE-\d{4}-\d{4,7}/', $value)) { + $cve_id = $value; + break; + } + + if (strtolower($key) === 'id' || strtolower($key) === 'cve_id' || strtolower($key) === 'vuln_id') { + if (is_string($value) && strpos($value, 'CVE-') === 0) { + $cve_id = $value; + break; + } + } + } + + // Skip if we couldn't find a CVE ID + if (empty($cve_id)) { + continue; + } + + // Try to map other fields + $description = $item['description'] ?? $item['summary'] ?? $item['details'] ?? ''; + $severity = $item['severity'] ?? $item['criticality'] ?? $item['risk'] ?? ''; + + // Prepare the data + $cve_data = [ + 'cve_id' => $cve_id, + 'description' => $description, + 'severity' => strtoupper($severity), + 'status' => 'NEW', + 'entities_id' => 0, // Root entity + 'is_recursive' => 1 // Available in all sub-entities + ]; + + // Additional processing would be done here + + // Check if this CVE already exists + $existing_id = $cve->getFromDBbyRequest([ + 'WHERE' => ['cve_id' => $cve_id] + ]); + + if ($existing_id) { + // Update existing CVE + $cve_data['id'] = $cve->getID(); + + // Only update certain fields, preserve status if already processed + unset($cve_data['status']); + + if ($cve->update($cve_data)) { + $count_updated++; + } + } else { + // Add new CVE + if ($cve->add($cve_data)) { + $count_added++; + + // Process the new CVE with rules + PluginCveCveRule::processCVE($cve); + } + } + } + + // Log results + Toolbox::logInFile('cve_plugin', sprintf('Custom source sync completed: %d CVEs added, %d CVEs updated', + $count_added, $count_updated)); + + return true; + } + + /** + * Cron task for syncing CVE sources + * + * @param CronTask $task CronTask object + * @return integer + */ + static function cronSyncSources($task) { + global $DB; + + $task->log("Starting CVE sources synchronization"); + $task->setVolume(0); + + $source = new self(); + + // Get all active sources due for sync + $query = "SELECT id FROM `" . self::getTable() . "` + WHERE `is_active` = 1 + AND ( + `last_sync` IS NULL + OR `last_sync` < DATE_SUB(NOW(), INTERVAL `sync_frequency` HOUR) + )"; + + $result = $DB->query($query); + + $success_count = 0; + $processed_count = 0; + + if ($result) { + while ($data = $DB->fetchAssoc($result)) { + $task->log("Processing source ID: " . $data['id']); + $processed_count++; + + if ($source->syncNow($data['id'])) { + $success_count++; + } + } + } + + $task->setVolume($processed_count); + + if ($processed_count > 0) { + $task->log("Synchronization completed: $success_count/$processed_count sources successfully synced"); + + if ($success_count > 0) { + // Trigger inventory analysis after successful sync + $inventory_task = new CronTask(); + if ($inventory_task->getFromDBbyName('PluginCveCveInventory', 'AnalyzeInventory')) { + $inventory_task->update([ + 'id' => $inventory_task->fields['id'], + 'state' => CronTask::STATE_WAITING + ]); + } + + return 1; // At least one source was successfully synced + } + } else { + $task->log("No sources to synchronize"); + } + + return 0; // No sources were successfully synced + } + + /** + * Dropdown options for source types + * + * @param array $options + * @return string + */ + static function getTypesDropdown($name = 'source_type', $options = []) { + $params['value'] = 0; + $params['toadd'] = []; + $params['width'] = '80%'; + + if (is_array($options) && count($options)) { + foreach ($options as $key => $val) { + $params[$key] = $val; + } + } + + $items = [ + 'NVD' => 'National Vulnerability Database (NVD)', + 'MITRE' => 'MITRE CVE Database', + 'CISA' => 'CISA Known Exploited Vulnerabilities (KEV)', + 'CUSTOM' => __('Custom', 'cve') + ]; + + return Dropdown::showFromArray($name, $items, $params); + } + + /** + * Check if user has the right to perform an action + * + * @param $action integer ID of the action + * @param $right string|integer Expected right [default READ] + * + * @return boolean + */ + static function canAction($action, $right = READ) { + return Session::haveRight(self::$rightname, $right); + } + + /** + * Add default CVE sources on plugin installation + * + * @return void + */ + static function addDefaultSources() { + $source = new self(); + + // Check if any sources already exist + if (countElementsInTable(self::getTable()) == 0) { + // Add NVD API + $source->add([ + 'name' => 'National Vulnerability Database (NVD)', + 'url' => 'https://services.nvd.nist.gov/rest/json/cves/2.0', + 'source_type' => 'NVD', + 'data_format' => 'JSON', + 'is_active' => 1, + 'sync_frequency' => 6, + 'description' => 'Official US government repository of standards-based vulnerability data', + 'date_creation' => $_SESSION['glpi_currenttime'] + ]); + + // Add MITRE CVE List + $source->add([ + 'name' => 'MITRE CVE Database', + 'url' => 'https://cveawg.mitre.org/api/cve', + 'source_type' => 'MITRE', + 'data_format' => 'JSON', + 'is_active' => 1, + 'sync_frequency' => 12, + 'description' => 'MITRE Corporation\'s list of Common Vulnerabilities and Exposures', + 'date_creation' => $_SESSION['glpi_currenttime'] + ]); + + // Add CISA KEV Catalog + $source->add([ + 'name' => 'CISA Known Exploited Vulnerabilities (KEV)', + 'url' => 'https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json', + 'source_type' => 'CISA', + 'data_format' => 'JSON', + 'is_active' => 1, + 'sync_frequency' => 24, + 'description' => 'U.S. CISA catalog of known exploited vulnerabilities', + 'date_creation' => $_SESSION['glpi_currenttime'] + ]); + } + } + + /** + * Install the plugin database schema + * + * @return boolean + */ + static function install(Migration $migration) { + global $DB; + + $table = self::getTable(); + + if (!$DB->tableExists($table)) { + $migration->displayMessage("Installing $table"); + + $query = "CREATE TABLE IF NOT EXISTS `$table` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `name` varchar(100) NOT NULL, + `url` varchar(255) NOT NULL, + `api_key` varchar(255) DEFAULT NULL, + `source_type` varchar(20) DEFAULT 'CUSTOM', + `data_format` varchar(20) DEFAULT 'JSON', + `is_active` tinyint(1) NOT NULL DEFAULT '0', + `sync_frequency` int(11) NOT NULL DEFAULT '24', + `last_sync` datetime DEFAULT NULL, + `sync_status` enum('SUCCESS','FAILED','IN_PROGRESS','PENDING') DEFAULT 'PENDING', + `description` text DEFAULT NULL, + `date_creation` datetime DEFAULT NULL, + `date_mod` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + KEY `name` (`name`), + KEY `is_active` (`is_active`), + KEY `source_type` (`source_type`), + KEY `last_sync` (`last_sync`), + KEY `sync_status` (`sync_status`), + KEY `date_creation` (`date_creation`), + KEY `date_mod` (`date_mod`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci"; + + $DB->query($query) or die("Error creating $table " . $DB->error()); + + // Add default sources + self::addDefaultSources(); + } + + return true; + } + + /** + * Uninstall the plugin database schema + * + * @return boolean + */ + static function uninstall(Migration $migration) { + global $DB; + + $table = self::getTable(); + + if ($DB->tableExists($table)) { + $migration->displayMessage("Uninstalling $table"); + $migration->dropTable($table); + } + + return true; + } +} \ No newline at end of file diff --git a/inc/cveticket.class.php b/inc/cveticket.class.php new file mode 100644 index 0000000..ef10c1e --- /dev/null +++ b/inc/cveticket.class.php @@ -0,0 +1,494 @@ + 'common', + 'name' => self::getTypeName(2) + ]; + + $tab[] = [ + 'id' => '1', + 'table' => $this->getTable(), + 'field' => 'id', + 'name' => __('ID', 'cve'), + 'massiveaction' => false, + 'datatype' => 'number' + ]; + + $tab[] = [ + 'id' => '2', + 'table' => 'glpi_plugin_cve_cves', + 'field' => 'cve_id', + 'name' => __('CVE ID', 'cve'), + 'massiveaction' => false, + 'datatype' => 'dropdown', + 'joinparams' => [ + 'jointype' => 'child', + 'condition' => 'AND NEWTABLE.`id` = REFTABLE.`cves_id`' + ] + ]; + + $tab[] = [ + 'id' => '3', + 'table' => 'glpi_tickets', + 'field' => 'name', + 'name' => __('Ticket', 'cve'), + 'massiveaction' => false, + 'datatype' => 'dropdown', + 'joinparams' => [ + 'jointype' => 'child', + 'condition' => 'AND NEWTABLE.`id` = REFTABLE.`tickets_id`' + ] + ]; + + $tab[] = [ + 'id' => '4', + 'table' => $this->getTable(), + 'field' => 'creation_type', + 'name' => __('Creation Type', 'cve'), + 'massiveaction' => false, + 'datatype' => 'specific', + 'searchtype' => ['equals', 'notequals'] + ]; + + $tab[] = [ + 'id' => '5', + 'table' => $this->getTable(), + 'field' => 'date_creation', + 'name' => __('Creation date', 'cve'), + 'datatype' => 'datetime', + 'massiveaction' => false + ]; + + return $tab; + } + + /** + * Show tickets for a CVE + * + * @param PluginCveCve $cve CVE object + * @return void + */ + static function showForCVE(PluginCveCve $cve) { + global $DB; + + $ID = $cve->getField('id'); + if (!$cve->can($ID, READ)) { + return false; + } + + $canedit = $cve->can($ID, UPDATE); + $rand = mt_rand(); + + $iterator = $DB->request([ + 'SELECT' => [ + 'glpi_plugin_cve_tickets.*', + 'glpi_tickets.name AS ticket_name', + 'glpi_tickets.status AS ticket_status', + 'glpi_tickets.date AS ticket_date', + 'glpi_tickets.priority AS ticket_priority' + ], + 'FROM' => 'glpi_plugin_cve_tickets', + 'LEFT JOIN' => [ + 'glpi_tickets' => [ + 'ON' => [ + 'glpi_plugin_cve_tickets' => 'tickets_id', + 'glpi_tickets' => 'id' + ] + ] + ], + 'WHERE' => [ + 'glpi_plugin_cve_tickets.cves_id' => $ID + ], + 'ORDER' => [ + 'glpi_tickets.date DESC' + ] + ]); + + $tickets = []; + $used = []; + + foreach ($iterator as $data) { + $tickets[$data['id']] = $data; + $used[$data['tickets_id']] = $data['tickets_id']; + } + + if ($canedit) { + echo "
"; + echo ""; + + echo ""; + echo ""; + + echo ""; + + echo "
" . __('Add a ticket', 'cve') . "
"; + echo ""; + Ticket::dropdown([ + 'used' => $used, + 'entity' => $cve->getEntityID(), + 'entity_sons' => $cve->isRecursive(), + 'displaywith' => ['id'] + ]); + echo ""; + echo ""; + echo "
"; + Html::closeForm(); + echo "
"; + } + + if ($canedit && count($tickets)) { + $massiveactionparams = [ + 'num_displayed' => min($_SESSION['glpilist_limit'], count($tickets)), + 'container' => 'mass' . __CLASS__ . $rand, + 'specific_actions' => [ + 'purge' => _x('button', 'Delete permanently') + ] + ]; + Html::showMassiveActions($massiveactionparams); + } + + echo ""; + $header_begin = ""; + $header_top = ''; + $header_bottom = ''; + $header_end = ''; + + if ($canedit && count($tickets)) { + $header_top .= ""; + $header_bottom .= ""; + } + + $header_end .= ""; + $header_end .= ""; + $header_end .= ""; + $header_end .= ""; + $header_end .= ""; + $header_end .= ""; + + echo $header_begin . $header_top . $header_end; + + foreach ($tickets as $data) { + echo ""; + + if ($canedit) { + echo ""; + } + + $ticket = new Ticket(); + $ticket->getFromDB($data['tickets_id']); + + echo ""; + + // Status + echo ""; + + // Priority + echo ""; + + // Date + echo ""; + + // Creation type + echo ""; + + echo ""; + } + + if ($header_bottom) { + echo $header_begin . $header_bottom . $header_end; + } + echo "
" . Html::getCheckAllAsCheckbox('mass' . __CLASS__ . $rand) . "" . Html::getCheckAllAsCheckbox('mass' . __CLASS__ . $rand) . "" . __('Ticket', 'cve') . "" . __('Status', 'cve') . "" . __('Priority', 'cve') . "" . __('Opening date', 'cve') . "" . __('Creation type', 'cve') . "
"; + Html::showMassiveActionCheckBox(__CLASS__, $data['id']); + echo ""; + if ($ticket->can($data['tickets_id'], READ)) { + echo ""; + echo $data['ticket_name'] . " (" . $data['tickets_id'] . ")"; + echo ""; + } else { + echo $data['ticket_name'] . " (" . $data['tickets_id'] . ")"; + } + echo ""; + echo Ticket::getStatus($data['ticket_status']); + echo ""; + echo Ticket::getPriorityName($data['ticket_priority']); + echo ""; + echo Html::convDateTime($data['ticket_date']); + echo ""; + echo $data['creation_type'] == 'AUTO' ? __('Automatic', 'cve') : __('Manual', 'cve'); + echo "
"; + + if ($canedit && count($tickets)) { + $massiveactionparams['ontop'] = false; + Html::showMassiveActions($massiveactionparams); + Html::closeForm(); + } + } + + /** + * Show CVEs for a ticket + * + * @param Ticket $ticket Ticket object + * @return void + */ + static function showForTicket(Ticket $ticket) { + global $DB; + + $ID = $ticket->getField('id'); + if (!$ticket->can($ID, READ)) { + return false; + } + + $canedit = $ticket->can($ID, UPDATE); + $rand = mt_rand(); + + $iterator = $DB->request([ + 'SELECT' => [ + 'glpi_plugin_cve_tickets.*', + 'glpi_plugin_cve_cves.cve_id', + 'glpi_plugin_cve_cves.severity', + 'glpi_plugin_cve_cves.cvss_score', + 'glpi_plugin_cve_cves.status AS cve_status' + ], + 'FROM' => 'glpi_plugin_cve_tickets', + 'LEFT JOIN' => [ + 'glpi_plugin_cve_cves' => [ + 'ON' => [ + 'glpi_plugin_cve_tickets' => 'cves_id', + 'glpi_plugin_cve_cves' => 'id' + ] + ] + ], + 'WHERE' => [ + 'glpi_plugin_cve_tickets.tickets_id' => $ID + ] + ]); + + $cvetickets = []; + $used = []; + + foreach ($iterator as $data) { + $cvetickets[$data['id']] = $data; + $used[$data['cves_id']] = $data['cves_id']; + } + + if ($canedit) { + echo "
"; + echo ""; + + echo ""; + echo ""; + + echo ""; + + echo "
" . __('Add a CVE', 'cve') . "
"; + echo ""; + + $cve = new PluginCveCve(); + $cve->dropdown([ + 'name' => 'cves_id', + 'entity' => $ticket->getEntityID(), + 'used' => $used + ]); + + echo ""; + echo ""; + echo "
"; + Html::closeForm(); + echo "
"; + } + + if ($canedit && count($cvetickets)) { + $massiveactionparams = [ + 'num_displayed' => min($_SESSION['glpilist_limit'], count($cvetickets)), + 'container' => 'mass' . __CLASS__ . $rand, + 'specific_actions' => [ + 'purge' => _x('button', 'Delete permanently') + ] + ]; + Html::showMassiveActions($massiveactionparams); + } + + echo ""; + $header_begin = ""; + $header_top = ''; + $header_bottom = ''; + $header_end = ''; + + if ($canedit && count($cvetickets)) { + $header_top .= ""; + $header_bottom .= ""; + } + + $header_end .= ""; + $header_end .= ""; + $header_end .= ""; + $header_end .= ""; + $header_end .= ""; + $header_end .= ""; + + echo $header_begin . $header_top . $header_end; + + foreach ($cvetickets as $data) { + echo ""; + + if ($canedit) { + echo ""; + } + + $cve = new PluginCveCve(); + $cve->getFromDB($data['cves_id']); + + echo ""; + + // Severity + echo ""; + + // CVSS Score + echo ""; + + // Status + echo ""; + + // Creation type + echo ""; + + echo ""; + } + + if ($header_bottom) { + echo $header_begin . $header_bottom . $header_end; + } + echo "
" . Html::getCheckAllAsCheckbox('mass' . __CLASS__ . $rand) . "" . Html::getCheckAllAsCheckbox('mass' . __CLASS__ . $rand) . "" . __('CVE ID', 'cve') . "" . __('Severity', 'cve') . "" . __('CVSS Score', 'cve') . "" . __('Status', 'cve') . "" . __('Creation Type', 'cve') . "
"; + Html::showMassiveActionCheckBox(__CLASS__, $data['id']); + echo ""; + if ($cve->can($data['cves_id'], READ)) { + echo ""; + echo $data['cve_id']; + echo ""; + } else { + echo $data['cve_id']; + } + echo ""; + echo ""; + echo $data['severity']; + echo ""; + echo ""; + echo $data['cvss_score']; + echo ""; + echo ""; + echo $data['cve_status']; + echo ""; + echo ""; + echo $data['creation_type'] == 'AUTO' ? __('Automatic', 'cve') : __('Manual', 'cve'); + echo "
"; + + if ($canedit && count($cvetickets)) { + $massiveactionparams['ontop'] = false; + Html::showMassiveActions($massiveactionparams); + Html::closeForm(); + } + } + + /** + * Add events to ticket notifications + * + * @param array $events Events array + * @return array Modified events array + */ + static function addEvents(&$events) { + $events['cve_added'] = __('CVE linked to ticket', 'cve'); + return $events; + } + + /** + * Install the plugin database schema + * + * @return boolean + */ + static function install(Migration $migration) { + global $DB; + + $table = self::getTable(); + + if (!$DB->tableExists($table)) { + $migration->displayMessage("Installing $table"); + + $query = "CREATE TABLE IF NOT EXISTS `$table` ( + `id` int(11) NOT NULL AUTO_INCREMENT, + `cves_id` int(11) NOT NULL, + `tickets_id` int(11) NOT NULL, + `creation_type` enum('AUTO','MANUAL') DEFAULT 'MANUAL', + `date_creation` datetime DEFAULT NULL, + PRIMARY KEY (`id`), + UNIQUE KEY `cves_id_tickets_id` (`cves_id`,`tickets_id`), + KEY `cves_id` (`cves_id`), + KEY `tickets_id` (`tickets_id`), + KEY `creation_type` (`creation_type`), + KEY `date_creation` (`date_creation`) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci"; + + $DB->query($query) or die("Error creating $table " . $DB->error()); + } + + return true; + } + + /** + * Uninstall the plugin database schema + * + * @return boolean + */ + static function uninstall(Migration $migration) { + global $DB; + + $table = self::getTable(); + + if ($DB->tableExists($table)) { + $migration->displayMessage("Uninstalling $table"); + $migration->dropTable($table); + } + + return true; + } +} \ No newline at end of file diff --git a/inc/define.php b/inc/define.php new file mode 100644 index 0000000..294ffa1 --- /dev/null +++ b/inc/define.php @@ -0,0 +1,78 @@ + 1) { + return $plural; + } + return $singular; +} + +/** + * Get the available languages for the plugin + * + * @return array + */ +function plugin_cve_getLanguages() { + return [ + 'en_GB' => 'English', + 'fr_FR' => 'Français', + 'de_DE' => 'Deutsch', + 'it_IT' => 'Italiano', + 'pl_PL' => 'Polski', + 'es_ES' => 'Español', + 'pt_PT' => 'Português' + ]; +} + +/** + * Initialize plugin localization + * + * @param string $hook_param Hook parameter + * @return void + */ +function plugin_cve_load_language($hook_param) { + global $CFG_GLPI; + + // Get user language + $user_language = Session::getLanguage(); + + // Path to plugin locales + $langpath = PLUGIN_CVE_DIR . '/locales/'; + + // Available languages for the plugin + $languages = plugin_cve_getLanguages(); + + // If user language is not available, fallback to English + if (!array_key_exists($user_language, $languages)) { + $user_language = 'en_GB'; + } + + // Load .mo file for the selected language + $mofile = $langpath . $user_language . '.mo'; + + // Check if the file exists + if (file_exists($mofile)) { + // Load the translation + load_plugin_textdomain('cve', false, $mofile); + } else { + // Try to fallback to English + $mofile = $langpath . 'en_GB.mo'; + if (file_exists($mofile)) { + load_plugin_textdomain('cve', false, $mofile); + } + } +} \ No newline at end of file diff --git a/inc/menu.class.php b/inc/menu.class.php new file mode 100644 index 0000000..29bb507 --- /dev/null +++ b/inc/menu.class.php @@ -0,0 +1,273 @@ + [ + 'title' => PluginCveCve::getTypeName(), + 'page' => '/plugins/cve/front/cve.php', + 'icon' => 'fas fa-shield-alt', + ], + 'cvesource' => [ + 'title' => PluginCveCveSource::getTypeName(), + 'page' => '/plugins/cve/front/cvesource.php', + 'icon' => 'fas fa-database', + ], + 'cverule' => [ + 'title' => PluginCveCveRule::getTypeName(), + 'page' => '/plugins/cve/front/cverule.php', + 'icon' => 'fas fa-cogs', + ] + ]; + + $menu['options']['dashboard'] = [ + 'title' => __('Dashboard', 'cve'), + 'page' => '/plugins/cve/front/dashboard.php', + 'icon' => 'fas fa-tachometer-alt', + ]; + + // Add inventory and alerts menu items + if (Session::haveRight('plugin_cve_inventory', READ)) { + $menu['options']['inventory'] = [ + 'title' => PluginCveCveInventory::getTypeName(), + 'page' => '/plugins/cve/front/inventory.php', + 'icon' => 'fas fa-laptop', + ]; + } + + if (Session::haveRight('plugin_cve_alert', READ)) { + $menu['options']['alert'] = [ + 'title' => PluginCveCveAlert::getTypeName(), + 'page' => '/plugins/cve/front/alert.php', + 'icon' => 'fas fa-exclamation-triangle', + ]; + } + } + + return $menu; + } + + /** + * Get main tabs + * + * @param array $options + * @return array + */ + function getTabNameForItem(CommonGLPI $item, $withtemplate = 0) { + if ($item->getType() == 'Ticket') { + if (PluginCveCve::canView()) { + return [1 => __('CVEs', 'cve')]; + } + } + + // Add tab to software + if ($item->getType() == 'Software' && Session::haveRight('plugin_cve_inventory', READ)) { + return [1 => __('Vulnerabilities', 'cve')]; + } + + return []; + } + + /** + * Display tabs content + * + * @param CommonGLPI $item + * @param int $tabnum + * @param int $withtemplate + * @return boolean + */ + static function displayTabContentForItem(CommonGLPI $item, $tabnum = 1, $withtemplate = 0) { + if ($item->getType() == 'Ticket') { + PluginCveCveTicket::showForTicket($item); + return true; + } + + if ($item->getType() == 'Software') { + self::showVulnerabilitiesForSoftware($item); + return true; + } + + return false; + } + + /** + * Show vulnerabilities for a software + * + * @param Software $software Software object + * @return void + */ + static function showVulnerabilitiesForSoftware(Software $software) { + global $DB; + + $ID = $software->getField('id'); + + echo "
"; + + // Get vulnerabilities for this software + $query = "SELECT a.*, + c.cve_id, + c.severity AS cve_severity, + c.cvss_score, + c.description, + v.name AS version_name + FROM `glpi_plugin_cve_alerts` AS a + LEFT JOIN `glpi_plugin_cve_cves` AS c ON c.id = a.cves_id + LEFT JOIN `glpi_softwareversions` AS v ON v.id = a.softwareversions_id + WHERE a.softwares_id = $ID + ORDER BY c.severity DESC, c.cvss_score DESC"; + + $result = $DB->query($query); + + if ($result && $DB->numrows($result) > 0) { + echo ""; + echo ""; + + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + echo ""; + + while ($data = $DB->fetchAssoc($result)) { + echo ""; + + // CVE ID + echo ""; + + // Version + echo ""; + + // Severity + echo ""; + + // CVSS Score + echo ""; + + // Description + echo ""; + + // Status + echo ""; + + echo ""; + } + + echo "
" . __('Vulnerabilities', 'cve') . "
" . __('CVE ID', 'cve') . "" . __('Version', 'cve') . "" . __('Severity', 'cve') . "" . __('CVSS Score', 'cve') . "" . __('Description', 'cve') . "" . __('Status', 'cve') . "
"; + echo ""; + echo $data['cve_id']; + echo ""; + echo ""; + echo $data['version_name']; + echo ""; + echo ""; + echo $data['severity']; + echo ""; + echo ""; + echo $data['cvss_score']; + echo ""; + echo Html::resume_text($data['description'], 100); + echo ""; + echo $data['status']; + if ($data['tickets_id'] > 0) { + echo " ("; + echo ""; + echo __('Ticket', 'cve') . " #" . $data['tickets_id']; + echo ""; + echo ")"; + } + echo "
"; + } else { + echo ""; + echo ""; + echo ""; + echo "
" . __('Vulnerabilities', 'cve') . "
" . __('No vulnerabilities found for this software', 'cve') . "
"; + } + + // Manual scan button + if (Session::haveRight("plugin_cve_inventory", UPDATE)) { + echo "
"; + echo ""; + echo ""; + Html::closeForm(); + echo "
"; + } + + echo "
"; + } +} \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..3ebee92 --- /dev/null +++ b/index.html @@ -0,0 +1,13 @@ + + + + + + + GLPI CVE Plugin Prototype + + +
+ + + \ No newline at end of file diff --git a/locales/de_DE.po b/locales/de_DE.po new file mode 100644 index 0000000..9963186 --- /dev/null +++ b/locales/de_DE.po @@ -0,0 +1,330 @@ +msgid "" +msgstr "" +"Project-Id-Version: GLPI CVE Plugin 1.0.0\n" +"POT-Creation-Date: 2024-01-15 12:00+0100\n" +"PO-Revision-Date: 2024-01-15 12:00+0100\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: de_DE\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Vulnérabilité" +msgstr "Schwachstelle" + +msgid "CVE" +msgid_plural "CVEs" +msgstr[0] "CVE" +msgstr[1] "CVEs" + +msgid "CVE ID" +msgstr "CVE ID" + +msgid "Severity" +msgstr "Schweregrad" + +msgid "Critical" +msgstr "Kritisch" + +msgid "High" +msgstr "Hoch" + +msgid "Medium" +msgstr "Mittel" + +msgid "Low" +msgstr "Niedrig" + +msgid "CVSS Score" +msgstr "CVSS Bewertung" + +msgid "CVSS Vector" +msgstr "CVSS Vektor" + +msgid "Published Date" +msgstr "Veröffentlichungsdatum" + +msgid "Modified Date" +msgstr "Änderungsdatum" + +msgid "Status" +msgstr "Status" + +msgid "New" +msgstr "Neu" + +msgid "Analyzed" +msgstr "Analysiert" + +msgid "Assigned" +msgstr "Zugewiesen" + +msgid "Resolved" +msgstr "Gelöst" + +msgid "Entity" +msgstr "Entität" + +msgid "Description" +msgstr "Beschreibung" + +msgid "References" +msgstr "Referenzen" + +msgid "Enter one URL per line" +msgstr "Eine URL pro Zeile eingeben" + +msgid "Affected Products" +msgstr "Betroffene Produkte" + +msgid "Enter one product per line" +msgstr "Ein Produkt pro Zeile eingeben" + +msgid "Creation date" +msgstr "Erstellungsdatum" + +msgid "Last update" +msgstr "Letzte Aktualisierung" + +msgid "Common Vulnerabilities and Exposures" +msgstr "Common Vulnerabilities and Exposures" + +msgid "Dashboard" +msgstr "Dashboard" + +msgid "CVE Rule" +msgid_plural "CVE Rules" +msgstr[0] "CVE-Regel" +msgstr[1] "CVE-Regeln" + +msgid "CVE Source" +msgid_plural "CVE Sources" +msgstr[0] "CVE-Quelle" +msgstr[1] "CVE-Quellen" + +msgid "Source Name" +msgstr "Quellenname" + +msgid "Active" +msgstr "Aktiv" + +msgid "API URL" +msgstr "API URL" + +msgid "Sync Frequency (hours)" +msgstr "Synchronisierungsfrequenz (Stunden)" + +msgid "API Key" +msgstr "API-Schlüssel" + +msgid "Last Sync" +msgstr "Letzte Synchronisierung" + +msgid "Never" +msgstr "Nie" + +msgid "Sync Status" +msgstr "Synchronisierungsstatus" + +msgid "Success" +msgstr "Erfolg" + +msgid "Failed" +msgstr "Fehlgeschlagen" + +msgid "In Progress" +msgstr "In Bearbeitung" + +msgid "Pending" +msgstr "Ausstehend" + +msgid "Unknown" +msgstr "Unbekannt" + +msgid "Sync Now" +msgstr "Jetzt synchronisieren" + +msgid "Synchronization completed successfully" +msgstr "Synchronisierung erfolgreich abgeschlossen" + +msgid "Rule Name" +msgstr "Regelname" + +msgid "Priority" +msgstr "Priorität" + +msgid "Lower numbers are processed first" +msgstr "Niedrigere Zahlen werden zuerst verarbeitet" + +msgid "Create Ticket" +msgstr "Ticket erstellen" + +msgid "Ticket Priority" +msgstr "Ticket-Priorität" + +msgid "Very High" +msgstr "Sehr hoch" + +msgid "Normal" +msgstr "Normal" + +msgid "Notify Administrators" +msgstr "Administratoren benachrichtigen" + +msgid "Add to Vulnerability Report" +msgstr "Zum Schwachstellenbericht hinzufügen" + +msgid "Active Rule" +msgstr "Aktive Regel" + +msgid "CVE Ticket" +msgid_plural "CVE Tickets" +msgstr[0] "CVE-Ticket" +msgstr[1] "CVE-Tickets" + +msgid "Add a ticket" +msgstr "Ticket hinzufügen" + +msgid "Ticket" +msgstr "Ticket" + +msgid "Opening date" +msgstr "Eröffnungsdatum" + +msgid "Creation type" +msgstr "Erstellungsart" + +msgid "Automatic" +msgstr "Automatisch" + +msgid "Manual" +msgstr "Manuell" + +msgid "Add a CVE" +msgstr "CVE hinzufügen" + +msgid "CVE linked to ticket" +msgstr "Mit Ticket verknüpfte CVE" + +msgid "Software Vulnerability Analysis" +msgid_plural "Software Vulnerability Analyses" +msgstr[0] "Software-Schwachstellenanalyse" +msgstr[1] "Software-Schwachstellenanalysen" + +msgid "Software Vulnerability Alert" +msgid_plural "Software Vulnerability Alerts" +msgstr[0] "Software-Schwachstellenalarm" +msgstr[1] "Software-Schwachstellenalarme" + +msgid "Software" +msgstr "Software" + +msgid "Version" +msgstr "Version" + +msgid "N/A" +msgstr "N/A" + +msgid "Unknown software" +msgstr "Unbekannte Software" + +msgid "Unknown version" +msgstr "Unbekannte Version" + +msgid "Unknown CVE" +msgstr "Unbekannte CVE" + +msgid "Unknown ticket" +msgstr "Unbekanntes Ticket" + +msgid "No ticket associated" +msgstr "Kein Ticket zugeordnet" + +msgid "Creation Date" +msgstr "Erstellungsdatum" + +msgid "ID" +msgstr "ID" + +msgid "Processed" +msgstr "Bearbeitet" + +msgid "Ignored" +msgstr "Ignoriert" + +msgid "CVE Statistics" +msgstr "CVE-Statistiken" + +msgid "CVE Severity Distribution" +msgstr "CVE-Schweregradverteilung" + +msgid "Recent CVEs" +msgstr "Aktuelle CVEs" + +msgid "Software Vulnerability Alerts" +msgstr "Software-Schwachstellenalarme" + +msgid "Scan Software Inventory Now" +msgstr "Software-Inventar jetzt scannen" + +msgid "Total Vulnerability Alerts" +msgstr "Gesamtzahl der Schwachstellenalarme" + +msgid "New Alerts" +msgstr "Neue Alarme" + +msgid "Critical Vulnerabilities" +msgstr "Kritische Schwachstellen" + +msgid "High Vulnerabilities" +msgstr "Schwerwiegende Schwachstellen" + +msgid "Recent Vulnerability Alerts" +msgstr "Aktuelle Schwachstellenalarme" + +msgid "No alerts found" +msgstr "Keine Alarme gefunden" + +msgid "View all vulnerability alerts" +msgstr "Alle Schwachstellenalarme anzeigen" + +msgid "Software vulnerability analysis completed successfully." +msgstr "Software-Schwachstellenanalyse erfolgreich abgeschlossen." + +msgid "Software vulnerability analysis completed with no new alerts." +msgstr "Software-Schwachstellenanalyse ohne neue Alarme abgeschlossen." + +msgid "Software vulnerability analysis task not found." +msgstr "Software-Schwachstellenanalyseaufgabe nicht gefunden." + +msgid "A vulnerability has been detected in your software inventory" +msgstr "In Ihrem Software-Inventar wurde eine Schwachstelle erkannt" + +msgid "Vulnerability" +msgstr "Schwachstelle" + +msgid "No vulnerabilities found for this software" +msgstr "Keine Schwachstellen für diese Software gefunden" + +msgid "Scan for vulnerabilities now" +msgstr "Jetzt nach Schwachstellen scannen" + +msgid "Vulnerabilities" +msgstr "Schwachstellen" + +msgid "Plugin activated successfully!" +msgstr "Plugin erfolgreich aktiviert!" + +msgid "CVE Management plugin has been activated." +msgstr "Das CVE-Management-Plugin wurde aktiviert." + +msgid "Initial CVE data is being synchronized in the background." +msgstr "Die ersten CVE-Daten werden im Hintergrund synchronisiert." + +msgid "Software inventory will be analyzed for vulnerabilities automatically." +msgstr "Das Software-Inventar wird automatisch auf Schwachstellen analysiert." + +msgid "You can now configure the data sources and rules." +msgstr "Sie können jetzt die Datenquellen und Regeln konfigurieren." \ No newline at end of file diff --git a/locales/en_GB.po b/locales/en_GB.po new file mode 100644 index 0000000..a6d75af --- /dev/null +++ b/locales/en_GB.po @@ -0,0 +1,351 @@ +msgid "" +msgstr "" +"Project-Id-Version: GLPI CVE Plugin 1.0.0\n" +"POT-Creation-Date: 2024-01-15 12:00+0100\n" +"PO-Revision-Date: 2024-01-15 12:00+0100\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: en_GB\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Vulnérabilité" +msgstr "Vulnerability" + +msgid "CVE" +msgid_plural "CVEs" +msgstr[0] "CVE" +msgstr[1] "CVEs" + +msgid "CVE ID" +msgstr "CVE ID" + +msgid "Severity" +msgstr "Severity" + +msgid "Critical" +msgstr "Critical" + +msgid "High" +msgstr "High" + +msgid "Medium" +msgstr "Medium" + +msgid "Low" +msgstr "Low" + +msgid "CVSS Score" +msgstr "CVSS Score" + +msgid "CVSS Vector" +msgstr "CVSS Vector" + +msgid "Published Date" +msgstr "Published Date" + +msgid "Modified Date" +msgstr "Modified Date" + +msgid "Status" +msgstr "Status" + +msgid "New" +msgstr "New" + +msgid "Analyzed" +msgstr "Analyzed" + +msgid "Assigned" +msgstr "Assigned" + +msgid "Resolved" +msgstr "Resolved" + +msgid "Entity" +msgstr "Entity" + +msgid "Description" +msgstr "Description" + +msgid "References" +msgstr "References" + +msgid "Enter one URL per line" +msgstr "Enter one URL per line" + +msgid "Affected Products" +msgstr "Affected Products" + +msgid "Enter one product per line" +msgstr "Enter one product per line" + +msgid "Creation date" +msgstr "Creation date" + +msgid "Last update" +msgstr "Last update" + +msgid "Common Vulnerabilities and Exposures" +msgstr "Common Vulnerabilities and Exposures" + +msgid "Dashboard" +msgstr "Dashboard" + +msgid "CVE Rule" +msgid_plural "CVE Rules" +msgstr[0] "CVE Rule" +msgstr[1] "CVE Rules" + +msgid "CVE Source" +msgid_plural "CVE Sources" +msgstr[0] "CVE Source" +msgstr[1] "CVE Sources" + +msgid "Source Name" +msgstr "Source Name" + +msgid "Active" +msgstr "Active" + +msgid "API URL" +msgstr "API URL" + +msgid "Sync Frequency (hours)" +msgstr "Sync Frequency (hours)" + +msgid "API Key" +msgstr "API Key" + +msgid "Last Sync" +msgstr "Last Sync" + +msgid "Never" +msgstr "Never" + +msgid "Sync Status" +msgstr "Sync Status" + +msgid "Success" +msgstr "Success" + +msgid "Failed" +msgstr "Failed" + +msgid "In Progress" +msgstr "In Progress" + +msgid "Pending" +msgstr "Pending" + +msgid "Unknown" +msgstr "Unknown" + +msgid "Sync Now" +msgstr "Sync Now" + +msgid "Synchronization completed successfully" +msgstr "Synchronization completed successfully" + +msgid "Rule Name" +msgstr "Rule Name" + +msgid "Priority" +msgstr "Priority" + +msgid "Lower numbers are processed first" +msgstr "Lower numbers are processed first" + +msgid "Create Ticket" +msgstr "Create Ticket" + +msgid "Ticket Priority" +msgstr "Ticket Priority" + +msgid "Very High" +msgstr "Very High" + +msgid "Normal" +msgstr "Normal" + +msgid "Notify Administrators" +msgstr "Notify Administrators" + +msgid "Add to Vulnerability Report" +msgstr "Add to Vulnerability Report" + +msgid "Active Rule" +msgstr "Active Rule" + +msgid "CVE Ticket" +msgid_plural "CVE Tickets" +msgstr[0] "CVE Ticket" +msgstr[1] "CVE Tickets" + +msgid "Add a ticket" +msgstr "Add a ticket" + +msgid "Ticket" +msgstr "Ticket" + +msgid "Opening date" +msgstr "Opening date" + +msgid "Creation type" +msgstr "Creation type" + +msgid "Automatic" +msgstr "Automatic" + +msgid "Manual" +msgstr "Manual" + +msgid "Add a CVE" +msgstr "Add a CVE" + +msgid "CVE linked to ticket" +msgstr "CVE linked to ticket" + +msgid "Software Vulnerability Analysis" +msgid_plural "Software Vulnerability Analyses" +msgstr[0] "Software Vulnerability Analysis" +msgstr[1] "Software Vulnerability Analyses" + +msgid "Software Vulnerability Alert" +msgid_plural "Software Vulnerability Alerts" +msgstr[0] "Software Vulnerability Alert" +msgstr[1] "Software Vulnerability Alerts" + +msgid "Software" +msgstr "Software" + +msgid "Version" +msgstr "Version" + +msgid "N/A" +msgstr "N/A" + +msgid "Unknown software" +msgstr "Unknown software" + +msgid "Unknown version" +msgstr "Unknown version" + +msgid "Unknown CVE" +msgstr "Unknown CVE" + +msgid "Unknown ticket" +msgstr "Unknown ticket" + +msgid "No ticket associated" +msgstr "No ticket associated" + +msgid "Creation Date" +msgstr "Creation Date" + +msgid "ID" +msgstr "ID" + +msgid "Processed" +msgstr "Processed" + +msgid "Ignored" +msgstr "Ignored" + +msgid "CVE Statistics" +msgstr "CVE Statistics" + +msgid "CVE Severity Distribution" +msgstr "CVE Severity Distribution" + +msgid "Recent CVEs" +msgstr "Recent CVEs" + +msgid "Software Vulnerability Alerts" +msgstr "Software Vulnerability Alerts" + +msgid "Scan Software Inventory Now" +msgstr "Scan Software Inventory Now" + +msgid "Total Vulnerability Alerts" +msgstr "Total Vulnerability Alerts" + +msgid "New Alerts" +msgstr "New Alerts" + +msgid "Critical Vulnerabilities" +msgstr "Critical Vulnerabilities" + +msgid "High Vulnerabilities" +msgstr "High Vulnerabilities" + +msgid "Recent Vulnerability Alerts" +msgstr "Recent Vulnerability Alerts" + +msgid "No alerts found" +msgstr "No alerts found" + +msgid "View all vulnerability alerts" +msgstr "View all vulnerability alerts" + +msgid "Software vulnerability analysis completed successfully." +msgstr "Software vulnerability analysis completed successfully." + +msgid "Software vulnerability analysis completed with no new alerts." +msgstr "Software vulnerability analysis completed with no new alerts." + +msgid "Software vulnerability analysis task not found." +msgstr "Software vulnerability analysis task not found." + +msgid "A vulnerability has been detected in your software inventory" +msgstr "A vulnerability has been detected in your software inventory" + +msgid "Vulnerability" +msgstr "Vulnerability" + +msgid "No vulnerabilities found for this software" +msgstr "No vulnerabilities found for this software" + +msgid "Scan for vulnerabilities now" +msgstr "Scan for vulnerabilities now" + +msgid "Vulnerabilities" +msgstr "Vulnerabilities" + +msgid "Plugin activated successfully!" +msgstr "Plugin activated successfully!" + +msgid "CVE Management plugin has been activated." +msgstr "CVE Management plugin has been activated." + +msgid "Initial CVE data is being synchronized in the background." +msgstr "Initial CVE data is being synchronized in the background." + +msgid "Software inventory will be analyzed for vulnerabilities automatically." +msgstr "Software inventory will be analyzed for vulnerabilities automatically." + +msgid "You can now configure the data sources and rules." +msgstr "You can now configure the data sources and rules." + +msgid "Source name is required" +msgstr "Source name is required" + +msgid "API URL is required" +msgstr "API URL is required" + +msgid "Invalid URL format" +msgstr "Invalid URL format" + +msgid "Leave empty to keep current value" +msgstr "Leave empty to keep current value" + +msgid "Source Type" +msgstr "Source Type" + +msgid "Custom" +msgstr "Custom" + +msgid "Data Format" +msgstr "Data Format" \ No newline at end of file diff --git a/locales/es_ES.po b/locales/es_ES.po new file mode 100644 index 0000000..017accf --- /dev/null +++ b/locales/es_ES.po @@ -0,0 +1,330 @@ +msgid "" +msgstr "" +"Project-Id-Version: GLPI CVE Plugin 1.0.0\n" +"POT-Creation-Date: 2024-01-15 12:00+0100\n" +"PO-Revision-Date: 2024-01-15 12:00+0100\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: es_ES\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Vulnérabilité" +msgstr "Vulnerabilidad" + +msgid "CVE" +msgid_plural "CVEs" +msgstr[0] "CVE" +msgstr[1] "CVEs" + +msgid "CVE ID" +msgstr "ID CVE" + +msgid "Severity" +msgstr "Gravedad" + +msgid "Critical" +msgstr "Crítica" + +msgid "High" +msgstr "Alta" + +msgid "Medium" +msgstr "Media" + +msgid "Low" +msgstr "Baja" + +msgid "CVSS Score" +msgstr "Puntuación CVSS" + +msgid "CVSS Vector" +msgstr "Vector CVSS" + +msgid "Published Date" +msgstr "Fecha de publicación" + +msgid "Modified Date" +msgstr "Fecha de modificación" + +msgid "Status" +msgstr "Estado" + +msgid "New" +msgstr "Nueva" + +msgid "Analyzed" +msgstr "Analizada" + +msgid "Assigned" +msgstr "Asignada" + +msgid "Resolved" +msgstr "Resuelta" + +msgid "Entity" +msgstr "Entidad" + +msgid "Description" +msgstr "Descripción" + +msgid "References" +msgstr "Referencias" + +msgid "Enter one URL per line" +msgstr "Introduzca una URL por línea" + +msgid "Affected Products" +msgstr "Productos afectados" + +msgid "Enter one product per line" +msgstr "Introduzca un producto por línea" + +msgid "Creation date" +msgstr "Fecha de creación" + +msgid "Last update" +msgstr "Última actualización" + +msgid "Common Vulnerabilities and Exposures" +msgstr "Vulnerabilidades y exposiciones comunes" + +msgid "Dashboard" +msgstr "Panel de control" + +msgid "CVE Rule" +msgid_plural "CVE Rules" +msgstr[0] "Regla CVE" +msgstr[1] "Reglas CVE" + +msgid "CVE Source" +msgid_plural "CVE Sources" +msgstr[0] "Fuente CVE" +msgstr[1] "Fuentes CVE" + +msgid "Source Name" +msgstr "Nombre de la fuente" + +msgid "Active" +msgstr "Activa" + +msgid "API URL" +msgstr "URL de API" + +msgid "Sync Frequency (hours)" +msgstr "Frecuencia de sincronización (horas)" + +msgid "API Key" +msgstr "Clave API" + +msgid "Last Sync" +msgstr "Última sincronización" + +msgid "Never" +msgstr "Nunca" + +msgid "Sync Status" +msgstr "Estado de sincronización" + +msgid "Success" +msgstr "Éxito" + +msgid "Failed" +msgstr "Fallido" + +msgid "In Progress" +msgstr "En progreso" + +msgid "Pending" +msgstr "Pendiente" + +msgid "Unknown" +msgstr "Desconocido" + +msgid "Sync Now" +msgstr "Sincronizar ahora" + +msgid "Synchronization completed successfully" +msgstr "Sincronización completada con éxito" + +msgid "Rule Name" +msgstr "Nombre de la regla" + +msgid "Priority" +msgstr "Prioridad" + +msgid "Lower numbers are processed first" +msgstr "Los números más bajos se procesan primero" + +msgid "Create Ticket" +msgstr "Crear ticket" + +msgid "Ticket Priority" +msgstr "Prioridad del ticket" + +msgid "Very High" +msgstr "Muy alta" + +msgid "Normal" +msgstr "Normal" + +msgid "Notify Administrators" +msgstr "Notificar a los administradores" + +msgid "Add to Vulnerability Report" +msgstr "Añadir al informe de vulnerabilidades" + +msgid "Active Rule" +msgstr "Regla activa" + +msgid "CVE Ticket" +msgid_plural "CVE Tickets" +msgstr[0] "Ticket CVE" +msgstr[1] "Tickets CVE" + +msgid "Add a ticket" +msgstr "Añadir un ticket" + +msgid "Ticket" +msgstr "Ticket" + +msgid "Opening date" +msgstr "Fecha de apertura" + +msgid "Creation type" +msgstr "Tipo de creación" + +msgid "Automatic" +msgstr "Automática" + +msgid "Manual" +msgstr "Manual" + +msgid "Add a CVE" +msgstr "Añadir una CVE" + +msgid "CVE linked to ticket" +msgstr "CVE vinculada al ticket" + +msgid "Software Vulnerability Analysis" +msgid_plural "Software Vulnerability Analyses" +msgstr[0] "Análisis de vulnerabilidad de software" +msgstr[1] "Análisis de vulnerabilidades de software" + +msgid "Software Vulnerability Alert" +msgid_plural "Software Vulnerability Alerts" +msgstr[0] "Alerta de vulnerabilidad de software" +msgstr[1] "Alertas de vulnerabilidad de software" + +msgid "Software" +msgstr "Software" + +msgid "Version" +msgstr "Versión" + +msgid "N/A" +msgstr "N/D" + +msgid "Unknown software" +msgstr "Software desconocido" + +msgid "Unknown version" +msgstr "Versión desconocida" + +msgid "Unknown CVE" +msgstr "CVE desconocida" + +msgid "Unknown ticket" +msgstr "Ticket desconocido" + +msgid "No ticket associated" +msgstr "Ningún ticket asociado" + +msgid "Creation Date" +msgstr "Fecha de creación" + +msgid "ID" +msgstr "ID" + +msgid "Processed" +msgstr "Procesada" + +msgid "Ignored" +msgstr "Ignorada" + +msgid "CVE Statistics" +msgstr "Estadísticas CVE" + +msgid "CVE Severity Distribution" +msgstr "Distribución de gravedad CVE" + +msgid "Recent CVEs" +msgstr "CVEs recientes" + +msgid "Software Vulnerability Alerts" +msgstr "Alertas de vulnerabilidad de software" + +msgid "Scan Software Inventory Now" +msgstr "Escanear inventario de software ahora" + +msgid "Total Vulnerability Alerts" +msgstr "Total de alertas de vulnerabilidad" + +msgid "New Alerts" +msgstr "Nuevas alertas" + +msgid "Critical Vulnerabilities" +msgstr "Vulnerabilidades críticas" + +msgid "High Vulnerabilities" +msgstr "Vulnerabilidades altas" + +msgid "Recent Vulnerability Alerts" +msgstr "Alertas de vulnerabilidad recientes" + +msgid "No alerts found" +msgstr "No se encontraron alertas" + +msgid "View all vulnerability alerts" +msgstr "Ver todas las alertas de vulnerabilidad" + +msgid "Software vulnerability analysis completed successfully." +msgstr "Análisis de vulnerabilidad de software completado con éxito." + +msgid "Software vulnerability analysis completed with no new alerts." +msgstr "Análisis de vulnerabilidad de software completado sin nuevas alertas." + +msgid "Software vulnerability analysis task not found." +msgstr "Tarea de análisis de vulnerabilidad de software no encontrada." + +msgid "A vulnerability has been detected in your software inventory" +msgstr "Se ha detectado una vulnerabilidad en su inventario de software" + +msgid "Vulnerability" +msgstr "Vulnerabilidad" + +msgid "No vulnerabilities found for this software" +msgstr "No se encontraron vulnerabilidades para este software" + +msgid "Scan for vulnerabilities now" +msgstr "Buscar vulnerabilidades ahora" + +msgid "Vulnerabilities" +msgstr "Vulnerabilidades" + +msgid "Plugin activated successfully!" +msgstr "¡Plugin activado con éxito!" + +msgid "CVE Management plugin has been activated." +msgstr "El plugin de gestión de CVE ha sido activado." + +msgid "Initial CVE data is being synchronized in the background." +msgstr "Los datos iniciales de CVE se están sincronizando en segundo plano." + +msgid "Software inventory will be analyzed for vulnerabilities automatically." +msgstr "El inventario de software será analizado automáticamente en busca de vulnerabilidades." + +msgid "You can now configure the data sources and rules." +msgstr "Ahora puede configurar las fuentes de datos y las reglas." \ No newline at end of file diff --git a/locales/fr_FR.po b/locales/fr_FR.po new file mode 100644 index 0000000..201c471 --- /dev/null +++ b/locales/fr_FR.po @@ -0,0 +1,330 @@ +msgid "" +msgstr "" +"Project-Id-Version: GLPI CVE Plugin 1.0.0\n" +"POT-Creation-Date: 2024-01-15 12:00+0100\n" +"PO-Revision-Date: 2024-01-15 12:00+0100\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: fr_FR\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +msgid "Vulnérabilité" +msgstr "Vulnérabilité" + +msgid "CVE" +msgid_plural "CVEs" +msgstr[0] "CVE" +msgstr[1] "CVEs" + +msgid "CVE ID" +msgstr "ID CVE" + +msgid "Severity" +msgstr "Sévérité" + +msgid "Critical" +msgstr "Critique" + +msgid "High" +msgstr "Haute" + +msgid "Medium" +msgstr "Moyenne" + +msgid "Low" +msgstr "Faible" + +msgid "CVSS Score" +msgstr "Score CVSS" + +msgid "CVSS Vector" +msgstr "Vecteur CVSS" + +msgid "Published Date" +msgstr "Date de publication" + +msgid "Modified Date" +msgstr "Date de modification" + +msgid "Status" +msgstr "Statut" + +msgid "New" +msgstr "Nouveau" + +msgid "Analyzed" +msgstr "Analysé" + +msgid "Assigned" +msgstr "Assigné" + +msgid "Resolved" +msgstr "Résolu" + +msgid "Entity" +msgstr "Entité" + +msgid "Description" +msgstr "Description" + +msgid "References" +msgstr "Références" + +msgid "Enter one URL per line" +msgstr "Entrez une URL par ligne" + +msgid "Affected Products" +msgstr "Produits affectés" + +msgid "Enter one product per line" +msgstr "Entrez un produit par ligne" + +msgid "Creation date" +msgstr "Date de création" + +msgid "Last update" +msgstr "Dernière mise à jour" + +msgid "Common Vulnerabilities and Exposures" +msgstr "Vulnérabilités et expositions communes" + +msgid "Dashboard" +msgstr "Tableau de bord" + +msgid "CVE Rule" +msgid_plural "CVE Rules" +msgstr[0] "Règle CVE" +msgstr[1] "Règles CVE" + +msgid "CVE Source" +msgid_plural "CVE Sources" +msgstr[0] "Source CVE" +msgstr[1] "Sources CVE" + +msgid "Source Name" +msgstr "Nom de la source" + +msgid "Active" +msgstr "Actif" + +msgid "API URL" +msgstr "URL de l'API" + +msgid "Sync Frequency (hours)" +msgstr "Fréquence de synchronisation (heures)" + +msgid "API Key" +msgstr "Clé d'API" + +msgid "Last Sync" +msgstr "Dernière synchronisation" + +msgid "Never" +msgstr "Jamais" + +msgid "Sync Status" +msgstr "Statut de synchronisation" + +msgid "Success" +msgstr "Succès" + +msgid "Failed" +msgstr "Échec" + +msgid "In Progress" +msgstr "En cours" + +msgid "Pending" +msgstr "En attente" + +msgid "Unknown" +msgstr "Inconnu" + +msgid "Sync Now" +msgstr "Synchroniser maintenant" + +msgid "Synchronization completed successfully" +msgstr "Synchronisation terminée avec succès" + +msgid "Rule Name" +msgstr "Nom de la règle" + +msgid "Priority" +msgstr "Priorité" + +msgid "Lower numbers are processed first" +msgstr "Les chiffres bas sont traités en premier" + +msgid "Create Ticket" +msgstr "Créer un ticket" + +msgid "Ticket Priority" +msgstr "Priorité du ticket" + +msgid "Very High" +msgstr "Très haute" + +msgid "Normal" +msgstr "Normale" + +msgid "Notify Administrators" +msgstr "Notifier les administrateurs" + +msgid "Add to Vulnerability Report" +msgstr "Ajouter au rapport de vulnérabilité" + +msgid "Active Rule" +msgstr "Règle active" + +msgid "CVE Ticket" +msgid_plural "CVE Tickets" +msgstr[0] "Ticket CVE" +msgstr[1] "Tickets CVE" + +msgid "Add a ticket" +msgstr "Ajouter un ticket" + +msgid "Ticket" +msgstr "Ticket" + +msgid "Opening date" +msgstr "Date d'ouverture" + +msgid "Creation type" +msgstr "Type de création" + +msgid "Automatic" +msgstr "Automatique" + +msgid "Manual" +msgstr "Manuel" + +msgid "Add a CVE" +msgstr "Ajouter une CVE" + +msgid "CVE linked to ticket" +msgstr "CVE liée au ticket" + +msgid "Software Vulnerability Analysis" +msgid_plural "Software Vulnerability Analyses" +msgstr[0] "Analyse de vulnérabilité logicielle" +msgstr[1] "Analyses de vulnérabilité logicielle" + +msgid "Software Vulnerability Alert" +msgid_plural "Software Vulnerability Alerts" +msgstr[0] "Alerte de vulnérabilité logicielle" +msgstr[1] "Alertes de vulnérabilité logicielle" + +msgid "Software" +msgstr "Logiciel" + +msgid "Version" +msgstr "Version" + +msgid "N/A" +msgstr "N/A" + +msgid "Unknown software" +msgstr "Logiciel inconnu" + +msgid "Unknown version" +msgstr "Version inconnue" + +msgid "Unknown CVE" +msgstr "CVE inconnue" + +msgid "Unknown ticket" +msgstr "Ticket inconnu" + +msgid "No ticket associated" +msgstr "Aucun ticket associé" + +msgid "Creation Date" +msgstr "Date de création" + +msgid "ID" +msgstr "ID" + +msgid "Processed" +msgstr "Traitée" + +msgid "Ignored" +msgstr "Ignorée" + +msgid "CVE Statistics" +msgstr "Statistiques CVE" + +msgid "CVE Severity Distribution" +msgstr "Répartition des sévérités CVE" + +msgid "Recent CVEs" +msgstr "CVEs récentes" + +msgid "Software Vulnerability Alerts" +msgstr "Alertes de vulnérabilité logicielle" + +msgid "Scan Software Inventory Now" +msgstr "Analyser l'inventaire logiciel maintenant" + +msgid "Total Vulnerability Alerts" +msgstr "Total des alertes de vulnérabilité" + +msgid "New Alerts" +msgstr "Nouvelles alertes" + +msgid "Critical Vulnerabilities" +msgstr "Vulnérabilités critiques" + +msgid "High Vulnerabilities" +msgstr "Vulnérabilités hautes" + +msgid "Recent Vulnerability Alerts" +msgstr "Alertes de vulnérabilité récentes" + +msgid "No alerts found" +msgstr "Aucune alerte trouvée" + +msgid "View all vulnerability alerts" +msgstr "Voir toutes les alertes de vulnérabilité" + +msgid "Software vulnerability analysis completed successfully." +msgstr "Analyse de vulnérabilité logicielle terminée avec succès." + +msgid "Software vulnerability analysis completed with no new alerts." +msgstr "Analyse de vulnérabilité logicielle terminée sans nouvelles alertes." + +msgid "Software vulnerability analysis task not found." +msgstr "Tâche d'analyse de vulnérabilité logicielle non trouvée." + +msgid "A vulnerability has been detected in your software inventory" +msgstr "Une vulnérabilité a été détectée dans votre inventaire logiciel" + +msgid "Vulnerability" +msgstr "Vulnérabilité" + +msgid "No vulnerabilities found for this software" +msgstr "Aucune vulnérabilité trouvée pour ce logiciel" + +msgid "Scan for vulnerabilities now" +msgstr "Rechercher des vulnérabilités maintenant" + +msgid "Vulnerabilities" +msgstr "Vulnérabilités" + +msgid "Plugin activated successfully!" +msgstr "Plugin activé avec succès !" + +msgid "CVE Management plugin has been activated." +msgstr "Le plugin de gestion des CVE a été activé." + +msgid "Initial CVE data is being synchronized in the background." +msgstr "Les données CVE initiales sont en cours de synchronisation en arrière-plan." + +msgid "Software inventory will be analyzed for vulnerabilities automatically." +msgstr "L'inventaire logiciel sera analysé automatiquement pour détecter les vulnérabilités." + +msgid "You can now configure the data sources and rules." +msgstr "Vous pouvez maintenant configurer les sources de données et les règles." \ No newline at end of file diff --git a/locales/it_IT.po b/locales/it_IT.po new file mode 100644 index 0000000..cb724c0 --- /dev/null +++ b/locales/it_IT.po @@ -0,0 +1,330 @@ +msgid "" +msgstr "" +"Project-Id-Version: GLPI CVE Plugin 1.0.0\n" +"POT-Creation-Date: 2024-01-15 12:00+0100\n" +"PO-Revision-Date: 2024-01-15 12:00+0100\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: it_IT\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Vulnérabilité" +msgstr "Vulnerabilità" + +msgid "CVE" +msgid_plural "CVEs" +msgstr[0] "CVE" +msgstr[1] "CVE" + +msgid "CVE ID" +msgstr "ID CVE" + +msgid "Severity" +msgstr "Gravità" + +msgid "Critical" +msgstr "Critica" + +msgid "High" +msgstr "Alta" + +msgid "Medium" +msgstr "Media" + +msgid "Low" +msgstr "Bassa" + +msgid "CVSS Score" +msgstr "Punteggio CVSS" + +msgid "CVSS Vector" +msgstr "Vettore CVSS" + +msgid "Published Date" +msgstr "Data di pubblicazione" + +msgid "Modified Date" +msgstr "Data di modifica" + +msgid "Status" +msgstr "Stato" + +msgid "New" +msgstr "Nuova" + +msgid "Analyzed" +msgstr "Analizzata" + +msgid "Assigned" +msgstr "Assegnata" + +msgid "Resolved" +msgstr "Risolta" + +msgid "Entity" +msgstr "Entità" + +msgid "Description" +msgstr "Descrizione" + +msgid "References" +msgstr "Riferimenti" + +msgid "Enter one URL per line" +msgstr "Inserisci un URL per riga" + +msgid "Affected Products" +msgstr "Prodotti interessati" + +msgid "Enter one product per line" +msgstr "Inserisci un prodotto per riga" + +msgid "Creation date" +msgstr "Data di creazione" + +msgid "Last update" +msgstr "Ultimo aggiornamento" + +msgid "Common Vulnerabilities and Exposures" +msgstr "Vulnerabilità ed esposizioni comuni" + +msgid "Dashboard" +msgstr "Dashboard" + +msgid "CVE Rule" +msgid_plural "CVE Rules" +msgstr[0] "Regola CVE" +msgstr[1] "Regole CVE" + +msgid "CVE Source" +msgid_plural "CVE Sources" +msgstr[0] "Fonte CVE" +msgstr[1] "Fonti CVE" + +msgid "Source Name" +msgstr "Nome fonte" + +msgid "Active" +msgstr "Attiva" + +msgid "API URL" +msgstr "URL API" + +msgid "Sync Frequency (hours)" +msgstr "Frequenza di sincronizzazione (ore)" + +msgid "API Key" +msgstr "Chiave API" + +msgid "Last Sync" +msgstr "Ultima sincronizzazione" + +msgid "Never" +msgstr "Mai" + +msgid "Sync Status" +msgstr "Stato sincronizzazione" + +msgid "Success" +msgstr "Successo" + +msgid "Failed" +msgstr "Fallito" + +msgid "In Progress" +msgstr "In corso" + +msgid "Pending" +msgstr "In attesa" + +msgid "Unknown" +msgstr "Sconosciuto" + +msgid "Sync Now" +msgstr "Sincronizza ora" + +msgid "Synchronization completed successfully" +msgstr "Sincronizzazione completata con successo" + +msgid "Rule Name" +msgstr "Nome regola" + +msgid "Priority" +msgstr "Priorità" + +msgid "Lower numbers are processed first" +msgstr "I numeri più bassi vengono elaborati per primi" + +msgid "Create Ticket" +msgstr "Crea ticket" + +msgid "Ticket Priority" +msgstr "Priorità ticket" + +msgid "Very High" +msgstr "Molto alta" + +msgid "Normal" +msgstr "Normale" + +msgid "Notify Administrators" +msgstr "Notifica amministratori" + +msgid "Add to Vulnerability Report" +msgstr "Aggiungi al rapporto vulnerabilità" + +msgid "Active Rule" +msgstr "Regola attiva" + +msgid "CVE Ticket" +msgid_plural "CVE Tickets" +msgstr[0] "Ticket CVE" +msgstr[1] "Ticket CVE" + +msgid "Add a ticket" +msgstr "Aggiungi un ticket" + +msgid "Ticket" +msgstr "Ticket" + +msgid "Opening date" +msgstr "Data di apertura" + +msgid "Creation type" +msgstr "Tipo di creazione" + +msgid "Automatic" +msgstr "Automatica" + +msgid "Manual" +msgstr "Manuale" + +msgid "Add a CVE" +msgstr "Aggiungi una CVE" + +msgid "CVE linked to ticket" +msgstr "CVE collegata al ticket" + +msgid "Software Vulnerability Analysis" +msgid_plural "Software Vulnerability Analyses" +msgstr[0] "Analisi vulnerabilità software" +msgstr[1] "Analisi vulnerabilità software" + +msgid "Software Vulnerability Alert" +msgid_plural "Software Vulnerability Alerts" +msgstr[0] "Avviso vulnerabilità software" +msgstr[1] "Avvisi vulnerabilità software" + +msgid "Software" +msgstr "Software" + +msgid "Version" +msgstr "Versione" + +msgid "N/A" +msgstr "N/D" + +msgid "Unknown software" +msgstr "Software sconosciuto" + +msgid "Unknown version" +msgstr "Versione sconosciuta" + +msgid "Unknown CVE" +msgstr "CVE sconosciuta" + +msgid "Unknown ticket" +msgstr "Ticket sconosciuto" + +msgid "No ticket associated" +msgstr "Nessun ticket associato" + +msgid "Creation Date" +msgstr "Data di creazione" + +msgid "ID" +msgstr "ID" + +msgid "Processed" +msgstr "Elaborata" + +msgid "Ignored" +msgstr "Ignorata" + +msgid "CVE Statistics" +msgstr "Statistiche CVE" + +msgid "CVE Severity Distribution" +msgstr "Distribuzione gravità CVE" + +msgid "Recent CVEs" +msgstr "CVE recenti" + +msgid "Software Vulnerability Alerts" +msgstr "Avvisi vulnerabilità software" + +msgid "Scan Software Inventory Now" +msgstr "Scansiona inventario software ora" + +msgid "Total Vulnerability Alerts" +msgstr "Totale avvisi vulnerabilità" + +msgid "New Alerts" +msgstr "Nuovi avvisi" + +msgid "Critical Vulnerabilities" +msgstr "Vulnerabilità critiche" + +msgid "High Vulnerabilities" +msgstr "Vulnerabilità alte" + +msgid "Recent Vulnerability Alerts" +msgstr "Avvisi vulnerabilità recenti" + +msgid "No alerts found" +msgstr "Nessun avviso trovato" + +msgid "View all vulnerability alerts" +msgstr "Visualizza tutti gli avvisi di vulnerabilità" + +msgid "Software vulnerability analysis completed successfully." +msgstr "Analisi vulnerabilità software completata con successo." + +msgid "Software vulnerability analysis completed with no new alerts." +msgstr "Analisi vulnerabilità software completata senza nuovi avvisi." + +msgid "Software vulnerability analysis task not found." +msgstr "Attività di analisi vulnerabilità software non trovata." + +msgid "A vulnerability has been detected in your software inventory" +msgstr "È stata rilevata una vulnerabilità nel tuo inventario software" + +msgid "Vulnerability" +msgstr "Vulnerabilità" + +msgid "No vulnerabilities found for this software" +msgstr "Nessuna vulnerabilità trovata per questo software" + +msgid "Scan for vulnerabilities now" +msgstr "Scansiona per vulnerabilità ora" + +msgid "Vulnerabilities" +msgstr "Vulnerabilità" + +msgid "Plugin activated successfully!" +msgstr "Plugin attivato con successo!" + +msgid "CVE Management plugin has been activated." +msgstr "Il plugin di gestione CVE è stato attivato." + +msgid "Initial CVE data is being synchronized in the background." +msgstr "I dati CVE iniziali sono in fase di sincronizzazione in background." + +msgid "Software inventory will be analyzed for vulnerabilities automatically." +msgstr "L'inventario software verrà analizzato automaticamente per individuare vulnerabilità." + +msgid "You can now configure the data sources and rules." +msgstr "Ora puoi configurare le fonti di dati e le regole." \ No newline at end of file diff --git a/locales/pl_PL.po b/locales/pl_PL.po new file mode 100644 index 0000000..8f9c319 --- /dev/null +++ b/locales/pl_PL.po @@ -0,0 +1,336 @@ +msgid "" +msgstr "" +"Project-Id-Version: GLPI CVE Plugin 1.0.0\n" +"POT-Creation-Date: 2024-01-15 12:00+0100\n" +"PO-Revision-Date: 2024-01-15 12:00+0100\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: pl_PL\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n" + +msgid "Vulnérabilité" +msgstr "Podatność" + +msgid "CVE" +msgid_plural "CVEs" +msgstr[0] "CVE" +msgstr[1] "CVE" +msgstr[2] "CVE" + +msgid "CVE ID" +msgstr "ID CVE" + +msgid "Severity" +msgstr "Dotkliwość" + +msgid "Critical" +msgstr "Krytyczna" + +msgid "High" +msgstr "Wysoka" + +msgid "Medium" +msgstr "Średnia" + +msgid "Low" +msgstr "Niska" + +msgid "CVSS Score" +msgstr "Wynik CVSS" + +msgid "CVSS Vector" +msgstr "Wektor CVSS" + +msgid "Published Date" +msgstr "Data publikacji" + +msgid "Modified Date" +msgstr "Data modyfikacji" + +msgid "Status" +msgstr "Status" + +msgid "New" +msgstr "Nowa" + +msgid "Analyzed" +msgstr "Przeanalizowana" + +msgid "Assigned" +msgstr "Przypisana" + +msgid "Resolved" +msgstr "Rozwiązana" + +msgid "Entity" +msgstr "Jednostka" + +msgid "Description" +msgstr "Opis" + +msgid "References" +msgstr "Odniesienia" + +msgid "Enter one URL per line" +msgstr "Wprowadź jeden adres URL w każdej linii" + +msgid "Affected Products" +msgstr "Dotknięte produkty" + +msgid "Enter one product per line" +msgstr "Wprowadź jeden produkt w każdej linii" + +msgid "Creation date" +msgstr "Data utworzenia" + +msgid "Last update" +msgstr "Ostatnia aktualizacja" + +msgid "Common Vulnerabilities and Exposures" +msgstr "Powszechne podatności i narażenia" + +msgid "Dashboard" +msgstr "Panel" + +msgid "CVE Rule" +msgid_plural "CVE Rules" +msgstr[0] "Reguła CVE" +msgstr[1] "Reguły CVE" +msgstr[2] "Reguł CVE" + +msgid "CVE Source" +msgid_plural "CVE Sources" +msgstr[0] "Źródło CVE" +msgstr[1] "Źródła CVE" +msgstr[2] "Źródeł CVE" + +msgid "Source Name" +msgstr "Nazwa źródła" + +msgid "Active" +msgstr "Aktywne" + +msgid "API URL" +msgstr "URL API" + +msgid "Sync Frequency (hours)" +msgstr "Częstotliwość synchronizacji (godziny)" + +msgid "API Key" +msgstr "Klucz API" + +msgid "Last Sync" +msgstr "Ostatnia synchronizacja" + +msgid "Never" +msgstr "Nigdy" + +msgid "Sync Status" +msgstr "Status synchronizacji" + +msgid "Success" +msgstr "Sukces" + +msgid "Failed" +msgstr "Niepowodzenie" + +msgid "In Progress" +msgstr "W trakcie" + +msgid "Pending" +msgstr "Oczekujące" + +msgid "Unknown" +msgstr "Nieznany" + +msgid "Sync Now" +msgstr "Synchronizuj teraz" + +msgid "Synchronization completed successfully" +msgstr "Synchronizacja zakończona powodzeniem" + +msgid "Rule Name" +msgstr "Nazwa reguły" + +msgid "Priority" +msgstr "Priorytet" + +msgid "Lower numbers are processed first" +msgstr "Niższe liczby są przetwarzane jako pierwsze" + +msgid "Create Ticket" +msgstr "Utwórz zgłoszenie" + +msgid "Ticket Priority" +msgstr "Priorytet zgłoszenia" + +msgid "Very High" +msgstr "Bardzo wysoki" + +msgid "Normal" +msgstr "Normalny" + +msgid "Notify Administrators" +msgstr "Powiadom administratorów" + +msgid "Add to Vulnerability Report" +msgstr "Dodaj do raportu o podatnościach" + +msgid "Active Rule" +msgstr "Aktywna reguła" + +msgid "CVE Ticket" +msgid_plural "CVE Tickets" +msgstr[0] "Zgłoszenie CVE" +msgstr[1] "Zgłoszenia CVE" +msgstr[2] "Zgłoszeń CVE" + +msgid "Add a ticket" +msgstr "Dodaj zgłoszenie" + +msgid "Ticket" +msgstr "Zgłoszenie" + +msgid "Opening date" +msgstr "Data otwarcia" + +msgid "Creation type" +msgstr "Typ utworzenia" + +msgid "Automatic" +msgstr "Automatyczny" + +msgid "Manual" +msgstr "Ręczny" + +msgid "Add a CVE" +msgstr "Dodaj CVE" + +msgid "CVE linked to ticket" +msgstr "CVE powiązana ze zgłoszeniem" + +msgid "Software Vulnerability Analysis" +msgid_plural "Software Vulnerability Analyses" +msgstr[0] "Analiza podatności oprogramowania" +msgstr[1] "Analizy podatności oprogramowania" +msgstr[2] "Analiz podatności oprogramowania" + +msgid "Software Vulnerability Alert" +msgid_plural "Software Vulnerability Alerts" +msgstr[0] "Alert o podatności oprogramowania" +msgstr[1] "Alerty o podatności oprogramowania" +msgstr[2] "Alertów o podatności oprogramowania" + +msgid "Software" +msgstr "Oprogramowanie" + +msgid "Version" +msgstr "Wersja" + +msgid "N/A" +msgstr "N/D" + +msgid "Unknown software" +msgstr "Nieznane oprogramowanie" + +msgid "Unknown version" +msgstr "Nieznana wersja" + +msgid "Unknown CVE" +msgstr "Nieznana CVE" + +msgid "Unknown ticket" +msgstr "Nieznane zgłoszenie" + +msgid "No ticket associated" +msgstr "Brak powiązanego zgłoszenia" + +msgid "Creation Date" +msgstr "Data utworzenia" + +msgid "ID" +msgstr "ID" + +msgid "Processed" +msgstr "Przetworzona" + +msgid "Ignored" +msgstr "Ignorowana" + +msgid "CVE Statistics" +msgstr "Statystyki CVE" + +msgid "CVE Severity Distribution" +msgstr "Rozkład dotkliwości CVE" + +msgid "Recent CVEs" +msgstr "Ostatnie CVE" + +msgid "Software Vulnerability Alerts" +msgstr "Alerty o podatności oprogramowania" + +msgid "Scan Software Inventory Now" +msgstr "Skanuj inwentarz oprogramowania teraz" + +msgid "Total Vulnerability Alerts" +msgstr "Całkowita liczba alertów o podatności" + +msgid "New Alerts" +msgstr "Nowe alerty" + +msgid "Critical Vulnerabilities" +msgstr "Krytyczne podatności" + +msgid "High Vulnerabilities" +msgstr "Wysokie podatności" + +msgid "Recent Vulnerability Alerts" +msgstr "Ostatnie alerty o podatności" + +msgid "No alerts found" +msgstr "Nie znaleziono alertów" + +msgid "View all vulnerability alerts" +msgstr "Wyświetl wszystkie alerty o podatności" + +msgid "Software vulnerability analysis completed successfully." +msgstr "Analiza podatności oprogramowania zakończona powodzeniem." + +msgid "Software vulnerability analysis completed with no new alerts." +msgstr "Analiza podatności oprogramowania zakończona bez nowych alertów." + +msgid "Software vulnerability analysis task not found." +msgstr "Nie znaleziono zadania analizy podatności oprogramowania." + +msgid "A vulnerability has been detected in your software inventory" +msgstr "W inwentarzu oprogramowania wykryto podatność" + +msgid "Vulnerability" +msgstr "Podatność" + +msgid "No vulnerabilities found for this software" +msgstr "Nie znaleziono podatności dla tego oprogramowania" + +msgid "Scan for vulnerabilities now" +msgstr "Skanuj w poszukiwaniu podatności teraz" + +msgid "Vulnerabilities" +msgstr "Podatności" + +msgid "Plugin activated successfully!" +msgstr "Wtyczka aktywowana pomyślnie!" + +msgid "CVE Management plugin has been activated." +msgstr "Wtyczka zarządzania CVE została aktywowana." + +msgid "Initial CVE data is being synchronized in the background." +msgstr "Początkowe dane CVE są synchronizowane w tle." + +msgid "Software inventory will be analyzed for vulnerabilities automatically." +msgstr "Inwentarz oprogramowania zostanie automatycznie przeanalizowany pod kątem podatności." + +msgid "You can now configure the data sources and rules." +msgstr "Możesz teraz skonfigurować źródła danych i reguły." \ No newline at end of file diff --git a/locales/pt_PT.po b/locales/pt_PT.po new file mode 100644 index 0000000..78ec9f9 --- /dev/null +++ b/locales/pt_PT.po @@ -0,0 +1,330 @@ +msgid "" +msgstr "" +"Project-Id-Version: GLPI CVE Plugin 1.0.0\n" +"POT-Creation-Date: 2024-01-15 12:00+0100\n" +"PO-Revision-Date: 2024-01-15 12:00+0100\n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: pt_PT\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +msgid "Vulnérabilité" +msgstr "Vulnerabilidade" + +msgid "CVE" +msgid_plural "CVEs" +msgstr[0] "CVE" +msgstr[1] "CVEs" + +msgid "CVE ID" +msgstr "ID CVE" + +msgid "Severity" +msgstr "Severidade" + +msgid "Critical" +msgstr "Crítica" + +msgid "High" +msgstr "Alta" + +msgid "Medium" +msgstr "Média" + +msgid "Low" +msgstr "Baixa" + +msgid "CVSS Score" +msgstr "Pontuação CVSS" + +msgid "CVSS Vector" +msgstr "Vetor CVSS" + +msgid "Published Date" +msgstr "Data de publicação" + +msgid "Modified Date" +msgstr "Data de modificação" + +msgid "Status" +msgstr "Estado" + +msgid "New" +msgstr "Nova" + +msgid "Analyzed" +msgstr "Analisada" + +msgid "Assigned" +msgstr "Atribuída" + +msgid "Resolved" +msgstr "Resolvida" + +msgid "Entity" +msgstr "Entidade" + +msgid "Description" +msgstr "Descrição" + +msgid "References" +msgstr "Referências" + +msgid "Enter one URL per line" +msgstr "Insira um URL por linha" + +msgid "Affected Products" +msgstr "Produtos afetados" + +msgid "Enter one product per line" +msgstr "Insira um produto por linha" + +msgid "Creation date" +msgstr "Data de criação" + +msgid "Last update" +msgstr "Última atualização" + +msgid "Common Vulnerabilities and Exposures" +msgstr "Vulnerabilidades e exposições comuns" + +msgid "Dashboard" +msgstr "Painel" + +msgid "CVE Rule" +msgid_plural "CVE Rules" +msgstr[0] "Regra CVE" +msgstr[1] "Regras CVE" + +msgid "CVE Source" +msgid_plural "CVE Sources" +msgstr[0] "Fonte CVE" +msgstr[1] "Fontes CVE" + +msgid "Source Name" +msgstr "Nome da fonte" + +msgid "Active" +msgstr "Ativa" + +msgid "API URL" +msgstr "URL da API" + +msgid "Sync Frequency (hours)" +msgstr "Frequência de sincronização (horas)" + +msgid "API Key" +msgstr "Chave API" + +msgid "Last Sync" +msgstr "Última sincronização" + +msgid "Never" +msgstr "Nunca" + +msgid "Sync Status" +msgstr "Estado de sincronização" + +msgid "Success" +msgstr "Sucesso" + +msgid "Failed" +msgstr "Falha" + +msgid "In Progress" +msgstr "Em progresso" + +msgid "Pending" +msgstr "Pendente" + +msgid "Unknown" +msgstr "Desconhecido" + +msgid "Sync Now" +msgstr "Sincronizar agora" + +msgid "Synchronization completed successfully" +msgstr "Sincronização concluída com sucesso" + +msgid "Rule Name" +msgstr "Nome da regra" + +msgid "Priority" +msgstr "Prioridade" + +msgid "Lower numbers are processed first" +msgstr "Números mais baixos são processados primeiro" + +msgid "Create Ticket" +msgstr "Criar ticket" + +msgid "Ticket Priority" +msgstr "Prioridade do ticket" + +msgid "Very High" +msgstr "Muito alta" + +msgid "Normal" +msgstr "Normal" + +msgid "Notify Administrators" +msgstr "Notificar administradores" + +msgid "Add to Vulnerability Report" +msgstr "Adicionar ao relatório de vulnerabilidades" + +msgid "Active Rule" +msgstr "Regra ativa" + +msgid "CVE Ticket" +msgid_plural "CVE Tickets" +msgstr[0] "Ticket CVE" +msgstr[1] "Tickets CVE" + +msgid "Add a ticket" +msgstr "Adicionar um ticket" + +msgid "Ticket" +msgstr "Ticket" + +msgid "Opening date" +msgstr "Data de abertura" + +msgid "Creation type" +msgstr "Tipo de criação" + +msgid "Automatic" +msgstr "Automática" + +msgid "Manual" +msgstr "Manual" + +msgid "Add a CVE" +msgstr "Adicionar uma CVE" + +msgid "CVE linked to ticket" +msgstr "CVE ligada ao ticket" + +msgid "Software Vulnerability Analysis" +msgid_plural "Software Vulnerability Analyses" +msgstr[0] "Análise de vulnerabilidade de software" +msgstr[1] "Análises de vulnerabilidade de software" + +msgid "Software Vulnerability Alert" +msgid_plural "Software Vulnerability Alerts" +msgstr[0] "Alerta de vulnerabilidade de software" +msgstr[1] "Alertas de vulnerabilidade de software" + +msgid "Software" +msgstr "Software" + +msgid "Version" +msgstr "Versão" + +msgid "N/A" +msgstr "N/D" + +msgid "Unknown software" +msgstr "Software desconhecido" + +msgid "Unknown version" +msgstr "Versão desconhecida" + +msgid "Unknown CVE" +msgstr "CVE desconhecida" + +msgid "Unknown ticket" +msgstr "Ticket desconhecido" + +msgid "No ticket associated" +msgstr "Nenhum ticket associado" + +msgid "Creation Date" +msgstr "Data de criação" + +msgid "ID" +msgstr "ID" + +msgid "Processed" +msgstr "Processada" + +msgid "Ignored" +msgstr "Ignorada" + +msgid "CVE Statistics" +msgstr "Estatísticas CVE" + +msgid "CVE Severity Distribution" +msgstr "Distribuição de severidade CVE" + +msgid "Recent CVEs" +msgstr "CVEs recentes" + +msgid "Software Vulnerability Alerts" +msgstr "Alertas de vulnerabilidade de software" + +msgid "Scan Software Inventory Now" +msgstr "Analisar inventário de software agora" + +msgid "Total Vulnerability Alerts" +msgstr "Total de alertas de vulnerabilidade" + +msgid "New Alerts" +msgstr "Novos alertas" + +msgid "Critical Vulnerabilities" +msgstr "Vulnerabilidades críticas" + +msgid "High Vulnerabilities" +msgstr "Vulnerabilidades altas" + +msgid "Recent Vulnerability Alerts" +msgstr "Alertas de vulnerabilidade recentes" + +msgid "No alerts found" +msgstr "Nenhum alerta encontrado" + +msgid "View all vulnerability alerts" +msgstr "Ver todos os alertas de vulnerabilidade" + +msgid "Software vulnerability analysis completed successfully." +msgstr "Análise de vulnerabilidade de software concluída com sucesso." + +msgid "Software vulnerability analysis completed with no new alerts." +msgstr "Análise de vulnerabilidade de software concluída sem novos alertas." + +msgid "Software vulnerability analysis task not found." +msgstr "Tarefa de análise de vulnerabilidade de software não encontrada." + +msgid "A vulnerability has been detected in your software inventory" +msgstr "Foi detectada uma vulnerabilidade no seu inventário de software" + +msgid "Vulnerability" +msgstr "Vulnerabilidade" + +msgid "No vulnerabilities found for this software" +msgstr "Nenhuma vulnerabilidade encontrada para este software" + +msgid "Scan for vulnerabilities now" +msgstr "Analisar vulnerabilidades agora" + +msgid "Vulnerabilities" +msgstr "Vulnerabilidades" + +msgid "Plugin activated successfully!" +msgstr "Plugin ativado com sucesso!" + +msgid "CVE Management plugin has been activated." +msgstr "O plugin de gestão de CVE foi ativado." + +msgid "Initial CVE data is being synchronized in the background." +msgstr "Os dados iniciais de CVE estão a ser sincronizados em segundo plano." + +msgid "Software inventory will be analyzed for vulnerabilities automatically." +msgstr "O inventário de software será analisado automaticamente para vulnerabilidades." + +msgid "You can now configure the data sources and rules." +msgstr "Agora pode configurar as fontes de dados e as regras." \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e09d2a2 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4076 @@ +{ + "name": "glpi-cve-plugin-prototype", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "glpi-cve-plugin-prototype", + "version": "0.0.0", + "dependencies": { + "jquery": "^3.7.1", + "lucide-react": "^0.344.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@eslint/js": "^9.9.1", + "@types/jquery": "^3.5.29", + "@types/react": "^18.3.5", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "autoprefixer": "^10.4.18", + "eslint": "^9.9.1", + "eslint-plugin-react-hooks": "^5.1.0-rc.0", + "eslint-plugin-react-refresh": "^0.4.11", + "globals": "^15.9.0", + "postcss": "^8.4.35", + "tailwindcss": "^3.4.1", + "typescript": "^5.5.3", + "typescript-eslint": "^8.3.0", + "vite": "^5.4.2" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.25.7.tgz", + "integrity": "sha512-0xZJFNE5XMpENsgfHYTw8FbX4kv53mFLn2i3XPoq69LyhYSCBJtitaHx9QnsVTrsogI4Z3+HtEfZ2/GFPOtf5g==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.25.7", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.7.tgz", + "integrity": "sha512-9ickoLz+hcXCeh7jrcin+/SLWm+GkxE2kTvoYyp38p4WkdFXfQJxDFGWp/YHjiKLPx06z2A7W8XKuqbReXDzsw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.7.tgz", + "integrity": "sha512-yJ474Zv3cwiSOO9nXJuqzvwEeM+chDuQ8GJirw+pZ91sCGCyOZ3dJkVE09fTV0VEVzXyLWhh3G/AolYTPX7Mow==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.25.7", + "@babel/generator": "^7.25.7", + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helpers": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/template": "^7.25.7", + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.7.tgz", + "integrity": "sha512-5Dqpl5fyV9pIAD62yK9P7fcA768uVPUyrQmqpqstHWgMma4feF1x/oFysBCVZLY5wJ2GkMUCdsNDnGZrPoR6rA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.7", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.7.tgz", + "integrity": "sha512-DniTEax0sv6isaw6qSQSfV4gVRNtw2rte8HHM45t9ZR0xILaufBRNkpMifCRiAPyvL4ACD6v0gfCwCmtOQaV4A==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.25.7", + "@babel/helper-validator-option": "^7.25.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.7.tgz", + "integrity": "sha512-o0xCgpNmRohmnoWKQ0Ij8IdddjyBFE4T2kagL/x6M3+4zUgc+4qTOUBoNe4XxDskt1HPKO007ZPiMgLDq2s7Kw==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.7.tgz", + "integrity": "sha512-k/6f8dKG3yDz/qCwSM+RKovjMix563SLxQFo0UhRNo239SP6n9u5/eLtKD6EAjwta2JHJ49CsD8pms2HdNiMMQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.25.7", + "@babel/helper-simple-access": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "@babel/traverse": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.25.7.tgz", + "integrity": "sha512-eaPZai0PiqCi09pPs3pAFfl/zYgGaE6IdXtYvmf0qlcDTd3WCtO7JWCcRd64e0EQrcYgiHibEZnOGsSY4QSgaw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.25.7.tgz", + "integrity": "sha512-FPGAkJmyoChQeM+ruBGIDyrT2tKfZJO8NcxdC+CWNJi7N8/rZpSxK7yvBJ5O/nF1gfu5KzN7VKG3YVSLFfRSxQ==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.7.tgz", + "integrity": "sha512-CbkjYdsJNHFk8uqpEkpCvRs3YRp9tY6FmFY7wLMSYuGYkrdUi7r2lc4/wqsvlHoMznX3WJ9IP8giGPq68T/Y6g==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.7.tgz", + "integrity": "sha512-AM6TzwYqGChO45oiuPqwL2t20/HdMC1rTPAesnBCgPCSF1x3oN9MVUwQV2iyz4xqWrctwK5RNC8LV22kaQCNYg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.7.tgz", + "integrity": "sha512-ytbPLsm+GjArDYXJ8Ydr1c/KJuutjF2besPNbIZnZ6MKUxi/uTA22t2ymmA4WFjZFpjiAMO0xuuJPqK2nvDVfQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.25.7.tgz", + "integrity": "sha512-Sv6pASx7Esm38KQpF/U/OXLwPPrdGHNKoeblRxgZRLXnAtnkEe4ptJPDtAZM7fBLadbc1Q07kQpSiGQ0Jg6tRA==", + "dev": true, + "dependencies": { + "@babel/template": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.7.tgz", + "integrity": "sha512-iYyACpW3iW8Fw+ZybQK+drQre+ns/tKpXbNESfrhNnPLIklLbXr7MYJ6gPEd0iETGLOK+SxMjVvKb/ffmk+FEw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.7", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.7.tgz", + "integrity": "sha512-aZn7ETtQsjjGG5HruveUK06cU3Hljuhd9Iojm4M8WWv3wLE6OkE5PWbDUkItmMgegmccaITudyuW5RPYrYlgWw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.25.7.tgz", + "integrity": "sha512-JD9MUnLbPL0WdVK8AWC7F7tTG2OS6u/AKKnsK+NdRhUiVdnzyR1S3kKQCaRLOiaULvUiqK6Z4JQE635VgtCFeg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.25.7.tgz", + "integrity": "sha512-S/JXG/KrbIY06iyJPKfxr0qRxnhNOdkNXYBl/rmwgDd72cQLH9tEGkDm/yJPGvcSIUoikzfjMios9i+xT/uv9w==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.7.tgz", + "integrity": "sha512-wRwtAgI3bAS+JGU2upWNL9lSlDcRCqD05BZ1n3X2ONLH1WilFP6O1otQjeMK/1g0pvYcXC7b/qVUB1keofjtZA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.7.tgz", + "integrity": "sha512-jatJPT1Zjqvh/1FyJs6qAHL+Dzb7sTb+xr7Q+gM1b+1oBsMsQQ4FkVKb6dFlJvLlVssqkRzV05Jzervt9yhnzg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.25.7", + "@babel/generator": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/template": "^7.25.7", + "@babel/types": "^7.25.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.7.tgz", + "integrity": "sha512-vwIVdXG+j+FOpkwqHRcBgHLYNL7XMkufrlaFvL9o6Ai9sJn9+PdyIL5qa0XzTZw084c+u9LOls53eoZWP/W5WQ==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.25.7", + "@babel/helper-validator-identifier": "^7.25.7", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.1.tgz", + "integrity": "sha512-m4DVN9ZqskZoLU5GlWZadwDnYo3vAEydiUayB9widCl9ffWx2IvPnp6n3on5rJmziJSw9Bv+Z3ChDVdMwXCY8Q==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz", + "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.6.0.tgz", + "integrity": "sha512-8I2Q8ykA4J0x0o7cg67FPVnehcqWTBehu/lmY+bolPFHGjh49YzGBMXTvpqVgEbBdvNCSxj6iFgiIyHzf03lzg==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.12.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.12.0.tgz", + "integrity": "sha512-eohesHH8WFRUprDNyEREgqP6beG6htMeUYeCpkEgBCieCMme5r9zFWjzAJp//9S+Kub4rqE+jXe9Cp1a7IYIIA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.0.tgz", + "integrity": "sha512-vH9PiIMMwvhCx31Af3HiGzsVNULDbyVkHXwlemn/B0TFj/00ho3y55efXrUZTfQipxoHC5u4xq6zblww1zm1Ig==", + "dev": true, + "dependencies": { + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.0.tgz", + "integrity": "sha512-2cbWIHbZVEweE853g8jymffCA+NCMiuqeECeBBLm8dg2oFdjuGJhgN4UAbI+6v0CKbbhvtXA4qV8YR5Ji86nmw==", + "dev": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.5", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.5.tgz", + "integrity": "sha512-KSPA4umqSG4LHYRodq31VDwKAvaTF4xmVlzM8Aeh4PlU1JQ3IG0wiA8C25d3RQ9nJyM3mBHyI53K06VVL/oFFg==", + "dev": true, + "dependencies": { + "@humanfs/core": "^0.19.0", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.24.0.tgz", + "integrity": "sha512-Q6HJd7Y6xdB48x8ZNVDOqsbh2uByBhgK8PiQgPhwkIw/HC/YX5Ghq2mQY5sRMZWHb3VsFkWooUVOZHKr7DmDIA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.24.0.tgz", + "integrity": "sha512-ijLnS1qFId8xhKjT81uBHuuJp2lU4x2yxa4ctFPtG+MqEE6+C5f/+X/bStmxapgmwLwiL3ih122xv8kVARNAZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.24.0.tgz", + "integrity": "sha512-bIv+X9xeSs1XCk6DVvkO+S/z8/2AMt/2lMqdQbMrmVpgFvXlmde9mLcbQpztXm1tajC3raFDqegsH18HQPMYtA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.24.0.tgz", + "integrity": "sha512-X6/nOwoFN7RT2svEQWUsW/5C/fYMBe4fnLK9DQk4SX4mgVBiTA9h64kjUYPvGQ0F/9xwJ5U5UfTbl6BEjaQdBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.24.0.tgz", + "integrity": "sha512-0KXvIJQMOImLCVCz9uvvdPgfyWo93aHHp8ui3FrtOP57svqrF/roSSR5pjqL2hcMp0ljeGlU4q9o/rQaAQ3AYA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.24.0.tgz", + "integrity": "sha512-it2BW6kKFVh8xk/BnHfakEeoLPv8STIISekpoF+nBgWM4d55CZKc7T4Dx1pEbTnYm/xEKMgy1MNtYuoA8RFIWw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.24.0.tgz", + "integrity": "sha512-i0xTLXjqap2eRfulFVlSnM5dEbTVque/3Pi4g2y7cxrs7+a9De42z4XxKLYJ7+OhE3IgxvfQM7vQc43bwTgPwA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.24.0.tgz", + "integrity": "sha512-9E6MKUJhDuDh604Qco5yP/3qn3y7SLXYuiC0Rpr89aMScS2UAmK1wHP2b7KAa1nSjWJc/f/Lc0Wl1L47qjiyQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.24.0.tgz", + "integrity": "sha512-2XFFPJ2XMEiF5Zi2EBf4h73oR1V/lycirxZxHZNc93SqDN/IWhYYSYj8I9381ikUFXZrz2v7r2tOVk2NBwxrWw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.24.0.tgz", + "integrity": "sha512-M3Dg4hlwuntUCdzU7KjYqbbd+BLq3JMAOhCKdBE3TcMGMZbKkDdJ5ivNdehOssMCIokNHFOsv7DO4rlEOfyKpg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.24.0.tgz", + "integrity": "sha512-mjBaoo4ocxJppTorZVKWFpy1bfFj9FeCMJqzlMQGjpNPY9JwQi7OuS1axzNIk0nMX6jSgy6ZURDZ2w0QW6D56g==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.24.0.tgz", + "integrity": "sha512-ZXFk7M72R0YYFN5q13niV0B7G8/5dcQ9JDp8keJSfr3GoZeXEoMHP/HlvqROA3OMbMdfr19IjCeNAnPUG93b6A==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.24.0.tgz", + "integrity": "sha512-w1i+L7kAXZNdYl+vFvzSZy8Y1arS7vMgIy8wusXJzRrPyof5LAb02KGr1PD2EkRcl73kHulIID0M501lN+vobQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.24.0.tgz", + "integrity": "sha512-VXBrnPWgBpVDCVY6XF3LEW0pOU51KbaHhccHw6AS6vBWIC60eqsH19DAeeObl+g8nKAz04QFdl/Cefta0xQtUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.24.0.tgz", + "integrity": "sha512-xrNcGDU0OxVcPTH/8n/ShH4UevZxKIO6HJFK0e15XItZP2UcaiLFd5kiX7hJnqCbSztUF8Qot+JWBC/QXRPYWQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.24.0.tgz", + "integrity": "sha512-fbMkAF7fufku0N2dE5TBXcNlg0pt0cJue4xBRE2Qc5Vqikxr4VCgKj/ht6SMdFcOacVA9rqF70APJ8RN/4vMJw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.8", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.8.tgz", + "integrity": "sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.6.tgz", + "integrity": "sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true + }, + "node_modules/@types/jquery": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.32.tgz", + "integrity": "sha512-b9Xbf4CkMqS02YH8zACqN1xzdxc3cO735Qe5AbSUFmyOiaWAbcpqh9Wna+Uk0vgACvoQHpWDg2rGdHkYPLmCiQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sizzle": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/prop-types": { + "version": "15.7.13", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", + "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.3.11", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.11.tgz", + "integrity": "sha512-r6QZ069rFTjrEYgFdOck1gK7FLVsgJE7tTz0pQBczlBNUhBNk0MQH4UbnFSwjpQLMkLzgqvBBa+qGpLje16eTQ==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/sizzle": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.9.tgz", + "integrity": "sha512-xzLEyKB50yqCUPUJkIsrVvoWNfFUbIZI+RspLWt8u+tIW/BetMBZtgV2LY/2o+tYH8dRvQ+eoPf3NdhQCcLE2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.1.tgz", + "integrity": "sha512-xfvdgA8AP/vxHgtgU310+WBnLB4uJQ9XdyP17RebG26rLtDrQJV3ZYrcopX91GrHmMoH8bdSwMRh2a//TiJ1jQ==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.8.1", + "@typescript-eslint/type-utils": "8.8.1", + "@typescript-eslint/utils": "8.8.1", + "@typescript-eslint/visitor-keys": "8.8.1", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.8.1.tgz", + "integrity": "sha512-hQUVn2Lij2NAxVFEdvIGxT9gP1tq2yM83m+by3whWFsWC+1y8pxxxHUFE1UqDu2VsGi2i6RLcv4QvouM84U+ow==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.8.1", + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/typescript-estree": "8.8.1", + "@typescript-eslint/visitor-keys": "8.8.1", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.8.1.tgz", + "integrity": "sha512-X4JdU+66Mazev/J0gfXlcC/dV6JI37h+93W9BRYXrSn0hrE64IoWgVkO9MSJgEzoWkxONgaQpICWg8vAN74wlA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/visitor-keys": "8.8.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.8.1.tgz", + "integrity": "sha512-qSVnpcbLP8CALORf0za+vjLYj1Wp8HSoiI8zYU5tHxRVj30702Z1Yw4cLwfNKhTPWp5+P+k1pjmD5Zd1nhxiZA==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "8.8.1", + "@typescript-eslint/utils": "8.8.1", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.8.1.tgz", + "integrity": "sha512-WCcTP4SDXzMd23N27u66zTKMuEevH4uzU8C9jf0RO4E04yVHgQgW+r+TeVTNnO1KIfrL8ebgVVYYMMO3+jC55Q==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.8.1.tgz", + "integrity": "sha512-A5d1R9p+X+1js4JogdNilDuuq+EHZdsH9MjTVxXOdVFfTJXunKJR/v+fNNyO4TnoOn5HqobzfRlc70NC6HTcdg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/visitor-keys": "8.8.1", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.8.1.tgz", + "integrity": "sha512-/QkNJDbV0bdL7H7d0/y0qBbV2HTtf0TIyjSDTvvmQEzeVx8jEImEbLuOA4EsvE8gIgqMitns0ifb5uQhMj8d9w==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.8.1", + "@typescript-eslint/types": "8.8.1", + "@typescript-eslint/typescript-estree": "8.8.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.8.1.tgz", + "integrity": "sha512-0/TdC3aeRAsW7MDvYRwEc1Uwm0TIBfzjPFgg60UU2Haj5qsCs9cc3zNgY71edqE3LbWfF/WoZQd3lJoDXFQpag==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.8.1", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.3.2.tgz", + "integrity": "sha512-hieu+o05v4glEBucTcKMK3dlES0OeJlD9YVOAPraVMOInBCwzumaIFiUjr4bHK7NPgnAHgiskUoceKercrN8vg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/plugin-transform-react-jsx-self": "^7.24.7", + "@babel/plugin-transform-react-jsx-source": "^7.24.7", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.14.2" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0" + } + }, + "node_modules/acorn": { + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/autoprefixer": { + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.0.tgz", + "integrity": "sha512-Rmb62sR1Zpjql25eSanFGEhAxcFwfA1K0GuQcLoaJBAcENegrQut3hYdhXFF1obQfiDyqIW/cLM5HSJ/9k884A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001663", + "electron-to-chromium": "^1.5.28", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001667", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001667.tgz", + "integrity": "sha512-7LTwJjcRkzKFmtqGsibMeuXmvFDfZq/nzIjnmgCGzKKRVzjD72selLDK1oPF/Oxzmt4fNcPvTDvGqSDG4tCALw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.5.33", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.33.tgz", + "integrity": "sha512-+cYTcFB1QqD4j4LegwLfpCNxifb6dDFUAwk6RsLusCwIaZI6or2f+q8rs5tTB2YC53HhOlIbEaqHMAAC8IOIwA==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint": { + "version": "9.12.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.12.0.tgz", + "integrity": "sha512-UVIOlTEWxwIopRL1wgSQYdnVDcEvs2wyaO6DGo5mXqe3r16IoCNWkR29iHhyaP4cICWjbgbmFUGAhh0GJRuGZw==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.11.0", + "@eslint/config-array": "^0.18.0", + "@eslint/core": "^0.6.0", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.12.0", + "@eslint/plugin-kit": "^0.2.0", + "@humanfs/node": "^0.16.5", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.1", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.1.0", + "eslint-visitor-keys": "^4.1.0", + "espree": "^10.2.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "5.1.0-rc-fb9a90fa48-20240614", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.1.0-rc-fb9a90fa48-20240614.tgz", + "integrity": "sha512-xsiRwaDNF5wWNC4ZHLut+x/YcAxksUd9Rizt7LaEn3bV8VyYRpXnRJQlLOfYaVy9esk4DFP4zPPnoNVjq5Gc0w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-react-refresh": { + "version": "0.4.12", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.12.tgz", + "integrity": "sha512-9neVjoGv20FwYtCP6CB1dzR1vr57ZDNOXst21wd2xJ/cTlM2xLq0GWVlSNTdMn/4BtP6cHYBMCSp1wFBJ9jBsg==", + "dev": true, + "peerDependencies": { + "eslint": ">=7" + } + }, + "node_modules/eslint-scope": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.1.0.tgz", + "integrity": "sha512-14dSvlhaVhKKsa9Fx1l8A17s7ah7Ef7wCakJ10LYk6+GYmP9yDti2oq2SEwcyndt6knfcZyhyxwY3i9yL78EQw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.1.0.tgz", + "integrity": "sha512-Q7lok0mqMUSf5a/AdAZkA5a/gHcO6snwQClVNNvFKCAVlxXucdU8pKydU5ZVZjBx5xr37vGbFFWtLQYreLzrZg==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/espree": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.2.0.tgz", + "integrity": "sha512-upbkBJbckcCNBDBDXEbuhjbP68n+scUd3k/U2EkyM9nw+I/jPiL4cLF/Al06CF96wRltFda16sxDFrxsI1v0/g==", + "dev": true, + "dependencies": { + "acorn": "^8.12.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "15.11.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.11.0.tgz", + "integrity": "sha512-yeyNSjdbyVaWurlwCpcA6XNBrHTMIeDdj0/hnvX/OLJ9ekOXYbLsLinH/MucQyGvNnXhidTdNhTtJaffL2sMfw==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", + "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.6", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", + "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/jquery": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", + "license": "MIT" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lucide-react": { + "version": "0.344.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.344.0.tgz", + "integrity": "sha512-6YyBnn91GB45VuVT96bYCOKElbJzUHqp65vX8cDcu55MQL9T969v4dhGClpljamuI/+KMO9P6w9Acq1CVQGvIQ==", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", + "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.24.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.24.0.tgz", + "integrity": "sha512-DOmrlGSXNk1DM0ljiQA+i+o0rSLhtii1je5wgk60j49d1jHT5YYttBv1iWOnYSTG+fZZESUOSNiAl89SIet+Cg==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.24.0", + "@rollup/rollup-android-arm64": "4.24.0", + "@rollup/rollup-darwin-arm64": "4.24.0", + "@rollup/rollup-darwin-x64": "4.24.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.24.0", + "@rollup/rollup-linux-arm-musleabihf": "4.24.0", + "@rollup/rollup-linux-arm64-gnu": "4.24.0", + "@rollup/rollup-linux-arm64-musl": "4.24.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.24.0", + "@rollup/rollup-linux-riscv64-gnu": "4.24.0", + "@rollup/rollup-linux-s390x-gnu": "4.24.0", + "@rollup/rollup-linux-x64-gnu": "4.24.0", + "@rollup/rollup-linux-x64-musl": "4.24.0", + "@rollup/rollup-win32-arm64-msvc": "4.24.0", + "@rollup/rollup-win32-ia32-msvc": "4.24.0", + "@rollup/rollup-win32-x64-msvc": "4.24.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.13", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.13.tgz", + "integrity": "sha512-KqjHOJKogOUt5Bs752ykCeiwvi0fKVkr5oqsFNt/8px/tA8scFPIlkygsf6jXrfCqGHz7VflA6+yytWuM+XhFw==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.0", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", + "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.8.1.tgz", + "integrity": "sha512-R0dsXFt6t4SAFjUSKFjMh4pXDtq04SsFKCVGDP3ZOzNP7itF0jBcZYU4fMsZr4y7O7V7Nc751dDeESbe4PbQMQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.8.1", + "@typescript-eslint/parser": "8.8.1", + "@typescript-eslint/utils": "8.8.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/vite": { + "version": "5.4.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz", + "integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yaml": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", + "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..7cef551 --- /dev/null +++ b/package.json @@ -0,0 +1,35 @@ +{ + "name": "glpi-cve-plugin-prototype", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "jquery": "^3.7.1", + "lucide-react": "^0.344.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@eslint/js": "^9.9.1", + "@types/jquery": "^3.5.29", + "@types/react": "^18.3.5", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "autoprefixer": "^10.4.18", + "eslint": "^9.9.1", + "eslint-plugin-react-hooks": "^5.1.0-rc.0", + "eslint-plugin-react-refresh": "^0.4.11", + "globals": "^15.9.0", + "postcss": "^8.4.35", + "tailwindcss": "^3.4.1", + "typescript": "^5.5.3", + "typescript-eslint": "^8.3.0", + "vite": "^5.4.2" + } +} \ No newline at end of file diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/public/cve_dashboard.html b/public/cve_dashboard.html new file mode 100644 index 0000000..409cab1 --- /dev/null +++ b/public/cve_dashboard.html @@ -0,0 +1,488 @@ + + + + + + GLPI CVE Plugin + + + + + + + + +
+ +
+
+

CVE Management Plugin

+

Manage and track Common Vulnerabilities and Exposures

+
+
+ + +
+ + Dashboard + + + CVE Management + + + Data Sources + + + Rules + +
+ + +
+ +
+
+
+ +
+
+
Critical Vulnerabilities
+
0
+
+
+
+
+
+ +
+
+ +
+
+
High Risk Vulnerabilities
+
0
+
+
+
+
+
+ +
+
+ +
+
+
Pending Analysis
+
0
+
+
+
+
+
+ +
+
+ +
+
+
Resolved Issues
+
0
+
+
+
+
+
+
+ + +
+
+ Recent Vulnerabilities +
+
+
+ + + + + + + + + + + + + +
CVE IDSeverityCVSSPublishedStatus
+
+
+
+ + +
+
+ Severity Distribution +
+
+
+
+
+
CRITICAL
+
0
+
+
+
+
HIGH
+
0
+
+
+
+
MEDIUM
+
0
+
+
+
+
LOW
+
0
+
+
+
+

Total vulnerabilities: 0

+
+
+
+
+ + + + + + + + + +
+ + + + + + + \ No newline at end of file diff --git a/public/cve_list.html b/public/cve_list.html new file mode 100644 index 0000000..3e4d368 --- /dev/null +++ b/public/cve_list.html @@ -0,0 +1,151 @@ + + + + + + GLPI CVE Plugin - CVE List + + + + + + + + +
+ +
+
+

CVE Management

+

Browse and manage Common Vulnerabilities and Exposures

+
+
+ + +
+
+ + +
+ +
+ + +
+ +
+ + +
+
+ + +
+
+ CVE List +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CVE IDDescriptionSeverityCVSSPublishedStatusActions
CVE-2024-1234A remote code execution vulnerability in Apache Log4j library affecting versions 2.0-beta9 to 2.14.1CRITICAL10.02024-05-12NEW + + + +
CVE-2024-5678SQL Injection vulnerability in WordPress plugin Contact Form 7 versions prior to 5.7.2HIGH8.82024-05-10ANALYZED + + + +
CVE-2024-9012Privilege escalation vulnerability in Microsoft Windows 11 affecting kernel-mode driversHIGH7.82024-05-08ASSIGNED + + + +
CVE-2024-3456Cross-site scripting (XSS) vulnerability in jQuery UI Dialog component prior to version 1.13.2MEDIUM5.42024-05-05RESOLVED + + + +
CVE-2024-7890Information disclosure vulnerability in OpenSSL 3.0.0 through 3.1.1LOW3.72024-05-02ANALYZED + + + +
+
+
+
+
+ + + + + + + \ No newline at end of file diff --git a/scripts/analyze_inventory.php b/scripts/analyze_inventory.php new file mode 100644 index 0000000..e65b1dd --- /dev/null +++ b/scripts/analyze_inventory.php @@ -0,0 +1,57 @@ +#!/usr/bin/env php +getFromDBbyName('PluginCveCveInventory', 'AnalyzeInventory')) { + echo "Starting software vulnerability analysis...\n"; + + $result = PluginCveCveInventory::cronAnalyzeInventory($task); + + if ($result) { + echo "Software vulnerability analysis completed successfully.\n"; + exit(0); + } else { + echo "Software vulnerability analysis completed with no new alerts.\n"; + exit(0); + } +} else { + echo "Software vulnerability analysis task not found.\n"; + exit(1); +} \ No newline at end of file diff --git a/scripts/cleanup.php b/scripts/cleanup.php new file mode 100644 index 0000000..a1fa27b --- /dev/null +++ b/scripts/cleanup.php @@ -0,0 +1,57 @@ +#!/usr/bin/env php +getFromDBbyName('PluginCveCve', 'CleanOldCVEs')) { + echo "Starting CVE cleanup...\n"; + + $result = PluginCveCve::cronCleanOldCVEs($task); + + if ($result) { + echo "CVE cleanup completed successfully.\n"; + exit(0); + } else { + echo "CVE cleanup failed or no data to clean.\n"; + exit(1); + } +} else { + echo "CVE cleanup task not found.\n"; + exit(1); +} \ No newline at end of file diff --git a/scripts/sync_cve.php b/scripts/sync_cve.php new file mode 100644 index 0000000..1b989bf --- /dev/null +++ b/scripts/sync_cve.php @@ -0,0 +1,57 @@ +#!/usr/bin/env php +getFromDBbyName('PluginCveCveSource', 'SyncCVESources')) { + echo "Starting CVE synchronization...\n"; + + $result = PluginCveCveSource::cronSyncSources($task); + + if ($result) { + echo "CVE synchronization completed successfully.\n"; + exit(0); + } else { + echo "CVE synchronization failed or no sources to sync.\n"; + exit(1); + } +} else { + echo "CVE synchronization task not found.\n"; + exit(1); +} \ No newline at end of file diff --git a/setup.php b/setup.php new file mode 100644 index 0000000..4a9a8e8 --- /dev/null +++ b/setup.php @@ -0,0 +1,403 @@ + + ['PluginCveCveTicket', 'addEvents']]; + + // Add in database synchronization + $PLUGIN_HOOKS['plugin_datainjection_populate']['cve'] = ['PluginCveCve' => + 'plugin_datainjection_populate_cve']; + + // Menu entry - Add to "Tools" menu only + $PLUGIN_HOOKS['menu_toadd']['cve'] = [ + 'tools' => 'PluginCveCveMenu' + ]; + + // Dashboard widgets + $PLUGIN_HOOKS['dashboard_cards']['cve'] = 'plugin_cve_dashboard_cards'; + + // Cron tasks registration + CronTask::register('PluginCveCveSource', 'SyncCVESources', 6 * HOUR_TIMESTAMP, + ['mode' => CronTask::MODE_EXTERNAL]); + CronTask::register('PluginCveCve', 'CleanOldCVEs', 7 * DAY_TIMESTAMP, + ['mode' => CronTask::MODE_EXTERNAL]); + CronTask::register('PluginCveCveInventory', 'AnalyzeInventory', 12 * HOUR_TIMESTAMP, + ['mode' => CronTask::MODE_EXTERNAL]); + + // Item events (hooks) + $PLUGIN_HOOKS['item_add']['cve'] = ['Ticket' => 'plugin_cve_item_add_ticket']; + $PLUGIN_HOOKS['item_update']['cve'] = ['Ticket' => 'plugin_cve_item_update_ticket']; + $PLUGIN_HOOKS['pre_item_purge']['cve'] = ['Ticket' => 'plugin_cve_pre_item_purge_ticket']; + + // Software inventory hook + $PLUGIN_HOOKS['item_add']['cve']['Software'] = 'plugin_cve_item_add_software'; + $PLUGIN_HOOKS['item_add']['cve']['SoftwareVersion'] = 'plugin_cve_item_add_softwareversion'; + + return true; +} + +/** + * Handler for software addition + * + * @param Software $software Software that was added + */ +function plugin_cve_item_add_software(Software $software) { + // Schedule a vulnerability analysis when new software is added + $task = new CronTask(); + if ($task->getFromDBbyName('PluginCveCveInventory', 'AnalyzeInventory')) { + $task->update([ + 'id' => $task->fields['id'], + 'state' => CronTask::STATE_WAITING + ]); + } +} + +/** + * Handler for software version addition + * + * @param SoftwareVersion $version Software version that was added + */ +function plugin_cve_item_add_softwareversion(SoftwareVersion $version) { + // Schedule a vulnerability analysis when new software version is added + $task = new CronTask(); + if ($task->getFromDBbyName('PluginCveCveInventory', 'AnalyzeInventory')) { + $task->update([ + 'id' => $task->fields['id'], + 'state' => CronTask::STATE_WAITING + ]); + } +} + +/** + * Plugin version information + * + * @return array Plugin version info + */ +function plugin_version_cve() { + return [ + 'name' => __('Vulnérabilité', 'cve'), + 'version' => PLUGIN_CVE_VERSION, + 'author' => 'Your Organization', + 'license' => 'GPL v2+', + 'homepage' => 'https://github.com/your-org/glpi-cve-plugin', + 'requirements' => [ + 'glpi' => [ + 'min' => PLUGIN_CVE_MIN_GLPI, + 'max' => PLUGIN_CVE_MAX_GLPI, + 'dev' => false + ], + 'php' => [ + 'min' => '7.4', + 'max' => '8.2', + 'required' => true + ] + ] + ]; +} + +/** + * Plugin initialization check + * + * @return boolean + */ +function plugin_cve_check_prerequisites() { + // Check GLPI version + if (version_compare(GLPI_VERSION, PLUGIN_CVE_MIN_GLPI, 'lt') || + version_compare(GLPI_VERSION, PLUGIN_CVE_MAX_GLPI, 'gt')) { + echo "This plugin requires GLPI >= " . PLUGIN_CVE_MIN_GLPI . " and GLPI <= " . PLUGIN_CVE_MAX_GLPI; + return false; + } + + // Check curl extension + if (!extension_loaded('curl')) { + echo "This plugin requires the PHP cURL extension"; + return false; + } + + return true; +} + +/** + * Plugin check config + * + * @return boolean + */ +function plugin_cve_check_config() { + return true; +} + +/** + * Plugin installation process + * + * @return boolean + */ +function plugin_cve_install() { + global $DB; + + $migration = new Migration(PLUGIN_CVE_VERSION); + + // Install core tables + PluginCveCve::install($migration); + PluginCveCveSource::install($migration); + PluginCveCveRule::install($migration); + PluginCveCveTicket::install($migration); + PluginCveCveInventory::install($migration); + PluginCveCveAlert::install($migration); + + // Create default notification templates + // This would be implemented here... + + $migration->executeMigration(); + + // Schedule the initial CVE data download as a background task + $task = new CronTask(); + + // Check if the synchronization task is already registered + $taskId = $task->find(['itemtype' => 'PluginCveCveSource', 'name' => 'SyncCVESources']); + + if (count($taskId) == 0) { + // Register the task if it doesn't exist + $taskId = $task->add([ + 'itemtype' => 'PluginCveCveSource', + 'name' => 'SyncCVESources', + 'frequency' => 6 * HOUR_TIMESTAMP, + 'param' => null, + 'state' => CronTask::STATE_WAITING, + 'mode' => CronTask::MODE_EXTERNAL, + 'allowmode' => CronTask::MODE_EXTERNAL, + 'hourmin' => 0, + 'hourmax' => 24, + 'logs_lifetime' => 30, + 'date_creation' => $_SESSION['glpi_currenttime'], + 'comment' => 'Synchronize CVE data from external sources' + ]); + } else { + $taskData = array_shift($taskId); + $taskId = $taskData['id']; + } + + // Register the inventory analysis task + $inventoryTaskId = $task->find(['itemtype' => 'PluginCveCveInventory', 'name' => 'AnalyzeInventory']); + + if (count($inventoryTaskId) == 0) { + // Register the task if it doesn't exist + $inventoryTaskId = $task->add([ + 'itemtype' => 'PluginCveCveInventory', + 'name' => 'AnalyzeInventory', + 'frequency' => 12 * HOUR_TIMESTAMP, + 'param' => null, + 'state' => CronTask::STATE_WAITING, + 'mode' => CronTask::MODE_EXTERNAL, + 'allowmode' => CronTask::MODE_EXTERNAL, + 'hourmin' => 0, + 'hourmax' => 24, + 'logs_lifetime' => 30, + 'date_creation' => $_SESSION['glpi_currenttime'], + 'comment' => 'Analyze software inventory for vulnerabilities' + ]); + } else { + $taskData = array_shift($inventoryTaskId); + $inventoryTaskId = $taskData['id']; + } + + // Force run the task immediately to download initial CVE data + if ($taskId > 0) { + // Log the action + Toolbox::logInFile('cve_plugin', 'Initial CVE data synchronization scheduled'); + + // Queue the task for immediate execution + $task->getFromDB($taskId); + $task->update([ + 'id' => $taskId, + 'state' => CronTask::STATE_WAITING + ]); + + // Start background sync process via AJAX or CLI + $syncUrl = Plugin::getWebDir('cve') . '/ajax/sync_now.php'; + + // Use asynchronous curl request to start the sync process + plugin_cve_async_request($syncUrl); + } + + // Schedule initial software inventory analysis + if ($inventoryTaskId > 0) { + // Log the action + Toolbox::logInFile('cve_plugin', 'Initial software vulnerability analysis scheduled'); + + // Queue the task for immediate execution after CVE data is synchronized + $task->getFromDB($inventoryTaskId); + $task->update([ + 'id' => $inventoryTaskId, + 'state' => CronTask::STATE_WAITING + ]); + } + + return true; +} + +/** + * Make an asynchronous HTTP request + * + * @param string $url URL to request + * @return boolean Success + */ +function plugin_cve_async_request($url) { + $ch = curl_init(); + + curl_setopt($ch, CURLOPT_URL, $url); + curl_setopt($ch, CURLOPT_HEADER, 0); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, 1); + curl_setopt($ch, CURLOPT_NOSIGNAL, 1); + + // Add security token to the request + $params = [ + '_glpi_csrf_token' => Session::getNewCSRFToken() + ]; + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($params)); + + curl_exec($ch); + curl_close($ch); + + return true; +} + +/** + * Plugin uninstallation process + * + * @return boolean + */ +function plugin_cve_uninstall() { + global $DB; + + $migration = new Migration(PLUGIN_CVE_VERSION); + + // Remove cron tasks + $crontask = new CronTask(); + $crontask->deleteByCriteria(['itemtype' => 'PluginCveCveSource', 'name' => 'SyncCVESources']); + $crontask->deleteByCriteria(['itemtype' => 'PluginCveCve', 'name' => 'CleanOldCVEs']); + $crontask->deleteByCriteria(['itemtype' => 'PluginCveCveInventory', 'name' => 'AnalyzeInventory']); + + // Uninstall core tables + PluginCveCve::uninstall($migration); + PluginCveCveSource::uninstall($migration); + PluginCveCveRule::uninstall($migration); + PluginCveCveTicket::uninstall($migration); + PluginCveCveInventory::uninstall($migration); + PluginCveCveAlert::uninstall($migration); + + // Remove user preferences + $query = "DELETE FROM `glpi_configs` WHERE `context` = 'plugin:cve'"; + $DB->query($query); + + $migration->executeMigration(); + + return true; +} + +/** + * Define dropdown relations + * + * @return array + */ +function plugin_cve_getDatabaseRelations() { + $plugin = new Plugin(); + + if ($plugin->isActivated('cve')) { + return [ + "glpi_entities" => [ + "glpi_plugin_cve_cves" => "entities_id", + "glpi_plugin_cve_alerts" => "entities_id" + ], + "glpi_tickets" => [ + "glpi_plugin_cve_tickets" => "tickets_id", + "glpi_plugin_cve_alerts" => "tickets_id" + ], + "glpi_softwares" => [ + "glpi_plugin_cve_alerts" => "softwares_id" + ], + "glpi_softwareversions" => [ + "glpi_plugin_cve_alerts" => "softwareversions_id" + ] + ]; + } + + return []; +} + +/** + * Define database table relations + * + * @return array + */ +function plugin_cve_getTablesNames() { + $plugin = new Plugin(); + + $tables = []; + if ($plugin->isActivated('cve')) { + $tables = [ + 'glpi_plugin_cve_cves', + 'glpi_plugin_cve_sources', + 'glpi_plugin_cve_rules', + 'glpi_plugin_cve_tickets', + 'glpi_plugin_cve_alerts' + ]; + } + + return $tables; +} + +/** + * Display a nice message when activated + */ +function plugin_cve_postinit() { + global $PLUGIN_HOOKS; + + // Add nice message when activated for the first time + if (isset($_GET['activate']) && $_GET['activate'] === 'cve') { + Html::displayTitle(__('Plugin activated successfully!', 'cve'), + __('CVE Management plugin has been activated.', 'cve') . '
' . + __('Initial CVE data is being synchronized in the background.', 'cve') . '
' . + __('Software inventory will be analyzed for vulnerabilities automatically.', 'cve') . '
' . + __('You can now configure the data sources and rules.', 'cve')); + } +} \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..cc71fb5 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,79 @@ +import React, { useState } from 'react'; +import { LayoutDashboard, Shield, Settings, Database, AlertCircle } from 'lucide-react'; +import Dashboard from './components/Dashboard'; +import CVEList from './components/CVEList'; +import SourcesConfig from './components/SourcesConfig'; +import RulesConfig from './components/RulesConfig'; + +function App() { + const [activeTab, setActiveTab] = useState('dashboard'); + + const navigation = [ + { id: 'dashboard', name: 'Dashboard', icon: }, + { id: 'cve', name: 'CVE Management', icon: }, + { id: 'sources', name: 'Data Sources', icon: }, + { id: 'rules', name: 'Rules', icon: }, + ]; + + return ( +
+ {/* Sidebar */} +
+
+ + GLPI CVE Plugin +
+ +
+ + {/* Mobile navbar */} +
+
+ + GLPI CVE Plugin +
+
+ + {/* Mobile navigation */} +
+ {navigation.map((item) => ( + + ))} +
+ + {/* Main content */} +
+ {activeTab === 'dashboard' && } + {activeTab === 'cve' && } + {activeTab === 'sources' && } + {activeTab === 'rules' && } +
+
+ ); +} + +export default App; \ No newline at end of file diff --git a/src/components/CVEList.tsx b/src/components/CVEList.tsx new file mode 100644 index 0000000..14eefe4 --- /dev/null +++ b/src/components/CVEList.tsx @@ -0,0 +1,208 @@ +import React, { useState } from 'react'; +import { Search, Filter, ChevronDown, ChevronUp, ExternalLink } from 'lucide-react'; +import { mockCVEs } from '../data/mockData'; +import { CVE } from '../types/cve'; + +const CVEList: React.FC = () => { + const [searchTerm, setSearchTerm] = useState(''); + const [filterSeverity, setFilterSeverity] = useState(''); + const [filterStatus, setFilterStatus] = useState(''); + const [sortField, setSortField] = useState('published_date'); + const [sortDirection, setSortDirection] = useState<'asc' | 'desc'>('desc'); + + const handleSort = (field: keyof CVE) => { + if (sortField === field) { + setSortDirection(sortDirection === 'asc' ? 'desc' : 'asc'); + } else { + setSortField(field); + setSortDirection('desc'); + } + }; + + const filteredCVEs = mockCVEs.filter(cve => { + return ( + (searchTerm === '' || + cve.cve_id.toLowerCase().includes(searchTerm.toLowerCase()) || + cve.description.toLowerCase().includes(searchTerm.toLowerCase())) && + (filterSeverity === '' || cve.severity === filterSeverity) && + (filterStatus === '' || cve.status === filterStatus) + ); + }).sort((a, b) => { + if (a[sortField] < b[sortField]) return sortDirection === 'asc' ? -1 : 1; + if (a[sortField] > b[sortField]) return sortDirection === 'asc' ? 1 : -1; + return 0; + }); + + const getSortIcon = (field: keyof CVE) => { + if (sortField !== field) return null; + return sortDirection === 'asc' ? : ; + }; + + return ( +
+
+

CVE Management

+

Browse and manage Common Vulnerabilities and Exposures

+
+ +
+
+
+ +
+ setSearchTerm(e.target.value)} + /> +
+ +
+
+
+ +
+ +
+ +
+
+ +
+ +
+
+
+ +
+
+ + + + + + + + + + + + + {filteredCVEs.map((cve) => ( + + + + + + + + + ))} + +
handleSort('cve_id')} + > +
+ CVE ID + {getSortIcon('cve_id')} +
+
handleSort('severity')} + > +
+ Severity + {getSortIcon('severity')} +
+
handleSort('cvss_score')} + > +
+ CVSS + {getSortIcon('cvss_score')} +
+
handleSort('published_date')} + > +
+ Published + {getSortIcon('published_date')} +
+
handleSort('status')} + > +
+ Status + {getSortIcon('status')} +
+
+ Actions +
+ {cve.cve_id} + + + {cve.severity} + + + {cve.cvss_score.toFixed(1)} + + {new Date(cve.published_date).toLocaleDateString()} + + + {cve.status} + + + + + + + +
+
+
+
+ ); +}; + +export default CVEList; \ No newline at end of file diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx new file mode 100644 index 0000000..c684af9 --- /dev/null +++ b/src/components/Dashboard.tsx @@ -0,0 +1,176 @@ +import React from 'react'; +import { PieChart as ChartPie, ShieldAlert, Clock, FileText } from 'lucide-react'; +import { mockCVEs } from '../data/mockData'; + +const severityColors = { + CRITICAL: 'bg-red-500', + HIGH: 'bg-orange-500', + MEDIUM: 'bg-yellow-500', + LOW: 'bg-blue-500' +}; + +const statusCounts = { + NEW: mockCVEs.filter(cve => cve.status === 'NEW').length, + ANALYZED: mockCVEs.filter(cve => cve.status === 'ANALYZED').length, + ASSIGNED: mockCVEs.filter(cve => cve.status === 'ASSIGNED').length, + RESOLVED: mockCVEs.filter(cve => cve.status === 'RESOLVED').length, +}; + +const severityCounts = { + CRITICAL: mockCVEs.filter(cve => cve.severity === 'CRITICAL').length, + HIGH: mockCVEs.filter(cve => cve.severity === 'HIGH').length, + MEDIUM: mockCVEs.filter(cve => cve.severity === 'MEDIUM').length, + LOW: mockCVEs.filter(cve => cve.severity === 'LOW').length, +}; + +const Dashboard = () => { + return ( +
+
+

CVE Dashboard

+

Overview of vulnerability management status

+
+ +
+
+
+
+ +
+
+

Critical Vulnerabilities

+

{severityCounts.CRITICAL}

+
+
+
+
+
+
+ +
+
+
+ +
+
+

High Risk Vulnerabilities

+

{severityCounts.HIGH}

+
+
+
+
+
+
+ +
+
+
+ +
+
+

Pending Analysis

+

{statusCounts.NEW}

+
+
+
+
+
+
+ +
+
+
+ +
+
+

Resolved Issues

+

{statusCounts.RESOLVED}

+
+
+
+
+
+
+
+ +
+
+
+

Recent Vulnerabilities

+
+
+
+ + + + + + + + + + + + {mockCVEs.slice(0, 5).map((cve) => ( + + + + + + + + ))} + +
CVE IDSeverityCVSSPublishedStatus
{cve.cve_id} + + {cve.severity} + + {cve.cvss_score.toFixed(1)} + {new Date(cve.published_date).toLocaleDateString()} + + + {cve.status} + +
+
+
+
+ +
+
+

Severity Distribution

+
+
+
+ {Object.entries(severityCounts).map(([severity, count]) => ( +
+
+
{severity}
+
{count}
+
+ ))} +
+
+ Total vulnerabilities: {mockCVEs.length} +
+
+
+
+
+ ); +}; + +export default Dashboard; \ No newline at end of file diff --git a/src/components/RulesConfig.tsx b/src/components/RulesConfig.tsx new file mode 100644 index 0000000..03d4712 --- /dev/null +++ b/src/components/RulesConfig.tsx @@ -0,0 +1,362 @@ +import React, { useState } from 'react'; +import { Save, Plus, Trash2 } from 'lucide-react'; +import { mockRules } from '../data/mockData'; +import { CVERule } from '../types/cve'; + +const RulesConfig: React.FC = () => { + const [rules, setRules] = useState(mockRules); + const [editingRule, setEditingRule] = useState | null>(null); + const [isEditing, setIsEditing] = useState(false); + + const handleEditRule = (rule: CVERule) => { + setEditingRule({ ...rule }); + setIsEditing(true); + }; + + const handleNewRule = () => { + setEditingRule({ + id: rules.length + 1, + name: '', + criteria: { severity: 'HIGH' }, + actions: { create_ticket: true, ticket_priority: 'NORMAL' }, + priority: rules.length + 1, + is_active: true + }); + setIsEditing(true); + }; + + const handleChange = (e: React.ChangeEvent) => { + const { name, value, type } = e.target as HTMLInputElement; + + if (name === 'severity' || name === 'ticket_priority') { + setEditingRule({ + ...editingRule, + criteria: name === 'severity' ? { ...editingRule?.criteria, severity: value } : editingRule?.criteria, + actions: name === 'ticket_priority' ? { ...editingRule?.actions, ticket_priority: value } : editingRule?.actions + }); + return; + } + + setEditingRule({ + ...editingRule, + [name]: type === 'checkbox' + ? (e.target as HTMLInputElement).checked + : type === 'number' + ? parseInt(value) + : value + }); + }; + + const handleActionChange = (e: React.ChangeEvent) => { + const { name, checked } = e.target; + + if (editingRule) { + setEditingRule({ + ...editingRule, + actions: { + ...editingRule.actions, + [name]: checked + } + }); + } + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (editingRule && editingRule.name) { + if (editingRule.id && rules.some(r => r.id === editingRule.id)) { + // Update existing rule + setRules(rules.map(rule => rule.id === editingRule.id ? editingRule as CVERule : rule)); + } else { + // Add new rule + setRules([...rules, editingRule as CVERule]); + } + + setIsEditing(false); + setEditingRule(null); + } + }; + + const handleDelete = (ruleId: number) => { + setRules(rules.filter(rule => rule.id !== ruleId)); + }; + + const handleCancel = () => { + setIsEditing(false); + setEditingRule(null); + }; + + return ( +
+
+

CVE Processing Rules

+

Configure automated actions for vulnerability management

+
+ +
+

+ Define rules that automatically process CVEs based on specific criteria. Rules are evaluated in priority order. +

+ +
+ + {isEditing && editingRule && ( +
+

+ {editingRule.id && rules.some(r => r.id === editingRule.id) ? 'Edit Rule' : 'Add New Rule'} +

+ +
+
+
+ + +
+ +
+

Criteria

+ +
+ + +
+ +
+ + +

Lower numbers are processed first

+
+
+ +
+

Actions

+ +
+ + +
+ + {editingRule.actions?.create_ticket && ( +
+ + +
+ )} + +
+ + +
+ +
+ + +
+
+ +
+ + +
+
+ +
+ + +
+
+
+ )} + +
+ + + + + + + + + + + + + {rules.map((rule) => ( + + + + + + + + + ))} + +
+ Priority + + Rule Name + + Criteria + + Actions + + Status + + Actions +
+ {rule.priority} + + {rule.name} + + {rule.criteria.severity && ( + + {rule.criteria.severity} + + )} + {rule.criteria.system_tags && ( + + System Tags + + )} + + {rule.actions.create_ticket && ( + + Create Ticket + + )} + {rule.actions.notify_admins && ( + + Notify + + )} + {rule.actions.add_to_report && ( + + Report + + )} + + + {rule.is_active ? 'Active' : 'Inactive'} + + + + +
+
+
+ ); +}; + +export default RulesConfig; \ No newline at end of file diff --git a/src/components/SourcesConfig.tsx b/src/components/SourcesConfig.tsx new file mode 100644 index 0000000..24e0842 --- /dev/null +++ b/src/components/SourcesConfig.tsx @@ -0,0 +1,286 @@ +import React, { useState } from 'react'; +import { Save, Plus, Trash2, RefreshCw } from 'lucide-react'; +import { mockSources } from '../data/mockData'; +import { CVESource } from '../types/cve'; + +const SourcesConfig: React.FC = () => { + const [sources, setSources] = useState(mockSources); + const [newSource, setNewSource] = useState(false); + const [formData, setFormData] = useState>({ + name: '', + url: '', + api_key: '', + is_active: true, + sync_frequency: 24 + }); + + const handleChange = (e: React.ChangeEvent) => { + const { name, value, type } = e.target as HTMLInputElement; + setFormData({ + ...formData, + [name]: type === 'checkbox' ? (e.target as HTMLInputElement).checked : + type === 'number' ? parseInt(value) : value + }); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (formData.name && formData.url) { + const newSourceObj: CVESource = { + id: sources.length + 1, + name: formData.name || '', + url: formData.url || '', + api_key: formData.api_key || '', + is_active: formData.is_active || false, + sync_frequency: formData.sync_frequency || 24, + last_sync: 'Never', + sync_status: 'PENDING' + }; + + setSources([...sources, newSourceObj]); + setNewSource(false); + setFormData({ + name: '', + url: '', + api_key: '', + is_active: true, + sync_frequency: 24 + }); + } + }; + + const handleSync = (sourceId: number) => { + // In a real implementation, this would trigger the sync process + // For this prototype, we'll just update the last_sync time + setSources( + sources.map(source => + source.id === sourceId + ? { + ...source, + last_sync: new Date().toISOString(), + sync_status: 'SUCCESS' as const + } + : source + ) + ); + }; + + const handleDelete = (sourceId: number) => { + setSources(sources.filter(source => source.id !== sourceId)); + }; + + return ( +
+
+

CVE Data Sources

+

Configure and manage vulnerability data sources

+
+ +
+

+ Configure external data sources for CVE information. The plugin will automatically sync with these sources based on the configured frequency. +

+ +
+ + {newSource && ( +
+

Add New Data Source

+
+
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ +
+ + +
+
+
+ )} + +
+ + + + + + + + + + + + + + {sources.map((source) => ( + + + + + + + + + + ))} + +
+ Source Name + + URL + + Frequency + + Last Sync + + Status + + Active + + Actions +
+ {source.name} + + {source.url} + + Every {source.sync_frequency} hours + + {source.last_sync === 'Never' + ? 'Never' + : new Date(source.last_sync).toLocaleString()} + + + {source.sync_status} + + + + {source.is_active ? 'Active' : 'Inactive'} + + + + +
+
+
+ ); +}; + +export default SourcesConfig; \ No newline at end of file diff --git a/src/css/cve.css b/src/css/cve.css new file mode 100644 index 0000000..7372fb1 --- /dev/null +++ b/src/css/cve.css @@ -0,0 +1,502 @@ +/** + * GLPI CVE Plugin - Main CSS + * This file contains styles specific to the CVE plugin + */ + +/* General Layout */ +.cve-container { + margin: 0; + padding: 15px; + background-color: #f5f5f5; +} + +.cve-card { + background: #fff; + border-radius: 4px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); + margin-bottom: 20px; + overflow: hidden; +} + +.cve-card-header { + border-bottom: 1px solid #e5e5e5; + padding: 15px; + font-weight: bold; + background-color: #f9f9f9; +} + +.cve-card-body { + padding: 15px; +} + +/* Dashboard Components */ +.cve-stats-container { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(240px, 1fr)); + gap: 20px; + margin-bottom: 20px; +} + +.cve-stat-card { + position: relative; + padding: 15px; + border-radius: 4px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); + background-color: #fff; +} + +.cve-stat-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 48px; + height: 48px; + border-radius: 50%; + margin-right: 15px; +} + +.cve-stat-content { + display: inline-block; + vertical-align: top; +} + +.cve-stat-label { + font-size: 14px; + color: #666; + margin-bottom: 5px; +} + +.cve-stat-value { + font-size: 24px; + font-weight: bold; + margin-top: 0; + margin-bottom: 10px; +} + +.cve-progress-bar { + height: 8px; + background-color: #e9ecef; + border-radius: 4px; + overflow: hidden; + margin-top: 10px; +} + +.cve-progress-bar-fill { + height: 100%; + border-radius: 4px; +} + +/* Severity colors */ +.cve-critical { + background-color: #ffebee; +} +.cve-critical .cve-stat-icon { + background-color: #ffcdd2; + color: #c62828; +} +.cve-critical .cve-progress-bar-fill { + background-color: #f44336; +} + +.cve-high { + background-color: #fff3e0; +} +.cve-high .cve-stat-icon { + background-color: #ffe0b2; + color: #ef6c00; +} +.cve-high .cve-progress-bar-fill { + background-color: #ff9800; +} + +.cve-pending { + background-color: #e3f2fd; +} +.cve-pending .cve-stat-icon { + background-color: #bbdefb; + color: #1565c0; +} +.cve-pending .cve-progress-bar-fill { + background-color: #2196f3; +} + +.cve-resolved { + background-color: #e8f5e9; +} +.cve-resolved .cve-stat-icon { + background-color: #c8e6c9; + color: #2e7d32; +} +.cve-resolved .cve-progress-bar-fill { + background-color: #4caf50; +} + +/* Tables */ +.cve-table { + width: 100%; + border-collapse: collapse; +} + +.cve-table th { + background-color: #f5f5f5; + text-align: left; + padding: 10px; + font-weight: bold; + border-bottom: 2px solid #ddd; +} + +.cve-table td { + padding: 10px; + border-bottom: 1px solid #ddd; +} + +.cve-table tr:hover { + background-color: #f9f9f9; +} + +.cve-sortable { + cursor: pointer; + position: relative; +} + +.cve-sortable:after { + content: '⇕'; + margin-left: 5px; + color: #999; + font-size: 12px; +} + +.cve-sortable.sort-asc:after { + content: '↑'; + color: #333; +} + +.cve-sortable.sort-desc:after { + content: '↓'; + color: #333; +} + +/* Badges */ +.badge { + display: inline-block; + padding: 3px 7px; + font-size: 12px; + font-weight: bold; + line-height: 1; + text-align: center; + white-space: nowrap; + vertical-align: middle; + border-radius: 10px; +} + +.badge-danger { + background-color: #ffcdd2; + color: #c62828; +} + +.badge-warning { + background-color: #ffe0b2; + color: #ef6c00; +} + +.badge-info { + background-color: #bbdefb; + color: #1565c0; +} + +.badge-primary { + background-color: #c5cae9; + color: #283593; +} + +.badge-success { + background-color: #c8e6c9; + color: #2e7d32; +} + +.badge-purple { + background-color: #e1bee7; + color: #6a1b9a; +} + +.badge-inactive { + background-color: #e0e0e0; + color: #616161; +} + +.badge-pending { + background-color: #fffde7; + color: #f57f17; +} + +/* Filters */ +.cve-filters { + display: flex; + flex-wrap: wrap; + gap: 15px; + margin-bottom: 20px; +} + +.cve-filter-group { + position: relative; + flex: 1; + min-width: 200px; +} + +.cve-filter-icon { + position: absolute; + left: 10px; + top: 10px; + color: #757575; +} + +.cve-filter-input, +.cve-filter-select { + width: 100%; + padding: 8px 8px 8px 35px; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 14px; +} + +/* Buttons */ +.cve-btn-container { + display: flex; + justify-content: flex-end; + margin-bottom: 20px; +} + +.btn { + display: inline-block; + font-weight: 400; + text-align: center; + white-space: nowrap; + vertical-align: middle; + user-select: none; + border: 1px solid transparent; + padding: 6px 12px; + font-size: 14px; + line-height: 1.5; + border-radius: 4px; + transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out; + cursor: pointer; +} + +.btn:focus, .btn:hover { + text-decoration: none; +} + +.btn-primary { + color: #fff; + background-color: #2196f3; + border-color: #2196f3; +} + +.btn-primary:hover { + background-color: #0d8bf2; + border-color: #0c83e9; +} + +.btn-success { + color: #fff; + background-color: #4caf50; + border-color: #4caf50; +} + +.btn-success:hover { + background-color: #43a047; + border-color: #3d9142; +} + +.btn-danger { + color: #fff; + background-color: #f44336; + border-color: #f44336; +} + +.btn-danger:hover { + background-color: #e53935; + border-color: #d32f2f; +} + +.btn-purple { + color: #fff; + background-color: #9c27b0; + border-color: #9c27b0; +} + +.btn-purple:hover { + background-color: #8e24aa; + border-color: #7b1fa2; +} + +.btn-secondary { + color: #fff; + background-color: #757575; + border-color: #757575; +} + +.btn-secondary:hover { + background-color: #616161; + border-color: #616161; +} + +.btn-sm { + padding: 4px 8px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} + +/* Forms */ +.cve-form-section { + background-color: #fff; + border-radius: 4px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24); + padding: 20px; + margin-bottom: 20px; +} + +.cve-form-title { + font-size: 18px; + margin-bottom: 15px; + font-weight: bold; +} + +.cve-form-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); + gap: 20px; + margin-bottom: 20px; +} + +.cve-form-group { + margin-bottom: 15px; +} + +.cve-form-label { + display: block; + margin-bottom: 5px; + font-weight: 500; + font-size: 14px; +} + +.cve-form-input, +.cve-form-select { + width: 100%; + padding: 8px; + border: 1px solid #ccc; + border-radius: 4px; + font-size: 14px; +} + +.cve-form-checkbox { + margin-right: 8px; +} + +.cve-form-actions { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 20px; +} + +/* Navigation */ +.cve-nav { + margin-bottom: 20px; + border-bottom: 1px solid #ddd; +} + +.cve-nav-item { + display: inline-block; + padding: 10px 15px; + margin-right: 5px; + margin-bottom: -1px; + cursor: pointer; + border: 1px solid transparent; + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} + +.cve-nav-item:hover { + background-color: #f5f5f5; +} + +.cve-nav-item.active { + background-color: #fff; + border: 1px solid #ddd; + border-bottom-color: #fff; + font-weight: bold; +} + +.cve-nav-icon { + margin-right: 5px; +} + +/* Mobile responsiveness */ +@media (max-width: 768px) { + .cve-stats-container { + grid-template-columns: 1fr; + } + + .cve-filters { + flex-direction: column; + } + + .cve-table { + display: block; + overflow-x: auto; + white-space: nowrap; + } + + .cve-form-grid { + grid-template-columns: 1fr; + } +} + +/* Chart styles */ +.cve-chart-container { + height: 300px; + display: flex; + align-items: flex-end; + justify-content: space-around; + padding: 20px; +} + +.cve-chart-bar { + width: 60px; + border-radius: 4px 4px 0 0; + display: flex; + flex-direction: column; + align-items: center; + transition: height 0.3s ease; +} + +.cve-chart-label { + margin-top: 8px; + font-size: 12px; + text-align: center; + font-weight: bold; +} + +.cve-chart-value { + margin-top: 4px; + font-size: 14px; + font-weight: 500; +} + +.cve-chart-critical { + background-color: #f44336; +} + +.cve-chart-high { + background-color: #ff9800; +} + +.cve-chart-medium { + background-color: #ffeb3b; +} + +.cve-chart-low { + background-color: #2196f3; +} \ No newline at end of file diff --git a/src/data/mockData.ts b/src/data/mockData.ts new file mode 100644 index 0000000..efa8d1b --- /dev/null +++ b/src/data/mockData.ts @@ -0,0 +1,181 @@ +import { CVE, CVESource, CVERule } from '../types/cve'; + +export const mockCVEs: CVE[] = [ + { + id: 1, + cve_id: 'CVE-2024-1234', + description: 'A remote code execution vulnerability in Apache Log4j library affecting versions 2.0-beta9 to 2.14.1', + cvss_score: 10.0, + cvss_vector: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H', + severity: 'CRITICAL', + published_date: '2024-05-12T00:00:00Z', + modified_date: '2024-05-14T00:00:00Z', + status: 'NEW', + references: [ + 'https://nvd.nist.gov/vuln/detail/CVE-2024-1234', + 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-1234' + ], + affected_products: ['Apache Log4j 2.0-2.14.1', 'Applications using affected Log4j versions'], + date_creation: '2024-05-15T10:30:00Z', + date_mod: '2024-05-15T10:30:00Z' + }, + { + id: 2, + cve_id: 'CVE-2024-5678', + description: 'SQL Injection vulnerability in WordPress plugin Contact Form 7 versions prior to 5.7.2', + cvss_score: 8.8, + cvss_vector: 'CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H', + severity: 'HIGH', + published_date: '2024-05-10T00:00:00Z', + modified_date: '2024-05-11T00:00:00Z', + status: 'ANALYZED', + references: [ + 'https://nvd.nist.gov/vuln/detail/CVE-2024-5678', + 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-5678' + ], + affected_products: ['WordPress Contact Form 7 < 5.7.2'], + date_creation: '2024-05-12T08:15:00Z', + date_mod: '2024-05-12T14:25:00Z' + }, + { + id: 3, + cve_id: 'CVE-2024-9012', + description: 'Privilege escalation vulnerability in Microsoft Windows 11 affecting kernel-mode drivers', + cvss_score: 7.8, + cvss_vector: 'CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H', + severity: 'HIGH', + published_date: '2024-05-08T00:00:00Z', + modified_date: '2024-05-09T00:00:00Z', + status: 'ASSIGNED', + references: [ + 'https://nvd.nist.gov/vuln/detail/CVE-2024-9012', + 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-9012' + ], + affected_products: ['Microsoft Windows 11 21H2', 'Microsoft Windows 11 22H2'], + date_creation: '2024-05-10T09:45:00Z', + date_mod: '2024-05-13T11:20:00Z' + }, + { + id: 4, + cve_id: 'CVE-2024-3456', + description: 'Cross-site scripting (XSS) vulnerability in jQuery UI Dialog component prior to version 1.13.2', + cvss_score: 5.4, + cvss_vector: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N', + severity: 'MEDIUM', + published_date: '2024-05-05T00:00:00Z', + modified_date: '2024-05-06T00:00:00Z', + status: 'RESOLVED', + references: [ + 'https://nvd.nist.gov/vuln/detail/CVE-2024-3456', + 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-3456' + ], + affected_products: ['jQuery UI < 1.13.2'], + date_creation: '2024-05-07T14:30:00Z', + date_mod: '2024-05-15T09:10:00Z' + }, + { + id: 5, + cve_id: 'CVE-2024-7890', + description: 'Information disclosure vulnerability in OpenSSL 3.0.0 through 3.1.1', + cvss_score: 3.7, + cvss_vector: 'CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N', + severity: 'LOW', + published_date: '2024-05-02T00:00:00Z', + modified_date: '2024-05-03T00:00:00Z', + status: 'ANALYZED', + references: [ + 'https://nvd.nist.gov/vuln/detail/CVE-2024-7890', + 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-7890' + ], + affected_products: ['OpenSSL 3.0.0-3.1.1'], + date_creation: '2024-05-04T16:45:00Z', + date_mod: '2024-05-10T13:25:00Z' + } +]; + +export const mockSources: CVESource[] = [ + { + id: 1, + name: 'National Vulnerability Database (NVD)', + url: 'https://services.nvd.nist.gov/rest/json/cves/2.0', + api_key: '********-****-****-****-************', + source_type: 'NVD', + data_format: 'JSON', + is_active: true, + sync_frequency: 4, + last_sync: '2024-05-15T08:00:00Z', + sync_status: 'SUCCESS' + }, + { + id: 2, + name: 'MITRE CVE Database', + url: 'https://cveawg.mitre.org/api/', + api_key: '', + source_type: 'MITRE', + data_format: 'JSON', + is_active: true, + sync_frequency: 24, + last_sync: '2024-05-14T22:00:00Z', + sync_status: 'SUCCESS' + }, + { + id: 3, + name: 'CISA KEV Catalog', + url: 'https://www.cisa.gov/sites/default/files/feeds/known_exploited_vulnerabilities.json', + api_key: '', + source_type: 'CISA', + data_format: 'JSON', + is_active: true, + sync_frequency: 12, + last_sync: '2024-05-15T04:00:00Z', + sync_status: 'SUCCESS' + } +]; + +export const mockRules: CVERule[] = [ + { + id: 1, + name: 'Critical Vulnerabilities - Immediate Ticket', + criteria: { + severity: 'CRITICAL', + affected_systems: { min: 1 } + }, + actions: { + create_ticket: true, + ticket_priority: 'VERY HIGH', + notify_admins: true + }, + priority: 1, + is_active: true + }, + { + id: 2, + name: 'High Risk - Production Systems', + criteria: { + severity: 'HIGH', + system_tags: ['production', 'customer-facing'] + }, + actions: { + create_ticket: true, + ticket_priority: 'HIGH', + notify_admins: false + }, + priority: 2, + is_active: true + }, + { + id: 3, + name: 'Medium Risk - Batch Report', + criteria: { + severity: 'MEDIUM', + system_count: { min: 1 } + }, + actions: { + create_ticket: false, + add_to_report: true, + schedule_report: 'WEEKLY' + }, + priority: 3, + is_active: true + } +]; \ No newline at end of file diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..b5c61c9 --- /dev/null +++ b/src/index.css @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/src/js/cve.js b/src/js/cve.js new file mode 100644 index 0000000..2bae660 --- /dev/null +++ b/src/js/cve.js @@ -0,0 +1,417 @@ +/** + * GLPI CVE Plugin - Main JavaScript + * This file contains jQuery code for the CVE plugin functionality + */ + +$(document).ready(function() { + // Navigation handling + $('.cve-nav-item').on('click', function(e) { + e.preventDefault(); + const target = $(this).data('target'); + + // Update active state + $('.cve-nav-item').removeClass('active'); + $(this).addClass('active'); + + // Show relevant content + $('.cve-content').hide(); + $(`#${target}`).show(); + + // Update URL hash for bookmarking + window.location.hash = target; + }); + + // Initialize based on hash or default to dashboard + const initPage = () => { + let target = window.location.hash.substring(1) || 'cve-dashboard'; + $(`.cve-nav-item[data-target="${target}"]`).trigger('click'); + }; + + initPage(); + + // CVE Search functionality + $('#cve-search').on('keyup', function() { + const searchTerm = $(this).val().toLowerCase(); + filterCVEs(searchTerm, $('#severity-filter').val(), $('#status-filter').val()); + }); + + // Severity and Status filters + $('#severity-filter, #status-filter').on('change', function() { + filterCVEs( + $('#cve-search').val().toLowerCase(), + $('#severity-filter').val(), + $('#status-filter').val() + ); + }); + + // Table sorting + $('.cve-sortable').on('click', function() { + const column = $(this).data('column'); + const currentDir = $(this).hasClass('sort-asc') ? 'asc' : 'desc'; + const newDir = currentDir === 'asc' ? 'desc' : 'asc'; + + // Update visual indicators + $('.cve-sortable').removeClass('sort-asc sort-desc'); + $(this).addClass(`sort-${newDir}`); + + sortCVETable(column, newDir); + }); + + // Sources form handling + $('#add-source-form').on('submit', function(e) { + e.preventDefault(); + + const newSource = { + name: $('#source-name').val(), + url: $('#source-url').val(), + api_key: $('#source-api-key').val(), + sync_frequency: parseInt($('#source-frequency').val()), + is_active: $('#source-active').prop('checked') + }; + + // In a real implementation, this would be an AJAX POST + console.log('Adding new source:', newSource); + + // For the prototype, just add to the table + addSourceToTable(newSource); + + // Reset form + $(this).trigger('reset'); + $('#add-source-section').hide(); + }); + + // New source button + $('#add-source-btn').on('click', function() { + $('#add-source-section').toggle(); + }); + + // Rule form handling + $('#add-rule-form').on('submit', function(e) { + e.preventDefault(); + + const newRule = { + name: $('#rule-name').val(), + criteria: { + severity: $('#rule-severity').val() + }, + actions: { + create_ticket: $('#create-ticket').prop('checked'), + ticket_priority: $('#ticket-priority').val(), + notify_admins: $('#notify-admins').prop('checked'), + add_to_report: $('#add-to-report').prop('checked') + }, + priority: parseInt($('#rule-priority').val()), + is_active: $('#rule-active').prop('checked') + }; + + // In a real implementation, this would be an AJAX POST + console.log('Adding new rule:', newRule); + + // For the prototype, just add to the table + addRuleToTable(newRule); + + // Reset form + $(this).trigger('reset'); + $('#add-rule-section').hide(); + }); + + // New rule button + $('#add-rule-btn').on('click', function() { + $('#add-rule-section').toggle(); + }); + + // Helper functions + function filterCVEs(searchTerm, severity, status) { + $('#cve-table tbody tr').each(function() { + const cveId = $(this).find('td:nth-child(1)').text().toLowerCase(); + const description = $(this).find('td:nth-child(2)').text().toLowerCase(); + const rowSeverity = $(this).find('td:nth-child(3)').text(); + const rowStatus = $(this).find('td:nth-child(6)').text(); + + const matchesSearch = !searchTerm || cveId.includes(searchTerm) || description.includes(searchTerm); + const matchesSeverity = !severity || rowSeverity === severity; + const matchesStatus = !status || rowStatus === status; + + if (matchesSearch && matchesSeverity && matchesStatus) { + $(this).show(); + } else { + $(this).hide(); + } + }); + } + + function sortCVETable(column, direction) { + const rows = $('#cve-table tbody tr').toArray(); + + rows.sort(function(a, b) { + let aValue = $(a).find(`td:nth-child(${getColumnIndex(column)})`).text(); + let bValue = $(b).find(`td:nth-child(${getColumnIndex(column)})`).text(); + + // Handle numeric values + if (column === 'cvss') { + aValue = parseFloat(aValue); + bValue = parseFloat(bValue); + } + + // Handle dates + if (column === 'published') { + aValue = new Date(aValue); + bValue = new Date(bValue); + } + + if (direction === 'asc') { + return aValue > bValue ? 1 : -1; + } else { + return aValue < bValue ? 1 : -1; + } + }); + + // Reappend sorted rows + $('#cve-table tbody').empty(); + $.each(rows, function(index, row) { + $('#cve-table tbody').append(row); + }); + } + + function getColumnIndex(columnName) { + const indices = { + 'cve_id': 1, + 'severity': 3, + 'cvss': 4, + 'published': 5, + 'status': 6 + }; + return indices[columnName] || 1; + } + + function addSourceToTable(source) { + const now = new Date().toLocaleString(); + + const newRow = ` + + ${source.name} + ${source.url} + Every ${source.sync_frequency} hours + Never + PENDING + ${source.is_active ? 'Active' : 'Inactive'} + + + + + + `; + + $('#sources-table tbody').append(newRow); + } + + function addRuleToTable(rule) { + const severityClass = { + 'CRITICAL': 'danger', + 'HIGH': 'warning', + 'MEDIUM': 'info', + 'LOW': 'primary' + }[rule.criteria.severity] || 'primary'; + + const actionLabels = []; + if (rule.actions.create_ticket) actionLabels.push('Create Ticket'); + if (rule.actions.notify_admins) actionLabels.push('Notify'); + if (rule.actions.add_to_report) actionLabels.push('Report'); + + const newRow = ` + + ${rule.priority} + ${rule.name} + ${rule.criteria.severity} + ${actionLabels.join(' ')} + ${rule.is_active ? 'Active' : 'Inactive'} + + + + + + `; + + $('#rules-table tbody').append(newRow); + } + + // Load mock data for the prototype + loadMockData(); + + function loadMockData() { + // This would be replaced by AJAX calls to the GLPI API in a real implementation + const mockCVEs = [ + { + id: 1, + cve_id: 'CVE-2024-1234', + description: 'A remote code execution vulnerability in Apache Log4j library affecting versions 2.0-beta9 to 2.14.1', + cvss_score: 10.0, + cvss_vector: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H', + severity: 'CRITICAL', + published_date: '2024-05-12', + modified_date: '2024-05-14', + status: 'NEW', + references: [ + 'https://nvd.nist.gov/vuln/detail/CVE-2024-1234', + 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-1234' + ], + affected_products: ['Apache Log4j 2.0-2.14.1', 'Applications using affected Log4j versions'], + date_creation: '2024-05-15', + date_mod: '2024-05-15' + }, + { + id: 2, + cve_id: 'CVE-2024-5678', + description: 'SQL Injection vulnerability in WordPress plugin Contact Form 7 versions prior to 5.7.2', + cvss_score: 8.8, + cvss_vector: 'CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H', + severity: 'HIGH', + published_date: '2024-05-10', + modified_date: '2024-05-11', + status: 'ANALYZED', + references: [ + 'https://nvd.nist.gov/vuln/detail/CVE-2024-5678', + 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-5678' + ], + affected_products: ['WordPress Contact Form 7 < 5.7.2'], + date_creation: '2024-05-12', + date_mod: '2024-05-12' + }, + { + id: 3, + cve_id: 'CVE-2024-9012', + description: 'Privilege escalation vulnerability in Microsoft Windows 11 affecting kernel-mode drivers', + cvss_score: 7.8, + cvss_vector: 'CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:H/I:H/A:H', + severity: 'HIGH', + published_date: '2024-05-08', + modified_date: '2024-05-09', + status: 'ASSIGNED', + references: [ + 'https://nvd.nist.gov/vuln/detail/CVE-2024-9012', + 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-9012' + ], + affected_products: ['Microsoft Windows 11 21H2', 'Microsoft Windows 11 22H2'], + date_creation: '2024-05-10', + date_mod: '2024-05-13' + }, + { + id: 4, + cve_id: 'CVE-2024-3456', + description: 'Cross-site scripting (XSS) vulnerability in jQuery UI Dialog component prior to version 1.13.2', + cvss_score: 5.4, + cvss_vector: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N', + severity: 'MEDIUM', + published_date: '2024-05-05', + modified_date: '2024-05-06', + status: 'RESOLVED', + references: [ + 'https://nvd.nist.gov/vuln/detail/CVE-2024-3456', + 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-3456' + ], + affected_products: ['jQuery UI < 1.13.2'], + date_creation: '2024-05-07', + date_mod: '2024-05-15' + }, + { + id: 5, + cve_id: 'CVE-2024-7890', + description: 'Information disclosure vulnerability in OpenSSL 3.0.0 through 3.1.1', + cvss_score: 3.7, + cvss_vector: 'CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:L/I:N/A:N', + severity: 'LOW', + published_date: '2024-05-02', + modified_date: '2024-05-03', + status: 'ANALYZED', + references: [ + 'https://nvd.nist.gov/vuln/detail/CVE-2024-7890', + 'https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2024-7890' + ], + affected_products: ['OpenSSL 3.0.0-3.1.1'], + date_creation: '2024-05-04', + date_mod: '2024-05-10' + } + ]; + + // Populate CVE table + mockCVEs.forEach(cve => { + const severityClass = { + 'CRITICAL': 'danger', + 'HIGH': 'warning', + 'MEDIUM': 'info', + 'LOW': 'primary' + }[cve.severity]; + + const statusClass = { + 'NEW': 'info', + 'ANALYZED': 'warning', + 'ASSIGNED': 'primary', + 'RESOLVED': 'success' + }[cve.status]; + + const row = ` + + ${cve.cve_id} + ${cve.description.substring(0, 50)}... + ${cve.severity} + ${cve.cvss_score.toFixed(1)} + ${cve.published_date} + ${cve.status} + + + + + + + `; + + $('#cve-table tbody').append(row); + }); + + // Update dashboard counters + const criticalCount = mockCVEs.filter(cve => cve.severity === 'CRITICAL').length; + const highCount = mockCVEs.filter(cve => cve.severity === 'HIGH').length; + const pendingCount = mockCVEs.filter(cve => cve.status === 'NEW').length; + const resolvedCount = mockCVEs.filter(cve => cve.status === 'RESOLVED').length; + + $('#critical-count').text(criticalCount); + $('#high-count').text(highCount); + $('#pending-count').text(pendingCount); + $('#resolved-count').text(resolvedCount); + + // Set progress bars + $('#critical-progress').css('width', `${(criticalCount / mockCVEs.length) * 100}%`); + $('#high-progress').css('width', `${(highCount / mockCVEs.length) * 100}%`); + $('#pending-progress').css('width', `${(pendingCount / mockCVEs.length) * 100}%`); + $('#resolved-progress').css('width', `${(resolvedCount / mockCVEs.length) * 100}%`); + + // Populate recent vulnerabilities table + mockCVEs.slice(0, 5).forEach(cve => { + const severityClass = { + 'CRITICAL': 'danger', + 'HIGH': 'warning', + 'MEDIUM': 'info', + 'LOW': 'primary' + }[cve.severity]; + + const statusClass = { + 'NEW': 'info', + 'ANALYZED': 'warning', + 'ASSIGNED': 'primary', + 'RESOLVED': 'success' + }[cve.status]; + + const row = ` + + ${cve.cve_id} + ${cve.severity} + ${cve.cvss_score.toFixed(1)} + ${cve.published_date} + ${cve.status} + + `; + + $('#recent-cve-table tbody').append(row); + }); + } +}); \ No newline at end of file diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..ea9e363 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import App from './App.tsx'; +import './index.css'; + +createRoot(document.getElementById('root')!).render( + + + +); diff --git a/src/types/cve.ts b/src/types/cve.ts new file mode 100644 index 0000000..1e2b251 --- /dev/null +++ b/src/types/cve.ts @@ -0,0 +1,45 @@ +export interface CVE { + id: number; + cve_id: string; + description: string; + cvss_score: number; + cvss_vector: string; + severity: 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL'; + published_date: string; + modified_date: string; + status: 'NEW' | 'ANALYZED' | 'ASSIGNED' | 'RESOLVED'; + references: string[]; + affected_products: string[]; + date_creation: string; + date_mod: string; +} + +export interface CVESource { + id: number; + name: string; + url: string; + api_key: string; + source_type?: string; + data_format?: string; + is_active: boolean; + sync_frequency: number; + last_sync: string; + sync_status: 'SUCCESS' | 'FAILED' | 'IN_PROGRESS' | 'PENDING'; +} + +export interface CVERule { + id: number; + name: string; + criteria: any; + actions: any; + priority: number; + is_active: boolean; +} + +export interface CVETicket { + id: number; + cves_id: number; + tickets_id: number; + creation_type: 'AUTO' | 'MANUAL'; + date_creation: string; +} \ No newline at end of file diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..d21f1cd --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/tools/extract_template.sh b/tools/extract_template.sh new file mode 100644 index 0000000..172506c --- /dev/null +++ b/tools/extract_template.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# Extract localization template from GLPI CVE Plugin + +# Define variables +NAME="GLPI CVE Plugin" +POTFILE=plugin_cve.pot +BASEDIR=.. + +# Extract strings +cd ${BASEDIR}/inc/ +xgettext *.php *.class.php -o ../locales/${POTFILE} -L PHP --add-comments=TRANS --from-code=UTF-8 --force-po --copyright-holder="${NAME}" --package-name="${NAME}" --package-version="1.0.0" --msgid-bugs-address="development@yourdomain.com" --keyword=__:1,2t --keyword=_e:1,2t --keyword=_n:1,2,4t --keyword=_s:1,2t --keyword=_sx:1,2,3t --keyword=_nx:1,2,4,5t + +cd ${BASEDIR}/front/ +xgettext *.php -o ../locales/${POTFILE} -L PHP --add-comments=TRANS --from-code=UTF-8 --force-po --join-existing --copyright-holder="${NAME}" --package-name="${NAME}" --package-version="1.0.0" --msgid-bugs-address="development@yourdomain.com" --keyword=__:1,2t --keyword=_e:1,2t --keyword=_n:1,2,4t --keyword=_s:1,2t --keyword=_sx:1,2,3t --keyword=_nx:1,2,4,5t + +cd ${BASEDIR}/ajax/ +xgettext *.php -o ../locales/${POTFILE} -L PHP --add-comments=TRANS --from-code=UTF-8 --force-po --join-existing --copyright-holder="${NAME}" --package-name="${NAME}" --package-version="1.0.0" --msgid-bugs-address="development@yourdomain.com" --keyword=__:1,2t --keyword=_e:1,2t --keyword=_n:1,2,4t --keyword=_s:1,2t --keyword=_sx:1,2,3t --keyword=_nx:1,2,4,5t + +cd ${BASEDIR}/ +xgettext *.php -o locales/${POTFILE} -L PHP --add-comments=TRANS --from-code=UTF-8 --force-po --join-existing --copyright-holder="${NAME}" --package-name="${NAME}" --package-version="1.0.0" --msgid-bugs-address="development@yourdomain.com" --keyword=__:1,2t --keyword=_e:1,2t --keyword=_n:1,2,4t --keyword=_s:1,2t --keyword=_sx:1,2,3t --keyword=_nx:1,2,4,5t + +echo "Localization template generated at locales/${POTFILE}" \ No newline at end of file diff --git a/tools/generate_mo.sh b/tools/generate_mo.sh new file mode 100644 index 0000000..0c64efd --- /dev/null +++ b/tools/generate_mo.sh @@ -0,0 +1,17 @@ +#!/bin/bash +# Generate MO files from PO files for GLPI CVE Plugin + +# Define base directory +BASEDIR=../locales + +# For each PO file +for pofile in ${BASEDIR}/*.po; do + # Get language code + lang=$(basename "$pofile" .po) + echo "Processing $lang..." + + # Generate MO file + msgfmt -v -o ${BASEDIR}/${lang}.mo ${pofile} +done + +echo "MO files generation completed" \ No newline at end of file diff --git a/tools/update_po.sh b/tools/update_po.sh new file mode 100644 index 0000000..0e72385 --- /dev/null +++ b/tools/update_po.sh @@ -0,0 +1,18 @@ +#!/bin/bash +# Update PO files from POT template for GLPI CVE Plugin + +# Define variables +POTFILE=plugin_cve.pot +BASEDIR=../locales + +# For each PO file +for pofile in ${BASEDIR}/*.po; do + # Get language code + lang=$(basename "$pofile" .po) + echo "Updating $lang..." + + # Update PO file from template + msgmerge --update --no-fuzzy-matching --backup=off ${pofile} ${BASEDIR}/${POTFILE} +done + +echo "PO files update completed" \ No newline at end of file diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..f0a2350 --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1ffef60 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,7 @@ +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..0d3d714 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..147380a --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + optimizeDeps: { + exclude: ['lucide-react'], + }, +});