183 lines
6.7 KiB
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()
|
|
];
|
|
}
|
|
}
|