Files
BRISTO/app/Services/CortexXdrService.php

183 lines
6.7 KiB
PHP

<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class CortexXdrService
{
protected ?string $apiKey;
protected ?string $apiKeyId;
protected ?string $baseUrl;
public function __construct()
{
$this->apiKey = config('services.cortex.key');
$this->apiKeyId = config('services.cortex.id');
$this->baseUrl = config('services.cortex.url');
}
/**
* Generate headers.
* Switched to Standard Authentication (Direct Key) based on documentation screenshot.
*/
protected function getHeaders()
{
// STANDARD AUTHENTICATION
return [
'x-xdr-auth-id' => $this->apiKeyId,
'Authorization' => $this->apiKey,
'Content-Type' => 'application/json',
'Accept' => 'application/json',
];
}
/**
* Get Incidents
* Using pagination to fetch up to $limit incidents.
*/
public function getIncidents($limit = 1000, $filters = [])
{
return $this->fetchAll('incidents/get_incidents', 'incidents', $limit, $filters);
}
/**
* Get Endpoints
* Using pagination to fetch up to $limit endpoints.
*/
public function getEndpoints($limit = 1000)
{
return $this->fetchAll('endpoints/get_endpoint', 'endpoints', $limit);
}
/**
* Generic fetch method with pagination.
*/
private function fetchAll($endpoint, $dataKey, $limit, $filters = [])
{
if (empty($this->apiKey) || empty($this->apiKeyId) || empty($this->baseUrl)) {
Log::warning('Cortex XDR credentials missing.');
return [];
}
$allResults = [];
$batchSize = 100; // API Limit
$offset = 0;
try {
while ($offset < $limit) {
// Adjust batch size for last page if needed
$currentBatchSize = min($batchSize, $limit - $offset);
// API confusingly uses search_to as count? Or offset + count?
// Documentation: "search_to: Integer representing the ending offset" ?
// Error message: "0 < search_size <= 100".
// Wait, request_data documentation usually says:
// search_from: integer
// search_to: integer (exclusive end index?) or count?
// Let's assume standard Cortex: offset based.
// If search_from=0, search_to=100 -> gets 0 to 99 (100 items).
$response = Http::withHeaders($this->getHeaders())
->post("{$this->baseUrl}/public_api/v1/{$endpoint}/", [
'request_data' => array_merge([
'search_from' => $offset,
'search_to' => $offset + $currentBatchSize,
], !empty($filters) ? ['filters' => $filters] : [])
]);
if ($response->successful()) {
$items = $response->json()['reply'][$dataKey] ?? [];
if (empty($items)) {
break; // No more items
}
$allResults = array_merge($allResults, $items);
$offset += count($items);
// If we got fewer items than requested, we are done
if (count($items) < $currentBatchSize) {
break;
}
} else {
$err = "Cortex API Error ({$endpoint}): " . $response->status() . ' - ' . $response->body();
Log::error($err);
throw new \Exception($err);
}
}
return $allResults;
} catch (\Exception $e) {
throw $e;
}
}
/**
* Get Dashboard Summary
*/
public function getSummary()
{
try {
// Filter to fetch only active incidents to improve performance
$filters = [
[
'field' => 'status',
'operator' => 'in',
'value' => ['new', 'under_investigation']
]
];
$incidents = $this->getIncidents(1000, $filters); // Limit incidents to 1000 recent active
$endpoints = $this->getEndpoints(1000); // Limit endpoints to 2000
} catch (\Exception $e) {
// In dashboard context, we verify configuration separately.
// Returning empty arrays here avoids crashing the page if just one call fails.
$incidents = [];
$endpoints = [];
}
// Calculations
$endpointsTotal = count($endpoints);
$endpointsConnected = collect($endpoints)->filter(fn($e) => strtolower($e['endpoint_status'] ?? '') === 'connected')->count();
$endpointsDisconnected = collect($endpoints)->filter(fn($e) => strtolower($e['endpoint_status'] ?? '') === 'disconnected')->count();
// Filter only Active Incidents (New or Under Investigation)
// Since we now filter at API level, we can just use the result,
// but keeping the collection filter adds a layer of safety if filters change.
$activeIncidents = collect($incidents)->filter(fn($i) => in_array($i['status'] ?? '', ['new', 'under_investigation']));
//die(var_dump($activeIncidents));
$incidentsCritical = $activeIncidents->where('severity', 'critical')->count();
$incidentsHigh = $activeIncidents->where('severity', 'high')->count();
$incidentsMedium = $activeIncidents->where('severity', 'medium')->count();
$incidentsLow = $activeIncidents->where('severity', 'low')->count();
// Endpoint Types (Workstation, Server, etc.)
$endpointTypes = collect($endpoints)->groupBy(function ($e) {
return ucfirst(strtolower($e['endpoint_type'] ?? 'Unknown'));
})->map->count();
return [
'incidents_total' => $activeIncidents->count(),
'incidents_critical' => $incidentsCritical,
'incidents_high' => $incidentsHigh,
'incidents_medium' => $incidentsMedium,
'incidents_low' => $incidentsLow,
'endpoints_total' => $endpointsTotal,
'endpoints_connected' => $endpointsConnected,
'endpoints_disconnected' => $endpointsDisconnected,
'endpoints_types' => $endpointTypes, // New Data
'recent_incidents' => collect($incidents)
->filter(fn($i) => !in_array($i['status'] ?? '', ['resolved_true_positive', 'resolved_false_positive']))
->sortByDesc('incident_id')
->slice(0, 10)
->values()
->all()
];
}
}