Files
GLPI-Plugin-CVE-Prototype/inc/cve.class.php
2025-05-31 10:03:48 +02:00

752 lines
22 KiB
PHP

<?php
/**
* GLPI CVE Plugin - Main CVE Class
* Handles the CVE entity operations
*/
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
/**
* PluginCveCve class for managing CVEs
*/
class PluginCveCve extends CommonDBTM {
// Rights management constants
const RIGHT_NONE = 0;
const RIGHT_READ = 1;
const RIGHT_WRITE = 2;
static $rightname = 'plugin_cve_cve';
/**
* 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', 'CVEs', $nb, 'cve');
}
/**
* Define tabs to display
*
* @param array $options
* @return array containing the tabs
*/
function defineTabs($options = []) {
$tabs = [];
$this->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 "<tr class='tab_bg_1'>";
// CVE ID
echo "<td>" . __('CVE ID', 'cve') . "</td>";
echo "<td>";
echo Html::input('cve_id', ['value' => $this->fields['cve_id'], 'size' => 20]);
echo "</td>";
// Severity
echo "<td>" . __('Severity', 'cve') . "</td>";
echo "<td>";
$severity_options = [
'CRITICAL' => __('Critical', 'cve'),
'HIGH' => __('High', 'cve'),
'MEDIUM' => __('Medium', 'cve'),
'LOW' => __('Low', 'cve')
];
Dropdown::showFromArray('severity', $severity_options,
['value' => $this->fields['severity']]);
echo "</td>";
echo "</tr>";
echo "<tr class='tab_bg_1'>";
// CVSS Score
echo "<td>" . __('CVSS Score', 'cve') . "</td>";
echo "<td>";
echo Html::input('cvss_score', ['value' => $this->fields['cvss_score'], 'size' => 5]);
echo "</td>";
// CVSS Vector
echo "<td>" . __('CVSS Vector', 'cve') . "</td>";
echo "<td>";
echo Html::input('cvss_vector', ['value' => $this->fields['cvss_vector'], 'size' => 40]);
echo "</td>";
echo "</tr>";
echo "<tr class='tab_bg_1'>";
// Published date
echo "<td>" . __('Published Date', 'cve') . "</td>";
echo "<td>";
Html::showDateField('published_date', ['value' => $this->fields['published_date']]);
echo "</td>";
// Modified date
echo "<td>" . __('Modified Date', 'cve') . "</td>";
echo "<td>";
Html::showDateField('modified_date', ['value' => $this->fields['modified_date']]);
echo "</td>";
echo "</tr>";
echo "<tr class='tab_bg_1'>";
// Status
echo "<td>" . __('Status', 'cve') . "</td>";
echo "<td>";
$status_options = [
'NEW' => __('New', 'cve'),
'ANALYZED' => __('Analyzed', 'cve'),
'ASSIGNED' => __('Assigned', 'cve'),
'RESOLVED' => __('Resolved', 'cve')
];
Dropdown::showFromArray('status', $status_options,
['value' => $this->fields['status']]);
echo "</td>";
// Add entity dropdown if needed
echo "<td>" . __('Entity', 'cve') . "</td>";
echo "<td>";
Entity::dropdown(['value' => $this->fields['entities_id']]);
echo "</td>";
echo "</tr>";
echo "<tr class='tab_bg_1'>";
// Description
echo "<td>" . __('Description', 'cve') . "</td>";
echo "<td colspan='3'>";
echo "<textarea name='description' cols='90' rows='4'>".$this->fields['description']."</textarea>";
echo "</td>";
echo "</tr>";
echo "<tr class='tab_bg_1'>";
// References
echo "<td>" . __('References', 'cve') . "</td>";
echo "<td colspan='3'>";
$references = json_decode($this->fields['references'], true) ?: [];
echo "<textarea name='references' cols='90' rows='4'>".implode("\n", $references)."</textarea>";
echo "<br><i>" . __('Enter one URL per line', 'cve') . "</i>";
echo "</td>";
echo "</tr>";
echo "<tr class='tab_bg_1'>";
// Affected Products
echo "<td>" . __('Affected Products', 'cve') . "</td>";
echo "<td colspan='3'>";
$affected_products = json_decode($this->fields['affected_products'], true) ?: [];
echo "<textarea name='affected_products' cols='90' rows='4'>".implode("\n", $affected_products)."</textarea>";
echo "<br><i>" . __('Enter one product per line', 'cve') . "</i>";
echo "</td>";
echo "</tr>";
$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;
}
}