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

358 lines
12 KiB
PHP

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