mirror of
https://github.com/tips-of-mine/GLPI-Plugin-CVE-Prototype.git
synced 2025-06-27 22:58:45 +02:00
1095 lines
34 KiB
PHP
1095 lines
34 KiB
PHP
<?php
|
|
/**
|
|
* GLPI CVE Plugin - CVE Source Class
|
|
* Manages external CVE data sources
|
|
*/
|
|
|
|
if (!defined('GLPI_ROOT')) {
|
|
die("Sorry. You can't access this file directly");
|
|
}
|
|
|
|
/**
|
|
* PluginCveCveSource class for managing CVE data sources
|
|
*/
|
|
class PluginCveCveSource extends CommonDBTM {
|
|
|
|
static $rightname = 'plugin_cve_source';
|
|
|
|
/**
|
|
* Get name of this type by language of the user connected
|
|
*
|
|
* @param integer $nb number of elements
|
|
* @return string name of this type
|
|
*/
|
|
static function getTypeName($nb = 0) {
|
|
return _n('CVE Source', 'CVE Sources', $nb, 'cve');
|
|
}
|
|
|
|
/**
|
|
* Define tabs to display
|
|
*
|
|
* @param array $options
|
|
* @return array containing the tabs
|
|
*/
|
|
function defineTabs($options = []) {
|
|
$tabs = [];
|
|
$this->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 "<tr class='tab_bg_1'>";
|
|
|
|
// Source Name
|
|
echo "<td>" . __('Source Name', 'cve') . "</td>";
|
|
echo "<td>";
|
|
echo Html::input('name', ['value' => $this->fields['name'], 'size' => 40]);
|
|
echo "</td>";
|
|
|
|
// Active
|
|
echo "<td>" . __('Active', 'cve') . "</td>";
|
|
echo "<td>";
|
|
Dropdown::showYesNo('is_active', $this->fields['is_active']);
|
|
echo "</td>";
|
|
|
|
echo "</tr>";
|
|
|
|
echo "<tr class='tab_bg_1'>";
|
|
|
|
// API URL
|
|
echo "<td>" . __('API URL', 'cve') . "</td>";
|
|
echo "<td>";
|
|
echo Html::input('url', ['value' => $this->fields['url'], 'size' => 60]);
|
|
echo "</td>";
|
|
|
|
// Sync Frequency
|
|
echo "<td>" . __('Sync Frequency (hours)', 'cve') . "</td>";
|
|
echo "<td>";
|
|
echo Html::input('sync_frequency', ['value' => $this->fields['sync_frequency'], 'type' => 'number', 'min' => 1, 'max' => 168]);
|
|
echo "</td>";
|
|
|
|
echo "</tr>";
|
|
|
|
echo "<tr class='tab_bg_1'>";
|
|
|
|
// API Key
|
|
echo "<td>" . __('API Key', 'cve') . "</td>";
|
|
echo "<td>";
|
|
echo Html::input('api_key', ['value' => $this->fields['api_key'], 'size' => 40, 'type' => 'password']);
|
|
echo "<br><i>" . __('Leave empty to keep current value', 'cve') . "</i>";
|
|
echo "</td>";
|
|
|
|
// Last Sync
|
|
echo "<td>" . __('Last Sync', 'cve') . "</td>";
|
|
echo "<td>";
|
|
if (empty($this->fields['last_sync'])) {
|
|
echo __('Never', 'cve');
|
|
} else {
|
|
echo Html::convDateTime($this->fields['last_sync']);
|
|
}
|
|
echo "</td>";
|
|
|
|
echo "</tr>";
|
|
|
|
echo "<tr class='tab_bg_1'>";
|
|
|
|
// Sync Status
|
|
echo "<td>" . __('Sync Status', 'cve') . "</td>";
|
|
echo "<td>";
|
|
$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 "</td>";
|
|
|
|
// Source Type
|
|
echo "<td>" . __('Source Type', 'cve') . "</td>";
|
|
echo "<td>";
|
|
$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 "</td>";
|
|
|
|
echo "</tr>";
|
|
|
|
echo "<tr class='tab_bg_1'>";
|
|
|
|
// Source Format
|
|
echo "<td>" . __('Data Format', 'cve') . "</td>";
|
|
echo "<td>";
|
|
$format_options = [
|
|
'JSON' => 'JSON',
|
|
'XML' => 'XML',
|
|
'CSV' => 'CSV'
|
|
];
|
|
|
|
Dropdown::showFromArray('data_format', $format_options,
|
|
['value' => $this->fields['data_format'] ?? 'JSON']);
|
|
echo "</td>";
|
|
|
|
// Description
|
|
echo "<td>" . __('Description', 'cve') . "</td>";
|
|
echo "<td>";
|
|
echo "<textarea name='description' cols='45' rows='3'>".$this->fields['description']."</textarea>";
|
|
echo "</td>";
|
|
|
|
echo "</tr>";
|
|
|
|
$this->showFormButtons($options);
|
|
|
|
// Add a Sync Now button if we're editing an existing source
|
|
if ($ID > 0 && $canedit) {
|
|
echo "<div class='center'>";
|
|
echo "<form method='post' action='".Plugin::getWebDir('cve')."/ajax/sync_now.php'>";
|
|
echo Html::hidden('id', ['value' => $ID]);
|
|
Html::showSecurityToken();
|
|
echo "<input type='submit' name='sync_now' value=\"" . __('Sync Now', 'cve') . "\" class='submit'>";
|
|
Html::closeForm();
|
|
echo "</div>";
|
|
}
|
|
|
|
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;
|
|
}
|
|
} |