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