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; } }