324 lines
16 KiB
PHP
324 lines
16 KiB
PHP
@extends('layouts.app')
|
|
|
|
@section('title', 'Cortex XDR')
|
|
|
|
@section('content')
|
|
<div class="mb-4">
|
|
<!-- Configuration Warning (Server-side check) -->
|
|
<!-- We can't check config in view easily if controller doesn't pass it,
|
|
but we'll handle errors in JS -->
|
|
|
|
<div class="d-flex justify-content-between align-items-center mb-4">
|
|
<div>
|
|
<h5 class="text-muted">Vue d'ensemble de la sécurité</h5>
|
|
</div>
|
|
<div>
|
|
<span class="badge bg-dark border border-secondary p-2" id="api-status-badge">
|
|
<span class="spinner-border spinner-border-sm text-secondary me-1" role="status" aria-hidden="true" id="status-spinner"></span>
|
|
<span id="api-status-text">Connexion...</span>
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Loader Overlay -->
|
|
<div id="loading-overlay" class="text-center py-5">
|
|
<div class="spinner-border text-primary" style="width: 3rem; height: 3rem;" role="status">
|
|
<span class="visually-hidden">Loading...</span>
|
|
</div>
|
|
<p class="mt-3 text-muted">Chargement des données Cortex XDR en cours...</p>
|
|
<p class="small text-muted">Cela peut prendre quelques secondes (récupération de ~1000 postes).</p>
|
|
</div>
|
|
|
|
<!-- Main Content (Hidden initially) -->
|
|
<div id="dashboard-content" class="d-none">
|
|
<!-- KPI Cards -->
|
|
<div class="row g-4 mb-4">
|
|
<!-- Incidents Total -->
|
|
<div class="col-md-3">
|
|
<div class="card shadow-sm border-0 h-100">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h6 class="text-muted mb-0">Total Incidents</h6>
|
|
<div class="icon-shape bg-primary bg-opacity-10 text-primary rounded-circle p-2">
|
|
<i class="bi bi-bug fs-4"></i>
|
|
</div>
|
|
</div>
|
|
<h2 class="display-6 fw-bold mb-0 text-white" id="incidents-total">-</h2>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Endpoints Total -->
|
|
<div class="col-md-3">
|
|
<div class="card shadow-sm border-0 h-100">
|
|
<div class="card-body">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h6 class="text-muted mb-0">Total Postes</h6>
|
|
<div class="icon-shape bg-info bg-opacity-10 text-info rounded-circle p-2">
|
|
<i class="bi bi-laptop fs-4"></i>
|
|
</div>
|
|
</div>
|
|
<h2 class="display-6 fw-bold mb-0 text-white" id="endpoints-total">-</h2>
|
|
<div class="mt-2 text-muted small">
|
|
<i class="bi bi-wifi text-success"></i> <span id="endpoints-connected-sm">-</span> Connectés
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Critical Incidents -->
|
|
<div class="col-md-3">
|
|
<div class="card shadow-sm border-0 h-100 position-relative overflow-hidden">
|
|
<div class="position-absolute top-0 start-0 w-1 pt-1 h-100 bg-danger"></div>
|
|
<div class="card-body ps-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h6 class="text-danger mb-0">Critiques</h6>
|
|
<i class="bi bi-lightning-charge-fill text-danger fs-4"></i>
|
|
</div>
|
|
<h2 class="display-6 fw-bold mb-0 text-white" id="incidents-critical">-</h2>
|
|
<small class="text-muted">Incidents nécessitant attention immédiate</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- High Incidents -->
|
|
<div class="col-md-3">
|
|
<div class="card shadow-sm border-0 h-100 position-relative overflow-hidden">
|
|
<div class="position-absolute top-0 start-0 w-1 pt-1 h-100 bg-warning"></div>
|
|
<div class="card-body ps-4">
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h6 class="text-warning mb-0">Élevés</h6>
|
|
<i class="bi bi-exclamation-circle-fill text-warning fs-4"></i>
|
|
</div>
|
|
<h2 class="display-6 fw-bold mb-0 text-white" id="incidents-high">-</h2>
|
|
<small class="text-muted">Incidents à traiter rapidement</small>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Detailed Breakdown Row -->
|
|
<div class="row g-4">
|
|
<!-- Recent Incidents Table -->
|
|
<div class="col-md-8">
|
|
<div class="card shadow-sm border-0 h-100">
|
|
<div class="card-header border-bottom-0 bg-transparent py-3">
|
|
<h5 class="card-title fw-bold text-white mb-0">
|
|
<i class="bi bi-clock-history me-2 text-primary"></i>Derniers Incidents
|
|
</h5>
|
|
</div>
|
|
<div class="card-body p-0">
|
|
<div class="table-responsive">
|
|
<table class="table table-hover align-middle mb-0">
|
|
<thead>
|
|
<tr>
|
|
<th class="ps-4">ID</th>
|
|
<th>Observed Hosts</th>
|
|
<th>Sévérité</th>
|
|
<th>Statut</th>
|
|
<th>Date</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="incidents-table-body">
|
|
<!-- Rows injected by JS -->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Endpoints Status -->
|
|
<div class="col-md-4">
|
|
<div class="card shadow-sm border-0 h-100">
|
|
<div class="card-header border-bottom-0 bg-transparent py-3">
|
|
<h5 class="card-title fw-bold text-white mb-0">
|
|
<i class="bi bi-hdd-network me-2 text-info"></i>État du Parc
|
|
</h5>
|
|
</div>
|
|
<div class="card-body">
|
|
<div class="mb-4">
|
|
<div class="d-flex justify-content-between mb-1">
|
|
<span class="text-white">Connectés</span>
|
|
<span class="text-success fw-bold" id="endpoints-connected">-</span>
|
|
</div>
|
|
<div class="progress" style="height: 6px;">
|
|
<div id="progress-connected" class="progress-bar bg-success" role="progressbar" style="width: 0%"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="mb-4">
|
|
<div class="d-flex justify-content-between mb-1">
|
|
<span class="text-white">Déconnectés</span>
|
|
<span class="text-secondary fw-bold" id="endpoints-disconnected">-</span>
|
|
</div>
|
|
<div class="progress" style="height: 6px;">
|
|
<div id="progress-disconnected" class="progress-bar bg-secondary" role="progressbar" style="width: 0%"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Endpoint Types Breakdown -->
|
|
<div class="mb-4">
|
|
<h6 class="text-white small fw-bold mb-2">Répartition par Type</h6>
|
|
<div class="d-flex flex-wrap gap-2" id="endpoint-types-container">
|
|
<span class="text-muted small">Chargement...</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="alert alert-dark border-secondary mt-3 mb-0">
|
|
<div class="d-flex">
|
|
<i class="bi bi-info-circle me-2 text-info"></i>
|
|
<div class="small text-muted">
|
|
Le parc est majoritairement <strong class="text-white" id="network-status-text">...</strong>.
|
|
<span id="disconnected-count-text">0</span> machines n'ont pas communiqué récemment.
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
fetchData();
|
|
});
|
|
|
|
function fetchData() {
|
|
fetch("{{ route('cortex.data') }}")
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error('Erreur réseau ou configuration');
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
console.log("Cortex Data Received:", data); // Debug
|
|
|
|
// Hide loader, show content
|
|
document.getElementById('loading-overlay').classList.add('d-none');
|
|
document.getElementById('dashboard-content').classList.remove('d-none');
|
|
|
|
// Update Status Badge
|
|
const statusText = document.getElementById('api-status-text');
|
|
const spinner = document.getElementById('status-spinner');
|
|
if (data.mock) {
|
|
statusText.innerText = 'Demo Mode';
|
|
} else {
|
|
statusText.innerText = 'Connecté';
|
|
const checkIcon = document.querySelector('.bi-shield-check');
|
|
if(checkIcon) checkIcon.classList.remove('d-none');
|
|
}
|
|
spinner.classList.add('d-none');
|
|
|
|
// Update Counters safely
|
|
document.getElementById('incidents-total').innerText = data.incidents_total || 0;
|
|
document.getElementById('endpoints-total').innerText = data.endpoints_total || 0;
|
|
document.getElementById('endpoints-connected-sm').innerText = data.endpoints_connected || 0;
|
|
document.getElementById('incidents-critical').innerText = data.incidents_critical || 0;
|
|
document.getElementById('incidents-high').innerText = data.incidents_high || 0;
|
|
|
|
// Update Breakdown
|
|
document.getElementById('endpoints-connected').innerText = data.endpoints_connected || 0;
|
|
document.getElementById('endpoints-disconnected').innerText = data.endpoints_disconnected || 0;
|
|
|
|
// Progress Bars
|
|
const total = (data.endpoints_total && data.endpoints_total > 0) ? data.endpoints_total : 1;
|
|
const connPct = ((data.endpoints_connected || 0) / total) * 100;
|
|
const discPct = ((data.endpoints_disconnected || 0) / total) * 100;
|
|
|
|
document.getElementById('progress-connected').style.width = connPct + '%';
|
|
document.getElementById('progress-disconnected').style.width = discPct + '%';
|
|
|
|
// Endpoint Types
|
|
const typesContainer = document.getElementById('endpoint-types-container');
|
|
typesContainer.innerHTML = '';
|
|
if (data.endpoints_types && typeof data.endpoints_types === 'object') {
|
|
Object.keys(data.endpoints_types).forEach(type => {
|
|
const count = data.endpoints_types[type];
|
|
let icon = 'bi-pc-display';
|
|
if (type.includes('Server')) icon = 'bi-hdd-rack';
|
|
else if (type.includes('Mobile') || type.includes('Phone')) icon = 'bi-phone';
|
|
else if (type.includes('Lab') || type.includes('Virtual')) icon = 'bi-box';
|
|
|
|
typesContainer.innerHTML += `
|
|
<div class="d-flex align-items-center bg-dark border border-secondary rounded px-2 py-1">
|
|
<i class="bi ${icon} me-2 text-muted"></i>
|
|
<span class="text-white small me-2">${type}</span>
|
|
<span class="badge bg-secondary rounded-pill">${count}</span>
|
|
</div>
|
|
`;
|
|
});
|
|
} else {
|
|
typesContainer.innerHTML = '<span class="text-muted small">Aucune donnée de type</span>';
|
|
}
|
|
|
|
// Info Text
|
|
|
|
// Info Text
|
|
document.getElementById('network-status-text').innerText = connPct > 50 ? 'connecté' : 'déconnecté';
|
|
document.getElementById('disconnected-count-text').innerText = data.endpoints_disconnected || 0;
|
|
|
|
// Render Table
|
|
const tbody = document.getElementById('incidents-table-body');
|
|
let tableContent = '';
|
|
|
|
if (data.recent_incidents && Array.isArray(data.recent_incidents) && data.recent_incidents.length > 0) {
|
|
data.recent_incidents.forEach(incident => {
|
|
const sev = (incident.severity || 'unknown').toString().toLowerCase();
|
|
let badgeClass = 'light';
|
|
if (sev === 'critical') badgeClass = 'danger';
|
|
else if (sev === 'high') badgeClass = 'warning';
|
|
else if (sev === 'medium') badgeClass = 'info';
|
|
else if (sev === 'low') badgeClass = 'secondary';
|
|
|
|
const dateStr = incident.creation_time ? new Date(incident.creation_time).toLocaleString() : '-';
|
|
const link = incident.xdr_url || '#';
|
|
const host = (incident.hosts && incident.hosts.length > 0) ? incident.hosts[0] : 'Unknown Host';
|
|
const status = incident.status || 'Active';
|
|
|
|
tableContent += `
|
|
<tr>
|
|
<td class="ps-4 text-white">#${incident.incident_id || '-'}</td>
|
|
<td>
|
|
<div class="d-flex align-items-center">
|
|
<div class="avatar-sm bg-dark rounded-circle me-2 d-flex justify-content-center align-items-center" style="width:30px;height:30px">
|
|
<i class="bi bi-pc-display text-muted small"></i>
|
|
</div>
|
|
<a href="${link}" target="_blank">${host}</a>
|
|
</div>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-${badgeClass}">${sev.charAt(0).toUpperCase() + sev.slice(1)}</span>
|
|
</td>
|
|
<td>
|
|
<span class="badge bg-dark border border-secondary">${status}</span>
|
|
</td>
|
|
<td class="text-muted">${dateStr}</td>
|
|
</tr>
|
|
`;
|
|
});
|
|
} else {
|
|
tableContent = `
|
|
<tr>
|
|
<td colspan="5" class="text-center py-5 text-muted">
|
|
<i class="bi bi-check-circle fs-1 d-block mb-3 text-success opacity-50"></i>
|
|
Aucun incident récent à afficher.
|
|
</td>
|
|
</tr>
|
|
`;
|
|
}
|
|
tbody.innerHTML = tableContent;
|
|
}).catch(error => {
|
|
console.error(error);
|
|
document.getElementById('loading-overlay').innerHTML = `
|
|
<div class="alert alert-danger">Erreur de chargement: ${error.message}</div>
|
|
<button class="btn btn-outline-light btn-sm mt-2" onclick="location.reload()">Réessayer</button>
|
|
`;
|
|
});
|
|
}
|
|
</script>
|
|
@endsection
|