515 lines
16 KiB
PHP
515 lines
16 KiB
PHP
<?php
|
|
/*
|
|
* Plugin SIEM-Wazuh pour GLPI
|
|
* Classe de gestion de l'API Wazuh
|
|
*/
|
|
|
|
if (!defined('GLPI_ROOT')) {
|
|
die("Sorry. You can't access this file directly");
|
|
}
|
|
|
|
class PluginSiemWazuhAPI {
|
|
|
|
private $server;
|
|
private $token;
|
|
private $config;
|
|
|
|
const API_VERSION = 'v1';
|
|
const TIMEOUT = 30;
|
|
const MAX_RETRIES = 3;
|
|
|
|
/**
|
|
* Constructor
|
|
*/
|
|
public function __construct(PluginSiemWazuhServer $server) {
|
|
$this->server = $server;
|
|
$this->config = new PluginSiemWazuhConfig();
|
|
}
|
|
|
|
/**
|
|
* Test connection to Wazuh server
|
|
*/
|
|
public function testConnection() {
|
|
try {
|
|
$result = $this->authenticate();
|
|
if ($result['success']) {
|
|
// Test basic API call
|
|
$info = $this->makeRequest('GET', '/');
|
|
if ($info['success']) {
|
|
return [
|
|
'success' => true,
|
|
'message' => __('Connection successful', 'siem-wazuh'),
|
|
'data' => $info['data']
|
|
];
|
|
} else {
|
|
return [
|
|
'success' => false,
|
|
'message' => __('Connection failed:', 'siem-wazuh') . ' ' . $info['message']
|
|
];
|
|
}
|
|
} else {
|
|
return [
|
|
'success' => false,
|
|
'message' => __('Authentication failed:', 'siem-wazuh') . ' ' . $result['message']
|
|
];
|
|
}
|
|
} catch (Exception $e) {
|
|
return [
|
|
'success' => false,
|
|
'message' => __('Connection error:', 'siem-wazuh') . ' ' . $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Authenticate with Wazuh API
|
|
*/
|
|
private function authenticate() {
|
|
if (!empty($this->token)) {
|
|
return ['success' => true, 'token' => $this->token];
|
|
}
|
|
|
|
$url = rtrim($this->server->fields['wazuh_url'], '/') . ':' . $this->server->fields['wazuh_port'] . '/security/user/authenticate';
|
|
|
|
$credentials = base64_encode($this->server->fields['wazuh_login'] . ':' . $this->server->getPassword());
|
|
|
|
$headers = [
|
|
'Authorization: Basic ' . $credentials,
|
|
'Content-Type: application/json'
|
|
];
|
|
|
|
try {
|
|
$response = $this->httpRequest('POST', $url, null, $headers);
|
|
|
|
if ($response['success'] && isset($response['data']['data']['token'])) {
|
|
$this->token = $response['data']['data']['token'];
|
|
return ['success' => true, 'token' => $this->token];
|
|
} else {
|
|
return [
|
|
'success' => false,
|
|
'message' => $response['message'] ?? __('Authentication failed', 'siem-wazuh')
|
|
];
|
|
}
|
|
} catch (Exception $e) {
|
|
return [
|
|
'success' => false,
|
|
'message' => $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get alerts from Wazuh
|
|
*/
|
|
public function getAlerts($params = []) {
|
|
$auth_result = $this->authenticate();
|
|
if (!$auth_result['success']) {
|
|
return $auth_result;
|
|
}
|
|
|
|
$default_params = [
|
|
'offset' => 0,
|
|
'limit' => $this->config->getConfiguration('max_alerts_per_sync', 100),
|
|
'sort' => '-timestamp',
|
|
'rule.level' => '>=' . $this->config->getConfiguration('min_rule_level', 5)
|
|
];
|
|
|
|
// Merge with provided parameters
|
|
$params = array_merge($default_params, $params);
|
|
|
|
// Construire l'URL avec les paramètres
|
|
$query_string = http_build_query($params);
|
|
$endpoint = '/alerts?' . $query_string;
|
|
|
|
return $this->makeRequest('GET', $endpoint);
|
|
}
|
|
|
|
/**
|
|
* Get alert by ID
|
|
*/
|
|
public function getAlert($alert_id) {
|
|
$auth_result = $this->authenticate();
|
|
if (!$auth_result['success']) {
|
|
return $auth_result;
|
|
}
|
|
|
|
return $this->makeRequest('GET', '/alerts/' . $alert_id);
|
|
}
|
|
|
|
/**
|
|
* Get agents
|
|
*/
|
|
public function getAgents($params = []) {
|
|
$auth_result = $this->authenticate();
|
|
if (!$auth_result['success']) {
|
|
return $auth_result;
|
|
}
|
|
|
|
$default_params = [
|
|
'offset' => 0,
|
|
'limit' => 500,
|
|
'sort' => '+name'
|
|
];
|
|
|
|
$params = array_merge($default_params, $params);
|
|
$query_string = http_build_query($params);
|
|
$endpoint = '/agents?' . $query_string;
|
|
|
|
return $this->makeRequest('GET', $endpoint);
|
|
}
|
|
|
|
/**
|
|
* Get agent by ID
|
|
*/
|
|
public function getAgent($agent_id) {
|
|
$auth_result = $this->authenticate();
|
|
if (!$auth_result['success']) {
|
|
return $auth_result;
|
|
}
|
|
|
|
return $this->makeRequest('GET', '/agents/' . $agent_id);
|
|
}
|
|
|
|
/**
|
|
* Get rules
|
|
*/
|
|
public function getRules($params = []) {
|
|
$auth_result = $this->authenticate();
|
|
if (!$auth_result['success']) {
|
|
return $auth_result;
|
|
}
|
|
|
|
$default_params = [
|
|
'offset' => 0,
|
|
'limit' => 500,
|
|
'sort' => '+id'
|
|
];
|
|
|
|
$params = array_merge($default_params, $params);
|
|
$query_string = http_build_query($params);
|
|
$endpoint = '/rules?' . $query_string;
|
|
|
|
return $this->makeRequest('GET', $endpoint);
|
|
}
|
|
|
|
/**
|
|
* Get decoders
|
|
*/
|
|
public function getDecoders($params = []) {
|
|
$auth_result = $this->authenticate();
|
|
if (!$auth_result['success']) {
|
|
return $auth_result;
|
|
}
|
|
|
|
$default_params = [
|
|
'offset' => 0,
|
|
'limit' => 500
|
|
];
|
|
|
|
$params = array_merge($default_params, $params);
|
|
$query_string = http_build_query($params);
|
|
$endpoint = '/decoders?' . $query_string;
|
|
|
|
return $this->makeRequest('GET', $endpoint);
|
|
}
|
|
|
|
/**
|
|
* Get cluster status
|
|
*/
|
|
public function getClusterStatus() {
|
|
$auth_result = $this->authenticate();
|
|
if (!$auth_result['success']) {
|
|
return $auth_result;
|
|
}
|
|
|
|
return $this->makeRequest('GET', '/cluster/status');
|
|
}
|
|
|
|
/**
|
|
* Get manager information
|
|
*/
|
|
public function getManagerInfo() {
|
|
$auth_result = $this->authenticate();
|
|
if (!$auth_result['success']) {
|
|
return $auth_result;
|
|
}
|
|
|
|
return $this->makeRequest('GET', '/manager/info');
|
|
}
|
|
|
|
/**
|
|
* Make API request
|
|
*/
|
|
private function makeRequest($method, $endpoint, $data = null, $extra_headers = []) {
|
|
if (!$this->token) {
|
|
$auth_result = $this->authenticate();
|
|
if (!$auth_result['success']) {
|
|
return $auth_result;
|
|
}
|
|
}
|
|
|
|
$url = rtrim($this->server->fields['wazuh_url'], '/') . ':' . $this->server->fields['wazuh_port'] . $endpoint;
|
|
|
|
$headers = array_merge([
|
|
'Authorization: Bearer ' . $this->token,
|
|
'Content-Type: application/json'
|
|
], $extra_headers);
|
|
|
|
return $this->httpRequest($method, $url, $data, $headers);
|
|
}
|
|
|
|
/**
|
|
* Make HTTP request with retry logic
|
|
*/
|
|
private function httpRequest($method, $url, $data = null, $headers = []) {
|
|
$retries = 0;
|
|
$last_error = null;
|
|
|
|
while ($retries < self::MAX_RETRIES) {
|
|
try {
|
|
$ch = curl_init();
|
|
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_URL => $url,
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_TIMEOUT => self::TIMEOUT,
|
|
CURLOPT_CONNECTTIMEOUT => 10,
|
|
CURLOPT_SSL_VERIFYPEER => false,
|
|
CURLOPT_SSL_VERIFYHOST => false,
|
|
CURLOPT_CUSTOMREQUEST => $method,
|
|
CURLOPT_HTTPHEADER => $headers,
|
|
CURLOPT_USERAGENT => 'GLPI-SIEM-Wazuh-Plugin/1.0'
|
|
]);
|
|
|
|
if ($data !== null) {
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, is_array($data) ? json_encode($data) : $data);
|
|
}
|
|
|
|
// Logging for debug mode
|
|
if ($this->config->getConfiguration('debug_mode', 0)) {
|
|
$this->logDebug("HTTP Request: $method $url", [
|
|
'headers' => $headers,
|
|
'data' => $data,
|
|
'retry' => $retries
|
|
]);
|
|
}
|
|
|
|
$response = curl_exec($ch);
|
|
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
$curl_error = curl_error($ch);
|
|
|
|
curl_close($ch);
|
|
|
|
if ($response === false) {
|
|
throw new Exception("cURL Error: $curl_error");
|
|
}
|
|
|
|
$decoded_response = json_decode($response, true);
|
|
|
|
if ($this->config->getConfiguration('debug_mode', 0)) {
|
|
$this->logDebug("HTTP Response: $http_code", [
|
|
'response' => $decoded_response,
|
|
'raw_response' => substr($response, 0, 1000)
|
|
]);
|
|
}
|
|
|
|
if ($http_code >= 200 && $http_code < 300) {
|
|
return [
|
|
'success' => true,
|
|
'data' => $decoded_response,
|
|
'http_code' => $http_code
|
|
];
|
|
} elseif ($http_code == 401) {
|
|
// Token expired, clear it and retry authentication
|
|
$this->token = null;
|
|
if ($retries < self::MAX_RETRIES - 1) {
|
|
$retries++;
|
|
continue;
|
|
}
|
|
}
|
|
|
|
$error_message = "HTTP $http_code";
|
|
if ($decoded_response && isset($decoded_response['detail'])) {
|
|
$error_message .= ": " . $decoded_response['detail'];
|
|
} elseif ($decoded_response && isset($decoded_response['message'])) {
|
|
$error_message .= ": " . $decoded_response['message'];
|
|
}
|
|
|
|
throw new Exception($error_message);
|
|
|
|
} catch (Exception $e) {
|
|
$last_error = $e;
|
|
$retries++;
|
|
|
|
if ($retries < self::MAX_RETRIES) {
|
|
sleep(pow(2, $retries)); // Exponential backoff
|
|
continue;
|
|
}
|
|
|
|
$this->logError("HTTP Request failed after $retries retries", [
|
|
'url' => $url,
|
|
'method' => $method,
|
|
'error' => $e->getMessage()
|
|
]);
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
return [
|
|
'success' => false,
|
|
'message' => $last_error ? $last_error->getMessage() : __('Request failed after maximum retries', 'siem-wazuh')
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Query Wazuh Indexer (OpenSearch/Elasticsearch)
|
|
*/
|
|
public function queryIndexer($index, $query = [], $params = []) {
|
|
if (empty($this->server->fields['indexer_url'])) {
|
|
return [
|
|
'success' => false,
|
|
'message' => __('Indexer not configured', 'siem-wazuh')
|
|
];
|
|
}
|
|
|
|
$url = rtrim($this->server->fields['indexer_url'], '/') . ':' . $this->server->fields['indexer_port'] . '/' . $index . '/_search';
|
|
|
|
$headers = [
|
|
'Content-Type: application/json'
|
|
];
|
|
|
|
if (!empty($this->server->fields['indexer_login'])) {
|
|
$credentials = base64_encode($this->server->fields['indexer_login'] . ':' . $this->server->getPassword('indexer_password'));
|
|
$headers[] = 'Authorization: Basic ' . $credentials;
|
|
}
|
|
|
|
$default_query = [
|
|
'size' => $this->config->getConfiguration('max_alerts_per_sync', 100),
|
|
'sort' => [
|
|
['timestamp' => ['order' => 'desc']]
|
|
]
|
|
];
|
|
|
|
$final_query = array_merge($default_query, $query);
|
|
|
|
return $this->httpRequest('POST', $url, $final_query, $headers);
|
|
}
|
|
|
|
/**
|
|
* Get recent alerts from indexer
|
|
*/
|
|
public function getRecentAlertsFromIndexer($minutes = 60) {
|
|
$query = [
|
|
'query' => [
|
|
'bool' => [
|
|
'must' => [
|
|
[
|
|
'range' => [
|
|
'timestamp' => [
|
|
'gte' => 'now-' . $minutes . 'm',
|
|
'lt' => 'now'
|
|
]
|
|
]
|
|
]
|
|
]
|
|
]
|
|
]
|
|
];
|
|
|
|
$min_level = $this->config->getConfiguration('min_rule_level', 5);
|
|
if ($min_level > 0) {
|
|
$query['query']['bool']['must'][] = [
|
|
'range' => [
|
|
'rule.level' => [
|
|
'gte' => $min_level
|
|
]
|
|
]
|
|
];
|
|
}
|
|
|
|
return $this->queryIndexer('wazuh-alerts-*', $query);
|
|
}
|
|
|
|
/**
|
|
* Log debug message
|
|
*/
|
|
private function logDebug($message, $context = []) {
|
|
if ($this->config->getConfiguration('debug_mode', 0)) {
|
|
$this->log('debug', $message, $context);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Log error message
|
|
*/
|
|
private function logError($message, $context = []) {
|
|
$this->log('error', $message, $context);
|
|
}
|
|
|
|
/**
|
|
* Log message
|
|
*/
|
|
private function log($level, $message, $context = []) {
|
|
global $DB;
|
|
|
|
$log_levels = ['debug', 'info', 'warning', 'error', 'critical'];
|
|
$current_log_level = $this->config->getConfiguration('log_level', 'info');
|
|
$current_level_index = array_search($current_log_level, $log_levels);
|
|
$message_level_index = array_search($level, $log_levels);
|
|
|
|
// Only log if message level is equal or higher than configured level
|
|
if ($message_level_index >= $current_level_index) {
|
|
try {
|
|
$DB->insert('glpi_plugin_siem_wazuh_logs', [
|
|
'wazuh_server_id' => $this->server->getID(),
|
|
'level' => $level,
|
|
'message' => $message,
|
|
'context' => json_encode($context),
|
|
'date_creation' => $_SESSION['glpi_currenttime']
|
|
]);
|
|
} catch (Exception $e) {
|
|
// Ignore logging errors to prevent recursion
|
|
error_log("SIEM-Wazuh Plugin: Failed to log message - " . $e->getMessage());
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get API statistics
|
|
*/
|
|
public function getStatistics() {
|
|
global $DB;
|
|
|
|
$stats = [];
|
|
|
|
// Requests count by server
|
|
$iterator = $DB->request([
|
|
'SELECT' => ['COUNT' => '* as count'],
|
|
'FROM' => 'glpi_plugin_siem_wazuh_logs',
|
|
'WHERE' => [
|
|
'wazuh_server_id' => $this->server->getID(),
|
|
'date_creation' => ['>=', date('Y-m-d 00:00:00')]
|
|
]
|
|
]);
|
|
|
|
$stats['requests_today'] = $iterator->current()['count'];
|
|
|
|
// Error rate
|
|
$iterator = $DB->request([
|
|
'SELECT' => ['COUNT' => '* as count'],
|
|
'FROM' => 'glpi_plugin_siem_wazuh_logs',
|
|
'WHERE' => [
|
|
'wazuh_server_id' => $this->server->getID(),
|
|
'level' => ['IN', ['error', 'critical']],
|
|
'date_creation' => ['>=', date('Y-m-d 00:00:00')]
|
|
]
|
|
]);
|
|
|
|
$stats['errors_today'] = $iterator->current()['count'];
|
|
$stats['error_rate'] = $stats['requests_today'] > 0 ?
|
|
round(($stats['errors_today'] / $stats['requests_today']) * 100, 2) : 0;
|
|
|
|
return $stats;
|
|
}
|
|
} |