mirror of
https://github.com/tips-of-mine/GLPI-Plugin-CVE-Prototype.git
synced 2025-06-28 07:08:44 +02:00
Start repository
This commit is contained in:
752
inc/cve.class.php
Normal file
752
inc/cve.class.php
Normal 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
414
inc/cvealert.class.php
Normal 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
358
inc/cveinventory.class.php
Normal 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
497
inc/cverule.class.php
Normal 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
1095
inc/cvesource.class.php
Normal file
File diff suppressed because it is too large
Load Diff
494
inc/cveticket.class.php
Normal file
494
inc/cveticket.class.php
Normal 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
78
inc/define.php
Normal 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
273
inc/menu.class.php
Normal 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>";
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user