Start repository

This commit is contained in:
tips-of-mine
2025-05-31 10:03:48 +02:00
commit 194322c9fc
57 changed files with 14723 additions and 0 deletions

752
inc/cve.class.php Normal file
View File

@ -0,0 +1,752 @@
<?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;
}
}

414
inc/cvealert.class.php Normal file
View File

@ -0,0 +1,414 @@
<?php
/**
* GLPI CVE Plugin - Software Vulnerability Alert Class
* Manages alerts for vulnerable software in inventory
*/
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
/**
* PluginCveCveAlert class for managing vulnerability alerts
*/
class PluginCveCveAlert extends CommonDBTM {
static $rightname = 'plugin_cve_alert';
/**
* 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('Software Vulnerability Alert', 'Software Vulnerability Alerts', $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 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 "<tr class='tab_bg_1'>";
// Software
echo "<td>" . __('Software', 'cve') . "</td>";
echo "<td>";
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 "</td>";
// Version
echo "<td>" . __('Version', 'cve') . "</td>";
echo "<td>";
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 "</td>";
echo "</tr>";
echo "<tr class='tab_bg_1'>";
// CVE
echo "<td>" . __('CVE', 'cve') . "</td>";
echo "<td>";
if ($this->fields['cves_id'] > 0) {
$cve = new PluginCveCve();
if ($cve->getFromDB($this->fields['cves_id'])) {
echo "<a href='" . PluginCveCve::getFormURLWithID($this->fields['cves_id']) . "'>";
echo $cve->fields['cve_id'];
echo "</a>";
} else {
echo __('Unknown CVE', 'cve');
}
} else {
echo __('N/A', 'cve');
}
echo "</td>";
// Severity
echo "<td>" . __('Severity', 'cve') . "</td>";
echo "<td>";
echo "<span class='" . PluginCveCve::getSeverityClass($this->fields['severity']) . "'>";
echo $this->fields['severity'];
echo "</span>";
echo "</td>";
echo "</tr>";
echo "<tr class='tab_bg_1'>";
// Status
echo "<td>" . __('Status', 'cve') . "</td>";
echo "<td>";
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 "</td>";
// Associated ticket
echo "<td>" . __('Ticket', 'cve') . "</td>";
echo "<td>";
if ($this->fields['tickets_id'] > 0) {
$ticket = new Ticket();
if ($ticket->getFromDB($this->fields['tickets_id'])) {
echo "<a href='" . Ticket::getFormURLWithID($this->fields['tickets_id']) . "'>";
echo $ticket->fields['name'] . " (" . $this->fields['tickets_id'] . ")";
echo "</a>";
} else {
echo __('Unknown ticket', 'cve');
}
} else {
if ($canedit && $this->fields['status'] == 'NEW') {
echo "<button type='submit' name='create_ticket' value='1' class='submit'>";
echo __('Create Ticket', 'cve');
echo "</button>";
} else {
echo __('No ticket associated', 'cve');
}
}
echo "</td>";
echo "</tr>";
// Add entity dropdown if needed
echo "<tr class='tab_bg_1'>";
echo "<td>" . __('Entity', 'cve') . "</td>";
echo "<td>";
echo Dropdown::getDropdownName('glpi_entities', $this->fields['entities_id']);
echo "</td>";
// Creation date
echo "<td>" . __('Creation Date', 'cve') . "</td>";
echo "<td>" . Html::convDateTime($this->fields['date_creation']) . "</td>";
echo "</tr>";
$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;
}
}

358
inc/cveinventory.class.php Normal file
View File

@ -0,0 +1,358 @@
<?php
/**
* GLPI CVE Plugin - Software Inventory Analysis Class
* Analyzes GLPI software inventory and matches it with known CVEs
*/
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
/**
* PluginCveCveInventory class for analyzing software inventory for vulnerabilities
*/
class PluginCveCveInventory extends CommonDBTM {
static $rightname = 'plugin_cve_inventory';
/**
* 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('Software Vulnerability Analysis', 'Software Vulnerability Analyses', $nb, 'cve');
}
/**
* Cron task for software inventory analysis
*
* @param CronTask $task CronTask object
* @return integer
*/
static function cronAnalyzeInventory($task) {
global $DB;
$task->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;
}
}

497
inc/cverule.class.php Normal file
View File

@ -0,0 +1,497 @@
<?php
/**
* GLPI CVE Plugin - Rule Class
* Manages CVE processing rules
*/
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
/**
* PluginCveCveRule class for managing CVE processing rules
*/
class PluginCveCveRule extends CommonDBTM {
static $rightname = 'plugin_cve_rule';
/**
* 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 Rule', 'CVE Rules', $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 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 "<tr class='tab_bg_1'>";
// Rule Name
echo "<td>" . __('Rule Name', 'cve') . "</td>";
echo "<td>";
echo Html::input('name', ['value' => $this->fields['name'], 'size' => 40]);
echo "</td>";
// Priority
echo "<td>" . __('Priority', 'cve') . "</td>";
echo "<td>";
echo Html::input('priority', ['value' => $this->fields['priority'], 'size' => 5, 'type' => 'number', 'min' => 1]);
echo "<br><i>" . __('Lower numbers are processed first', 'cve') . "</i>";
echo "</td>";
echo "</tr>";
echo "<tr class='tab_bg_1'>";
// Criteria - Severity
echo "<td>" . __('Severity', 'cve') . "</td>";
echo "<td>";
$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 "</td>";
// Status
echo "<td>" . __('Status', 'cve') . "</td>";
echo "<td>";
$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 "</td>";
echo "</tr>";
echo "<tr class='tab_bg_1'>";
// Actions - Create Ticket
echo "<td>" . __('Create Ticket', 'cve') . "</td>";
echo "<td>";
$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 "</td>";
// Ticket Priority
echo "<td>" . __('Ticket Priority', 'cve') . "</td>";
echo "<td>";
$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 "</td>";
echo "</tr>";
echo "<tr class='tab_bg_1'>";
// Actions - Notify Admins
echo "<td>" . __('Notify Administrators', 'cve') . "</td>";
echo "<td>";
$notify_admins = isset($actions['notify_admins']) ? $actions['notify_admins'] : false;
Dropdown::showYesNo('actions_notify_admins', $notify_admins);
echo "</td>";
// Actions - Add to Report
echo "<td>" . __('Add to Vulnerability Report', 'cve') . "</td>";
echo "<td>";
$add_to_report = isset($actions['add_to_report']) ? $actions['add_to_report'] : false;
Dropdown::showYesNo('actions_add_to_report', $add_to_report);
echo "</td>";
echo "</tr>";
echo "<tr class='tab_bg_1'>";
// Is Active
echo "<td>" . __('Active Rule', 'cve') . "</td>";
echo "<td>";
Dropdown::showYesNo('is_active', $this->fields['is_active']);
echo "</td>";
echo "<td colspan='2'></td>";
echo "</tr>";
$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;
}
}

1095
inc/cvesource.class.php Normal file

File diff suppressed because it is too large Load Diff

494
inc/cveticket.class.php Normal file
View File

@ -0,0 +1,494 @@
<?php
/**
* GLPI CVE Plugin - CVE Ticket Class
* Manages links between CVEs and GLPI Tickets
*/
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
/**
* PluginCveCveTicket class for managing CVE-Ticket relations
*/
class PluginCveCveTicket extends CommonDBRelation {
// From CommonDBRelation
static public $itemtype_1 = 'PluginCveCve';
static public $items_id_1 = 'cves_id';
static public $itemtype_2 = 'Ticket';
static public $items_id_2 = 'tickets_id';
static $rightname = 'plugin_cve_ticket';
/**
* 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 Ticket', 'CVE Tickets', $nb, 'cve');
}
/**
* 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_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 "<div class='firstbloc'>";
echo "<form name='cveticket_form$rand' id='cveticket_form$rand' method='post'
action='" . Toolbox::getItemTypeFormURL(__CLASS__) . "'>";
echo "<table class='tab_cadre_fixe'>";
echo "<tr class='tab_bg_2'><th colspan='2'>" . __('Add a ticket', 'cve') . "</th></tr>";
echo "<tr class='tab_bg_1'><td class='right'>";
echo "<input type='hidden' name='cves_id' value='$ID'>";
Ticket::dropdown([
'used' => $used,
'entity' => $cve->getEntityID(),
'entity_sons' => $cve->isRecursive(),
'displaywith' => ['id']
]);
echo "</td><td class='center'>";
echo "<input type='submit' name='add' value=\"" . _sx('button', 'Add') . "\" class='submit'>";
echo "</td></tr>";
echo "</table>";
Html::closeForm();
echo "</div>";
}
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 "<table class='tab_cadre_fixehov'>";
$header_begin = "<tr>";
$header_top = '';
$header_bottom = '';
$header_end = '';
if ($canedit && count($tickets)) {
$header_top .= "<th width='10'>" . Html::getCheckAllAsCheckbox('mass' . __CLASS__ . $rand) . "</th>";
$header_bottom .= "<th width='10'>" . Html::getCheckAllAsCheckbox('mass' . __CLASS__ . $rand) . "</th>";
}
$header_end .= "<th>" . __('Ticket', 'cve') . "</th>";
$header_end .= "<th>" . __('Status', 'cve') . "</th>";
$header_end .= "<th>" . __('Priority', 'cve') . "</th>";
$header_end .= "<th>" . __('Opening date', 'cve') . "</th>";
$header_end .= "<th>" . __('Creation type', 'cve') . "</th>";
$header_end .= "</tr>";
echo $header_begin . $header_top . $header_end;
foreach ($tickets as $data) {
echo "<tr class='tab_bg_1'>";
if ($canedit) {
echo "<td width='10'>";
Html::showMassiveActionCheckBox(__CLASS__, $data['id']);
echo "</td>";
}
$ticket = new Ticket();
$ticket->getFromDB($data['tickets_id']);
echo "<td class='center'>";
if ($ticket->can($data['tickets_id'], READ)) {
echo "<a href=\"" . Ticket::getFormURLWithID($data['tickets_id']) . "\">";
echo $data['ticket_name'] . " (" . $data['tickets_id'] . ")";
echo "</a>";
} else {
echo $data['ticket_name'] . " (" . $data['tickets_id'] . ")";
}
echo "</td>";
// Status
echo "<td class='center'>";
echo Ticket::getStatus($data['ticket_status']);
echo "</td>";
// Priority
echo "<td class='center'>";
echo Ticket::getPriorityName($data['ticket_priority']);
echo "</td>";
// Date
echo "<td class='center'>";
echo Html::convDateTime($data['ticket_date']);
echo "</td>";
// Creation type
echo "<td class='center'>";
echo $data['creation_type'] == 'AUTO' ? __('Automatic', 'cve') : __('Manual', 'cve');
echo "</td>";
echo "</tr>";
}
if ($header_bottom) {
echo $header_begin . $header_bottom . $header_end;
}
echo "</table>";
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 "<div class='firstbloc'>";
echo "<form name='ticketcve_form$rand' id='ticketcve_form$rand' method='post'
action='" . Toolbox::getItemTypeFormURL(__CLASS__) . "'>";
echo "<table class='tab_cadre_fixe'>";
echo "<tr class='tab_bg_2'><th colspan='2'>" . __('Add a CVE', 'cve') . "</th></tr>";
echo "<tr class='tab_bg_1'><td class='right'>";
echo "<input type='hidden' name='tickets_id' value='$ID'>";
$cve = new PluginCveCve();
$cve->dropdown([
'name' => 'cves_id',
'entity' => $ticket->getEntityID(),
'used' => $used
]);
echo "</td><td class='center'>";
echo "<input type='submit' name='add' value=\"" . _sx('button', 'Add') . "\" class='submit'>";
echo "</td></tr>";
echo "</table>";
Html::closeForm();
echo "</div>";
}
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 "<table class='tab_cadre_fixehov'>";
$header_begin = "<tr>";
$header_top = '';
$header_bottom = '';
$header_end = '';
if ($canedit && count($cvetickets)) {
$header_top .= "<th width='10'>" . Html::getCheckAllAsCheckbox('mass' . __CLASS__ . $rand) . "</th>";
$header_bottom .= "<th width='10'>" . Html::getCheckAllAsCheckbox('mass' . __CLASS__ . $rand) . "</th>";
}
$header_end .= "<th>" . __('CVE ID', 'cve') . "</th>";
$header_end .= "<th>" . __('Severity', 'cve') . "</th>";
$header_end .= "<th>" . __('CVSS Score', 'cve') . "</th>";
$header_end .= "<th>" . __('Status', 'cve') . "</th>";
$header_end .= "<th>" . __('Creation Type', 'cve') . "</th>";
$header_end .= "</tr>";
echo $header_begin . $header_top . $header_end;
foreach ($cvetickets as $data) {
echo "<tr class='tab_bg_1'>";
if ($canedit) {
echo "<td width='10'>";
Html::showMassiveActionCheckBox(__CLASS__, $data['id']);
echo "</td>";
}
$cve = new PluginCveCve();
$cve->getFromDB($data['cves_id']);
echo "<td class='center'>";
if ($cve->can($data['cves_id'], READ)) {
echo "<a href=\"" . PluginCveCve::getFormURLWithID($data['cves_id']) . "\">";
echo $data['cve_id'];
echo "</a>";
} else {
echo $data['cve_id'];
}
echo "</td>";
// Severity
echo "<td class='center'>";
echo "<span class='" . PluginCveCve::getSeverityClass($data['severity']) . "'>";
echo $data['severity'];
echo "</span>";
echo "</td>";
// CVSS Score
echo "<td class='center'>";
echo $data['cvss_score'];
echo "</td>";
// Status
echo "<td class='center'>";
echo "<span class='" . PluginCveCve::getStatusClass($data['cve_status']) . "'>";
echo $data['cve_status'];
echo "</span>";
echo "</td>";
// Creation type
echo "<td class='center'>";
echo $data['creation_type'] == 'AUTO' ? __('Automatic', 'cve') : __('Manual', 'cve');
echo "</td>";
echo "</tr>";
}
if ($header_bottom) {
echo $header_begin . $header_bottom . $header_end;
}
echo "</table>";
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;
}
}

78
inc/define.php Normal file
View File

@ -0,0 +1,78 @@
<?php
/**
* GLPI CVE Plugin - Language Definition
* Handles plugin language loading
*/
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
// Translation function for the plugin
function plugin_cve_gettext($text) {
return $text;
}
// Translation function with plurals
function plugin_cve_gettextn($singular, $plural, $number) {
if ($number > 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);
}
}
}

273
inc/menu.class.php Normal file
View File

@ -0,0 +1,273 @@
<?php
/**
* GLPI CVE Plugin - Menu Class
* Manages the plugin menu entries
*/
if (!defined('GLPI_ROOT')) {
die("Sorry. You can't access this file directly");
}
/**
* PluginCveCveMenu class for managing plugin menu entries
*/
class PluginCveCveMenu extends CommonGLPI {
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 __('Vulnérabilité', 'cve');
}
/**
* Get menu name
*
* @return string
*/
static function getMenuName() {
return __('Vulnérabilité', 'cve');
}
/**
* Get menu comment
*
* @return string
*/
static function getMenuComment() {
return __('Common Vulnerabilities and Exposures', 'cve');
}
/**
* Check plugin's rights
*
* @return boolean
*/
static function canView() {
return Session::haveRight(self::$rightname, READ);
}
/**
* Check plugin's rights for creation
*
* @return boolean
*/
static function canCreate() {
return Session::haveRight(self::$rightname, CREATE);
}
/**
* Get plugin menu items
*
* @param string $menu Menu name
* @return array Menu entry
*/
static function getMenuContent() {
$menu = [];
if (PluginCveCve::canView()) {
$menu['title'] = self::getMenuName();
$menu['page'] = '/plugins/cve/front/cve.php';
$menu['icon'] = 'fas fa-shield-alt';
$menu['options'] = [
'cve' => [
'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 "<div class='center'>";
// 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 "<table class='tab_cadre_fixe'>";
echo "<tr class='tab_bg_2'><th colspan='6'>" . __('Vulnerabilities', 'cve') . "</th></tr>";
echo "<tr class='tab_bg_1'>";
echo "<th>" . __('CVE ID', 'cve') . "</th>";
echo "<th>" . __('Version', 'cve') . "</th>";
echo "<th>" . __('Severity', 'cve') . "</th>";
echo "<th>" . __('CVSS Score', 'cve') . "</th>";
echo "<th>" . __('Description', 'cve') . "</th>";
echo "<th>" . __('Status', 'cve') . "</th>";
echo "</tr>";
while ($data = $DB->fetchAssoc($result)) {
echo "<tr class='tab_bg_1'>";
// CVE ID
echo "<td>";
echo "<a href='" . PluginCveCve::getFormURLWithID($data['cves_id']) . "'>";
echo $data['cve_id'];
echo "</a>";
echo "</td>";
// Version
echo "<td>";
echo $data['version_name'];
echo "</td>";
// Severity
echo "<td>";
echo "<span class='" . PluginCveCve::getSeverityClass($data['severity']) . "'>";
echo $data['severity'];
echo "</span>";
echo "</td>";
// CVSS Score
echo "<td>";
echo $data['cvss_score'];
echo "</td>";
// Description
echo "<td>";
echo Html::resume_text($data['description'], 100);
echo "</td>";
// Status
echo "<td>";
echo $data['status'];
if ($data['tickets_id'] > 0) {
echo " (";
echo "<a href='" . Ticket::getFormURLWithID($data['tickets_id']) . "'>";
echo __('Ticket', 'cve') . " #" . $data['tickets_id'];
echo "</a>";
echo ")";
}
echo "</td>";
echo "</tr>";
}
echo "</table>";
} else {
echo "<table class='tab_cadre_fixe'>";
echo "<tr class='tab_bg_2'><th>" . __('Vulnerabilities', 'cve') . "</th></tr>";
echo "<tr class='tab_bg_1'><td class='center'>" . __('No vulnerabilities found for this software', 'cve') . "</td></tr>";
echo "</table>";
}
// Manual scan button
if (Session::haveRight("plugin_cve_inventory", UPDATE)) {
echo "<div class='center' style='margin-top: 10px;'>";
echo "<form method='post' action='/plugins/cve/front/inventory.php'>";
echo "<input type='submit' name='scan_now' value=\"" . __('Scan for vulnerabilities now', 'cve') . "\" class='submit'>";
Html::closeForm();
echo "</div>";
}
echo "</div>";
}
}